From 8c20f448ed13c8c2783e2526a1b95a55bee06b75 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 1 Jun 2026 13:13:17 -0400 Subject: [PATCH] control center: improve drag handling misc: fix layer shell enum usage --- quickshell/Common/LayerShell.qml | 91 +++++++ .../Modals/Common/DankModalConnected.qml | 22 +- .../Modals/Common/DankModalStandalone.qml | 22 +- .../DankLauncherV2ModalConnected.qml | 20 +- .../DankLauncherV2ModalSpotlight.qml | 20 +- .../DankLauncherV2ModalStandalone.qml | 20 +- .../ControlCenter/Components/DragDropGrid.qml | 75 ++++-- .../ControlCenter/Components/EditModeGrid.qml | 110 ++++++++ .../Components/EditModeWidgetDelegate.qml | 242 ++++++++++++++++++ .../ControlCenter/Models/WidgetModel.qml | 4 + .../Modules/ControlCenter/utils/layout.js | 68 +++++ quickshell/Modules/DankBar/DankBarWindow.qml | 15 +- .../Notifications/Popup/NotificationPopup.qml | 25 +- quickshell/Modules/Plugins/BasePill.qml | 14 +- .../Modules/Settings/WidgetsTabSection.qml | 2 +- quickshell/Widgets/DankOSD.qml | 19 +- quickshell/Widgets/DankPopout.qml | 12 +- quickshell/Widgets/DankPopoutConnected.qml | 19 +- quickshell/Widgets/DankPopoutStandalone.qml | 19 +- scripts/format-staged.py | 23 +- 20 files changed, 634 insertions(+), 208 deletions(-) create mode 100644 quickshell/Common/LayerShell.qml create mode 100644 quickshell/Modules/ControlCenter/Components/EditModeGrid.qml create mode 100644 quickshell/Modules/ControlCenter/Components/EditModeWidgetDelegate.qml diff --git a/quickshell/Common/LayerShell.qml b/quickshell/Common/LayerShell.qml new file mode 100644 index 00000000..5ae01da3 --- /dev/null +++ b/quickshell/Common/LayerShell.qml @@ -0,0 +1,91 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Services + +Singleton { + id: root + + readonly property var log: Log.scoped("LayerShell") + + function _toLayer(name) { + switch (name) { + case "background": + return WlrLayer.Background; + case "bottom": + return WlrLayer.Bottom; + case "top": + return WlrLayer.Top; + case "overlay": + return WlrLayer.Overlay; + } + return undefined; + } + + function _toName(layer) { + switch (layer) { + case WlrLayer.Background: + return "background"; + case WlrLayer.Bottom: + return "bottom"; + case WlrLayer.Top: + return "top"; + case WlrLayer.Overlay: + return "overlay"; + } + return "top"; + } + + // Resolve a WlrLayer from a DMS_*_LAYER env override. + // name: env var to read, e.g. "DMS_OSD_LAYER" + // fallback: WlrLayer used when the var is unset or unrecognized + // opts (optional): + // allow: array of honored layer names; recognized names outside it + // are treated as invalid + // invalidLayer: WlrLayer used for a recognized-but-disallowed value + // (default: fallback) + // label: context for the diagnostic, e.g. "OSDs"; omit to stay silent + // error: log at error level instead of warn + function fromEnv(name, fallback, opts) { + const value = Quickshell.env(name); + if (!value) + return fallback; + + const requested = _toLayer(value); + if (requested === undefined) + return fallback; + + const allow = opts?.allow; + if (!allow || allow.indexOf(value) !== -1) + return requested; + + const invalid = opts?.invalidLayer ?? fallback; + if (opts?.label) { + const msg = `'${value}' layer is not valid for ${opts.label}. Defaulting to '${_toName(invalid)}' layer.`; + if (opts?.error) + log.error(msg); + else + log.warn(msg); + } + return invalid; + } + + // For call sites that only need "is the override the overlay layer?". + // Honors "overlay" (true) and bottom/background/top (false); anything else + // returns `fallback`. + function envUsesOverlay(name, fallback) { + switch (Quickshell.env(name)) { + case "overlay": + return true; + case "bottom": + case "background": + case "top": + return false; + default: + return fallback; + } + } +} diff --git a/quickshell/Modals/Common/DankModalConnected.qml b/quickshell/Modals/Common/DankModalConnected.qml index 79afe204..cd91b743 100644 --- a/quickshell/Modals/Common/DankModalConnected.qml +++ b/quickshell/Modals/Common/DankModalConnected.qml @@ -497,22 +497,12 @@ Item { } WlrLayershell.namespace: root.layerNamespace - WlrLayershell.layer: { - if (root.useOverlayLayer) - return WlrLayershell.Overlay; - switch (Quickshell.env("DMS_MODAL_LAYER")) { - case "bottom": - log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "background": - log.error("'background' layer is not valid for modals. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "overlay": - return WlrLayershell.Overlay; - default: - return WlrLayershell.Top; - } - } + WlrLayershell.layer: root.useOverlayLayer ? WlrLayer.Overlay : LayerShell.fromEnv("DMS_MODAL_LAYER", WlrLayer.Top, { + "allow": ["top", "overlay"], + "invalidLayer": WlrLayer.Top, + "label": "modals", + "error": true + }) WlrLayershell.exclusiveZone: -1 WlrLayershell.keyboardFocus: { if (customKeyboardFocus !== null) diff --git a/quickshell/Modals/Common/DankModalStandalone.qml b/quickshell/Modals/Common/DankModalStandalone.qml index 0e96b511..8d77e529 100644 --- a/quickshell/Modals/Common/DankModalStandalone.qml +++ b/quickshell/Modals/Common/DankModalStandalone.qml @@ -251,22 +251,12 @@ Item { } WlrLayershell.namespace: root.layerNamespace - WlrLayershell.layer: { - if (root.useOverlayLayer) - return WlrLayershell.Overlay; - switch (Quickshell.env("DMS_MODAL_LAYER")) { - case "bottom": - log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "background": - log.error("'background' layer is not valid for modals. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "overlay": - return WlrLayershell.Overlay; - default: - return WlrLayershell.Top; - } - } + WlrLayershell.layer: root.useOverlayLayer ? WlrLayer.Overlay : LayerShell.fromEnv("DMS_MODAL_LAYER", WlrLayer.Top, { + "allow": ["top", "overlay"], + "invalidLayer": WlrLayer.Top, + "label": "modals", + "error": true + }) WlrLayershell.exclusiveZone: -1 WlrLayershell.keyboardFocus: { if (customKeyboardFocus !== null) diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml index 520e3b0e..f3b74274 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalConnected.qml @@ -42,20 +42,12 @@ Item { readonly property real screenHeight: effectiveScreen?.height ?? 1080 readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 readonly property bool usesOverlayLayer: SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer - readonly property var effectiveLauncherLayer: { - switch (Quickshell.env("DMS_MODAL_LAYER")) { - case "bottom": - log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "background": - log.error("'background' layer is not valid for modals. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "overlay": - return WlrLayershell.Overlay; - default: - return root.usesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top; - } - } + readonly property var effectiveLauncherLayer: LayerShell.fromEnv("DMS_MODAL_LAYER", root.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, { + "allow": ["top", "overlay"], + "invalidLayer": WlrLayer.Top, + "label": "modals", + "error": true + }) readonly property int baseWidth: { switch (SettingsData.dankLauncherV2Size) { diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalSpotlight.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalSpotlight.qml index 75bee252..44e5e83b 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalSpotlight.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalSpotlight.qml @@ -32,20 +32,12 @@ Item { readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 readonly property bool useBackgroundDarken: !SettingsData.frameEnabled && SettingsData.modalDarkenBackground readonly property bool usesOverlayLayer: useBackgroundDarken || SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer - readonly property var effectiveLauncherLayer: { - switch (Quickshell.env("DMS_MODAL_LAYER")) { - case "bottom": - log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "background": - log.error("'background' layer is not valid for modals. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "overlay": - return WlrLayershell.Overlay; - default: - return root.usesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top; - } - } + readonly property var effectiveLauncherLayer: LayerShell.fromEnv("DMS_MODAL_LAYER", root.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, { + "allow": ["top", "overlay"], + "invalidLayer": WlrLayer.Top, + "label": "modals", + "error": true + }) readonly property int _openDuration: 50 readonly property int _closeDuration: 40 diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml index 55901d7a..69f1944e 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalStandalone.qml @@ -81,20 +81,12 @@ Item { readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) readonly property bool useBackgroundDarken: !SettingsData.frameEnabled && SettingsData.modalDarkenBackground readonly property bool usesOverlayLayer: useBackgroundDarken || SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer - readonly property var effectiveLauncherLayer: { - switch (Quickshell.env("DMS_MODAL_LAYER")) { - case "bottom": - log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "background": - log.error("'background' layer is not valid for modals. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "overlay": - return WlrLayershell.Overlay; - default: - return root.usesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top; - } - } + readonly property var effectiveLauncherLayer: LayerShell.fromEnv("DMS_MODAL_LAYER", root.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, { + "allow": ["top", "overlay"], + "invalidLayer": WlrLayer.Top, + "label": "modals", + "error": true + }) readonly property real cornerRadius: Theme.cornerRadius readonly property color borderColor: { if (!SettingsData.dankLauncherV2BorderEnabled) diff --git a/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml b/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml index 4b2718da..90d8ec72 100644 --- a/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml +++ b/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml @@ -54,6 +54,8 @@ Column { } readonly property real targetImplicitHeight: { + if (editMode) + return editModeGrid.implicitHeight; const rows = layoutResult.rows; let totalHeight = 0; for (let i = 0; i < rows.length; i++) { @@ -106,8 +108,40 @@ Column { item.z = 1000; } + function componentForWidget(widgetData) { + const id = widgetData.id || ""; + const widgetWidth = widgetData.width || 50; + if (id.startsWith("builtin_")) + return builtinPluginWidgetComponent; + if (id.startsWith("plugin_")) + return pluginWidgetComponent; + switch (id) { + case "wifi": + case "bluetooth": + case "audioOutput": + case "audioInput": + return compoundPillComponent; + case "volumeSlider": + return audioSliderComponent; + case "brightnessSlider": + return brightnessSliderComponent; + case "inputVolumeSlider": + return inputAudioSliderComponent; + case "battery": + return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent; + case "diskUsage": + return widgetWidth <= 25 ? smallDiskUsageComponent : diskUsagePillComponent; + case "colorPicker": + return colorPickerPillComponent; + case "doNotDisturb": + return widgetWidth <= 25 ? smallToggleComponent : dndPillComponent; + default: + return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent; + } + } + Repeater { - model: root.layoutResult.rows + model: root.editMode ? [] : root.layoutResult.rows Column { width: root.width @@ -174,32 +208,7 @@ Column { return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider"; } - widgetComponent: { - const id = modelData.id || ""; - if (id.startsWith("builtin_")) { - return builtinPluginWidgetComponent; - } else if (id.startsWith("plugin_")) { - return pluginWidgetComponent; - } else if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") { - return compoundPillComponent; - } else if (id === "volumeSlider") { - return audioSliderComponent; - } else if (id === "brightnessSlider") { - return brightnessSliderComponent; - } else if (id === "inputVolumeSlider") { - return inputAudioSliderComponent; - } else if (id === "battery") { - return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent; - } else if (id === "diskUsage") { - return widgetWidth <= 25 ? smallDiskUsageComponent : diskUsagePillComponent; - } else if (id === "colorPicker") { - return colorPickerPillComponent; - } else if (id === "doNotDisturb") { - return widgetWidth <= 25 ? smallToggleComponent : dndPillComponent; - } else { - return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent; - } - } + widgetComponent: root.componentForWidget(modelData) onWidgetMoved: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) onRemoveWidget: index => root.removeWidget(index) @@ -279,6 +288,18 @@ Column { } } + EditModeGrid { + id: editModeGrid + width: root.width + visible: root.editMode + active: root.editMode + model: root.model + componentProvider: root + onRemoveWidget: index => root.removeWidget(index) + onToggleWidgetSize: index => root.toggleWidgetSize(index) + onConfigRequested: (idx, data, anchor) => root.configRequested(idx, data, anchor) + } + Component { id: errorPillComponent ErrorPill { diff --git a/quickshell/Modules/ControlCenter/Components/EditModeGrid.qml b/quickshell/Modules/ControlCenter/Components/EditModeGrid.qml new file mode 100644 index 00000000..78ba3e4c --- /dev/null +++ b/quickshell/Modules/ControlCenter/Components/EditModeGrid.qml @@ -0,0 +1,110 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Modules.ControlCenter.Components +import "../utils/layout.js" as LayoutUtils + +Item { + id: root + + property var model: null + property var componentProvider: null + property bool active: true + + signal removeWidget(int index) + signal toggleWidgetSize(int index) + signal configRequested(int index, var widgetData, var anchor) + + property var sourceWidgets: SettingsData.controlCenterWidgets || [] + property var visualOrder: [] + property int draggingSourceIndex: -1 + property var dragStartOrder: [] + + readonly property real rowSpacing: Theme.spacingL + readonly property real sliderCellHeight: 48 + readonly property real normalCellHeight: 60 + + readonly property var slotLayout: LayoutUtils.computeSlots(sourceWidgets, visualOrder, width, Theme.spacingS, rowSpacing, sliderCellHeight, normalCellHeight) + + implicitHeight: slotLayout.totalHeight + + function rebuildOrder() { + const n = (sourceWidgets || []).length; + const arr = []; + for (var i = 0; i < n; i++) + arr.push(i); + visualOrder = arr; + } + + onSourceWidgetsChanged: rebuildOrder() + Component.onCompleted: rebuildOrder() + + function beginDrag(sourceIndex) { + draggingSourceIndex = sourceIndex; + dragStartOrder = visualOrder.slice(); + } + + function sameOrder(a, b) { + if (a.length !== b.length) + return false; + for (var i = 0; i < a.length; i++) { + if (a[i] !== b[i]) + return false; + } + return true; + } + + function updateDragTarget(centerX, centerY) { + if (draggingSourceIndex < 0) + return; + const p = LayoutUtils.slotContainingPoint(slotLayout.slots, visualOrder, centerX, centerY); + if (p < 0) + return; + const arr = visualOrder.slice(); + const d = arr.indexOf(draggingSourceIndex); + if (d < 0 || d === p) + return; + arr.splice(d, 1); + arr.splice(p, 0, draggingSourceIndex); + visualOrder = arr; + } + + function endDrag() { + if (draggingSourceIndex < 0) + return; + draggingSourceIndex = -1; + if (!sameOrder(visualOrder, dragStartOrder)) + commit(); + } + + function commit() { + const widgets = sourceWidgets || []; + const arr = visualOrder.map(i => widgets[i]); + if (root.model) + root.model.reorderWidgets(arr); + } + + Repeater { + model: root.active ? root.sourceWidgets : [] + + EditModeWidgetDelegate { + required property int index + required property var modelData + + grid: root + sourceIndex: index + widgetData: modelData + isSlider: LayoutUtils.isSliderWidget(modelData.id || "") + widgetComponent: root.componentProvider ? root.componentProvider.componentForWidget(modelData) : null + + slotX: root.slotLayout.slots[index] ? root.slotLayout.slots[index].x : 0 + slotY: root.slotLayout.slots[index] ? root.slotLayout.slots[index].y : 0 + cellW: root.slotLayout.slots[index] ? root.slotLayout.slots[index].w : root.width + cellH: root.slotLayout.slots[index] ? root.slotLayout.slots[index].h : root.normalCellHeight + + onRemoveWidget: idx => root.removeWidget(idx) + onToggleWidgetSize: idx => root.toggleWidgetSize(idx) + onConfigRequested: (idx, data, anchor) => root.configRequested(idx, data, anchor) + } + } +} diff --git a/quickshell/Modules/ControlCenter/Components/EditModeWidgetDelegate.qml b/quickshell/Modules/ControlCenter/Components/EditModeWidgetDelegate.qml new file mode 100644 index 00000000..426ab27a --- /dev/null +++ b/quickshell/Modules/ControlCenter/Components/EditModeWidgetDelegate.qml @@ -0,0 +1,242 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property var grid: null + property int sourceIndex: -1 + property var widgetData: null + property Component widgetComponent: null + property bool isSlider: false + + property real slotX: 0 + property real slotY: 0 + property real cellW: 100 + property real cellH: 60 + + property bool dragging: !!grid && grid.draggingSourceIndex === sourceIndex + + signal removeWidget(int index) + signal toggleWidgetSize(int index) + signal configRequested(int index, var widgetData, var anchor) + + width: cellW + height: cellH + z: dragging ? 10000 : 1 + + Binding { + target: root + property: "x" + value: root.slotX + when: !root.dragging + restoreMode: Binding.RestoreNone + } + Binding { + target: root + property: "y" + value: root.slotY + when: !root.dragging + restoreMode: Binding.RestoreNone + } + + onXChanged: { + if (dragging && grid) + grid.updateDragTarget(x + width / 2, y + height / 2); + } + onYChanged: { + if (dragging && grid) + grid.updateDragTarget(x + width / 2, y + height / 2); + } + + Behavior on x { + enabled: !root.dragging + NumberAnimation { + duration: Theme.expressiveDurations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveEffects + } + } + Behavior on y { + enabled: !root.dragging + NumberAnimation { + duration: Theme.expressiveDurations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveEffects + } + } + + Rectangle { + id: dragIndicator + anchors.fill: parent + color: "transparent" + border.color: Theme.primary + border.width: root.dragging ? 2 : 0 + radius: Theme.cornerRadius + opacity: root.dragging ? 0.8 : 1.0 + z: root.dragging ? 10000 : 1 + + Behavior on border.width { + NumberAnimation { + duration: 150 + } + } + Behavior on opacity { + NumberAnimation { + duration: 150 + } + } + } + + Loader { + id: widgetLoader + anchors.fill: parent + sourceComponent: root.widgetComponent + property var widgetData: root.widgetData + property int widgetIndex: root.sourceIndex + property int globalWidgetIndex: root.sourceIndex + property int widgetWidth: root.widgetData?.width || 50 + + MouseArea { + id: editModeBlocker + anchors.fill: parent + enabled: true + acceptedButtons: Qt.AllButtons + onPressed: function (mouse) { + mouse.accepted = true; + } + onWheel: function (wheel) { + wheel.accepted = true; + } + z: 100 + } + } + + MouseArea { + id: dragArea + anchors.fill: parent + cursorShape: Qt.OpenHandCursor + drag.target: root + drag.axis: Drag.XAndYAxis + drag.smoothed: false + + onPressed: function (mouse) { + cursorShape = Qt.ClosedHandCursor; + if (root.grid) + root.grid.beginDrag(root.sourceIndex); + } + + onReleased: function (mouse) { + cursorShape = Qt.OpenHandCursor; + if (root.grid) + root.grid.endDrag(); + } + } + + Rectangle { + id: removeButton + width: 16 + height: 16 + radius: 8 + color: Theme.error + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: -4 + z: 10 + + DankIcon { + anchors.centerIn: parent + name: "close" + size: 12 + color: Theme.primaryText + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.removeWidget(root.sourceIndex) + } + } + + SizeControls { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.margins: -6 + z: 10 + currentSize: root.widgetData?.width || 50 + isSlider: root.isSlider + widgetIndex: root.sourceIndex + onSizeChanged: newSize => { + var widgets = SettingsData.controlCenterWidgets.slice(); + if (root.sourceIndex >= 0 && root.sourceIndex < widgets.length) { + widgets[root.sourceIndex].width = newSize; + SettingsData.set("controlCenterWidgets", widgets); + } + } + } + + readonly property bool hasConfigMenu: widgetData?.id === "diskUsage" + + Rectangle { + id: configButton + width: 16 + height: 16 + radius: 8 + color: Theme.primary + anchors.top: removeButton.top + anchors.right: removeButton.left + anchors.rightMargin: 4 + visible: root.hasConfigMenu + z: 10 + + DankIcon { + anchors.centerIn: parent + name: "settings" + size: 12 + color: Theme.primaryText + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.configRequested(root.sourceIndex, root.widgetData, configButton) + } + } + + Rectangle { + id: dragHandle + width: 16 + height: 12 + radius: 2 + color: Theme.primary + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: 4 + z: 15 + opacity: root.dragging ? 1.0 : 0.7 + + DankIcon { + anchors.centerIn: parent + name: "drag_indicator" + size: 10 + color: Theme.primaryText + } + + Behavior on opacity { + NumberAnimation { + duration: 150 + } + } + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) + radius: Theme.cornerRadius + border.color: "transparent" + border.width: 0 + z: -1 + } +} diff --git a/quickshell/Modules/ControlCenter/Models/WidgetModel.qml b/quickshell/Modules/ControlCenter/Models/WidgetModel.qml index 273f0450..1612c37e 100644 --- a/quickshell/Modules/ControlCenter/Models/WidgetModel.qml +++ b/quickshell/Modules/ControlCenter/Models/WidgetModel.qml @@ -352,6 +352,10 @@ QtObject { WidgetUtils.moveWidget(fromIndex, toIndex); } + function reorderWidgets(newOrder) { + WidgetUtils.reorderWidgets(newOrder); + } + function resetToDefault() { WidgetUtils.resetToDefault(); } diff --git a/quickshell/Modules/ControlCenter/utils/layout.js b/quickshell/Modules/ControlCenter/utils/layout.js index b8e5651f..dfc26bc3 100644 --- a/quickshell/Modules/ControlCenter/utils/layout.js +++ b/quickshell/Modules/ControlCenter/utils/layout.js @@ -1,3 +1,71 @@ +function spanWidthFor(baseWidth, widgetWidth, spacing) { + const w = widgetWidth || 50 + if (w <= 25) + return (baseWidth - spacing * 3) / 4 + if (w <= 50) + return (baseWidth - spacing) / 2 + if (w <= 75) + return (baseWidth - spacing * 2) * 0.75 + return baseWidth +} + +function isSliderWidget(id) { + return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider" +} + +function computeSlots(widgets, order, baseWidth, spacing, rowSpacing, sliderHeight, normalHeight) { + const slots = [] + let x = 0 + let y = 0 + let rowRight = 0 + let rowMaxH = 0 + let countInRow = 0 + + for (let p = 0; p < order.length; p++) { + const sourceIndex = order[p] + const widget = widgets[sourceIndex] + if (!widget) + continue + + const itemW = spanWidthFor(baseWidth, widget.width, spacing) + const itemH = isSliderWidget(widget.id || "") ? sliderHeight : normalHeight + + if (countInRow > 0 && (rowRight + spacing + itemW > baseWidth + 0.5)) { + y += rowMaxH + rowSpacing + rowRight = 0 + rowMaxH = 0 + countInRow = 0 + } + + x = countInRow === 0 ? 0 : rowRight + spacing + slots[sourceIndex] = { + "x": x, + "y": y, + "w": itemW, + "h": itemH + } + rowRight = x + itemW + rowMaxH = Math.max(rowMaxH, itemH) + countInRow++ + } + + return { + "slots": slots, + "totalHeight": y + rowMaxH + } +} + +function slotContainingPoint(slots, order, px, py) { + for (let p = 0; p < order.length; p++) { + const s = slots[order[p]] + if (!s) + continue + if (px >= s.x && px < s.x + s.w && py >= s.y && py < s.y + s.h) + return p + } + return -1 +} + function calculateRowsAndWidgets(controlCenterColumn, expandedSection, expandedWidgetIndex) { var rows = [] var currentRow = [] diff --git a/quickshell/Modules/DankBar/DankBarWindow.qml b/quickshell/Modules/DankBar/DankBarWindow.qml index 6a21b156..6a33048b 100644 --- a/quickshell/Modules/DankBar/DankBarWindow.qml +++ b/quickshell/Modules/DankBar/DankBarWindow.qml @@ -110,20 +110,7 @@ PanelWindow { readonly property bool usesOverlayLayer: CompositorService.framePeerSurfacesUseOverlayForScreen(barWindow.screen) || (barConfig?.useOverlayLayer ?? false) - readonly property var dBarLayer: { - switch (Quickshell.env("DMS_DANKBAR_LAYER")) { - case "bottom": - return WlrLayer.Bottom; - case "overlay": - return WlrLayer.Overlay; - case "background": - return WlrLayer.Background; - case "top": - return WlrLayer.Top; - default: - return barWindow.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top; - } - } + readonly property var dBarLayer: LayerShell.fromEnv("DMS_DANKBAR_LAYER", barWindow.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top) property var blurRegion: null property var _blurWidgetItems: [] diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index 43d24f9b..63449d13 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -156,28 +156,9 @@ PanelWindow { visible: !_finalized WlrLayershell.layer: { - const envLayer = Quickshell.env("DMS_NOTIFICATION_LAYER"); - if (envLayer) { - switch (envLayer) { - case "bottom": - return WlrLayershell.Bottom; - case "overlay": - return WlrLayershell.Overlay; - case "background": - return WlrLayershell.Background; - case "top": - return WlrLayershell.Top; - } - } - - if (!notificationData) - return WlrLayershell.Top; - - SettingsData.notificationOverlayEnabled; - - const shouldUseOverlay = (SettingsData.notificationOverlayEnabled) || (notificationData.urgency === NotificationUrgency.Critical); - - return shouldUseOverlay ? WlrLayershell.Overlay : WlrLayershell.Top; + const shouldUseOverlay = notificationData && (SettingsData.notificationOverlayEnabled || notificationData.urgency === NotificationUrgency.Critical); + const fallback = shouldUseOverlay ? WlrLayer.Overlay : WlrLayer.Top; + return LayerShell.fromEnv("DMS_NOTIFICATION_LAYER", fallback); } WlrLayershell.exclusiveZone: -1 WlrLayershell.keyboardFocus: WlrKeyboardFocus.None diff --git a/quickshell/Modules/Plugins/BasePill.qml b/quickshell/Modules/Plugins/BasePill.qml index d76fffc5..64e6705e 100644 --- a/quickshell/Modules/Plugins/BasePill.qml +++ b/quickshell/Modules/Plugins/BasePill.qml @@ -1,5 +1,4 @@ import QtQuick -import Quickshell import qs.Common import qs.Services import qs.Widgets @@ -39,18 +38,7 @@ Item { readonly property real rightMargin: !isVerticalOrientation ? (isRightBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0 readonly property real topMargin: isVerticalOrientation ? (isTopBarEdge && isFirst ? barEdgeExtension : (isFirst ? gapExtension : gapExtension / 2)) : 0 readonly property real bottomMargin: isVerticalOrientation ? (isBottomBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0 - readonly property bool barUsesOverlayLayer: { - switch (Quickshell.env("DMS_DANKBAR_LAYER")) { - case "overlay": - return true; - case "bottom": - case "background": - case "top": - return false; - default: - return (barConfig?.useOverlayLayer ?? false) || CompositorService.framePeerSurfacesUseOverlayForScreen(parentScreen); - } - } + readonly property bool barUsesOverlayLayer: LayerShell.envUsesOverlay("DMS_DANKBAR_LAYER", (barConfig?.useOverlayLayer ?? false) || CompositorService.framePeerSurfacesUseOverlayForScreen(parentScreen)) signal clicked signal rightClicked(real rootX, real rootY) diff --git a/quickshell/Modules/Settings/WidgetsTabSection.qml b/quickshell/Modules/Settings/WidgetsTabSection.qml index 80b1e4fb..dc71c532 100644 --- a/quickshell/Modules/Settings/WidgetsTabSection.qml +++ b/quickshell/Modules/Settings/WidgetsTabSection.qml @@ -409,7 +409,7 @@ Column { var xPos = buttonPos.x - popupWidth - Theme.spacingS; if (xPos < 0) - xPos = buttonPos.x + focusedWindowMenuButton.width + Theme.spacingS; + xPos = buttonPos.x + focusedWindowMenuButton.width + Theme.spacingS; var yPos = buttonPos.y - popupHeight / 2 + focusedWindowMenuButton.height / 2; if (yPos < 0) { diff --git a/quickshell/Widgets/DankOSD.qml b/quickshell/Widgets/DankOSD.qml index dfd617a1..284be7ff 100644 --- a/quickshell/Widgets/DankOSD.qml +++ b/quickshell/Widgets/DankOSD.qml @@ -92,20 +92,11 @@ PanelWindow { } } - WlrLayershell.layer: { - switch (Quickshell.env("DMS_OSD_LAYER")) { - case "bottom": - log.warn("'bottom' layer is not valid for OSDs. Defaulting to 'overlay' layer."); - return WlrLayershell.Overlay; - case "background": - log.warn("'background' layer is not valid for OSDs. Defaulting to 'overlay' layer."); - return WlrLayershell.Overlay; - case "top": - return WlrLayershell.Top; - default: - return WlrLayershell.Overlay; - } - } + WlrLayershell.layer: LayerShell.fromEnv("DMS_OSD_LAYER", WlrLayer.Overlay, { + "allow": ["top", "overlay"], + "invalidLayer": WlrLayer.Overlay, + "label": "OSDs" + }) WlrLayershell.exclusiveZone: -1 WlrLayershell.keyboardFocus: WlrKeyboardFocus.None diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index 27ddb0ba..117b28fc 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -1,5 +1,4 @@ import QtQuick -import Quickshell import qs.Common import qs.Services @@ -170,16 +169,7 @@ Item { } function _triggerBarUsesOverlayLayer(targetScreen, barConfig) { - switch (Quickshell.env("DMS_DANKBAR_LAYER")) { - case "overlay": - return true; - case "bottom": - case "background": - case "top": - return false; - default: - return (barConfig?.useOverlayLayer ?? false) || CompositorService.framePeerSurfacesUseOverlayForScreen(targetScreen); - } + return LayerShell.envUsesOverlay("DMS_DANKBAR_LAYER", (barConfig?.useOverlayLayer ?? false) || CompositorService.framePeerSurfacesUseOverlayForScreen(targetScreen)); } function setTriggerPosition(x, y, width, section, targetScreen, barPosition, barThickness, barSpacing, barConfig) { diff --git a/quickshell/Widgets/DankPopoutConnected.qml b/quickshell/Widgets/DankPopoutConnected.qml index 2db2cb8a..e944e31c 100644 --- a/quickshell/Widgets/DankPopoutConnected.qml +++ b/quickshell/Widgets/DankPopoutConnected.qml @@ -57,20 +57,11 @@ Item { property var screen: null // Connected resize uses one full-screen surface; body-sized regions are masks. readonly property bool useBackgroundWindow: false - readonly property var effectivePopoutLayer: { - switch (Quickshell.env("DMS_POPOUT_LAYER")) { - case "bottom": - log.warn("'bottom' layer is not valid for popouts. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "background": - log.warn("'background' layer is not valid for popouts. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "overlay": - return WlrLayershell.Overlay; - default: - return root.triggerUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top; - } - } + readonly property var effectivePopoutLayer: LayerShell.fromEnv("DMS_POPOUT_LAYER", root.triggerUsesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, { + "allow": ["top", "overlay"], + "invalidLayer": WlrLayer.Top, + "label": "popouts" + }) readonly property real effectiveBarThickness: { if (root.usesConnectedSurfaceChrome) diff --git a/quickshell/Widgets/DankPopoutStandalone.qml b/quickshell/Widgets/DankPopoutStandalone.qml index 9126c0bb..91ac2660 100644 --- a/quickshell/Widgets/DankPopoutStandalone.qml +++ b/quickshell/Widgets/DankPopoutStandalone.qml @@ -65,20 +65,11 @@ Item { readonly property bool backgroundDismissWindowRequired: backgroundInteractive readonly property bool backgroundWindowRequired: backgroundDismissWindowRequired || root.overlayContent !== null readonly property bool _fullHeight: fullHeightSurface - readonly property var effectivePopoutLayer: { - switch (Quickshell.env("DMS_POPOUT_LAYER")) { - case "bottom": - root.log.warn("'bottom' layer is not valid for popouts. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "background": - root.log.warn("'background' layer is not valid for popouts. Defaulting to 'top' layer."); - return WlrLayershell.Top; - case "overlay": - return WlrLayershell.Overlay; - default: - return root.triggerUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top; - } - } + readonly property var effectivePopoutLayer: LayerShell.fromEnv("DMS_POPOUT_LAYER", root.triggerUsesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, { + "allow": ["top", "overlay"], + "invalidLayer": WlrLayer.Top, + "label": "popouts" + }) function _frameEdgeInset(side) { if (!screen) diff --git a/scripts/format-staged.py b/scripts/format-staged.py index 523b6161..dd989450 100755 --- a/scripts/format-staged.py +++ b/scripts/format-staged.py @@ -221,6 +221,7 @@ def main(): client = LspClient([qmlls]) changed = 0 + skipped = 0 unused_by_file = {} try: client.request("initialize", { @@ -252,10 +253,22 @@ def main(): }, }) - edits = client.request("textDocument/formatting", { - "textDocument": {"uri": uri}, - "options": {"tabSize": TAB_SIZE, "insertSpaces": True}, - }) + try: + edits = client.request("textDocument/formatting", { + "textDocument": {"uri": uri}, + "options": {"tabSize": TAB_SIZE, "insertSpaces": True}, + }) + except RuntimeError as exc: + # qmlls (qmlformat's DOM) refuses some valid files — notably + # "Cannot format invalid documents!" on constructs qmllint + # accepts. Don't let one file's formatter bug abort the commit. + client.notify("textDocument/didClose", {"textDocument": {"uri": uri}}) + if "invalid document" in str(exc).lower(): + print("skipped (qmlls rejected as invalid;") + else: + print(f"skipped ({exc})") + skipped += 1 + continue client.notify("textDocument/didClose", {"textDocument": {"uri": uri}}) @@ -276,6 +289,8 @@ def main(): unused_by_file[file] = findings print(f"\n{changed} of {len(files)} file(s) changed.") + if skipped: + print(f"{skipped} file(s) skipped (could not be formatted; see above).") if unused_by_file: print("\nUnused import warnings (informational, not auto-removed):")