From f77efc409b34a23dc9f45fcdf3fd7fef62011fe1 Mon Sep 17 00:00:00 2001 From: lysec Date: Thu, 9 Oct 2025 13:56:35 +0200 Subject: [PATCH] QuickSettings: customization!? --- Assets/Translations/en.json | 22 ++ Assets/settings-default.json | 31 +- Commons/Settings.qml | 1 + Modules/ControlCenter/Cards/TopCard.qml | 4 +- Modules/ControlCenter/Widgets/Bluetooth.qml | 9 +- .../ControlCenter/Widgets/DoNotDisturb.qml | 11 +- Modules/ControlCenter/Widgets/KeepAwake.qml | 11 +- Modules/ControlCenter/Widgets/NightLight.qml | 18 +- .../ControlCenter/Widgets/PowerProfile.qml | 9 +- .../ControlCenter/Widgets/ScreenRecorder.qml | 10 +- .../Widgets/WallpaperSelector.qml | 10 +- Modules/ControlCenter/Widgets/WiFi.qml | 40 ++- Modules/Settings/Tabs/ControlCenterTab.qml | 35 ++ Widgets/NButton.qml | 2 +- Widgets/NQuickSetting.qml | 327 ++++++++++++++++++ 15 files changed, 507 insertions(+), 33 deletions(-) create mode 100644 Widgets/NQuickSetting.qml diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index ff91166a..62d3e9c7 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -95,6 +95,24 @@ "label": "Position", "description": "Choose where the Control Center panel appears when opened." } + }, + "controlCenter": { + "quickSettingsStyle": { + "section": { + "label": "Quick Settings Style", + "description": "Choose the visual style for quick settings buttons." + }, + "style": { + "label": "Display Style", + "description": "Select between modern card-style buttons or classic icon buttons." + } + }, + "widgets": { + "section": { + "label": "Widgets", + "description": "Manage and configure Control Center widgets." + } + } } }, "audio": { @@ -1241,6 +1259,10 @@ "bottom_right": "Bottom right", "bottom_center": "Bottom center", "top_center": "Top center" + }, + "quickSettingsStyle": { + "modern": "Modern Cards", + "classic": "Classic Icons" } }, "osd": { diff --git a/Assets/settings-default.json b/Assets/settings-default.json index a80e2bb4..05675bd4 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -112,7 +112,36 @@ "terminalCommand": "xterm -e" }, "controlCenter": { - "position": "close_to_bar_button" + "position": "close_to_bar_button", + "quickSettingsStyle": "modern", + "widgets": { + "quickSettings": [ + { + "id": "WiFi" + }, + { + "id": "Bluetooth" + }, + { + "id": "DoNotDisturb" + }, + { + "id": "NightLight" + }, + { + "id": "KeepAwake" + }, + { + "id": "PowerProfile" + }, + { + "id": "ScreenRecorder" + }, + { + "id": "WallpaperSelector" + } + ] + } }, "dock": { "displayMode": "always_visible", diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 61b4d184..8ab90c93 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -246,6 +246,7 @@ Singleton { property JsonObject controlCenter: JsonObject { // Position: close_to_bar_button, center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center property string position: "close_to_bar_button" + property string quickSettingsStyle: "modern" // "modern" or "classic" property JsonObject widgets widgets: JsonObject { property list quickSettings: [{ diff --git a/Modules/ControlCenter/Cards/TopCard.qml b/Modules/ControlCenter/Cards/TopCard.qml index a7123bfb..51aeb619 100644 --- a/Modules/ControlCenter/Cards/TopCard.qml +++ b/Modules/ControlCenter/Cards/TopCard.qml @@ -106,8 +106,8 @@ NBox { id: grid Layout.fillWidth: true columns: 3 - columnSpacing: Style.marginL * scaling - rowSpacing: Style.marginM * scaling + columnSpacing: Style.marginM * scaling + rowSpacing: Style.marginS * scaling Repeater { model: Settings.data.controlCenter.widgets.quickSettings diff --git a/Modules/ControlCenter/Widgets/Bluetooth.qml b/Modules/ControlCenter/Widgets/Bluetooth.qml index 7129ba64..9b1cd17d 100644 --- a/Modules/ControlCenter/Widgets/Bluetooth.qml +++ b/Modules/ControlCenter/Widgets/Bluetooth.qml @@ -4,14 +4,17 @@ import qs.Commons import qs.Services import qs.Widgets -NButton { +NQuickSetting { property ShellScreen screen property real scaling: 1.0 - outlined: true text: "Bluetooth" fontSize: Style.fontSizeS * scaling - fontWeight: Style.fontWeightRegular + fontWeight: Style.fontWeightMedium icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" + active: BluetoothService.enabled + tooltipText: BluetoothService.enabled ? "Bluetooth enabled" : "Bluetooth disabled" + style: Settings.data.controlCenter.quickSettingsStyle || "modern" + onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) } diff --git a/Modules/ControlCenter/Widgets/DoNotDisturb.qml b/Modules/ControlCenter/Widgets/DoNotDisturb.qml index fed665d6..cc2a1d1a 100644 --- a/Modules/ControlCenter/Widgets/DoNotDisturb.qml +++ b/Modules/ControlCenter/Widgets/DoNotDisturb.qml @@ -4,14 +4,17 @@ import qs.Commons import qs.Services import qs.Widgets -NButton { +NQuickSetting { property ShellScreen screen property real scaling: 1.0 - outlined: true text: "Do not Disturb" fontSize: Style.fontSizeS * scaling - fontWeight: Style.fontWeightRegular - icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell" + fontWeight: Style.fontWeightMedium + icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell" + active: Settings.data.notifications.doNotDisturb + tooltipText: Settings.data.notifications.doNotDisturb ? "Turn off Do Not Disturb" : "Turn on Do Not Disturb" + style: Settings.data.controlCenter.quickSettingsStyle || "modern" + onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb } diff --git a/Modules/ControlCenter/Widgets/KeepAwake.qml b/Modules/ControlCenter/Widgets/KeepAwake.qml index 9840c5b3..a254f8df 100644 --- a/Modules/ControlCenter/Widgets/KeepAwake.qml +++ b/Modules/ControlCenter/Widgets/KeepAwake.qml @@ -4,14 +4,17 @@ import qs.Commons import qs.Services import qs.Widgets -NButton { +NQuickSetting { property ShellScreen screen property real scaling: 1.0 - outlined: true - text: IdleInhibitorService.isInhibited ? "Keep-awake" : "Keep-awake" + text: "Keep-awake" fontSize: Style.fontSizeS * scaling - fontWeight: Style.fontWeightRegular + fontWeight: Style.fontWeightMedium icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off" + active: IdleInhibitorService.isInhibited + tooltipText: IdleInhibitorService.isInhibited ? "Disable keep-awake" : "Enable keep-awake" + style: Settings.data.controlCenter.quickSettingsStyle || "modern" + onClicked: IdleInhibitorService.manualToggle() } diff --git a/Modules/ControlCenter/Widgets/NightLight.qml b/Modules/ControlCenter/Widgets/NightLight.qml index 841cc560..21077f37 100644 --- a/Modules/ControlCenter/Widgets/NightLight.qml +++ b/Modules/ControlCenter/Widgets/NightLight.qml @@ -4,16 +4,27 @@ import qs.Commons import qs.Services import qs.Widgets -NButton { +NQuickSetting { property ShellScreen screen property real scaling: 1.0 - outlined: true enabled: ProgramCheckerService.wlsunsetAvailable text: "Night Light" fontSize: Style.fontSizeS * scaling - fontWeight: Style.fontWeightRegular + fontWeight: Style.fontWeightMedium icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off" + active: Settings.data.nightLight.enabled + style: Settings.data.controlCenter.quickSettingsStyle || "modern" + tooltipText: { + if (!Settings.data.nightLight.enabled) { + return "Turn on Night Light" + } else if (Settings.data.nightLight.forced) { + return "Night Light forced on" + } else { + return "Turn off Night Light" + } + } + onClicked: { if (!Settings.data.nightLight.enabled) { Settings.data.nightLight.enabled = true @@ -25,6 +36,7 @@ NButton { Settings.data.nightLight.forced = false } } + onRightClicked: { var settingsPanel = PanelService.getPanel("settingsPanel") settingsPanel.requestedTab = SettingsPanel.Tab.Display diff --git a/Modules/ControlCenter/Widgets/PowerProfile.qml b/Modules/ControlCenter/Widgets/PowerProfile.qml index b5b0df8d..2636d40e 100644 --- a/Modules/ControlCenter/Widgets/PowerProfile.qml +++ b/Modules/ControlCenter/Widgets/PowerProfile.qml @@ -6,17 +6,20 @@ import qs.Services import qs.Widgets // Performance -NButton { +NQuickSetting { property ShellScreen screen property real scaling: 1.0 readonly property bool hasPP: PowerProfileService.available enabled: hasPP - outlined: true text: PowerProfileService.getName() fontSize: Style.fontSizeS * scaling - fontWeight: Style.fontWeightRegular + fontWeight: Style.fontWeightMedium icon: PowerProfileService.getIcon() + active: hasPP + tooltipText: hasPP ? "Current: " + PowerProfileService.getName() : "Power profiles not available" + style: Settings.data.controlCenter.quickSettingsStyle || "modern" + onClicked: { PowerProfileService.cycleProfile() } diff --git a/Modules/ControlCenter/Widgets/ScreenRecorder.qml b/Modules/ControlCenter/Widgets/ScreenRecorder.qml index fbcb9bd9..0523bf7e 100644 --- a/Modules/ControlCenter/Widgets/ScreenRecorder.qml +++ b/Modules/ControlCenter/Widgets/ScreenRecorder.qml @@ -4,17 +4,19 @@ import qs.Commons import qs.Services import qs.Widgets -NButton { - +NQuickSetting { property ShellScreen screen property real scaling: 1.0 enabled: ProgramCheckerService.gpuScreenRecorderAvailable - outlined: true icon: "camera-video" text: "Screen Rec." fontSize: Style.fontSizeS * scaling - fontWeight: Style.fontWeightRegular + fontWeight: Style.fontWeightMedium + active: ScreenRecorderService.isRecording + tooltipText: ScreenRecorderService.isRecording ? "Stop recording" : "Start screen recording" + style: Settings.data.controlCenter.quickSettingsStyle || "modern" + onClicked: { ScreenRecorderService.toggleRecording() if (!ScreenRecorderService.isRecording) { diff --git a/Modules/ControlCenter/Widgets/WallpaperSelector.qml b/Modules/ControlCenter/Widgets/WallpaperSelector.qml index fd5a7b81..9ec4db51 100644 --- a/Modules/ControlCenter/Widgets/WallpaperSelector.qml +++ b/Modules/ControlCenter/Widgets/WallpaperSelector.qml @@ -4,17 +4,19 @@ import qs.Commons import qs.Services import qs.Widgets -NButton { +NQuickSetting { property ShellScreen screen property real scaling: 1.0 - enabled: Settings.data.wallpaper.enabled - outlined: true icon: "wallpaper-selector" text: "Wallpaper" fontSize: Style.fontSizeS * scaling - fontWeight: Style.fontWeightRegular + fontWeight: Style.fontWeightMedium + active: Settings.data.wallpaper.enabled + tooltipText: "Open wallpaper selector" + style: Settings.data.controlCenter.quickSettingsStyle || "modern" + onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this) onRightClicked: WallpaperService.setRandomWallpaper() } diff --git a/Modules/ControlCenter/Widgets/WiFi.qml b/Modules/ControlCenter/Widgets/WiFi.qml index 28d4e047..30a4f635 100644 --- a/Modules/ControlCenter/Widgets/WiFi.qml +++ b/Modules/ControlCenter/Widgets/WiFi.qml @@ -4,12 +4,10 @@ import qs.Commons import qs.Services import qs.Widgets -NButton { +NQuickSetting { property ShellScreen screen property real scaling: 1.0 - - outlined: true icon: { try { if (NetworkService.ethernetConnected) { @@ -30,13 +28,47 @@ NButton { return "signal_wifi_bad" } } + text: { if (NetworkService.ethernetConnected) { return "Network" } return "Wi-Fi" } + fontSize: Style.fontSizeS * scaling - fontWeight: Style.fontWeightRegular + fontWeight: Style.fontWeightMedium + style: Settings.data.controlCenter.quickSettingsStyle || "modern" + + active: { + if (NetworkService.ethernetConnected) { + return true + } + try { + for (const net in NetworkService.networks) { + if (NetworkService.networks[net].connected) { + return true + } + } + return false + } catch (error) { + return false + } + } + + tooltipText: { + if (NetworkService.ethernetConnected) { + return "Ethernet connected" + } + let connected = false + for (const net in NetworkService.networks) { + if (NetworkService.networks[net].connected) { + connected = true + break + } + } + return connected ? "Wi-Fi connected" : "Wi-Fi disconnected" + } + onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) } diff --git a/Modules/Settings/Tabs/ControlCenterTab.qml b/Modules/Settings/Tabs/ControlCenterTab.qml index 9f432137..6a6a3b16 100644 --- a/Modules/Settings/Tabs/ControlCenterTab.qml +++ b/Modules/Settings/Tabs/ControlCenterTab.qml @@ -27,6 +27,41 @@ ColumnLayout { } } + // Quick Settings Style Section + ColumnLayout { + spacing: Style.marginL * scaling + Layout.fillWidth: true + + NHeader { + label: I18n.tr("settings.controlCenter.quickSettingsStyle.section.label") + description: I18n.tr("settings.controlCenter.quickSettingsStyle.section.description") + } + + NComboBox { + id: quickSettingsStyle + label: I18n.tr("settings.controlCenter.quickSettingsStyle.style.label") + description: I18n.tr("settings.controlCenter.quickSettingsStyle.style.description") + Layout.fillWidth: true + model: [{ + "key": "modern", + "name": I18n.tr("options.controlCenter.quickSettingsStyle.modern") + }, { + "key": "classic", + "name": I18n.tr("options.controlCenter.quickSettingsStyle.classic") + }] + currentKey: Settings.data.controlCenter.quickSettingsStyle || "modern" + onSelected: function (key) { + Settings.data.controlCenter.quickSettingsStyle = key + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } + // Widgets Management Section ColumnLayout { spacing: Style.marginXXS * scaling diff --git a/Widgets/NButton.qml b/Widgets/NButton.qml index 2f4f5daf..3d1b05a3 100644 --- a/Widgets/NButton.qml +++ b/Widgets/NButton.qml @@ -19,7 +19,7 @@ Rectangle { property int fontWeight: Style.fontWeightBold property real iconSize: Style.fontSizeL * scaling property bool outlined: false - property int horizontalAlignment: Qt.AlignHCenter + property int horizontalAlignment: Qt.AlignHCenter // Signals signal clicked diff --git a/Widgets/NQuickSetting.qml b/Widgets/NQuickSetting.qml new file mode 100644 index 00000000..3c199c16 --- /dev/null +++ b/Widgets/NQuickSetting.qml @@ -0,0 +1,327 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import qs.Commons +import qs.Services + +Rectangle { + id: root + + // Public properties + property string text: "" + property string icon: "" + property string tooltipText: "" + property bool enabled: true + property bool active: false + property bool compact: false + property string style: "modern" // "modern" or "classic" + + // Styling properties + property real fontSize: Style.fontSizeS * scaling + property int fontWeight: Style.fontWeightMedium + property real iconSize: Style.fontSizeL * scaling + property real cornerRadius: Style.radiusM * scaling + + // Colors - Style-dependent colors + property color backgroundColor: style === "classic" ? Color.mSurfaceVariant : Color.mSurface + property color textColor: Color.mOnSurface + property color iconColor: style === "classic" ? Color.mPrimary : (active ? Color.mPrimary : Color.mOnSurface) + property color borderColor: Color.mOutline + property color hoverColor: style === "classic" ? Color.mTertiary : Color.mPrimary + property color pressedColor: style === "classic" ? Color.mTertiary : Qt.darker(Color.mPrimary, 1.1) + property color hoverTextColor: Color.mOnPrimary + property color hoverIconColor: style === "classic" ? Color.mOnTertiary : Color.mOnPrimary + + // Signals + signal clicked + signal rightClicked + signal middleClicked + + // Internal properties + property bool hovered: false + property bool pressed: false + property real scaling: 1.0 + + // Dimensions - Style-dependent sizing + implicitWidth: { + if (style === "classic") { + return Style.baseWidgetSize * scaling + } + return compact ? Math.max(100 * scaling, contentRow.implicitWidth + (Style.marginL * scaling)) : Math.max(120 * scaling, contentRow.implicitWidth + (Style.marginL * scaling)) + } + implicitHeight: { + if (style === "classic") { + return Style.baseWidgetSize * scaling + } + return compact ? Math.max(48 * scaling, contentRow.implicitHeight + (Style.marginM * scaling)) : Math.max(56 * scaling, contentRow.implicitHeight + (Style.marginL * scaling)) + } + + // Appearance - Style-dependent styling + radius: style === "classic" ? width * 0.5 : cornerRadius + color: { + if (!enabled) + return Qt.lighter(Color.mSurface, 1.1) + if (pressed) + return pressedColor + if (hovered) + return hoverColor + return backgroundColor + } + + border.width: style === "classic" ? Math.max(1, Style.borderS * scaling) : 0 + border.color: style === "classic" ? borderColor : "transparent" + + opacity: enabled ? (style === "classic" ? Style.opacityFull : 1.0) : (style === "classic" ? Style.opacityMedium : 0.6) + + // Smooth animations + Behavior on color { + ColorAnimation { + duration: style === "classic" ? Style.animationNormal : Style.animationFast + easing.type: style === "classic" ? Easing.InOutQuad : Easing.OutCubic + } + } + + Behavior on border.color { + ColorAnimation { + duration: style === "classic" ? Style.animationNormal : Style.animationFast + easing.type: style === "classic" ? Easing.InOutQuad : Easing.OutCubic + } + } + + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + // Hover scale effect + scale: hovered ? 1.02 : 1.0 + + // Subtle shadow/elevation effect + Rectangle { + anchors.fill: parent + radius: parent.radius + color: Qt.rgba(0, 0, 0, 0.1) + visible: active + z: -1 + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + // Modern style - icon above text + ColumnLayout { + id: contentRow + anchors.centerIn: parent + spacing: Style.marginXXS * scaling + visible: root.style !== "classic" + + // Icon + NIcon { + Layout.alignment: Qt.AlignHCenter + visible: root.icon !== "" + icon: root.icon + pointSize: root.iconSize + color: { + if (!root.enabled) + return Color.mOnSurfaceVariant + if (root.hovered) + return root.hoverIconColor + return root.iconColor + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + // Text content + NText { + Layout.alignment: Qt.AlignHCenter + visible: root.text !== "" && !compact + text: root.text + pointSize: root.fontSize + font.weight: root.fontWeight + color: { + if (!root.enabled) + return Color.mOnSurfaceVariant + if (root.hovered) + return root.hoverTextColor + return root.textColor + } + elide: Text.ElideRight + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + } + + // Classic style - EXACTLY like NIconButton (icon + text) + RowLayout { + anchors.centerIn: parent + visible: root.style === "classic" + spacing: Style.marginXS * scaling + + NIcon { + visible: root.icon !== "" + icon: root.icon + pointSize: Style.fontSizeM * scaling + color: { + if (!root.enabled) + return Color.mOnSurfaceVariant + if (root.hovered) + return root.hoverIconColor + return root.iconColor + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + NText { + visible: root.text !== "" + text: root.text + pointSize: root.fontSize + font.weight: root.fontWeight + color: { + if (!root.enabled) + return Color.mOnSurfaceVariant + if (root.hovered) + return root.hoverTextColor + return root.textColor + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + } + + // Mouse interaction with enhanced feedback + MouseArea { + id: mouseArea + anchors.fill: parent + enabled: root.enabled + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + + onEntered: { + root.hovered = true + if (tooltipText) { + TooltipService.show(Screen, root, root.tooltipText) + } + } + + onExited: { + root.hovered = false + if (tooltipText) { + TooltipService.hide() + } + } + + onPressed: mouse => { + root.pressed = true + root.scale = 0.95 + if (tooltipText) { + TooltipService.hide() + } + } + + onReleased: mouse => { + root.pressed = false + root.scale = 1.0 + + if (mouse.button === Qt.LeftButton) { + root.clicked() + } else if (mouse.button === Qt.RightButton) { + root.rightClicked() + } else if (mouse.button === Qt.MiddleButton) { + root.middleClicked() + } + } + + onCanceled: { + root.hovered = false + root.pressed = false + root.scale = 1.0 + if (tooltipText) { + TooltipService.hide() + } + } + } + + // Ripple effect for M3-style interaction feedback + Rectangle { + id: ripple + anchors.fill: parent + radius: parent.radius + color: Qt.rgba(1, 1, 1, 0.2) + scale: 0 + opacity: 0 + visible: false + + SequentialAnimation { + id: rippleAnimation + running: false + + ParallelAnimation { + NumberAnimation { + target: ripple + property: "scale" + from: 0 + to: 1.2 + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: ripple + property: "opacity" + from: 0.6 + to: 0 + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } + } + + // Trigger ripple effect on click + Connections { + target: root + function onClicked() { + ripple.visible = true + rippleAnimation.start() + } + } + + // Clean up ripple after animation + Connections { + target: rippleAnimation + function onFinished() { + ripple.visible = false + ripple.scale = 0 + ripple.opacity = 0 + } + } +}