diff --git a/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml b/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml index 2d5077f2..ecada5d5 100644 --- a/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml +++ b/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml @@ -1,3 +1,4 @@ +pragma ComponentBehavior: Bound import QtQuick import Quickshell import qs.Common @@ -38,12 +39,20 @@ BasePill { property var _vAudio: null property var _vBrightness: null property var _vMic: null + property var _interactionDelegates: [] + readonly property var defaultControlCenterGroupOrder: ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"] + readonly property var effectiveControlCenterGroupOrder: getEffectiveControlCenterGroupOrder() + readonly property var controlCenterRenderModel: getControlCenterRenderModel() + + onIsVerticalOrientationChanged: root.clearInteractionRefs() onWheel: function (wheelEvent) { const delta = wheelEvent.angleDelta.y; if (delta === 0) return; + root.refreshInteractionRefs(); + const rootX = wheelEvent.x - root.leftMargin; const rootY = wheelEvent.y - root.topMargin; @@ -72,6 +81,8 @@ BasePill { } onRightClicked: function (rootX, rootY) { + root.refreshInteractionRefs(); + if (root.isVerticalOrientation && _vCol) { const pos = root.mapToItem(_vCol, rootX, rootY); if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) { @@ -279,26 +290,142 @@ BasePill { return CupsService.getTotalJobsNum() > 0; } + function getControlCenterIconSize() { + return Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale); + } + + function getEffectiveControlCenterGroupOrder() { + const knownIds = root.defaultControlCenterGroupOrder; + const savedOrder = root.widgetData?.controlCenterGroupOrder; + const result = []; + const seen = {}; + + if (savedOrder && typeof savedOrder.length === "number") { + for (let i = 0; i < savedOrder.length; ++i) { + const groupId = savedOrder[i]; + if (knownIds.indexOf(groupId) === -1 || seen[groupId]) + continue; + + seen[groupId] = true; + result.push(groupId); + } + } + + for (let i = 0; i < knownIds.length; ++i) { + const groupId = knownIds[i]; + if (seen[groupId]) + continue; + + seen[groupId] = true; + result.push(groupId); + } + + return result; + } + + function isGroupVisible(groupId) { + switch (groupId) { + case "screenSharing": + return root.showScreenSharingIcon && NiriService.hasCasts; + case "network": + return root.showNetworkIcon && NetworkService.networkAvailable; + case "vpn": + return root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected; + case "bluetooth": + return root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled; + case "audio": + return root.showAudioIcon; + case "microphone": + return root.showMicIcon; + case "brightness": + return root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice(); + case "battery": + return root.showBatteryIcon && BatteryService.batteryAvailable; + case "printer": + return root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs(); + default: + return false; + } + } + + function isCompositeGroup(groupId) { + return groupId === "audio" || groupId === "microphone" || groupId === "brightness"; + } + + function getControlCenterRenderModel() { + return root.effectiveControlCenterGroupOrder.map(groupId => ({ + "id": groupId, + "visible": root.isGroupVisible(groupId), + "composite": root.isCompositeGroup(groupId) + })); + } + + function clearInteractionRefs() { + root._hAudio = null; + root._hBrightness = null; + root._hMic = null; + root._vAudio = null; + root._vBrightness = null; + root._vMic = null; + } + + function registerInteractionDelegate(isVertical, item) { + if (!item) + return; + + for (let i = 0; i < root._interactionDelegates.length; ++i) { + const entry = root._interactionDelegates[i]; + if (entry && entry.item === item) { + entry.isVertical = isVertical; + return; + } + } + + root._interactionDelegates = root._interactionDelegates.concat([ + { + "isVertical": isVertical, + "item": item + } + ]); + } + + function unregisterInteractionDelegate(item) { + if (!item) + return; + + root._interactionDelegates = root._interactionDelegates.filter(entry => entry && entry.item !== item); + } + + function refreshInteractionRefs() { + root.clearInteractionRefs(); + + for (let i = 0; i < root._interactionDelegates.length; ++i) { + const entry = root._interactionDelegates[i]; + const item = entry?.item; + if (!item || !item.visible) + continue; + + const groupId = item.interactionGroupId; + if (entry.isVertical) { + if (groupId === "audio") + root._vAudio = item; + else if (groupId === "microphone") + root._vMic = item; + else if (groupId === "brightness") + root._vBrightness = item; + } else { + if (groupId === "audio") + root._hAudio = item; + else if (groupId === "microphone") + root._hMic = item; + else if (groupId === "brightness") + root._hBrightness = item; + } + } + } + function hasNoVisibleIcons() { - if (root.showScreenSharingIcon && NiriService.hasCasts) - return false; - if (root.showNetworkIcon && NetworkService.networkAvailable) - return false; - if (root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected) - return false; - if (root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled) - return false; - if (root.showAudioIcon) - return false; - if (root.showMicIcon) - return false; - if (root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()) - return false; - if (root.showBatteryIcon && BatteryService.batteryAvailable) - return false; - if (root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()) - return false; - return true; + return !root.controlCenterRenderModel.some(entry => entry.visible); } content: Component { @@ -309,12 +436,7 @@ BasePill { Component.onCompleted: { root._hRow = controlIndicators; root._vCol = controlColumn; - root._hAudio = audioIcon.parent; - root._hBrightness = brightnessIcon.parent; - root._hMic = micIcon.parent; - root._vAudio = audioIconV.parent; - root._vBrightness = brightnessIconV.parent; - root._vMic = micIconV.parent; + root.clearInteractionRefs(); } Column { @@ -324,162 +446,151 @@ BasePill { anchors.horizontalCenter: parent.horizontalCenter spacing: Theme.spacingXS - Item { - width: parent.width - height: root.vIconSize - visible: root.showScreenSharingIcon && NiriService.hasCasts + Repeater { + model: root.controlCenterRenderModel + Item { + id: verticalGroupItem + required property var modelData + required property int index + property string interactionGroupId: modelData.id - DankIcon { - name: "screen_record" - size: root.vIconSize - color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText - anchors.centerIn: parent - } - } + width: parent.width + height: { + switch (modelData.id) { + case "audio": + return root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0); + case "microphone": + return root.vIconSize + (root.showMicPercent ? micPercentV.implicitHeight + 2 : 0); + case "brightness": + return root.vIconSize + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight + 2 : 0); + default: + return root.vIconSize; + } + } + visible: modelData.visible - Item { - width: parent.width - height: root.vIconSize - visible: root.showNetworkIcon && NetworkService.networkAvailable + Component.onCompleted: { + root.registerInteractionDelegate(true, verticalGroupItem); + root.refreshInteractionRefs(); + } + Component.onDestruction: { + if (root) { + root.unregisterInteractionDelegate(verticalGroupItem); + root.refreshInteractionRefs(); + } + } + onVisibleChanged: root.refreshInteractionRefs() + onInteractionGroupIdChanged: { + root.refreshInteractionRefs(); + } - DankIcon { - name: root.getNetworkIconName() - size: root.vIconSize - color: root.getNetworkIconColor() - anchors.centerIn: parent - } - } + DankIcon { + anchors.centerIn: parent + visible: !verticalGroupItem.modelData.composite + name: { + switch (verticalGroupItem.modelData.id) { + case "screenSharing": + return "screen_record"; + case "network": + return root.getNetworkIconName(); + case "vpn": + return "vpn_lock"; + case "bluetooth": + return "bluetooth"; + case "battery": + return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable); + case "printer": + return "print"; + default: + return "settings"; + } + } + size: root.vIconSize + color: { + switch (verticalGroupItem.modelData.id) { + case "screenSharing": + return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText; + case "network": + return root.getNetworkIconColor(); + case "vpn": + return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText; + case "bluetooth": + return BluetoothService.connected ? Theme.primary : Theme.surfaceText; + case "battery": + return root.getBatteryIconColor(); + case "printer": + return Theme.primary; + default: + return Theme.widgetIconColor; + } + } + } - Item { - width: parent.width - height: root.vIconSize - visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected + DankIcon { + id: audioIconV + visible: verticalGroupItem.modelData.id === "audio" + name: root.getVolumeIconName() + size: root.vIconSize + color: Theme.widgetIconColor + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + } - DankIcon { - name: "vpn_lock" - size: root.vIconSize - color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText - anchors.centerIn: parent - } - } + NumericText { + id: audioPercentV + visible: verticalGroupItem.modelData.id === "audio" && root.showAudioPercent + text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" + reserveText: "100%" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: audioIconV.bottom + anchors.topMargin: 2 + } - Item { - width: parent.width - height: root.vIconSize - visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled + DankIcon { + id: micIconV + visible: verticalGroupItem.modelData.id === "microphone" + name: root.getMicIconName() + size: root.vIconSize + color: root.getMicIconColor() + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + } - DankIcon { - name: "bluetooth" - size: root.vIconSize - color: BluetoothService.connected ? Theme.primary : Theme.surfaceText - anchors.centerIn: parent - } - } + NumericText { + id: micPercentV + visible: verticalGroupItem.modelData.id === "microphone" && root.showMicPercent + text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" + reserveText: "100%" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: micIconV.bottom + anchors.topMargin: 2 + } - Item { - width: parent.width - height: root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0) - visible: root.showAudioIcon + DankIcon { + id: brightnessIconV + visible: verticalGroupItem.modelData.id === "brightness" + name: root.getBrightnessIconName() + size: root.vIconSize + color: Theme.widgetIconColor + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + } - DankIcon { - id: audioIconV - name: root.getVolumeIconName() - size: root.vIconSize - color: Theme.widgetIconColor - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - } - - NumericText { - id: audioPercentV - visible: root.showAudioPercent - text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" - reserveText: "100%" - font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) - color: Theme.widgetTextColor - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: audioIconV.bottom - anchors.topMargin: 2 - } - } - - Item { - width: parent.width - height: root.vIconSize + (root.showMicPercent ? micPercentV.implicitHeight + 2 : 0) - visible: root.showMicIcon - - DankIcon { - id: micIconV - name: root.getMicIconName() - size: root.vIconSize - color: root.getMicIconColor() - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - } - - NumericText { - id: micPercentV - visible: root.showMicPercent - text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" - reserveText: "100%" - font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) - color: Theme.widgetTextColor - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: micIconV.bottom - anchors.topMargin: 2 - } - } - - Item { - width: parent.width - height: root.vIconSize + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight + 2 : 0) - visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice() - - DankIcon { - id: brightnessIconV - name: root.getBrightnessIconName() - size: root.vIconSize - color: Theme.widgetIconColor - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - } - - NumericText { - id: brightnessPercentV - visible: root.showBrightnessPercent - text: Math.round(getBrightness() * 100) + "%" - reserveText: "100%" - font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) - color: Theme.widgetTextColor - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: brightnessIconV.bottom - anchors.topMargin: 2 - } - } - - Item { - width: parent.width - height: root.vIconSize - visible: root.showBatteryIcon && BatteryService.batteryAvailable - - DankIcon { - name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable) - size: root.vIconSize - color: root.getBatteryIconColor() - anchors.centerIn: parent - } - } - - Item { - width: parent.width - height: root.vIconSize - visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs() - - DankIcon { - name: "print" - size: root.vIconSize - color: Theme.primary - anchors.centerIn: parent + NumericText { + id: brightnessPercentV + visible: verticalGroupItem.modelData.id === "brightness" && root.showBrightnessPercent + text: Math.round(getBrightness() * 100) + "%" + reserveText: "100%" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: brightnessIconV.bottom + anchors.topMargin: 2 + } } } @@ -503,157 +614,206 @@ BasePill { anchors.centerIn: parent spacing: Theme.spacingXS - DankIcon { - name: "screen_record" - size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) - color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - visible: root.showScreenSharingIcon && NiriService.hasCasts - } + Repeater { + model: root.controlCenterRenderModel - DankIcon { - id: networkIcon - name: root.getNetworkIconName() - size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) - color: root.getNetworkIconColor() - anchors.verticalCenter: parent.verticalCenter - visible: root.showNetworkIcon && NetworkService.networkAvailable - } + Item { + id: horizontalGroupItem + required property var modelData + required property int index + property string interactionGroupId: modelData.id - DankIcon { - id: vpnIcon - name: "vpn_lock" - size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) - color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected - } + width: { + switch (modelData.id) { + case "audio": + return audioGroup.width; + case "microphone": + return micGroup.width; + case "brightness": + return brightnessGroup.width; + default: + return root.getControlCenterIconSize(); + } + } + implicitWidth: width + height: root.widgetThickness - root.horizontalPadding * 2 + visible: modelData.visible - DankIcon { - id: bluetoothIcon - name: "bluetooth" - size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) - color: BluetoothService.connected ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled - } + Component.onCompleted: { + root.registerInteractionDelegate(false, horizontalGroupItem); + root.refreshInteractionRefs(); + } + Component.onDestruction: { + if (root) { + root.unregisterInteractionDelegate(horizontalGroupItem); + root.refreshInteractionRefs(); + } + } + onVisibleChanged: root.refreshInteractionRefs() + onInteractionGroupIdChanged: { + root.refreshInteractionRefs(); + } - Rectangle { - width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.reservedWidth : 0) + 4 - implicitWidth: width - height: root.widgetThickness - root.horizontalPadding * 2 - color: "transparent" - anchors.verticalCenter: parent.verticalCenter - visible: root.showAudioIcon + DankIcon { + id: iconOnlyItem + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + visible: !horizontalGroupItem.modelData.composite + name: { + switch (horizontalGroupItem.modelData.id) { + case "screenSharing": + return "screen_record"; + case "network": + return root.getNetworkIconName(); + case "vpn": + return "vpn_lock"; + case "bluetooth": + return "bluetooth"; + case "battery": + return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable); + case "printer": + return "print"; + default: + return "settings"; + } + } + size: root.getControlCenterIconSize() + color: { + switch (horizontalGroupItem.modelData.id) { + case "screenSharing": + return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText; + case "network": + return root.getNetworkIconColor(); + case "vpn": + return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText; + case "bluetooth": + return BluetoothService.connected ? Theme.primary : Theme.surfaceText; + case "battery": + return root.getBatteryIconColor(); + case "printer": + return Theme.primary; + default: + return Theme.widgetIconColor; + } + } + } - DankIcon { - id: audioIcon - name: root.getVolumeIconName() - size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) - color: Theme.widgetIconColor - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 2 + Rectangle { + id: audioGroup + width: audioContent.implicitWidth + 2 + implicitWidth: width + height: parent.height + color: "transparent" + anchors.verticalCenter: parent.verticalCenter + visible: horizontalGroupItem.modelData.id === "audio" + + Row { + id: audioContent + anchors.left: parent.left + anchors.leftMargin: 1 + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + DankIcon { + id: audioIcon + name: root.getVolumeIconName() + size: root.getControlCenterIconSize() + color: Theme.widgetIconColor + anchors.verticalCenter: parent.verticalCenter + } + + NumericText { + id: audioPercent + visible: root.showAudioPercent + text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" + reserveText: "100%" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + width: visible ? implicitWidth : 0 + } + } + } + + Rectangle { + id: micGroup + width: micContent.implicitWidth + 2 + implicitWidth: width + height: parent.height + color: "transparent" + anchors.verticalCenter: parent.verticalCenter + visible: horizontalGroupItem.modelData.id === "microphone" + + Row { + id: micContent + anchors.left: parent.left + anchors.leftMargin: 1 + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + DankIcon { + id: micIcon + name: root.getMicIconName() + size: root.getControlCenterIconSize() + color: root.getMicIconColor() + anchors.verticalCenter: parent.verticalCenter + } + + NumericText { + id: micPercent + visible: root.showMicPercent + text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" + reserveText: "100%" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + width: visible ? implicitWidth : 0 + } + } + } + + Rectangle { + id: brightnessGroup + width: brightnessContent.implicitWidth + 2 + implicitWidth: width + height: parent.height + color: "transparent" + anchors.verticalCenter: parent.verticalCenter + visible: horizontalGroupItem.modelData.id === "brightness" + + Row { + id: brightnessContent + anchors.left: parent.left + anchors.leftMargin: 1 + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + DankIcon { + id: brightnessIcon + name: root.getBrightnessIconName() + size: root.getControlCenterIconSize() + color: Theme.widgetIconColor + anchors.verticalCenter: parent.verticalCenter + } + + NumericText { + id: brightnessPercent + visible: root.showBrightnessPercent + text: Math.round(getBrightness() * 100) + "%" + reserveText: "100%" + font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) + color: Theme.widgetTextColor + anchors.verticalCenter: parent.verticalCenter + width: visible ? implicitWidth : 0 + } + } + } } - - NumericText { - id: audioPercent - visible: root.showAudioPercent - text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" - reserveText: "100%" - font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) - color: Theme.widgetTextColor - anchors.verticalCenter: parent.verticalCenter - anchors.left: audioIcon.right - anchors.leftMargin: 2 - width: reservedWidth - } - } - - Rectangle { - width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.reservedWidth : 0) + 4 - implicitWidth: width - height: root.widgetThickness - root.horizontalPadding * 2 - color: "transparent" - anchors.verticalCenter: parent.verticalCenter - visible: root.showMicIcon - - DankIcon { - id: micIcon - name: root.getMicIconName() - size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) - color: root.getMicIconColor() - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 2 - } - - NumericText { - id: micPercent - visible: root.showMicPercent - text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" - reserveText: "100%" - font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) - color: Theme.widgetTextColor - anchors.verticalCenter: parent.verticalCenter - anchors.left: micIcon.right - anchors.leftMargin: 2 - width: reservedWidth - } - } - - Rectangle { - width: brightnessIcon.implicitWidth + (root.showBrightnessPercent ? brightnessPercent.reservedWidth : 0) + 4 - height: root.widgetThickness - root.horizontalPadding * 2 - color: "transparent" - anchors.verticalCenter: parent.verticalCenter - visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice() - - DankIcon { - id: brightnessIcon - name: root.getBrightnessIconName() - size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) - color: Theme.widgetIconColor - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 2 - } - - NumericText { - id: brightnessPercent - visible: root.showBrightnessPercent - text: Math.round(getBrightness() * 100) + "%" - reserveText: "100%" - font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) - color: Theme.widgetTextColor - anchors.verticalCenter: parent.verticalCenter - anchors.left: brightnessIcon.right - anchors.leftMargin: 2 - width: reservedWidth - } - } - - DankIcon { - id: batteryIcon - name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable) - size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) - color: root.getBatteryIconColor() - anchors.verticalCenter: parent.verticalCenter - visible: root.showBatteryIcon && BatteryService.batteryAvailable - } - - DankIcon { - id: printerIcon - name: "print" - size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter - visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs() } DankIcon { name: "settings" - size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + size: root.getControlCenterIconSize() color: root.isActive ? Theme.primary : Theme.widgetIconColor anchors.verticalCenter: parent.verticalCenter visible: root.hasNoVisibleIcons() diff --git a/quickshell/Modules/Settings/WidgetsTab.qml b/quickshell/Modules/Settings/WidgetsTab.qml index fd5eb580..0cdc2eff 100644 --- a/quickshell/Modules/Settings/WidgetsTab.qml +++ b/quickshell/Modules/Settings/WidgetsTab.qml @@ -391,6 +391,7 @@ Item { widgetObj.showBatteryIcon = SettingsData.controlCenterShowBatteryIcon; widgetObj.showPrinterIcon = SettingsData.controlCenterShowPrinterIcon; widgetObj.showScreenSharingIcon = SettingsData.controlCenterShowScreenSharingIcon; + widgetObj.controlCenterGroupOrder = ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"]; } if (widgetId === "runningApps") { widgetObj.runningAppsCompactMode = SettingsData.runningAppsCompactMode; @@ -429,7 +430,7 @@ Item { "id": widget.id, "enabled": widget.enabled }; - var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"]; + var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"]; for (var i = 0; i < keys.length; i++) { if (widget[keys[i]] !== undefined) result[keys[i]] = widget[keys[i]]; @@ -498,6 +499,32 @@ Item { return; var newWidget = cloneWidgetData(widgets[widgetIndex]); newWidget[settingName] = value; + + if (!value) { + switch (settingName) { + case "showAudioIcon": + newWidget.showAudioPercent = false; + break; + case "showMicIcon": + newWidget.showMicPercent = false; + break; + case "showBrightnessIcon": + newWidget.showBrightnessPercent = false; + break; + } + } + + widgets[widgetIndex] = newWidget; + setWidgetsForSection(sectionId, widgets); + } + + function handleControlCenterGroupOrderChanged(sectionId, widgetIndex, groupOrder) { + var widgets = getWidgetsForSection(sectionId).slice(); + if (widgetIndex < 0 || widgetIndex >= widgets.length) + return; + var previousWidget = widgets[widgetIndex]; + var newWidget = cloneWidgetData(previousWidget); + newWidget.controlCenterGroupOrder = groupOrder.slice(); widgets[widgetIndex] = newWidget; setWidgetsForSection(sectionId, widgets); } @@ -655,6 +682,8 @@ Item { item.showPrinterIcon = widget.showPrinterIcon; if (widget.showScreenSharingIcon !== undefined) item.showScreenSharingIcon = widget.showScreenSharingIcon; + if (widget.controlCenterGroupOrder !== undefined) + item.controlCenterGroupOrder = widget.controlCenterGroupOrder; if (widget.minimumWidth !== undefined) item.minimumWidth = widget.minimumWidth; if (widget.showSwap !== undefined) @@ -948,6 +977,9 @@ Item { onControlCenterSettingChanged: (sectionId, index, setting, value) => { widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value); } + onControlCenterGroupOrderChanged: (sectionId, index, groupOrder) => { + widgetsTab.handleControlCenterGroupOrderChanged(sectionId, index, groupOrder); + } onPrivacySettingChanged: (sectionId, index, setting, value) => { widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value); } @@ -1012,6 +1044,9 @@ Item { onControlCenterSettingChanged: (sectionId, index, setting, value) => { widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value); } + onControlCenterGroupOrderChanged: (sectionId, index, groupOrder) => { + widgetsTab.handleControlCenterGroupOrderChanged(sectionId, index, groupOrder); + } onPrivacySettingChanged: (sectionId, index, setting, value) => { widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value); } @@ -1076,6 +1111,9 @@ Item { onControlCenterSettingChanged: (sectionId, index, setting, value) => { widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value); } + onControlCenterGroupOrderChanged: (sectionId, index, groupOrder) => { + widgetsTab.handleControlCenterGroupOrderChanged(sectionId, index, groupOrder); + } onPrivacySettingChanged: (sectionId, index, setting, value) => { widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value); } diff --git a/quickshell/Modules/Settings/WidgetsTabSection.qml b/quickshell/Modules/Settings/WidgetsTabSection.qml index 3d898da7..2023ae9e 100644 --- a/quickshell/Modules/Settings/WidgetsTabSection.qml +++ b/quickshell/Modules/Settings/WidgetsTabSection.qml @@ -27,6 +27,7 @@ Column { signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex) signal diskMountSelectionChanged(string sectionId, int widgetIndex, string mountPath) signal controlCenterSettingChanged(string sectionId, int widgetIndex, string settingName, bool value) + signal controlCenterGroupOrderChanged(string sectionId, int widgetIndex, var groupOrder) signal privacySettingChanged(string sectionId, int widgetIndex, string settingName, bool value) signal minimumWidthChanged(string sectionId, int widgetIndex, bool enabled) signal showSwapChanged(string sectionId, int widgetIndex, bool enabled) @@ -39,7 +40,7 @@ Column { "id": widget.id, "enabled": widget.enabled }; - var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"]; + var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"]; for (var i = 0; i < keys.length; i++) { if (widget[keys[i]] !== undefined) result[keys[i]] = widget[keys[i]]; @@ -90,7 +91,6 @@ Column { height: 70 z: held ? 2 : 1 - Rectangle { id: itemBackground @@ -587,6 +587,7 @@ Column { controlCenterContextMenu.widgetData = modelData; controlCenterContextMenu.sectionId = root.sectionId; controlCenterContextMenu.widgetIndex = index; + controlCenterContextMenu.controlCenterGroups = controlCenterContextMenu.getOrderedControlCenterGroups(); var buttonPos = ccMenuButton.mapToItem(root, 0, 0); var popupWidth = controlCenterContextMenu.width; @@ -1054,13 +1055,236 @@ Column { property string sectionId: "" property int widgetIndex: -1 - width: 220 - height: menuColumn.implicitHeight + Theme.spacingS * 2 + readonly property real minimumContentWidth: controlCenterContentMetrics.implicitWidth + Theme.spacingS * 2 + readonly property real controlCenterRowHeight: 32 + readonly property real controlCenterRowSpacing: 1 + readonly property real controlCenterGroupVerticalPadding: Theme.spacingXS * 2 + readonly property real controlCenterMenuSpacing: 2 + width: Math.max(220, minimumContentWidth) + height: getControlCenterPopupHeight(controlCenterGroups) padding: 0 modal: true focus: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + onClosed: { + cancelControlCenterDrag(); + } + + readonly property var defaultControlCenterGroups: [ + { + id: "network", + rows: [ + { + icon: "lan", + label: I18n.tr("Network"), + setting: "showNetworkIcon" + } + ] + }, + { + id: "vpn", + rows: [ + { + icon: "vpn_lock", + label: I18n.tr("VPN"), + setting: "showVpnIcon" + } + ] + }, + { + id: "bluetooth", + rows: [ + { + icon: "bluetooth", + label: I18n.tr("Bluetooth"), + setting: "showBluetoothIcon" + } + ] + }, + { + id: "audio", + rows: [ + { + icon: "volume_up", + label: I18n.tr("Audio"), + setting: "showAudioIcon" + }, + { + icon: "percent", + label: I18n.tr("Volume"), + setting: "showAudioPercent" + } + ] + }, + { + id: "microphone", + rows: [ + { + icon: "mic", + label: I18n.tr("Microphone"), + setting: "showMicIcon" + }, + { + icon: "percent", + label: I18n.tr("Microphone Volume"), + setting: "showMicPercent" + } + ] + }, + { + id: "brightness", + rows: [ + { + icon: "brightness_high", + label: I18n.tr("Brightness"), + setting: "showBrightnessIcon" + }, + { + icon: "percent", + label: I18n.tr("Brightness Value"), + setting: "showBrightnessPercent" + } + ] + }, + { + id: "battery", + rows: [ + { + icon: "battery_full", + label: I18n.tr("Battery"), + setting: "showBatteryIcon" + } + ] + }, + { + id: "printer", + rows: [ + { + icon: "print", + label: I18n.tr("Printer"), + setting: "showPrinterIcon" + } + ] + }, + { + id: "screenSharing", + rows: [ + { + icon: "screen_record", + label: I18n.tr("Screen Sharing"), + setting: "showScreenSharingIcon" + } + ] + } + ] + property var controlCenterGroups: defaultControlCenterGroups + property int draggedControlCenterGroupIndex: -1 + property int controlCenterGroupDropIndex: -1 + + function updateControlCenterGroupDropIndex(draggedIndex, localY) { + const totalGroups = controlCenterGroups.length; + let dropIndex = totalGroups; + + for (let i = 0; i < totalGroups; i++) { + const delegate = groupRepeater.itemAt(i); + if (!delegate) + continue; + + const midpoint = delegate.y + delegate.height / 2; + if (localY < midpoint) { + dropIndex = i; + break; + } + } + + controlCenterGroupDropIndex = Math.max(0, Math.min(totalGroups, dropIndex)); + draggedControlCenterGroupIndex = draggedIndex; + } + + function finishControlCenterDrag() { + if (draggedControlCenterGroupIndex < 0) { + controlCenterGroupDropIndex = -1; + return; + } + + const fromIndex = draggedControlCenterGroupIndex; + let toIndex = controlCenterGroupDropIndex; + + draggedControlCenterGroupIndex = -1; + controlCenterGroupDropIndex = -1; + + if (toIndex < 0 || toIndex > controlCenterGroups.length || toIndex === fromIndex || toIndex === fromIndex + 1) + return; + + const groups = controlCenterGroups.slice(); + const moved = groups.splice(fromIndex, 1)[0]; + + if (toIndex > fromIndex) + toIndex -= 1; + + groups.splice(toIndex, 0, moved); + controlCenterGroups = groups; + const reorderedGroupIds = groups.map(group => group.id); + root.controlCenterGroupOrderChanged(sectionId, widgetIndex, reorderedGroupIds); + } + + function cancelControlCenterDrag() { + draggedControlCenterGroupIndex = -1; + controlCenterGroupDropIndex = -1; + } + + function getControlCenterGroupHeight(group) { + const rowCount = group?.rows?.length ?? 0; + if (rowCount <= 0) + return controlCenterGroupVerticalPadding; + + return rowCount * controlCenterRowHeight + Math.max(0, rowCount - 1) * controlCenterRowSpacing + controlCenterGroupVerticalPadding; + } + + function getControlCenterPopupHeight(groups) { + const orderedGroups = groups || []; + let totalHeight = Theme.spacingS * 2; + + for (let i = 0; i < orderedGroups.length; i++) { + totalHeight += getControlCenterGroupHeight(orderedGroups[i]); + if (i < orderedGroups.length - 1) + totalHeight += controlCenterMenuSpacing; + } + + return totalHeight; + } + + function getOrderedControlCenterGroups() { + const baseGroups = defaultControlCenterGroups.slice(); + const currentWidget = contentItem.getCurrentWidgetData(); + const savedOrder = currentWidget?.controlCenterGroupOrder; + if (!savedOrder || !savedOrder.length) + return baseGroups; + + const groupMap = {}; + for (let i = 0; i < baseGroups.length; i++) + groupMap[baseGroups[i].id] = baseGroups[i]; + + const orderedGroups = []; + for (let i = 0; i < savedOrder.length; i++) { + const groupId = savedOrder[i]; + const group = groupMap[groupId]; + if (group) { + orderedGroups.push(group); + delete groupMap[groupId]; + } + } + + for (let i = 0; i < baseGroups.length; i++) { + const group = baseGroups[i]; + if (groupMap[group.id]) + orderedGroups.push(group); + } + + return orderedGroups; + } + background: Rectangle { color: Theme.surfaceContainer radius: Theme.cornerRadius @@ -1069,83 +1293,64 @@ Column { } contentItem: Item { + function getCurrentWidgetData() { + const widgets = root.items || []; + if (controlCenterContextMenu.widgetIndex >= 0 && controlCenterContextMenu.widgetIndex < widgets.length) + return widgets[controlCenterContextMenu.widgetIndex]; + return controlCenterContextMenu.widgetData; + } + Column { id: menuColumn anchors.fill: parent anchors.margins: Theme.spacingS spacing: 2 - Repeater { - model: [ - { - icon: "lan", - label: I18n.tr("Network"), - setting: "showNetworkIcon" - }, - { - icon: "vpn_lock", - label: I18n.tr("VPN"), - setting: "showVpnIcon" - }, - { - icon: "bluetooth", - label: I18n.tr("Bluetooth"), - setting: "showBluetoothIcon" - }, - { - icon: "volume_up", - label: I18n.tr("Audio"), - setting: "showAudioIcon" - }, - { - icon: "percent", - label: I18n.tr("Volume"), - setting: "showAudioPercent" - }, - { - icon: "mic", - label: I18n.tr("Microphone"), - setting: "showMicIcon" - }, - { - icon: "percent", - label: I18n.tr("Microphone Volume"), - setting: "showMicPercent" - }, - { - icon: "brightness_high", - label: I18n.tr("Brightness"), - setting: "showBrightnessIcon" - }, - { - icon: "percent", - label: I18n.tr("Brightness Value"), - setting: "showBrightnessPercent" - }, - { - icon: "battery_full", - label: I18n.tr("Battery"), - setting: "showBatteryIcon" - }, - { - icon: "print", - label: I18n.tr("Printer"), - setting: "showPrinterIcon" - }, - { - icon: "screen_record", - label: I18n.tr("Screen Sharing"), - setting: "showScreenSharingIcon" - } - ] + Item { + id: controlCenterContentMetrics + visible: false + implicitWidth: 16 + Theme.spacingS + 16 + Theme.spacingS + longestControlCenterLabelMetrics.advanceWidth + Theme.spacingM + 40 + Theme.spacingS * 2 + Theme.spacingM + } + + TextMetrics { + id: longestControlCenterLabelMetrics + font.pixelSize: Theme.fontSizeSmall + text: { + const labels = [ + I18n.tr("Network"), + I18n.tr("VPN"), + I18n.tr("Bluetooth"), + I18n.tr("Audio"), + I18n.tr("Volume"), + I18n.tr("Microphone"), + I18n.tr("Microphone Volume"), + I18n.tr("Brightness"), + I18n.tr("Brightness Value"), + I18n.tr("Battery"), + I18n.tr("Printer"), + I18n.tr("Screen Sharing") + ]; + let longest = ""; + for (let i = 0; i < labels.length; i++) { + if (labels[i].length > longest.length) + longest = labels[i]; + } + return longest; + } + } + + Repeater { + model: controlCenterContextMenu.controlCenterGroups + + delegate: Item { + id: delegateRoot - delegate: Rectangle { required property var modelData required property int index - function getCheckedState() { - var wd = controlCenterContextMenu.widgetData; - switch (modelData.setting) { + function getCheckedState(settingName) { + const wd = controlCenterContextMenu.contentItem.getCurrentWidgetData(); + switch (settingName) { case "showNetworkIcon": return wd?.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon; case "showVpnIcon": @@ -1175,57 +1380,197 @@ Column { } } + readonly property string rootSetting: modelData.rows[0]?.setting ?? "" + readonly property bool rootEnabled: rootSetting ? getCheckedState(rootSetting) : true + readonly property bool isDragged: controlCenterContextMenu.draggedControlCenterGroupIndex === index + readonly property bool showDropIndicatorAbove: controlCenterContextMenu.controlCenterGroupDropIndex === index + readonly property bool showDropIndicatorBelow: controlCenterContextMenu.controlCenterGroupDropIndex === controlCenterContextMenu.controlCenterGroups.length && index === controlCenterContextMenu.controlCenterGroups.length - 1 + width: menuColumn.width - height: 32 - radius: Theme.cornerRadius - color: toggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + height: groupBackground.height - Row { + Rectangle { + id: groupBackground anchors.left: parent.left - anchors.leftMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingS - - DankIcon { - name: modelData.icon - size: 16 - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: modelData.label - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Normal - anchors.verticalCenter: parent.verticalCenter - } - } - - DankToggle { - id: toggle anchors.right: parent.right - anchors.rightMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - width: 40 - height: 20 - checked: getCheckedState() - onToggled: { - root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggled); - } + anchors.top: parent.top + height: groupContent.implicitHeight + Theme.spacingXS * 2 + radius: Theme.cornerRadius + color: isDragged ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.18) : (groupHoverArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent") + opacity: isDragged ? 0.75 : 1.0 } - MouseArea { - id: toggleArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: { - toggle.checked = !toggle.checked; - root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggle.checked); + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: -1 + height: 2 + radius: 1 + color: Theme.primary + visible: showDropIndicatorAbove + z: 3 + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: -1 + height: 2 + radius: 1 + color: Theme.primary + visible: showDropIndicatorBelow + z: 3 + } + + Item { + id: groupContent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: Theme.spacingXS + implicitHeight: groupColumn.implicitHeight + + Column { + id: groupColumn + anchors.left: parent.left + anchors.right: parent.right + spacing: 1 + + Repeater { + id: groupColumnRepeater + model: modelData.rows + + delegate: Rectangle { + required property var modelData + required property int index + + readonly property var rowData: modelData + readonly property bool isFirstRow: index === 0 + readonly property bool rowEnabled: isFirstRow ? true : delegateRoot.rootEnabled + readonly property bool computedCheckedState: rowEnabled ? getCheckedState(rowData.setting) : false + readonly property bool rowHovered: rowEnabled && (toggleArea.containsMouse || (isFirstRow && groupDragHandleArea.containsMouse)) + + width: groupColumn.width + height: 32 + radius: Theme.cornerRadius + opacity: rowEnabled ? 1.0 : 0.5 + color: rowHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: toggle.left + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + Item { + width: 16 + height: 16 + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "drag_indicator" + size: 16 + color: groupDragHandleArea.pressed || isDragged ? Theme.primary : Theme.outline + visible: isFirstRow + } + + MouseArea { + id: groupDragHandleArea + anchors.fill: parent + hoverEnabled: true + preventStealing: true + enabled: isFirstRow + cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor + + onPressed: mouse => { + mouse.accepted = true; + const point = mapToItem(menuColumn, mouse.x, mouse.y); + controlCenterContextMenu.updateControlCenterGroupDropIndex(delegateRoot.index, point.y); + } + onPositionChanged: mouse => { + if (!pressed) + return; + mouse.accepted = true; + const point = mapToItem(menuColumn, mouse.x, mouse.y); + controlCenterContextMenu.updateControlCenterGroupDropIndex(delegateRoot.index, point.y); + } + onReleased: mouse => { + mouse.accepted = true; + const point = mapToItem(menuColumn, mouse.x, mouse.y); + controlCenterContextMenu.updateControlCenterGroupDropIndex(delegateRoot.index, point.y); + controlCenterContextMenu.finishControlCenterDrag(); + } + onCanceled: { + controlCenterContextMenu.cancelControlCenterDrag(); + } + } + } + + DankIcon { + name: rowData.icon + size: 16 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: rowData.label + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + DankToggle { + id: toggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 20 + enabled: rowEnabled + checked: computedCheckedState + + onToggled: { + if (!rowEnabled) + return; + root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, rowData.setting, toggled); + } + } + + MouseArea { + id: toggleArea + anchors.fill: parent + anchors.leftMargin: 16 + Theme.spacingS * 2 + hoverEnabled: true + cursorShape: rowEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: rowEnabled && controlCenterContextMenu.draggedControlCenterGroupIndex < 0 + onPressed: { + if (!rowEnabled) + return; + root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, rowData.setting, !computedCheckedState); + } + } + } + } + } + + MouseArea { + id: groupHoverArea + anchors.fill: parent + hoverEnabled: true + enabled: false } } } + + id: groupRepeater } } } diff --git a/quickshell/Widgets/DankToggle.qml b/quickshell/Widgets/DankToggle.qml index 3476f514..3b19a5a7 100644 --- a/quickshell/Widgets/DankToggle.qml +++ b/quickshell/Widgets/DankToggle.qml @@ -99,8 +99,8 @@ Item { anchors.verticalCenter: parent.verticalCenter radius: Theme.cornerRadius - // M3 disabled track: on surface 12% opacity - color: !toggle.enabled ? Qt.alpha(Theme.surfaceText, 0.12) : (toggle.checked ? Theme.primary : Theme.surfaceVariantAlpha) + // Distinguish disabled checked vs unchecked so unchecked disabled switches don't look enabled + color: !toggle.enabled ? (toggle.checked ? Qt.alpha(Theme.surfaceText, 0.12) : "transparent") : (toggle.checked ? Theme.primary : Theme.surfaceVariantAlpha) opacity: toggle.toggling ? 0.6 : 1 // M3 disabled unchecked border: on surface 12% opacity @@ -119,8 +119,8 @@ Item { anchors.verticalCenter: parent.verticalCenter // M3 disabled thumb: - // checked = solid surface | unchecked = on surface 38% - color: !toggle.enabled ? (toggle.checked ? Theme.surface : Qt.alpha(Theme.surfaceText, 0.38)) : (toggle.checked ? Theme.surface : Theme.outline) + // checked = solid surface | unchecked = outlined off-state thumb + color: !toggle.enabled ? (toggle.checked ? Theme.surface : "transparent") : (toggle.checked ? Theme.surface : Theme.outline) border.color: !toggle.enabled ? (toggle.checked ? "transparent" : Qt.alpha(Theme.surfaceText, 0.38)) : Theme.outline border.width: (toggle.checked && toggle.enabled) ? 1 : 2 @@ -165,8 +165,8 @@ Item { // M3 disabled icon: on surface 38% color: toggle.enabled ? Theme.surfaceText : Qt.alpha(Theme.surfaceText, 0.38) filled: true - opacity: toggle.checked ? 1 : 0 - scale: toggle.checked ? 1 : 0.6 + opacity: (toggle.checked && toggle.enabled) ? 1 : 0 + scale: (toggle.checked && toggle.enabled) ? 1 : 0.6 Behavior on opacity { NumberAnimation {