diff --git a/Commons/Settings.qml b/Commons/Settings.qml index c0939caf..61b4d184 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -246,6 +246,16 @@ 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 JsonObject widgets + widgets: JsonObject { + property list quickSettings: [{ + "id": "Bluetooth" + }, { + "id": "WiFi" + }, { + "id": "PowerProfile" + }] + } } // dock diff --git a/Modules/Bar/Extras/BarWidgetLoader.qml b/Modules/Bar/Extras/BarWidgetLoader.qml index 8697db5b..69b08623 100644 --- a/Modules/Bar/Extras/BarWidgetLoader.qml +++ b/Modules/Bar/Extras/BarWidgetLoader.qml @@ -77,7 +77,7 @@ Item { // Error handling onWidgetIdChanged: { if (widgetId && !BarWidgetRegistry.hasWidget(widgetId)) { - Logger.warn("BarWidgetLoader", "Widget not found in bar registry:", widgetId) + Logger.warn("BarWidgetLoader", "Widget not found in registry:", widgetId) } } } diff --git a/Modules/ControlCenter/Cards/TopCard.qml b/Modules/ControlCenter/Cards/TopCard.qml index d68d84be..1bb36148 100644 --- a/Modules/ControlCenter/Cards/TopCard.qml +++ b/Modules/ControlCenter/Cards/TopCard.qml @@ -4,9 +4,9 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Widgets -import Quickshell.Services.UPower import qs.Modules.Settings import qs.Modules.ControlCenter +import qs.Modules.ControlCenter.Extras import qs.Commons import qs.Services import qs.Widgets @@ -17,7 +17,6 @@ NBox { property string uptimeText: "--" property real spacing: Style.marginS * scaling - readonly property bool hasPP: PowerProfileService.available ColumnLayout { anchors.fill: parent @@ -97,205 +96,34 @@ NBox { } } - RowLayout { - id: utilitiesRow - Layout.alignment: Qt.AlignVCenter + NDivider { + Layout.fillWidth: true Layout.topMargin: Style.marginM * scaling Layout.bottomMargin: Style.marginM * scaling + } + + GridLayout { + id: grid Layout.fillWidth: true + columns: 2 + columnSpacing: Style.marginL * scaling + rowSpacing: Style.marginM * scaling - // Left group - Media & Display - Rectangle { - color: Color.mSurface - radius: Style.radiusM * scaling - Layout.preferredHeight: Style.baseWidgetSize * 1.2 * scaling - Layout.preferredWidth: childrenRect.width + (Style.marginS * scaling * 2) - - RowLayout { - anchors.centerIn: parent - spacing: Style.marginM * scaling - - // Screen Recorder - NIconButton { - baseSize: Style.baseWidgetSize * 0.9 - icon: "camera-video" - visible: ProgramCheckerService.gpuScreenRecorderAvailable - tooltipText: ScreenRecorderService.isRecording ? I18n.tr("tooltips.stop-screen-recording") : I18n.tr("tooltips.start-screen-recording") - colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant - colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary - onClicked: { - ScreenRecorderService.toggleRecording() - if (!ScreenRecorderService.isRecording) { - var panel = PanelService.getPanel("controlCenterPanel") - panel?.close() - } - } - } - - // Wallpaper - NIconButton { - baseSize: Style.baseWidgetSize * 0.9 - visible: Settings.data.wallpaper.enabled - icon: "wallpaper-selector" - tooltipText: I18n.tr("tooltips.wallpaper-selector") - onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this) - onRightClicked: WallpaperService.setRandomWallpaper() - } - - // Night Light - NIconButton { - baseSize: Style.baseWidgetSize * 0.9 - visible: ProgramCheckerService.wlsunsetAvailable - colorBg: Settings.data.nightLight.forced ? Color.mPrimary : Color.transparent - colorFg: Settings.data.nightLight.forced ? Color.mOnPrimary : Color.mPrimary - icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off" - tooltipText: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? I18n.tr("tooltips.night-light-forced") : I18n.tr("tooltips.night-light-enabled")) : I18n.tr("tooltips.night-light-disabled") - onClicked: { - if (!Settings.data.nightLight.enabled) { - Settings.data.nightLight.enabled = true - Settings.data.nightLight.forced = false - } else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) { - Settings.data.nightLight.forced = true - } else { - Settings.data.nightLight.enabled = false - Settings.data.nightLight.forced = false - } - } - - onRightClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel") - settingsPanel.requestedTab = SettingsPanel.Tab.Display - settingsPanel.open() - } - } - } - } - - // Spacer - Item { - Layout.fillWidth: true - } - - // Center group - Network & Caffeine - Rectangle { - color: Color.mSurface - radius: Style.radiusM * scaling - Layout.preferredHeight: Style.baseWidgetSize * 1.2 * scaling - Layout.preferredWidth: childrenRect.width + (Style.marginS * scaling * 2) - - RowLayout { - anchors.centerIn: parent - spacing: Style.marginM * scaling - - // Wifi - NIconButton { - id: wifiButton - baseSize: Style.baseWidgetSize * 0.9 - tooltipText: I18n.tr("tooltips.manage-wifi") - icon: { - try { - if (NetworkService.ethernetConnected) { - return "ethernet" - } - let connected = false - let signalStrength = 0 - for (const net in NetworkService.networks) { - if (NetworkService.networks[net].connected) { - connected = true - signalStrength = NetworkService.networks[net].signal - break - } - } - return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off" - } catch (error) { - Logger.error("Wi-Fi", "Error getting icon:", error) - return "signal_wifi_bad" - } - } - onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) - onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this) - } - - // Bluetooth - NIconButton { - baseSize: Style.baseWidgetSize * 0.9 - tooltipText: I18n.tr("tooltips.bluetooth-devices") - icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" - onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) - onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) - } - - // Caffeine (Keep Awake) - NIconButton { - baseSize: Style.baseWidgetSize * 0.9 - icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off" - tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.disable-keep-awake") : I18n.tr("tooltips.enable-keep-awake") - colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant - colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary - onClicked: { - IdleInhibitorService.manualToggle() - } - } - } - } - - // Spacer - Item { - Layout.fillWidth: true - } - - // Right group - Power Profiles - Rectangle { - color: Color.mSurface - radius: Style.radiusM * scaling - Layout.preferredHeight: Style.baseWidgetSize * 1.2 * scaling - Layout.preferredWidth: childrenRect.width + (Style.marginS * scaling * 2) - - RowLayout { - anchors.centerIn: parent - spacing: Style.marginM * scaling - - // Performance - NIconButton { - baseSize: Style.baseWidgetSize * 0.9 - icon: PowerProfileService.getIcon(PowerProfile.Performance) - tooltipText: I18n.tr("tooltips.set-power-profile", { - "profile": PowerProfileService.getName(PowerProfile.Performance) - }) - enabled: hasPP - opacity: enabled ? Style.opacityFull : Style.opacityMedium - colorBg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant - colorFg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mOnPrimary : Color.mPrimary - onClicked: PowerProfileService.setProfile(PowerProfile.Performance) - } - - // Balanced - NIconButton { - baseSize: Style.baseWidgetSize * 0.9 - icon: PowerProfileService.getIcon(PowerProfile.Balanced) - tooltipText: I18n.tr("tooltips.set-power-profile", { - "profile": PowerProfileService.getName(PowerProfile.Balanced) - }) - enabled: hasPP - opacity: enabled ? Style.opacityFull : Style.opacityMedium - colorBg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant - colorFg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnPrimary : Color.mPrimary - onClicked: PowerProfileService.setProfile(PowerProfile.Balanced) - } - - // Eco - NIconButton { - baseSize: Style.baseWidgetSize * 0.9 - icon: PowerProfileService.getIcon(PowerProfile.PowerSaver) - tooltipText: I18n.tr("tooltips.set-power-profile", { - "profile": PowerProfileService.getName(PowerProfile.PowerSaver) - }) - enabled: hasPP - opacity: enabled ? Style.opacityFull : Style.opacityMedium - colorBg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant - colorFg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mOnPrimary : Color.mPrimary - onClicked: PowerProfileService.setProfile(PowerProfile.PowerSaver) + Repeater { + model: Settings.data.controlCenter.widgets.quickSettings + delegate: ControlCenterWidgetLoader { + Layout.fillWidth: true + Layout.preferredWidth: (grid.width - grid.columnSpacing) / 2 + widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetProps: { + "screen": root.modelData || null, + "scaling": ScalingService.getScreenScale(screen), + "widgetId": modelData.id, + "section": "quickSettings", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.controlCenter.widgets.quickSettings.length } + Layout.alignment: Qt.AlignVCenter } } } diff --git a/Modules/ControlCenter/Cards/WeatherCard.qml b/Modules/ControlCenter/Cards/WeatherCard.qml deleted file mode 100644 index d9e8510a..00000000 --- a/Modules/ControlCenter/Cards/WeatherCard.qml +++ /dev/null @@ -1,130 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import qs.Commons -import qs.Services -import qs.Widgets - -// Weather overview card (placeholder data) -NBox { - id: root - - readonly property bool weatherReady: (LocationService.data.weather !== null) - - ColumnLayout { - id: content - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Style.marginM * scaling - spacing: Style.marginM * scaling - clip: true - - RowLayout { - spacing: Style.marginS * scaling - NIcon { - Layout.alignment: Qt.AlignVCenter - icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "" - pointSize: Style.fontSizeXXXL * 1.75 * scaling - color: Color.mPrimary - } - - ColumnLayout { - spacing: Style.marginXXS * scaling - NText { - text: { - // Ensure the name is not too long if one had to specify the country - const chunks = Settings.data.location.name.split(",") - return chunks[0] - } - pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - } - - RowLayout { - NText { - visible: weatherReady - text: { - if (!weatherReady) { - return "" - } - var temp = LocationService.data.weather.current_weather.temperature - var suffix = "C" - if (Settings.data.location.useFahrenheit) { - temp = LocationService.celsiusToFahrenheit(temp) - var suffix = "F" - } - temp = Math.round(temp) - return `${temp}°${suffix}` - } - pointSize: Style.fontSizeXL * scaling - font.weight: Style.fontWeightBold - } - - NText { - text: weatherReady ? `(${LocationService.data.weather.timezone_abbreviation})` : "" - pointSize: Style.fontSizeXS * scaling - color: Color.mOnSurfaceVariant - visible: LocationService.data.weather - } - } - } - } - - NDivider { - visible: weatherReady - Layout.fillWidth: true - } - - RowLayout { - visible: weatherReady - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - spacing: Style.marginL * scaling - Repeater { - model: weatherReady ? LocationService.data.weather.daily.time : [] - delegate: ColumnLayout { - Layout.alignment: Qt.AlignHCenter - spacing: Style.marginS * scaling - NText { - text: { - var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/")) - return Qt.locale().toString(weatherDate, "ddd") - } - color: Color.mOnSurface - Layout.alignment: Qt.AlignHCenter - } - NIcon { - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index]) - pointSize: Style.fontSizeXXL * 1.6 * scaling - color: Color.mPrimary - } - NText { - Layout.alignment: Qt.AlignHCenter - text: { - var max = LocationService.data.weather.daily.temperature_2m_max[index] - var min = LocationService.data.weather.daily.temperature_2m_min[index] - if (Settings.data.location.useFahrenheit) { - max = LocationService.celsiusToFahrenheit(max) - min = LocationService.celsiusToFahrenheit(min) - } - max = Math.round(max) - min = Math.round(min) - return `${max}°/${min}°` - } - pointSize: Style.fontSizeXS * scaling - color: Color.mOnSurfaceVariant - } - } - } - } - - RowLayout { - visible: !weatherReady - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter - NBusyIndicator {} - } - } -} diff --git a/Modules/ControlCenter/ControlCenterPanel.qml b/Modules/ControlCenter/ControlCenterPanel.qml index c2290cc0..e47c593c 100644 --- a/Modules/ControlCenter/ControlCenterPanel.qml +++ b/Modules/ControlCenter/ControlCenterPanel.qml @@ -10,8 +10,8 @@ import qs.Widgets NPanel { id: root - preferredWidth: 480 - preferredHeight: 580 + preferredWidth: 440 + preferredHeight: 540 panelKeyboardFocus: true // Positioning @@ -38,13 +38,7 @@ NPanel { // Top Card: profile + utilities TopCard { Layout.fillWidth: true - Layout.preferredHeight: Math.max(124 * scaling) - } - - // Weather - WeatherCard { - Layout.fillWidth: true - Layout.preferredHeight: Math.max(196 * scaling) + Layout.preferredHeight: Math.max(280 * scaling) } // Media + stats column @@ -55,13 +49,13 @@ NPanel { // Media card MediaCard { - Layout.preferredWidth: Math.max(270 * scaling) - Layout.fillHeight: true + Layout.preferredWidth: Math.max(250 * scaling) + Layout.preferredHeight: Math.max(196 * scaling) } // System monitors combined in one card SystemMonitorCard { - Layout.preferredWidth: Math.max(160 * scaling) + Layout.preferredWidth: Math.max(140 * scaling) Layout.preferredHeight: Math.max(196 * scaling) } } diff --git a/Modules/ControlCenter/Extras/ControlCenterWidgetLoader.qml b/Modules/ControlCenter/Extras/ControlCenterWidgetLoader.qml new file mode 100644 index 00000000..4e7577ee --- /dev/null +++ b/Modules/ControlCenter/Extras/ControlCenterWidgetLoader.qml @@ -0,0 +1,74 @@ +import QtQuick +import Quickshell +import qs.Services +import qs.Commons + +Item { + id: root + + property string widgetId: "" + property var widgetProps: ({}) + property string screenName: widgetProps && widgetProps.screen ? widgetProps.screen.name : "" + property string section: widgetProps && widgetProps.section || "" + property int sectionIndex: widgetProps && widgetProps.sectionWidgetIndex || 0 + + // Don't reserve space unless the loaded widget is really visible + implicitWidth: getImplicitSize(loader.item, "implicitWidth") + implicitHeight: getImplicitSize(loader.item, "implicitHeight") + + Connections { + target: ScalingService + enabled: loader.item && (loader.item.screen !== undefined) + function onScaleChanged(aScreenName, scale) { + if (loader.item && loader.item.screen && aScreenName === screenName) { + loader.item['scaling'] = scale + } + } + } + + function getImplicitSize(item, prop) { + return (item && item.visible) ? item[prop] : 0 + } + + Loader { + id: loader + anchors.fill: parent + active: widgetId !== "" + asynchronous: false + sourceComponent: { + if (!active) { + return null + } + return ControlCenterWidgetRegistry.getWidget(widgetId) + } + + onLoaded: { + if (item && widgetProps) { + // Apply properties to loaded widget + for (var prop in widgetProps) { + if (item.hasOwnProperty(prop)) { + item[prop] = widgetProps[prop] + } + } + } + + if (item.hasOwnProperty("onLoaded")) { + item.onLoaded() + } + + //Logger.log("ControlCenterWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name) + } + + Component.onDestruction: { + // Explicitly clear references + widgetProps = null + } + } + + // Error handling + onWidgetIdChanged: { + if (widgetId && !ControlCenterWidgetRegistry.hasWidget(widgetId)) { + Logger.warn("ControlCenterWidgetLoader", "Widget not found in registry:", widgetId) + } + } +} diff --git a/Modules/ControlCenter/Widgets/Bluetooth.qml b/Modules/ControlCenter/Widgets/Bluetooth.qml new file mode 100644 index 00000000..7129ba64 --- /dev/null +++ b/Modules/ControlCenter/Widgets/Bluetooth.qml @@ -0,0 +1,17 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +NButton { + property ShellScreen screen + property real scaling: 1.0 + + outlined: true + text: "Bluetooth" + fontSize: Style.fontSizeS * scaling + fontWeight: Style.fontWeightRegular + icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" + onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) +} diff --git a/Modules/ControlCenter/Widgets/KeepAwake.qml b/Modules/ControlCenter/Widgets/KeepAwake.qml new file mode 100644 index 00000000..9840c5b3 --- /dev/null +++ b/Modules/ControlCenter/Widgets/KeepAwake.qml @@ -0,0 +1,17 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +NButton { + property ShellScreen screen + property real scaling: 1.0 + + outlined: true + text: IdleInhibitorService.isInhibited ? "Keep-awake" : "Keep-awake" + fontSize: Style.fontSizeS * scaling + fontWeight: Style.fontWeightRegular + icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off" + onClicked: IdleInhibitorService.manualToggle() +} diff --git a/Modules/ControlCenter/Widgets/NightLight.qml b/Modules/ControlCenter/Widgets/NightLight.qml new file mode 100644 index 00000000..841cc560 --- /dev/null +++ b/Modules/ControlCenter/Widgets/NightLight.qml @@ -0,0 +1,33 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +NButton { + property ShellScreen screen + property real scaling: 1.0 + + outlined: true + enabled: ProgramCheckerService.wlsunsetAvailable + text: "Night Light" + fontSize: Style.fontSizeS * scaling + fontWeight: Style.fontWeightRegular + icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off" + onClicked: { + if (!Settings.data.nightLight.enabled) { + Settings.data.nightLight.enabled = true + Settings.data.nightLight.forced = false + } else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) { + Settings.data.nightLight.forced = true + } else { + Settings.data.nightLight.enabled = false + Settings.data.nightLight.forced = false + } + } + onRightClicked: { + var settingsPanel = PanelService.getPanel("settingsPanel") + settingsPanel.requestedTab = SettingsPanel.Tab.Display + settingsPanel.open() + } +} diff --git a/Modules/ControlCenter/Widgets/PowerProfile.qml b/Modules/ControlCenter/Widgets/PowerProfile.qml new file mode 100644 index 00000000..b5b0df8d --- /dev/null +++ b/Modules/ControlCenter/Widgets/PowerProfile.qml @@ -0,0 +1,23 @@ +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.UPower +import qs.Commons +import qs.Services +import qs.Widgets + +// Performance +NButton { + 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 + icon: PowerProfileService.getIcon() + onClicked: { + PowerProfileService.cycleProfile() + } +} diff --git a/Modules/ControlCenter/Widgets/ScreenRecorder.qml b/Modules/ControlCenter/Widgets/ScreenRecorder.qml new file mode 100644 index 00000000..fb4b0825 --- /dev/null +++ b/Modules/ControlCenter/Widgets/ScreenRecorder.qml @@ -0,0 +1,24 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +NButton { + + property ShellScreen screen + property real scaling: 1.0 + + enabled: ProgramCheckerService.gpuScreenRecorderAvailable + outlined: true + icon: "camera-video" + text: "Screen Recorder" + fontWeight: Style.fontWeightRegular + onClicked: { + ScreenRecorderService.toggleRecording() + if (!ScreenRecorderService.isRecording) { + var panel = PanelService.getPanel("controlCenterPanel") + panel?.close() + } + } +} diff --git a/Modules/ControlCenter/Widgets/WallpaperSelector.qml b/Modules/ControlCenter/Widgets/WallpaperSelector.qml new file mode 100644 index 00000000..fd5a7b81 --- /dev/null +++ b/Modules/ControlCenter/Widgets/WallpaperSelector.qml @@ -0,0 +1,20 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +NButton { + 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 + onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this) + onRightClicked: WallpaperService.setRandomWallpaper() +} diff --git a/Modules/ControlCenter/Widgets/WiFi.qml b/Modules/ControlCenter/Widgets/WiFi.qml new file mode 100644 index 00000000..28d4e047 --- /dev/null +++ b/Modules/ControlCenter/Widgets/WiFi.qml @@ -0,0 +1,42 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +NButton { + property ShellScreen screen + property real scaling: 1.0 + + + outlined: true + icon: { + try { + if (NetworkService.ethernetConnected) { + return "ethernet" + } + let connected = false + let signalStrength = 0 + for (const net in NetworkService.networks) { + if (NetworkService.networks[net].connected) { + connected = true + signalStrength = NetworkService.networks[net].signal + break + } + } + return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off" + } catch (error) { + Logger.error("Wi-Fi", "Error getting icon:", error) + return "signal_wifi_bad" + } + } + text: { + if (NetworkService.ethernetConnected) { + return "Network" + } + return "Wi-Fi" + } + fontSize: Style.fontSizeS * scaling + fontWeight: Style.fontWeightRegular + onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) +} diff --git a/Modules/Settings/Bar/BarSectionEditor.qml b/Modules/Settings/Extras/SectionEditor.qml similarity index 96% rename from Modules/Settings/Bar/BarSectionEditor.qml rename to Modules/Settings/Extras/SectionEditor.qml index a36a1b17..af5a8380 100644 --- a/Modules/Settings/Bar/BarSectionEditor.qml +++ b/Modules/Settings/Extras/SectionEditor.qml @@ -13,6 +13,10 @@ NBox { property string sectionId: "" property var widgetModel: [] property var availableWidgets: [] + property bool enableMoveBetweenSections: true + + property var widgetRegistry: null + property string settingsDialogComponent: "BarWidgetSettingsDialog.qml" readonly property real miniButtonSize: Style.baseWidgetSize * 0.65 @@ -154,7 +158,7 @@ NBox { // Store the widget index for drag operations property int widgetIndex: index readonly property int buttonsWidth: Math.round(20 * scaling) - readonly property int buttonsCount: 1 + BarWidgetRegistry.widgetHasUserSettings(modelData.id) + readonly property int buttonsCount: 1 + (root.widgetRegistry ? root.widgetRegistry.widgetHasUserSettings(modelData.id) : 0) // Visual feedback during drag opacity: flowDragArea.draggedIndex === index ? 0.5 : 1.0 @@ -197,9 +201,10 @@ NBox { onTriggered: action => root.moveWidget(root.sectionId, index, action) } - // Update the MouseArea to use the new context menu + // MouseArea for the context menu MouseArea { id: contextMouseArea + enabled: enableMoveBetweenSections anchors.fill: parent acceptedButtons: Qt.RightButton z: -1 // Below the buttons but above background @@ -209,9 +214,7 @@ NBox { // Check if click is not on the buttons area const localX = mouse.x const buttonsStartX = parent.width - (parent.buttonsCount * parent.buttonsWidth) - if (localX < buttonsStartX) { - // Use the helper function to open at mouse position contextMenu.openAtItem(widgetItem, mouse.x, mouse.y) } } @@ -236,7 +239,7 @@ NBox { Layout.preferredWidth: buttonsCount * buttonsWidth Loader { - active: BarWidgetRegistry.widgetHasUserSettings(modelData.id) + active: root.widgetRegistry && root.widgetRegistry.widgetHasUserSettings(modelData.id) sourceComponent: NIconButton { icon: "settings" tooltipText: I18n.tr("tooltips.widget-settings") @@ -247,7 +250,7 @@ NBox { colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight) colorFgHover: Color.mOnPrimary onClicked: { - var component = Qt.createComponent(Qt.resolvedUrl("BarWidgetSettingsDialog.qml")) + var component = Qt.createComponent(Qt.resolvedUrl(root.settingsDialogComponent)) function instantiateAndOpen() { var dialog = component.createObject(root, { "widgetIndex": index, @@ -258,19 +261,19 @@ NBox { if (dialog) { dialog.open() } else { - Logger.error("BarSectionEditor", "Failed to create settings dialog instance") + Logger.error("WidgetSectionEditor", "Failed to create settings dialog instance") } } if (component.status === Component.Ready) { instantiateAndOpen() } else if (component.status === Component.Error) { - Logger.error("BarSectionEditor", component.errorString()) + Logger.error("WidgetSectionEditor", component.errorString()) } else { component.statusChanged.connect(function () { if (component.status === Component.Ready) { instantiateAndOpen() } else if (component.status === Component.Error) { - Logger.error("BarSectionEditor", component.errorString()) + Logger.error("WidgetSectionEditor", component.errorString()) } }) } diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index d13f5e9e..cb76dff4 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -29,6 +29,7 @@ NPanel { Audio, Bar, ColorScheme, + ControlCenter, OSD, Display, Dock, @@ -111,6 +112,10 @@ NPanel { id: notificationsTab NotificationsTab {} } + Component { + id: controlCenterTab + ControlCenterTab {} + } // Order *DOES* matter function updateTabsModel() { @@ -124,6 +129,11 @@ NPanel { "label": "settings.bar.title", "icon": "settings-bar", "source": barTab + }, { + "id": SettingsPanel.Tab.ControlCenter, + "label": "settings.control-center.title", + "icon": "settings-bar", + "source": controlCenterTab }, { "id": SettingsPanel.Tab.Dock, "label": "settings.dock.title", diff --git a/Modules/Settings/Tabs/BarTab.qml b/Modules/Settings/Tabs/BarTab.qml index 31405f5c..fff686ca 100644 --- a/Modules/Settings/Tabs/BarTab.qml +++ b/Modules/Settings/Tabs/BarTab.qml @@ -5,7 +5,7 @@ import Quickshell import qs.Commons import qs.Services import qs.Widgets -import qs.Modules.Settings.Bar +import qs.Modules.Settings.Extras ColumnLayout { id: root @@ -201,9 +201,11 @@ ColumnLayout { spacing: Style.marginM * scaling // Left Section - BarSectionEditor { + SectionEditor { sectionName: "Left" sectionId: "left" + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Settings/Bar/BarWidgetSettingsDialog.qml") + widgetRegistry: BarWidgetRegistry widgetModel: Settings.data.bar.widgets.left availableWidgets: availableWidgets onAddWidget: (widgetId, section) => _addWidgetToSection(widgetId, section) @@ -216,9 +218,11 @@ ColumnLayout { } // Center Section - BarSectionEditor { + SectionEditor { sectionName: "Center" sectionId: "center" + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Settings/Bar/BarWidgetSettingsDialog.qml") + widgetRegistry: BarWidgetRegistry widgetModel: Settings.data.bar.widgets.center availableWidgets: availableWidgets onAddWidget: (widgetId, section) => _addWidgetToSection(widgetId, section) @@ -231,9 +235,11 @@ ColumnLayout { } // Right Section - BarSectionEditor { + SectionEditor { sectionName: "Right" sectionId: "right" + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Settings/Bar/BarWidgetSettingsDialog.qml") + widgetRegistry: BarWidgetRegistry widgetModel: Settings.data.bar.widgets.right availableWidgets: availableWidgets onAddWidget: (widgetId, section) => _addWidgetToSection(widgetId, section) diff --git a/Modules/Settings/Tabs/ControlCenterTab.qml b/Modules/Settings/Tabs/ControlCenterTab.qml new file mode 100644 index 00000000..9f432137 --- /dev/null +++ b/Modules/Settings/Tabs/ControlCenterTab.qml @@ -0,0 +1,140 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Extras + +ColumnLayout { + id: root + spacing: Style.marginL * scaling + + // Handler for drag start - disables panel background clicks + function handleDragStart() { + var panel = PanelService.getPanel("settingsPanel") + if (panel && panel.disableBackgroundClick) { + panel.disableBackgroundClick() + } + } + + // Handler for drag end - re-enables panel background clicks + function handleDragEnd() { + var panel = PanelService.getPanel("settingsPanel") + if (panel && panel.enableBackgroundClick) { + panel.enableBackgroundClick() + } + } + + // Widgets Management Section + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + + NHeader { + label: I18n.tr("settings.controlCenter.widgets.section.label") + description: I18n.tr("settings.controlCenter.widgets.section.description") + } + + // Bar Sections + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: Style.marginM * scaling + spacing: Style.marginM * scaling + + // Quick Settings + SectionEditor { + sectionName: "Quick Settings" + sectionId: "quickSettings" + settingsDialogComponent: "" + widgetRegistry: ControlCenterWidgetRegistry + widgetModel: Settings.data.controlCenter.widgets["quickSettings"] + availableWidgets: availableWidgets + enableMoveBetweenSections: false + onAddWidget: (widgetId, section) => _addWidgetToSection(widgetId, section) + onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index) + onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex) + onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings) + onDragPotentialStarted: root.handleDragStart() + onDragPotentialEnded: root.handleDragEnd() + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } + + // --------------------------------- + // Signal functions + // --------------------------------- + function _addWidgetToSection(widgetId, section) { + var newWidget = { + "id": widgetId + } + if (ControlCenterWidgetRegistry.widgetHasUserSettings(widgetId)) { + var metadata = ControlCenterWidgetRegistry.widgetMetadata[widgetId] + if (metadata) { + Object.keys(metadata).forEach(function (key) { + if (key !== "allowUserSettings") { + newWidget[key] = metadata[key] + } + }) + } + } + Settings.data.controlCenter.widgets[section].push(newWidget) + } + + function _removeWidgetFromSection(section, index) { + if (index >= 0 && index < Settings.data.controlCenter.widgets[section].length) { + var newArray = Settings.data.controlCenter.widgets[section].slice() + var removedWidgets = newArray.splice(index, 1) + Settings.data.controlCenter.widgets[section] = newArray + + // Check that we still have a control center + if (removedWidgets[0].id === "ControlCenter" && BarService.lookupWidget("ControlCenter") === undefined) { + ToastService.showWarning(I18n.tr("toast.missing-control-center.label"), I18n.tr("toast.missing-control-center.description"), 12000) + } + } + } + + function _reorderWidgetInSection(section, fromIndex, toIndex) { + if (fromIndex >= 0 && fromIndex < Settings.data.controlCenter.widgets[section].length && toIndex >= 0 && toIndex < Settings.data.controlCenter.widgets[section].length) { + + // Create a new array to avoid modifying the original + var newArray = Settings.data.controlCenter.widgets[section].slice() + var item = newArray[fromIndex] + newArray.splice(fromIndex, 1) + newArray.splice(toIndex, 0, item) + + Settings.data.controlCenter.widgets[section] = newArray + //Logger.log("BarTab", "Widget reordered. New array:", JSON.stringify(newArray)) + } + } + + function _updateWidgetSettingsInSection(section, index, settings) { + // Update the widget settings in the Settings data + Settings.data.controlCenter.widgets[section][index] = settings + //Logger.log("BarTab", `Updated widget settings for ${settings.id} in ${section} section`) + } + + // Base list model for all combo boxes + ListModel { + id: availableWidgets + } + + Component.onCompleted: { + // Fill out availableWidgets ListModel + availableWidgets.clear() + ControlCenterWidgetRegistry.getAvailableWidgets().forEach(entry => { + availableWidgets.append({ + "key": entry, + "name": entry + }) + }) + } +} diff --git a/Services/ControlCenterWidgetRegistry.qml b/Services/ControlCenterWidgetRegistry.qml new file mode 100644 index 00000000..5fac8e00 --- /dev/null +++ b/Services/ControlCenterWidgetRegistry.qml @@ -0,0 +1,71 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Modules.ControlCenter.Widgets + +Singleton { + id: root + + // Widget registry object mapping widget names to components + property var widgets: ({ + "Bluetooth": bluetoothComponent, + "KeepAwake": keepAwakeComponent, + "NightLight": nightLightComponent, + "PowerProfile": powerProfileComponent, + "ScreenRecorder": screenRecorderComponent, + "WiFi": wiFiComponent, + "WallpaperSelector": wallpaperSelectorComponent + }) + + property var widgetMetadata: ({}) + + // Component definitions - these are loaded once at startup + property Component bluetoothComponent: Component { + Bluetooth {} + } + property Component keepAwakeComponent: Component { + KeepAwake {} + } + property Component nightLightComponent: Component { + NightLight {} + } + property Component powerProfileComponent: Component { + PowerProfile {} + } + property Component screenRecorderComponent: Component { + ScreenRecorder {} + } + property Component wiFiComponent: Component { + WiFi {} + } + property Component wallpaperSelectorComponent: Component { + WallpaperSelector {} + } + + function init() { + Logger.log("ControlCenterWidgetRegistry", "Service started") + } + + // ------------------------------ + // Helper function to get widget component by name + function getWidget(id) { + return widgets[id] || null + } + + // Helper function to check if widget exists + function hasWidget(id) { + return id in widgets + } + + // Get list of available widget id + function getAvailableWidgets() { + return Object.keys(widgets) + } + + // Helper function to check if widget has user settings + function widgetHasUserSettings(id) { + return (widgetMetadata[id] !== undefined) && (widgetMetadata[id].allowUserSettings === true) + } +} diff --git a/Widgets/NButton.qml b/Widgets/NButton.qml index 6ce1c75b..2f4f5daf 100644 --- a/Widgets/NButton.qml +++ b/Widgets/NButton.qml @@ -19,6 +19,7 @@ Rectangle { property int fontWeight: Style.fontWeightBold property real iconSize: Style.fontSizeL * scaling property bool outlined: false + property int horizontalAlignment: Qt.AlignHCenter // Signals signal clicked @@ -27,7 +28,6 @@ Rectangle { // Internal properties property bool hovered: false - property bool pressed: false // Dimensions implicitWidth: contentRow.implicitWidth + (Style.marginL * 2 * scaling) @@ -47,7 +47,7 @@ Rectangle { border.color: { if (!enabled) return Color.mOutline - if (pressed || hovered) + if (hovered) return backgroundColor return outlined ? backgroundColor : Color.transparent } @@ -71,7 +71,10 @@ Rectangle { // Content RowLayout { id: contentRow - anchors.centerIn: parent + anchors.verticalCenter: parent.verticalCenter + anchors.left: root.horizontalAlignment === Qt.AlignLeft ? parent.left : undefined + anchors.horizontalCenter: root.horizontalAlignment === Qt.AlignHCenter ? parent.horizontalCenter : undefined + anchors.leftMargin: root.horizontalAlignment === Qt.AlignLeft ? Style.marginL * scaling : 0 spacing: Style.marginXS * scaling // Icon (optional) @@ -84,8 +87,8 @@ Rectangle { if (!root.enabled) return Color.mOnSurfaceVariant if (root.outlined) { - if (root.pressed || root.hovered) - return root.backgroundColor + if (root.hovered) + return root.textColor return root.backgroundColor } return root.textColor