From 6e64a1e5b3f2863cb63fd3c4d0f2e990ae118b72 Mon Sep 17 00:00:00 2001 From: MrFruitDude Date: Tue, 10 Mar 2026 11:01:37 -0400 Subject: [PATCH] fix: handle plugin path splitting and timeline wheel scroll --- include/xstudio/utility/string_helpers.hpp | 14 ++++- src/global_store/src/global_store.cpp | 4 +- .../src/plugin_manager_actor.cpp | 2 +- src/ui/qml/studio/src/qml_setup.cpp | 4 +- src/utility/test/string_helpers_test.cpp | 24 +++++++- ui/qml/xstudio/views/timeline/XsTimeline.qml | 56 ++++++++++++++++--- 6 files changed, 88 insertions(+), 16 deletions(-) diff --git a/include/xstudio/utility/string_helpers.hpp b/include/xstudio/utility/string_helpers.hpp index fed1569aa..5480a2a8e 100644 --- a/include/xstudio/utility/string_helpers.hpp +++ b/include/xstudio/utility/string_helpers.hpp @@ -121,6 +121,18 @@ namespace utility { return elems; } + inline constexpr char path_list_separator() { +#ifdef _WIN32 + return ';'; +#else + return ':'; +#endif + } + + inline std::vector split_path_list(const std::string &s) { + return split(s, path_list_separator()); + } + // not optimal.. inline bool starts_with(const std::string &haystack, const std::string &needle) { if (haystack.size() < needle.size()) @@ -361,4 +373,4 @@ namespace utility { } // namespace utility -} // namespace xstudio \ No newline at end of file +} // namespace xstudio diff --git a/src/global_store/src/global_store.cpp b/src/global_store/src/global_store.cpp index f97ca826a..f45abf249 100644 --- a/src/global_store/src/global_store.cpp +++ b/src/global_store/src/global_store.cpp @@ -88,7 +88,7 @@ bool xstudio::global_store::load_preferences( // folders char *plugin_path = std::getenv("XSTUDIO_PLUGIN_PATH"); if (plugin_path) { - for (const auto &p : xstudio::utility::split(plugin_path, ':')) { + for (const auto &p : xstudio::utility::split_path_list(plugin_path)) { if (fs::is_directory(p + "/preferences")) preference_load_defaults(prefs, p + "/preferences"); } @@ -441,4 +441,4 @@ utility::JsonStore GlobalStoreHelper::get_existing_or_create_new_preference( JsonStoreHelper::set(v, path, async, broadcast_change); } return default_; -} \ No newline at end of file +} diff --git a/src/plugin_manager/src/plugin_manager_actor.cpp b/src/plugin_manager/src/plugin_manager_actor.cpp index ccf87c81e..09876757a 100644 --- a/src/plugin_manager/src/plugin_manager_actor.cpp +++ b/src/plugin_manager/src/plugin_manager_actor.cpp @@ -30,7 +30,7 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base // xstudio plugins char *plugin_path = std::getenv("XSTUDIO_PLUGIN_PATH"); if (plugin_path) { - for (const auto &p : xstudio::utility::split(plugin_path, ':')) { + for (const auto &p : xstudio::utility::split_path_list(plugin_path)) { manager_.emplace_front_path(p); } } diff --git a/src/ui/qml/studio/src/qml_setup.cpp b/src/ui/qml/studio/src/qml_setup.cpp index c8bac5c08..38497eb87 100644 --- a/src/ui/qml/studio/src/qml_setup.cpp +++ b/src/ui/qml/studio/src/qml_setup.cpp @@ -130,7 +130,7 @@ void xstudio::ui::qml::setup_xstudio_qml_emgine(QQmlEngine *engine, caf::actor_s // with plugins char *plugin_path = std::getenv("XSTUDIO_PLUGIN_PATH"); if (plugin_path) { - for (const auto &p : xstudio::utility::split(plugin_path, ':')) { + for (const auto &p : xstudio::utility::split_path_list(plugin_path)) { // note - some xSTUDIO plugins have the backend plugin component // and a Qt/QML plugin component built into the same binary. @@ -144,4 +144,4 @@ void xstudio::ui::qml::setup_xstudio_qml_emgine(QQmlEngine *engine, caf::actor_s engine->addImportPath(QStringFromStd(p + "/qml")); } } -} \ No newline at end of file +} diff --git a/src/utility/test/string_helpers_test.cpp b/src/utility/test/string_helpers_test.cpp index 9445ee85e..12662916c 100644 --- a/src/utility/test/string_helpers_test.cpp +++ b/src/utility/test/string_helpers_test.cpp @@ -52,4 +52,26 @@ TEST(StringHelpersTest, split_vector) { EXPECT_EQ(split11[0].size(), 10); EXPECT_EQ(split11[0][0], 1); EXPECT_EQ(split11[0][9], 10); -} \ No newline at end of file +} + +TEST(StringHelpersTest, path_list_separator) { +#ifdef _WIN32 + EXPECT_EQ(path_list_separator(), ';'); +#else + EXPECT_EQ(path_list_separator(), ':'); +#endif +} + +TEST(StringHelpersTest, split_path_list) { +#ifdef _WIN32 + const auto paths = split_path_list("C:\\plugins;D:\\plugins"); + ASSERT_EQ(paths.size(), 2); + EXPECT_EQ(paths[0], "C:\\plugins"); + EXPECT_EQ(paths[1], "D:\\plugins"); +#else + const auto paths = split_path_list("/tmp/plugins:/opt/plugins"); + ASSERT_EQ(paths.size(), 2); + EXPECT_EQ(paths[0], "/tmp/plugins"); + EXPECT_EQ(paths[1], "/opt/plugins"); +#endif +} diff --git a/ui/qml/xstudio/views/timeline/XsTimeline.qml b/ui/qml/xstudio/views/timeline/XsTimeline.qml index 6a5789b80..0ebcea8ed 100644 --- a/ui/qml/xstudio/views/timeline/XsTimeline.qml +++ b/ui/qml/xstudio/views/timeline/XsTimeline.qml @@ -1391,6 +1391,43 @@ Rectangle { property var initialValue: 0 property real minScaleX: 0 + function wheelDelta(pixelDelta, angleDelta) { + return pixelDelta !== 0 ? pixelDelta : angleDelta + } + + function scrollTimelineHorizontally(deltaX) { + let stackItem = list_view.itemAtIndex(0) + if( + !stackItem || + Math.abs(deltaX) < 1 || + stackItem.scrollbar.size >= 1.0 || + stackItem.scrollbar.width <= 0 + ) { + return false + } + + let positionDelta = (stackItem.scrollbar.size / stackItem.scrollbar.width) * deltaX + stackItem.jumpToPosition(stackItem.currentPosition() + positionDelta) + return true + } + + function scrollTimelineVertically(deltaY) { + if( + hovered == null || + Math.abs(deltaY) < 1 || + !["Video Track", "Audio Track", "Gap", "Clip"].includes(hovered.itemTypeRole) + ) { + return false + } + + if(["Video Track", "Audio Track"].includes(hovered.itemTypeRole)) + hovered.parentLV.flick(0, deltaY > 0 ? 500 : -500) + else if(["Gap", "Clip"].includes(hovered.itemTypeRole)) + hovered.parentLV.parentLV.flick(0, deltaY > 0 ? 500 : -500) + + return true + } + Rectangle { id: region visible: ma.isRegionSelection @@ -1600,16 +1637,18 @@ Rectangle { } onWheel: wheel => { + let deltaX = wheelDelta(wheel.pixelDelta.x, wheel.angleDelta.x) + let deltaY = wheelDelta(wheel.pixelDelta.y, wheel.angleDelta.y) // maintain position as we zoom.. if(wheel.modifiers == Qt.ShiftModifier) { // wheel.angleDelta.y always return 0 on MacOS laptops // when SHIFT is pressed and a mouse wheel is used, but in // that case the x component is updating and usable. - let deltaY = wheel.angleDelta.y == 0 ? wheel.angleDelta.x : wheel.angleDelta.y + let zoomDelta = deltaY == 0 ? deltaX : deltaY // Limit the scale to keep it within a usable range and // avoid a negative scaleY value. - if(deltaY > 1) { + if(zoomDelta > 1) { scaleY = Math.min(2.0, scaleY + 0.2) } else { scaleY = Math.max(0.6, scaleY - 0.2) @@ -1617,7 +1656,8 @@ Rectangle { wheel.accepted = true } else if(wheel.modifiers == Qt.ControlModifier) { let tmp = scaleX - if(wheel.angleDelta.y > 1) { + let zoomDelta = deltaY == 0 ? deltaX : deltaY + if(zoomDelta > 1) { tmp += 0.2 } else { tmp -= 0.2 @@ -1625,11 +1665,9 @@ Rectangle { scaleX = Math.max((list_view.width - trackHeaderWidth) / theSessionData.timelineRect([timeline_items.rootIndex]).width, tmp) list_view.itemAtIndex(0).jumpToFrame(timelinePlayhead.logicalFrame, ListView.Center) wheel.accepted = true - } else if(hovered != null && ["Video Track", "Audio Track","Gap","Clip"].includes(hovered.itemTypeRole)) { - if(["Video Track", "Audio Track"].includes(hovered.itemTypeRole)) - hovered.parentLV.flick(0, wheel.angleDelta.y > 1 ? 500 : -500) - else if(["Gap", "Clip"].includes(hovered.itemTypeRole)) - hovered.parentLV.parentLV.flick(0, wheel.angleDelta.y > 1 ? 500 : -500) + } else if(Math.abs(deltaX) > Math.abs(deltaY)) { + wheel.accepted = scrollTimelineHorizontally(deltaX) + } else if(scrollTimelineVertically(deltaY)) { wheel.accepted = true } else { wheel.accepted = false @@ -2224,4 +2262,4 @@ Rectangle { // } // } -} \ No newline at end of file +}