diff --git a/dank-qml-common b/dank-qml-common index df82b1265..212ac8cc7 160000 --- a/dank-qml-common +++ b/dank-qml-common @@ -1 +1 @@ -Subproject commit df82b1265c085f615d7403e6b3320e234221fd3b +Subproject commit 212ac8cc7d83e1b5d10ae63535abfab0b1903624 diff --git a/flake.lock b/flake.lock index 39b6e3303..1dad7d3b3 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "dank-qml-common": { "flake": false, "locked": { - "lastModified": 1791492221, - "narHash": "sha256-RzuPqnlzsoAIXpyPDaAV9yPO4Tj39PfZ4JRftDpa4UQ=", + "lastModified": 1791563938, + "narHash": "sha256-Pg9M+qGfLKqKih7thzP4QLuKsi9jauSIEiQy8T6+EoY=", "owner": "AvengeMedia", "repo": "dank-qml-common", - "rev": "df82b1265c085f615d7403e6b3320e234221fd3b", + "rev": "212ac8cc7d83e1b5d10ae63535abfab0b1903624", "type": "github" }, "original": { diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 5283ace81..846e4b594 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -23,7 +23,7 @@ Singleton { id: root readonly property var log: Log.scoped("SettingsData") - readonly property int settingsConfigVersion: 39 + readonly property int settingsConfigVersion: 40 readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 5419447c0..ecc5528d8 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -1553,7 +1553,7 @@ var LOCAL_SPEC = { maximizeDetection: true, useOverlayLayer: false, scrollEnabled: true, - scrollXBehavior: "column", + scrollXBehavior: "focusWindow", scrollYBehavior: "workspace", middleClickAction: "none", rightClickAction: "none", diff --git a/quickshell/Common/settings/SettingsStore.js b/quickshell/Common/settings/SettingsStore.js index eb20da97d..c9c7ca912 100644 --- a/quickshell/Common/settings/SettingsStore.js +++ b/quickshell/Common/settings/SettingsStore.js @@ -206,7 +206,8 @@ function toJson(root, setKeys) { value = withoutInstancePositions(value); if (k === "builtInPluginSettings") value = withoutSessionBackedPluginState(value); - if (!setKeys.has(k) && Util.isDefault(value, SPEC[k].def)) continue; + if (!setKeys.has(k) && Util.isDefault(value, SPEC[k].def)) + continue; out[k] = value; } out.configVersion = root.settingsConfigVersion; @@ -722,9 +723,26 @@ function migrateToVersion(obj, targetVersion) { settings.configVersion = 39; } + if (currentVersion < 40 && targetVersion >= 40) { + migrateScrollColumnBehavior(settings); + settings.configVersion = 40; + } + return settings; } +// v40: the niri-only "column" scroll behavior became the compositor-neutral "focusWindow" +function migrateScrollColumnBehavior(settings) { + for (const bar of Array.isArray(settings.barConfigs) ? settings.barConfigs : []) { + if (!bar || typeof bar !== "object") + continue; + for (const key of ["scrollXBehavior", "scrollYBehavior"]) { + if (bar[key] === "column") + bar[key] = "focusWindow"; + } + } +} + // v39: the per-island toggle folded into the global reduceMotion; any bar that had it on turns the global one on function migrateIslandReducedMotion(settings) { for (const bar of Array.isArray(settings.barConfigs) ? settings.barConfigs : []) { diff --git a/quickshell/Modules/DBar/BarScrollArea.qml b/quickshell/Modules/DBar/BarScrollArea.qml index 92d2a2310..dfb78f31c 100644 --- a/quickshell/Modules/DBar/BarScrollArea.qml +++ b/quickshell/Modules/DBar/BarScrollArea.qml @@ -9,7 +9,7 @@ MouseArea { id: root property bool scrollEnabled: true - property string xBehavior: "column" + property string xBehavior: "focusWindow" property string yBehavior: "workspace" property string screenName: "" property var barConfig: null @@ -18,6 +18,8 @@ MouseArea { property real mouseAccumulatorX: 0 property real mouseAccumulatorY: 0 property bool actionInProgress: false + property bool gestureActive: false + property bool gestureHorizontal: false signal workspaceSwitchRequested(int direction) @@ -34,14 +36,8 @@ MouseArea { case "workspace": workspaceSwitchRequested(direction); return true; - case "column": - if (!CompositorService.isNiri) - return false; - if (direction > 0) - NiriService.moveColumnRight(screenName); - else - NiriService.moveColumnLeft(screenName); - return true; + case "focusWindow": + return CompositorService.stepWindowFocus(screenName, direction); default: return false; } @@ -88,22 +84,34 @@ MouseArea { mouseAccumulatorY = 0; } + function horizontalGesture(wheel, isTouchpad) { + if (!isTouchpad) + return WheelInput.isHorizontal(wheel); + if (!gestureActive || wheel.phase === Qt.ScrollBegin) + gestureHorizontal = WheelInput.isHorizontal(wheel); + gestureActive = true; + return gestureHorizontal; + } + function processWheel(wheel) { wheel.accepted = false; + if (wheel.phase === Qt.ScrollEnd) + gestureActive = false; if (!scrollEnabled || actionInProgress) return; - const deltaY = wheel.angleDelta.y; - const deltaX = wheel.angleDelta.x; + const delta = WheelInput.dominantDelta(wheel.angleDelta); + if (delta === 0) + return; const isTouchpad = WheelInput.isTouchpad(wheel); - - if (CompositorService.isNiri && xBehavior !== "none" && Math.abs(deltaX) > Math.abs(deltaY)) { - accumulateX(isTouchpad, deltaX, xBehavior); + if (horizontalGesture(wheel, isTouchpad)) { + if (xBehavior !== "none") + accumulateX(isTouchpad, delta, xBehavior); return; } if (yBehavior === "none") return; - accumulateY(isTouchpad, deltaY, yBehavior); + accumulateY(isTouchpad, delta, yBehavior); } onWheel: wheel => processWheel(wheel) diff --git a/quickshell/Modules/DBar/DBarBody.qml b/quickshell/Modules/DBar/DBarBody.qml index 862bedb44..b08ae3aef 100644 --- a/quickshell/Modules/DBar/DBarBody.qml +++ b/quickshell/Modules/DBar/DBarBody.qml @@ -1162,7 +1162,7 @@ Item { propagateComposedEvents: true z: -1 scrollEnabled: barWindow.barConfig?.scrollEnabled ?? true - xBehavior: barWindow.barConfig?.scrollXBehavior ?? "column" + xBehavior: barWindow.barConfig?.scrollXBehavior ?? "focusWindow" yBehavior: barWindow.barConfig?.scrollYBehavior ?? "workspace" screenName: barWindow.screenName barConfig: barWindow.barConfig diff --git a/quickshell/Modules/DBar/Popouts/BatteryHistoryChart.qml b/quickshell/Modules/DBar/Popouts/BatteryHistoryChart.qml index 8278cf944..91e12c6a9 100644 --- a/quickshell/Modules/DBar/Popouts/BatteryHistoryChart.qml +++ b/quickshell/Modules/DBar/Popouts/BatteryHistoryChart.qml @@ -294,13 +294,12 @@ DCard { acceptedButtons: Qt.NoButton hoverEnabled: true onWheel: wheel => { - const horizontal = wheel.angleDelta.x !== 0 || wheel.pixelDelta.x !== 0 || (wheel.modifiers & Qt.ShiftModifier); - if (!horizontal || !root.scrollable) { + if (!WheelInput.isHorizontal(wheel) || !root.scrollable) { wheel.accepted = false; return; } - const pixels = WheelInput.isTouchpad(wheel) ? wheel.pixelDelta.x || wheel.pixelDelta.y : 0; - const steps = (wheel.angleDelta.x || wheel.angleDelta.y) / 120; + const pixels = WheelInput.isTouchpad(wheel) ? WheelInput.dominantDelta(wheel.pixelDelta) : 0; + const steps = WheelInput.dominantDelta(wheel.angleDelta) / 120; root.scrollTo(timeline.contentX - (pixels || steps * timeline.mouseWheelSpeed)); wheel.accepted = true; } diff --git a/quickshell/Modules/DBar/Widgets/WorkspaceSwitcher.qml b/quickshell/Modules/DBar/Widgets/WorkspaceSwitcher.qml index d8995d0c5..6c05b594b 100644 --- a/quickshell/Modules/DBar/Widgets/WorkspaceSwitcher.qml +++ b/quickshell/Modules/DBar/Widgets/WorkspaceSwitcher.qml @@ -9,6 +9,7 @@ import qs.Modules.Plugins import qs.Services import qs.DCommon.Widgets import qs.Widgets +import "../../../DCommon/Common/WheelInput.js" as WheelInput BasePill { id: root @@ -230,7 +231,7 @@ BasePill { } onWheel: wheel => { - if (Math.abs(wheel.angleDelta.x) > Math.abs(wheel.angleDelta.y)) + if (WheelInput.isHorizontal(wheel)) return; wheel.accepted = true; workspaces.handleWheel(wheel); diff --git a/quickshell/Modules/DDash/Weather/WeatherForecastChart.qml b/quickshell/Modules/DDash/Weather/WeatherForecastChart.qml index 821a8180e..06357b0ee 100644 --- a/quickshell/Modules/DDash/Weather/WeatherForecastChart.qml +++ b/quickshell/Modules/DDash/Weather/WeatherForecastChart.qml @@ -5,6 +5,7 @@ import qs.Common import qs.Services import qs.DCommon.Widgets import qs.Modules.DDash +import "../../../DCommon/Common/WheelInput.js" as WheelInput Item { id: root @@ -60,12 +61,11 @@ Item { anchors.fill: parent acceptedButtons: Qt.NoButton onWheel: wheel => { - const horizontal = wheel.angleDelta.x !== 0 || (wheel.modifiers & Qt.ShiftModifier); - if (!horizontal || root.maxStart === 0) { + if (!WheelInput.isHorizontal(wheel) || root.maxStart === 0) { wheel.accepted = false; return; } - const delta = wheel.angleDelta.x || wheel.angleDelta.y; + const delta = WheelInput.dominantDelta(wheel.angleDelta); const step = delta > 0 ? -1 : 1; const next = root.start + step; if (delta !== 0 && next >= 0 && next <= root.maxStart) diff --git a/quickshell/Modules/Settings/DBarTab.qml b/quickshell/Modules/Settings/DBarTab.qml index c2c334092..303983512 100644 --- a/quickshell/Modules/Settings/DBarTab.qml +++ b/quickshell/Modules/Settings/DBarTab.qml @@ -458,11 +458,12 @@ Item { } SettingsToggleCard { + id: scrollCard iconName: "mouse" settingKey: "barScrollWheel" resetStore: bar resetKeys: ["scrollEnabled"] - tags: ["scroll", "wheel", "workspace", "column", "axis"] + tags: ["scroll", "wheel", "workspace", "focus", "window", "axis"] title: I18n.tr("Scroll wheel") visible: (bar.selectedBarConfig?.enabled ?? false) && !bar.selectedBarIsIsland checked: bar.selectedBarConfig?.scrollEnabled ?? true @@ -470,43 +471,23 @@ Item { scrollEnabled: checked }) + readonly property var behaviors: CompositorService.canStepWindowFocus ? ["none", "workspace", "focusWindow"] : ["none", "workspace"] + readonly property var behaviorLabels: CompositorService.canStepWindowFocus ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Focus window")] : [I18n.tr("None"), I18n.tr("Workspace")] + SettingsButtonGroupRow { text: I18n.tr("Y axis") resetStore: bar resetKeys: ["scrollYBehavior"] - model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column", "noun, bar scroll behavior option, niri window column")] : [I18n.tr("None"), I18n.tr("Workspace")] + model: scrollCard.behaviorLabels buttonPadding: Theme.spacingS minButtonWidth: 44 textSize: Theme.fontSizeSmall - currentIndex: { - switch (bar.selectedBarConfig?.scrollYBehavior || "workspace") { - case "none": - return 0; - case "workspace": - return 1; - case "column": - return 2; - default: - return 1; - } - } + currentIndex: Math.max(0, scrollCard.behaviors.indexOf(bar.selectedBarConfig?.scrollYBehavior || "workspace")) onSelectionChanged: (index, selected) => { if (!selected) return; - let behavior = "workspace"; - switch (index) { - case 0: - behavior = "none"; - break; - case 1: - behavior = "workspace"; - break; - case 2: - behavior = "column"; - break; - } SettingsData.updateBarConfig(bar.selectedBarId, { - scrollYBehavior: behavior + scrollYBehavior: scrollCard.behaviors[index] }); } } @@ -515,40 +496,16 @@ Item { text: I18n.tr("X axis") resetStore: bar resetKeys: ["scrollXBehavior"] - visible: CompositorService.isNiri - model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] + model: scrollCard.behaviorLabels buttonPadding: Theme.spacingS minButtonWidth: 44 textSize: Theme.fontSizeSmall - currentIndex: { - switch (bar.selectedBarConfig?.scrollXBehavior || "column") { - case "none": - return 0; - case "workspace": - return 1; - case "column": - return 2; - default: - return 2; - } - } + currentIndex: Math.max(0, scrollCard.behaviors.indexOf(bar.selectedBarConfig?.scrollXBehavior || "focusWindow")) onSelectionChanged: (index, selected) => { if (!selected) return; - let behavior = "column"; - switch (index) { - case 0: - behavior = "none"; - break; - case 1: - behavior = "workspace"; - break; - case 2: - behavior = "column"; - break; - } SettingsData.updateBarConfig(bar.selectedBarId, { - scrollXBehavior: behavior + scrollXBehavior: scrollCard.behaviors[index] }); } } diff --git a/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml b/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml index 0da53261c..cc348bfd8 100644 --- a/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml +++ b/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml @@ -208,13 +208,13 @@ Scope { } if (event.key === Qt.Key_Left) { - NiriService.moveColumnLeft(); + NiriService.focusColumnLeft(); event.accepted = true; return; } if (event.key === Qt.Key_Right) { - NiriService.moveColumnRight(); + NiriService.focusColumnRight(); event.accepted = true; return; } diff --git a/quickshell/Services/CompositorService.qml b/quickshell/Services/CompositorService.qml index 65ac79ebe..18baafcd6 100644 --- a/quickshell/Services/CompositorService.qml +++ b/quickshell/Services/CompositorService.qml @@ -2126,6 +2126,35 @@ Singleton { } } + readonly property bool canStepWindowFocus: compositor === "niri" || compositor === "hyprland" || compositor === "mango" + + function stepWindowFocus(screenName, direction) { + switch (compositor) { + case "niri": + if (direction > 0) + NiriService.focusColumnRight(screenName); + else + NiriService.focusColumnLeft(screenName); + return true; + case "hyprland": + if (screenName && screenName !== Hyprland.focusedMonitor?.name) + HyprlandService.focusMonitor(screenName); + HyprlandService.cycleWindow(direction > 0); + return true; + case "mango": + { + const command = direction > 0 ? "focusstack,next" : "focusstack,prev"; + if (screenName) + MangoService.dispatchOnOutput(screenName, command); + else + MangoService.dispatch(command); + return true; + } + default: + return false; + } + } + function focusWindow(windowId) { switch (compositor) { case "aqueous": diff --git a/quickshell/Services/HyprlandService.qml b/quickshell/Services/HyprlandService.qml index 326ae20e1..1af0bf222 100644 --- a/quickshell/Services/HyprlandService.qml +++ b/quickshell/Services/HyprlandService.qml @@ -10,7 +10,7 @@ import "../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve import qs.Services import "../Common/OutputModel.js" as OutputModel import "../Common/BlurStrength.js" as BlurStrength -import "../Common/WorkspaceModel.js" as WorkspaceModel +import "../Common/WorkspaceModel.js" as WorkspaceModel // qmllint disable unused-imports Singleton { id: root @@ -549,6 +549,7 @@ hl.config({ ${sections.join("\n")} }) `; + if (layoutXrayEnabled) { content += ` hl.layer_rule({ @@ -969,6 +970,14 @@ hl.layer_rule({ Hyprland.dispatch(`hl.dsp.window.move({ workspace = ${luaValue(workspace)}, window = ${luaString(selector)}, follow = ${follow ? "true" : "false"} })`); } + function cycleWindow(forward) { + if (!luaConfigActive) { + Hyprland.dispatch(forward ? "cyclenext" : "cyclenext prev"); + return; + } + Hyprland.dispatch(`hl.dsp.window.cycle_next({ next = ${forward ? "true" : "false"} })`); + } + function focusMonitor(monitor) { if (!luaConfigActive) { Hyprland.dispatch(`focusmonitor ${monitor}`); diff --git a/quickshell/Services/NiriService.qml b/quickshell/Services/NiriService.qml index 2634e324f..d4a835d90 100644 --- a/quickshell/Services/NiriService.qml +++ b/quickshell/Services/NiriService.qml @@ -836,7 +836,7 @@ Singleton { }); } - function moveColumnLeft(outputName) { + function focusColumnLeft(outputName) { if (outputName && outputName !== currentOutput) focusMonitor(outputName); return send({ @@ -846,7 +846,7 @@ Singleton { }); } - function moveColumnRight(outputName) { + function focusColumnRight(outputName) { if (outputName && outputName !== currentOutput) focusMonitor(outputName); return send({ diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 34b7d7ebe..8397ee6e5 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -4161,7 +4161,7 @@ "keywords": [ "axis", "bar", - "column", + "focus", "general", "panel", "scroll", @@ -4169,6 +4169,7 @@ "taskbar", "topbar", "wheel", + "window", "workspace" ], "parentLabel": "Bar",