From 5716249bd9c5c73914464fec09bb16c1c27b8496 Mon Sep 17 00:00:00 2001 From: Youseffo13 <110458691+Youseffo13@users.noreply.github.com> Date: Sat, 6 Jun 2026 01:46:01 +0200 Subject: [PATCH] (Control Center): revamp of 25% pill option (#2568) * revamp of control center * update comment of SmallCompoundButton.qml --- .../BuiltinPlugins/CupsWidget.qml | 2 +- .../BuiltinPlugins/VpnWidget.qml | 2 +- .../ControlCenter/Components/DragDropGrid.qml | 320 +++++++++--------- .../Modules/ControlCenter/Widgets/DndPill.qml | 2 +- .../Widgets/SmallColorPickerButton.qml | 64 ++++ .../Widgets/SmallCompoundButton.qml | 107 ++++++ 6 files changed, 341 insertions(+), 156 deletions(-) create mode 100644 quickshell/Modules/ControlCenter/Widgets/SmallColorPickerButton.qml create mode 100644 quickshell/Modules/ControlCenter/Widgets/SmallCompoundButton.qml diff --git a/quickshell/Modules/ControlCenter/BuiltinPlugins/CupsWidget.qml b/quickshell/Modules/ControlCenter/BuiltinPlugins/CupsWidget.qml index 655f8781..b3dadaae 100644 --- a/quickshell/Modules/ControlCenter/BuiltinPlugins/CupsWidget.qml +++ b/quickshell/Modules/ControlCenter/BuiltinPlugins/CupsWidget.qml @@ -12,7 +12,7 @@ PluginComponent { service: CupsService } - ccWidgetIcon: CupsService.cupsAvailable && CupsService.getPrintersNum() > 0 ? "print" : "print_disabled" + ccWidgetIcon: "print" ccWidgetPrimaryText: I18n.tr("Printers") ccWidgetSecondaryText: { if (CupsService.cupsAvailable && CupsService.getPrintersNum() > 0) { diff --git a/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml b/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml index 531045e8..f971490b 100644 --- a/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml +++ b/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml @@ -11,7 +11,7 @@ PluginComponent { service: DMSNetworkService } - ccWidgetIcon: DMSNetworkService.isBusy ? "sync" : (DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off") + ccWidgetIcon: "vpn_key" ccWidgetPrimaryText: I18n.tr("VPN") ccWidgetSecondaryText: { if (!DMSNetworkService.connected) diff --git a/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml b/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml index 0e248125..907f0f76 100644 --- a/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml +++ b/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml @@ -102,6 +102,120 @@ Column { item.z = 1000; } + function getCompoundPillIconBlinking(id) { + if (id === "wifi") return NetworkService.isWifiConnecting; + if (id === "bluetooth") return BluetoothService.connecting; + return false; + } + + function getCompoundPillIconName(id, widgetDef) { + switch (id) { + case "wifi": { + if (NetworkService.wifiToggling) return "sync"; + if (NetworkService.isConnecting && !NetworkService.ethernetConnected) return NetworkService.wifiSignalIcon; + const status = NetworkService.networkStatus; + if (status === "ethernet") return "settings_ethernet"; + if (status === "vpn") return NetworkService.ethernetConnected ? "settings_ethernet" : NetworkService.wifiSignalIcon; + if (status === "wifi") return NetworkService.wifiSignalIcon; + return "wifi"; + } + case "bluetooth": { + return "bluetooth"; + } + case "audioOutput": { + if (!AudioService.sink?.audio) return "volume_off"; + let volume = AudioService.sink.audio.volume; + let muted = AudioService.sink.audio.muted; + if (muted) return "volume_off"; + if (volume === 0.0) return "volume_mute"; + if (volume <= 0.33) return "volume_down"; + if (volume <= 0.66) return "volume_up"; + return "volume_up"; + } + case "audioInput": { + if (!AudioService.source?.audio) return "mic_off"; + return AudioService.source.audio.muted ? "mic_off" : "mic"; + } + default: + return widgetDef?.icon || "help"; + } + } + + function getCompoundPillIsActive(id) { + switch (id) { + case "wifi": { + if (NetworkService.wifiToggling) return false; + const status = NetworkService.networkStatus; + if (status === "ethernet") return true; + if (status === "vpn") return NetworkService.ethernetConnected || NetworkService.wifiConnected; + if (status === "wifi") return true; + return NetworkService.wifiEnabled; + } + case "bluetooth": + return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled); + case "audioOutput": + return !!(AudioService.sink?.audio && !AudioService.sink.audio.muted); + case "audioInput": + return !!(AudioService.source?.audio && !AudioService.source.audio.muted); + default: + return false; + } + } + + function handleCompoundPillToggled(id) { + switch (id) { + case "wifi": { + if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) { + NetworkService.toggleWifiRadio(); + } + break; + } + case "bluetooth": { + if (BluetoothService.available && BluetoothService.adapter) { + BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled; + } + break; + } + case "audioOutput": { + if (AudioService.sink && AudioService.sink.audio) { + AudioService.sink.audio.muted = !AudioService.sink.audio.muted; + } + break; + } + case "audioInput": { + if (AudioService.source && AudioService.source.audio) { + AudioService.source.audio.muted = !AudioService.source.audio.muted; + } + break; + } + } + } + + function handleCompoundPillWheelEvent(id, wheelEvent) { + if (id === "audioOutput") { + if (!AudioService.sink || !AudioService.sink.audio) return; + let delta = wheelEvent.angleDelta.y; + let maxVol = AudioService.sinkMaxVolume; + let currentVolume = AudioService.sink.audio.volume * 100; + let newVolume; + if (delta > 0) newVolume = Math.min(maxVol, currentVolume + 5); + else newVolume = Math.max(0, currentVolume - 5); + AudioService.sink.audio.muted = false; + AudioService.sink.audio.volume = newVolume / 100; + wheelEvent.accepted = true; + } else if (id === "audioInput") { + if (!AudioService.source || !AudioService.source.audio) return; + let delta = wheelEvent.angleDelta.y; + let currentVolume = AudioService.source.audio.volume * 100; + let newVolume; + if (delta > 0) newVolume = Math.min(100, currentVolume + 5); + else newVolume = Math.max(0, currentVolume - 5); + AudioService.source.audio.muted = false; + AudioService.source.audio.volume = newVolume / 100; + wheelEvent.accepted = true; + } + } + function componentForWidget(widgetData) { const id = widgetData.id || ""; const widgetWidth = widgetData.width || 50; @@ -114,7 +228,7 @@ Column { case "bluetooth": case "audioOutput": case "audioInput": - return compoundPillComponent; + return widgetWidth <= 25 ? smallCompoundComponent : compoundPillComponent; case "volumeSlider": return audioSliderComponent; case "brightnessSlider": @@ -126,7 +240,7 @@ Column { case "diskUsage": return widgetWidth <= 25 ? smallDiskUsageComponent : diskUsagePillComponent; case "colorPicker": - return colorPickerPillComponent; + return widgetWidth <= 25 ? smallColorPickerComponent : colorPickerPillComponent; case "doNotDisturb": return widgetWidth <= 25 ? smallToggleComponent : dndPillComponent; default: @@ -329,69 +443,8 @@ Column { property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") width: parent.width height: 60 - iconBlinking: { - const id = widgetData.id || ""; - if (id === "wifi") - return NetworkService.isWifiConnecting; - if (id === "bluetooth") - return BluetoothService.connecting; - return false; - } - iconName: { - switch (widgetData.id || "") { - case "wifi": - { - if (NetworkService.wifiToggling) - return "sync"; - if (NetworkService.isConnecting && !NetworkService.ethernetConnected) - return NetworkService.wifiSignalIcon; - - const status = NetworkService.networkStatus; - if (status === "ethernet") - return "settings_ethernet"; - if (status === "vpn") - return NetworkService.ethernetConnected ? "settings_ethernet" : NetworkService.wifiSignalIcon; - if (status === "wifi") - return NetworkService.wifiSignalIcon; - if (NetworkService.wifiEnabled) - return "wifi_off"; - return "wifi_off"; - } - case "bluetooth": - { - if (!BluetoothService.available) - return "bluetooth_disabled"; - if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) - return "bluetooth_disabled"; - return "bluetooth"; - } - case "audioOutput": - { - if (!AudioService.sink?.audio) - return "volume_off"; - let volume = AudioService.sink.audio.volume; - let muted = AudioService.sink.audio.muted; - if (muted) - return "volume_off"; - if (volume === 0.0) - return "volume_mute"; - if (volume <= 0.33) - return "volume_down"; - if (volume <= 0.66) - return "volume_up"; - return "volume_up"; - } - case "audioInput": - { - if (!AudioService.source?.audio) - return "mic_off"; - let muted = AudioService.source.audio.muted; - return muted ? "mic_off" : "mic"; - } - default: - return widgetDef?.icon || "help"; - } - } + iconBlinking: root.getCompoundPillIconBlinking(widgetData.id || "") + iconName: root.getCompoundPillIconName(widgetData.id || "", widgetDef) primaryText: { switch (widgetData.id || "") { case "wifi": @@ -506,66 +559,12 @@ Column { return widgetDef?.description || ""; } } - isActive: { - switch (widgetData.id || "") { - case "wifi": - { - if (NetworkService.wifiToggling) - return false; - - const status = NetworkService.networkStatus; - if (status === "ethernet") - return true; - if (status === "vpn") - return NetworkService.ethernetConnected || NetworkService.wifiConnected; - if (status === "wifi") - return true; - return NetworkService.wifiEnabled; - } - case "bluetooth": - return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled); - case "audioOutput": - return !!(AudioService.sink?.audio && !AudioService.sink.audio.muted); - case "audioInput": - return !!(AudioService.source?.audio && !AudioService.source.audio.muted); - default: - return false; - } - } + isActive: root.getCompoundPillIsActive(widgetData.id || "") enabled: widgetDef?.enabled ?? true onToggled: { if (root.editMode) return; - switch (widgetData.id || "") { - case "wifi": - { - if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) { - NetworkService.toggleWifiRadio(); - } - break; - } - case "bluetooth": - { - if (BluetoothService.available && BluetoothService.adapter) { - BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled; - } - break; - } - case "audioOutput": - { - if (AudioService.sink && AudioService.sink.audio) { - AudioService.sink.audio.muted = !AudioService.sink.audio.muted; - } - break; - } - case "audioInput": - { - if (AudioService.source && AudioService.source.audio) { - AudioService.source.audio.muted = !AudioService.source.audio.muted; - } - break; - } - } + root.handleCompoundPillToggled(widgetData.id || ""); } onExpandClicked: { if (root.editMode) @@ -575,35 +574,7 @@ Column { onWheelEvent: function (wheelEvent) { if (root.editMode) return; - const id = widgetData.id || ""; - if (id === "audioOutput") { - if (!AudioService.sink || !AudioService.sink.audio) - return; - let delta = wheelEvent.angleDelta.y; - let maxVol = AudioService.sinkMaxVolume; - let currentVolume = AudioService.sink.audio.volume * 100; - let newVolume; - if (delta > 0) - newVolume = Math.min(maxVol, currentVolume + 5); - else - newVolume = Math.max(0, currentVolume - 5); - AudioService.sink.audio.muted = false; - AudioService.sink.audio.volume = newVolume / 100; - wheelEvent.accepted = true; - } else if (id === "audioInput") { - if (!AudioService.source || !AudioService.source.audio) - return; - let delta = wheelEvent.angleDelta.y; - let currentVolume = AudioService.source.audio.volume * 100; - let newVolume; - if (delta > 0) - newVolume = Math.min(100, currentVolume + 5); - else - newVolume = Math.max(0, currentVolume - 5); - AudioService.source.audio.muted = false; - AudioService.source.audio.volume = newVolume / 100; - wheelEvent.accepted = true; - } + root.handleCompoundPillWheelEvent(widgetData.id || "", wheelEvent); } } } @@ -736,7 +707,7 @@ Column { case "darkMode": return "contrast"; case "idleInhibitor": - return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"; + return "motion_sensor_active"; default: return "help"; } @@ -821,9 +792,9 @@ Column { case "darkMode": return "contrast"; case "doNotDisturb": - return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off"; + return "do_not_disturb_on"; case "idleInhibitor": - return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"; + return "motion_sensor_active"; default: return "help"; } @@ -1223,4 +1194,47 @@ Column { } } } + + Component { + id: smallCompoundComponent + SmallCompoundButton { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 + property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") + width: parent.width + height: 48 + iconBlinking: root.getCompoundPillIconBlinking(widgetData.id || "") + iconName: root.getCompoundPillIconName(widgetData.id || "", widgetDef) + isActive: root.getCompoundPillIsActive(widgetData.id || "") + enabled: (widgetDef?.enabled ?? true) && !root.editMode + onToggled: { + if (root.editMode) return; + root.handleCompoundPillToggled(widgetData.id || ""); + } + onExpandClicked: { + if (root.editMode) return; + root.expandClicked(widgetData, widgetIndex); + } + onWheelEvent: function(wheelEvent) { + if (root.editMode) return; + root.handleCompoundPillWheelEvent(widgetData.id || "", wheelEvent); + } + } + } + + Component { + id: smallColorPickerComponent + SmallColorPickerButton { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 + width: parent.width + height: 48 + colorPickerModal: root.colorPickerModal + onClicked: { + if (root.editMode) return; + if (root.colorPickerModal) + root.colorPickerModal.show(); + } + } + } } diff --git a/quickshell/Modules/ControlCenter/Widgets/DndPill.qml b/quickshell/Modules/ControlCenter/Widgets/DndPill.qml index 24507e97..b04c67f3 100644 --- a/quickshell/Modules/ControlCenter/Widgets/DndPill.qml +++ b/quickshell/Modules/ControlCenter/Widgets/DndPill.qml @@ -5,7 +5,7 @@ import qs.Modules.ControlCenter.Widgets CompoundPill { id: root - iconName: SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off" + iconName: "do_not_disturb_on" iconColor: SessionData.doNotDisturb ? Theme.primary : Theme.surfaceText primaryText: I18n.tr("Do Not Disturb") isActive: SessionData.doNotDisturb diff --git a/quickshell/Modules/ControlCenter/Widgets/SmallColorPickerButton.qml b/quickshell/Modules/ControlCenter/Widgets/SmallColorPickerButton.qml new file mode 100644 index 00000000..d2d761df --- /dev/null +++ b/quickshell/Modules/ControlCenter/Widgets/SmallColorPickerButton.qml @@ -0,0 +1,64 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property var colorPickerModal: null + + signal clicked + + width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48 + height: 48 + radius: Theme.cornerRadius === 0 ? 0 : Theme.cornerRadius + + function hoverTint(base) { + const factor = 1.2; + return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor); + } + + color: Theme.primary + border.color: Theme.ccTileRing + border.width: 1 + antialiasing: true + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: hoverTint(root.color) + opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0) + visible: opacity > 0 + antialiasing: true + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + } + } + } + + DankIcon { + anchors.centerIn: parent + name: "palette" + size: Theme.iconSize + color: Theme.primaryText + } + + DankRipple { + id: ripple + cornerRadius: root.radius + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: root.enabled + onPressed: mouse => ripple.trigger(mouse.x, mouse.y) + onClicked: root.clicked() + } +} diff --git a/quickshell/Modules/ControlCenter/Widgets/SmallCompoundButton.qml b/quickshell/Modules/ControlCenter/Widgets/SmallCompoundButton.qml new file mode 100644 index 00000000..bb9d2417 --- /dev/null +++ b/quickshell/Modules/ControlCenter/Widgets/SmallCompoundButton.qml @@ -0,0 +1,107 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property string iconName: "" + property bool isActive: false + property bool iconBlinking: false + + // Left click expands the widget (primary detail action), right click toggles on/off. + signal toggled + signal expandClicked + signal wheelEvent(var wheelEvent) + + width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48 + height: 48 + radius: { + if (Theme.cornerRadius === 0) + return 0; + return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4; + } + + function hoverTint(base) { + const factor = 1.2; + return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor); + } + + readonly property color _tileBgActive: Theme.ccTileActiveBg + readonly property color _tileBgInactive: Theme.ccPillInactiveBg + readonly property color _tileRingActive: Theme.ccTileRing + readonly property color _tileIconActive: Theme.ccTileActiveText + readonly property color _tileIconInactive: Theme.ccTileInactiveIcon + + color: { + if (isActive) + return _tileBgActive; + const baseColor = mouseArea.containsMouse ? Theme.ccPillInactiveHoverBg : _tileBgInactive; + return baseColor; + } + border.color: isActive ? _tileRingActive : Theme.outlineMedium + border.width: isActive ? 1 : Theme.layerOutlineWidth + antialiasing: true + opacity: enabled ? 1.0 : 0.6 + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: hoverTint(root.color) + opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0) + visible: opacity > 0 + antialiasing: true + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + } + } + } + + DankIcon { + id: tileIcon + anchors.centerIn: parent + name: iconName + size: Theme.iconSize + color: isActive ? _tileIconActive : _tileIconInactive + + DankBlink { + target: tileIcon + running: root.iconBlinking + } + } + + DankRipple { + id: ripple + cornerRadius: root.radius + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + enabled: root.enabled + onPressed: mouse => ripple.trigger(mouse.x, mouse.y) + onClicked: mouse => { + if (mouse.button === Qt.RightButton) + root.toggled(); + else + root.expandClicked(); + } + onWheel: function (ev) { + root.wheelEvent(ev); + } + } + + Behavior on radius { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +}