From 10090cbd301220e569a59a5078b1eb63c1bdb1cc Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 13 Oct 2025 18:30:19 -0400 Subject: [PATCH] ControlCenter: modularity! --- Assets/Translations/de.json | 16 +- Assets/Translations/en.json | 16 +- Assets/Translations/es.json | 16 +- Assets/Translations/fr.json | 16 +- Assets/Translations/pt.json | 18 +- Assets/Translations/zh-CN.json | 16 +- Commons/Settings.qml | 22 +- .../ControlCenter/Cards/PowerProfilesCard.qml | 66 ---- Modules/ControlCenter/Cards/ShortcutsCard.qml | 84 +++++ Modules/ControlCenter/Cards/UtilitiesCard.qml | 66 ---- Modules/ControlCenter/ControlCenterPanel.qml | 128 +++++--- Modules/ControlCenter/Widgets/Bluetooth.qml | 2 +- Modules/ControlCenter/Widgets/KeepAwake.qml | 2 +- Modules/ControlCenter/Widgets/NightLight.qml | 2 +- .../ControlCenter/Widgets/Notifications.qml | 2 +- .../ControlCenter/Widgets/PowerProfile.qml | 2 +- .../ControlCenter/Widgets/ScreenRecorder.qml | 7 +- .../Widgets/WallpaperSelector.qml | 2 +- Modules/ControlCenter/Widgets/WiFi.qml | 2 +- Modules/Settings/Extras/SectionEditor.qml | 23 +- Modules/Settings/Tabs/ControlCenterTab.qml | 278 ++++++++++------ Widgets/NIconButtonHot.qml | 178 +++++++++++ Widgets/NQuickSetting.qml | 160 ---------- Widgets/NReorderCheckboxes.qml | 299 ++++++++++++++++++ 24 files changed, 930 insertions(+), 493 deletions(-) delete mode 100644 Modules/ControlCenter/Cards/PowerProfilesCard.qml create mode 100644 Modules/ControlCenter/Cards/ShortcutsCard.qml delete mode 100644 Modules/ControlCenter/Cards/UtilitiesCard.qml create mode 100644 Widgets/NIconButtonHot.qml delete mode 100644 Widgets/NQuickSetting.qml create mode 100644 Widgets/NReorderCheckboxes.qml diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 1d6e4b48..779f6227 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -730,14 +730,16 @@ "label": "Aussehen" }, "title": "Kontrollzentrum", - "audio-controls": { - "description": "Verschiedene Audio-Lautstärken sofort über das Kontrollzentrum anpassen.", - "label": "Lautstärkeregler" - }, - "quick-settings": { + "cards": { "section": { - "description": "Konfigurieren und verwalten Sie die Schnellzugriff-Widgets.", - "label": "Schnelleinstellungen-Widgets" + "description": "Passen Sie an, welche Steuerelemente im Kontrollzentrum angezeigt werden und in welcher Reihenfolge.", + "label": "Karten" + } + }, + "shortcuts": { + "section": { + "description": "Konfigurieren und verwalten Sie die Verknüpfungs-Widgets.", + "label": "Widgets für Kurzbefehle" }, "sectionLeft": "Links", "sectionRight": "Richtig" diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 7c1cdaa5..2390684b 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -731,17 +731,19 @@ "label": "Position", "description": "Choose where the Control Center panel appears when opened." }, - "quick-settings": { + "cards": { "section": { - "label": "Quick settings widgets", - "description": "Configure and manage the quick settings widgets." + "label": "Cards", + "description": "Customize which controls appear in the Control Center and in what order." + } + }, + "shortcuts": { + "section": { + "label": "Shortcuts widgets", + "description": "Configure and manage the shortcuts widgets." }, "sectionLeft": "Left", "sectionRight": "Right" - }, - "audio-controls": { - "label": "Volume sliders", - "description": "Instantly adjust different audio volumes from the Control Center." } }, "user-interface": { diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 3fa757bf..480f6f04 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -730,14 +730,16 @@ "label": "Apariencia" }, "title": "Centro de control", - "audio-controls": { - "description": "Ajusta instantáneamente diferentes volúmenes de audio desde el Centro de control.", - "label": "Deslizadores de volumen" - }, - "quick-settings": { + "cards": { "section": { - "description": "Configurar y administrar los widgets de configuración rápida.", - "label": "Widgets de configuración rápida" + "description": "Personaliza qué controles aparecen en el Centro de control y en qué orden.", + "label": "Tarjetas" + } + }, + "shortcuts": { + "section": { + "description": "Configurar y administrar los widgets de accesos directos.", + "label": "Widgets de accesos directos" }, "sectionLeft": "Izquierda", "sectionRight": "Derecha" diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 5c105b26..6ef274e4 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -730,14 +730,16 @@ "label": "Apparence" }, "title": "Centre de contrôle", - "audio-controls": { - "description": "Ajustez instantanément différents volumes audio depuis le Centre de contrôle.", - "label": "Curseurs de volume" - }, - "quick-settings": { + "cards": { "section": { - "description": "Configurer et gérer les widgets des paramètres rapides.", - "label": "Widgets de paramètres rapides" + "description": "Personnalisez les commandes qui apparaissent dans le Centre de contrôle et leur ordre d'affichage.", + "label": "Cartes" + } + }, + "shortcuts": { + "section": { + "description": "Configurer et gérer les widgets de raccourcis.", + "label": "Widgets de raccourcis" }, "sectionLeft": "Gauche", "sectionRight": "Droite" diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 6b591e0c..30b3d5f9 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -730,17 +730,19 @@ "label": "Aparência" }, "title": "Central de Controle", - "audio-controls": { - "description": "Ajuste instantaneamente diferentes volumes de áudio a partir da Central de Controle.", - "label": "Controles deslizantes de volume" - }, - "quick-settings": { + "cards": { "section": { - "description": "Configure e gerencie os widgets de configurações rápidas.", - "label": "Widgets de configurações rápidas" + "description": "Personalize quais controles aparecem na Central de Controle e em que ordem.", + "label": "Cartas" + } + }, + "shortcuts": { + "section": { + "description": "Configure e gerencie os widgets de atalhos.", + "label": "Widgets de atalhos" }, "sectionLeft": "Esquerda", - "sectionRight": "Direito/Certo/Correto" + "sectionRight": "Direito/Certo/À direita" } }, "user-interface": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 000b0b7f..801fd614 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -730,14 +730,16 @@ "label": "外观" }, "title": "控制中心", - "audio-controls": { - "description": "从控制中心即时调整不同的音频音量。", - "label": "音量滑块" - }, - "quick-settings": { + "cards": { "section": { - "description": "配置和管理快速设置小部件。", - "label": "快速设置小部件" + "description": "自定义在控制中心显示的控制项及其顺序。", + "label": "卡片" + } + }, + "shortcuts": { + "section": { + "description": "配置和管理快捷方式小部件。", + "label": "快捷方式小部件" }, "sectionLeft": "左", "sectionRight": "右" diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 29f9441c..07d59764 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -159,10 +159,6 @@ Singleton { "id": "Tray" }, { "id": "NotificationHistory" - }, { - "id": "WiFi" - }, { - "id": "Bluetooth" }, { "id": "Battery" }, { @@ -245,9 +241,8 @@ 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 bool audioControlsEnabled: true - property JsonObject widgets - widgets: JsonObject { + property JsonObject shortcuts + shortcuts: JsonObject { property list left: [{ "id": "WiFi" }, { @@ -263,6 +258,19 @@ Singleton { "id": "WallpaperSelector" }] } + property list cards: [{ + "id": "profile-card", + "enabled": true + }, { + "id": "shortcuts-card", + "enabled": true + }, { + "id": "audio-card", + "enabled": true + }, { + "id": "media-sysmon-card", + "enabled": true + }] } // dock diff --git a/Modules/ControlCenter/Cards/PowerProfilesCard.qml b/Modules/ControlCenter/Cards/PowerProfilesCard.qml deleted file mode 100644 index d4e85695..00000000 --- a/Modules/ControlCenter/Cards/PowerProfilesCard.qml +++ /dev/null @@ -1,66 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Services.UPower -import qs.Commons -import qs.Services -import qs.Widgets - -// Power Profiles: performance, balanced, eco -NBox { - - property real spacing: 0 - - // Centralized service - readonly property bool hasPP: PowerProfileService.available - - RowLayout { - id: powerRow - anchors.fill: parent - anchors.margins: Style.marginS - spacing: spacing - Item { - Layout.fillWidth: true - } - // Performance - NIconButton { - 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 { - 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 { - 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) - } - Item { - Layout.fillWidth: true - } - } -} diff --git a/Modules/ControlCenter/Cards/ShortcutsCard.qml b/Modules/ControlCenter/Cards/ShortcutsCard.qml new file mode 100644 index 00000000..3c5526bc --- /dev/null +++ b/Modules/ControlCenter/Cards/ShortcutsCard.qml @@ -0,0 +1,84 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Modules.ControlCenter.Cards +import qs.Commons +import qs.Services +import qs.Widgets +import qs.Modules.ControlCenter.Extras + +RowLayout { + Layout.fillWidth: true + spacing: Style.marginL + + NBox { + Layout.fillWidth: true + Layout.preferredHeight: root.shortcutsHeight + + RowLayout { + id: leftContent + anchors.fill: parent + spacing: Style.marginS + + Item { + Layout.fillWidth: true + } + + Repeater { + model: Settings.data.controlCenter.shortcuts.left + delegate: ControlCenterWidgetLoader { + Layout.fillWidth: false + widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetProps: { + "screen": root.modelData || null, + "widgetId": modelData.id, + "section": "quickSettings", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.controlCenter.shortcuts.left.length + } + Layout.alignment: Qt.AlignVCenter + } + } + + Item { + Layout.fillWidth: true + } + } + } + + NBox { + Layout.fillWidth: true + Layout.preferredHeight: root.shortcutsHeight + + RowLayout { + id: rightContent + anchors.fill: parent + spacing: Style.marginS + + Item { + Layout.fillWidth: true + } + + Repeater { + model: Settings.data.controlCenter.shortcuts.right + delegate: ControlCenterWidgetLoader { + Layout.fillWidth: false + widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetProps: { + "screen": root.modelData || null, + "widgetId": modelData.id, + "section": "quickSettings", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.controlCenter.shortcuts.right.length + } + Layout.alignment: Qt.AlignVCenter + } + } + + Item { + Layout.fillWidth: true + } + } + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Cards/UtilitiesCard.qml b/Modules/ControlCenter/Cards/UtilitiesCard.qml deleted file mode 100644 index 29b821ef..00000000 --- a/Modules/ControlCenter/Cards/UtilitiesCard.qml +++ /dev/null @@ -1,66 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import qs.Commons -import qs.Modules.Settings -import qs.Services -import qs.Widgets - -// Utilities: record & wallpaper -NBox { - - property real spacing: 0 - - RowLayout { - id: utilRow - anchors.fill: parent - anchors.margins: Style.marginS - spacing: spacing - Item { - Layout.fillWidth: true - } - // Screen Recorder - NIconButton { - icon: "camera-video" - enabled: ScreenRecorderService.isAvailable - tooltipText: ScreenRecorderService.isAvailable ? (ScreenRecorderService.isRecording ? I18n.tr("tooltips.stop-screen-recording") : I18n.tr("tooltips.start-screen-recording")) : I18n.tr("tooltips.screen-recorder-not-installed") - colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant - colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary - onClicked: { - if (!ScreenRecorderService.isAvailable) - return - ScreenRecorderService.toggleRecording() - // If we were not recording and we just initiated a start, close the panel - if (!ScreenRecorderService.isRecording) { - var panel = PanelService.getPanel("controlCenterPanel") - panel?.close() - } - } - } - - // Idle Inhibitor - NIconButton { - 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() - } - } - - // Wallpaper - NIconButton { - visible: Settings.data.wallpaper.enabled - icon: "wallpaper-selector" - tooltipText: I18n.tr("tooltips.wallpaper-selector") - onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this) - onRightClicked: WallpaperService.setRandomWallpaper() - } - - Item { - Layout.fillWidth: true - } - } -} diff --git a/Modules/ControlCenter/ControlCenterPanel.qml b/Modules/ControlCenter/ControlCenterPanel.qml index f07f8e65..4f997b17 100644 --- a/Modules/ControlCenter/ControlCenterPanel.qml +++ b/Modules/ControlCenter/ControlCenterPanel.qml @@ -13,11 +13,33 @@ NPanel { panelKeyboardFocus: true preferredWidth: Math.round(460 * Style.uiScaleRatio) preferredHeight: { - let height = profileHeight + weatherHeight + mediaSysMonHeight + utilsHeight - let count = 4 - if (Settings.data.controlCenter.audioControlsEnabled) { + var height = 0 + var count = 0 + for (var i = 0; i < Settings.data.controlCenter.cards.length; i++) { + const card = Settings.data.controlCenter.cards[i] + if (!card.enabled) { + continue + } count++ - height += audioHeight + switch (card.id) { + case "profile-card": + height += profileHeight + break + case "shortcuts-card": + height += shortcutsHeight + break + case "audio-card": + height += audioHeight + break + case "weather-card": + height += weatherHeight + break + case "media-sysmon-card": + height += mediaSysMonHeight + break + default: + break + } } return height + (count + 1) * Style.marginL } @@ -32,15 +54,14 @@ NPanel { panelAnchorTop: controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("top_") readonly property int profileHeight: Math.round(64 * Style.uiScaleRatio) + readonly property int shortcutsHeight: Math.round(52 * Style.uiScaleRatio) + readonly property int audioHeight: Math.round(120 * Style.uiScaleRatio) readonly property int weatherHeight: Math.round(190 * Style.uiScaleRatio) readonly property int mediaSysMonHeight: Math.round(260 * Style.uiScaleRatio) - readonly property int audioHeight: Math.round(120 * Style.uiScaleRatio) - readonly property int utilsHeight: Math.round(52 * Style.uiScaleRatio) panelContent: Item { id: content - // Layout content ColumnLayout { id: layout x: Style.marginL @@ -48,50 +69,69 @@ NPanel { width: parent.width - (Style.marginL * 2) spacing: Style.marginL - // Profile - ProfileCard { - Layout.fillWidth: true - Layout.preferredHeight: profileHeight - } - - // Utils - RowLayout { - Layout.fillWidth: true - Layout.preferredHeight: utilsHeight - spacing: Style.marginL - - // Power Profiles switcher - PowerProfilesCard { + Repeater { + model: Settings.data.controlCenter.cards + Loader { + active: modelData.enabled + visible: active Layout.fillWidth: true - Layout.fillHeight: true - spacing: Style.marginL - } - - // Utilities buttons - UtilitiesCard { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: Style.marginL + Layout.preferredHeight: { + switch (modelData.id) { + case "profile-card": + return profileHeight + case "shortcuts-card": + return shortcutsHeight + case "audio-card": + return audioHeight + case "weather-card": + return weatherHeight + case "media-sysmon-card": + return mediaSysMonHeight + default: + return 0 + } + } + sourceComponent: { + switch (modelData.id) { + case "profile-card": + return profileCard + case "shortcuts-card": + return shortcutsCard + case "audio-card": + return audioCard + case "weather-card": + return weatherCard + case "media-sysmon-card": + return mediaSysMonCard + } + } } } + } - // Audio controls - AudioCard { - visible: Settings.data.controlCenter.audioControlsEnabled - Layout.fillWidth: true - Layout.preferredHeight: audioHeight - } + Component { + id: profileCard + ProfileCard {} + } - // Weather - WeatherCard { - Layout.fillWidth: true - Layout.preferredHeight: weatherHeight - } + Component { + id: shortcutsCard + ShortcutsCard {} + } - // Media + SysMon + Component { + id: audioCard + AudioCard {} + } + + Component { + id: weatherCard + WeatherCard {} + } + + Component { + id: mediaSysMonCard RowLayout { - Layout.fillWidth: true - Layout.preferredHeight: mediaSysMonHeight spacing: Style.marginL // Media card diff --git a/Modules/ControlCenter/Widgets/Bluetooth.qml b/Modules/ControlCenter/Widgets/Bluetooth.qml index fbaccbe8..e4549a31 100644 --- a/Modules/ControlCenter/Widgets/Bluetooth.qml +++ b/Modules/ControlCenter/Widgets/Bluetooth.qml @@ -4,7 +4,7 @@ import qs.Commons import qs.Services import qs.Widgets -NQuickSetting { +NIconButtonHot { property ShellScreen screen icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" diff --git a/Modules/ControlCenter/Widgets/KeepAwake.qml b/Modules/ControlCenter/Widgets/KeepAwake.qml index 5a423e1d..96d60733 100644 --- a/Modules/ControlCenter/Widgets/KeepAwake.qml +++ b/Modules/ControlCenter/Widgets/KeepAwake.qml @@ -4,7 +4,7 @@ import qs.Commons import qs.Services import qs.Widgets -NQuickSetting { +NIconButtonHot { property ShellScreen screen icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off" diff --git a/Modules/ControlCenter/Widgets/NightLight.qml b/Modules/ControlCenter/Widgets/NightLight.qml index 22306ff9..eb338882 100644 --- a/Modules/ControlCenter/Widgets/NightLight.qml +++ b/Modules/ControlCenter/Widgets/NightLight.qml @@ -4,7 +4,7 @@ import qs.Commons import qs.Services import qs.Widgets -NQuickSetting { +NIconButtonHot { property ShellScreen screen enabled: ProgramCheckerService.wlsunsetAvailable diff --git a/Modules/ControlCenter/Widgets/Notifications.qml b/Modules/ControlCenter/Widgets/Notifications.qml index cd5fb1c1..ae291517 100644 --- a/Modules/ControlCenter/Widgets/Notifications.qml +++ b/Modules/ControlCenter/Widgets/Notifications.qml @@ -4,7 +4,7 @@ import qs.Commons import qs.Services import qs.Widgets -NQuickSetting { +NIconButtonHot { property ShellScreen screen icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell" diff --git a/Modules/ControlCenter/Widgets/PowerProfile.qml b/Modules/ControlCenter/Widgets/PowerProfile.qml index f09f3205..ca5f497f 100644 --- a/Modules/ControlCenter/Widgets/PowerProfile.qml +++ b/Modules/ControlCenter/Widgets/PowerProfile.qml @@ -6,7 +6,7 @@ import qs.Services import qs.Widgets // Performance -NQuickSetting { +NIconButtonHot { property ShellScreen screen readonly property bool hasPP: PowerProfileService.available diff --git a/Modules/ControlCenter/Widgets/ScreenRecorder.qml b/Modules/ControlCenter/Widgets/ScreenRecorder.qml index b616a4b6..7a58c8f3 100644 --- a/Modules/ControlCenter/Widgets/ScreenRecorder.qml +++ b/Modules/ControlCenter/Widgets/ScreenRecorder.qml @@ -4,18 +4,13 @@ import qs.Commons import qs.Services import qs.Widgets -NQuickSetting { +NIconButtonHot { property ShellScreen screen enabled: ProgramCheckerService.gpuScreenRecorderAvailable icon: "camera-video" hot: ScreenRecorderService.isRecording tooltipText: I18n.tr("quickSettings.screenRecorder.tooltip.action") - - // Force hover state when recording to get hover colors - property bool originalHovered: hovered - hovered: ScreenRecorderService.isRecording || originalHovered - onClicked: { ScreenRecorderService.toggleRecording() if (!ScreenRecorderService.isRecording) { diff --git a/Modules/ControlCenter/Widgets/WallpaperSelector.qml b/Modules/ControlCenter/Widgets/WallpaperSelector.qml index b0bb5f86..ff513bc1 100644 --- a/Modules/ControlCenter/Widgets/WallpaperSelector.qml +++ b/Modules/ControlCenter/Widgets/WallpaperSelector.qml @@ -4,7 +4,7 @@ import qs.Commons import qs.Services import qs.Widgets -NQuickSetting { +NIconButtonHot { property ShellScreen screen enabled: Settings.data.wallpaper.enabled diff --git a/Modules/ControlCenter/Widgets/WiFi.qml b/Modules/ControlCenter/Widgets/WiFi.qml index 33051009..faaedc73 100644 --- a/Modules/ControlCenter/Widgets/WiFi.qml +++ b/Modules/ControlCenter/Widgets/WiFi.qml @@ -4,7 +4,7 @@ import qs.Commons import qs.Services import qs.Widgets -NQuickSetting { +NIconButtonHot { property ShellScreen screen icon: { diff --git a/Modules/Settings/Extras/SectionEditor.qml b/Modules/Settings/Extras/SectionEditor.qml index 58989aa4..66936eec 100644 --- a/Modules/Settings/Extras/SectionEditor.qml +++ b/Modules/Settings/Extras/SectionEditor.qml @@ -14,11 +14,13 @@ NBox { property var widgetModel: [] property var availableWidgets: [] property bool enableMoveBetweenSections: true + property int maxWidgets: -1 // -1 means unlimited property var widgetRegistry: null property string settingsDialogComponent: "BarWidgetSettingsDialog.qml" readonly property real miniButtonSize: Style.baseWidgetSize * 0.65 + readonly property bool isAtMaxCapacity: maxWidgets > 0 && widgetModel.length >= maxWidgets signal addWidget(string widgetId, string section) signal removeWidget(string section, int index) @@ -80,6 +82,16 @@ NBox { Layout.alignment: Qt.AlignVCenter } + // Widget count indicator (when max is set) + NText { + visible: root.maxWidgets > 0 + text: "(" + widgetModel.length + "/" + root.maxWidgets + ")" + pointSize: Style.fontSizeS + color: root.isAtMaxCapacity ? Color.mError : Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Style.marginXS + } + Item { Layout.fillWidth: true } @@ -93,6 +105,7 @@ NBox { onSelected: key => comboBox.currentKey = key popupHeight: 340 minimumWidth: 200 + enabled: !root.isAtMaxCapacity Layout.alignment: Qt.AlignVCenter @@ -114,12 +127,14 @@ NBox { colorFg: Color.mOnPrimary colorBgHover: Color.mSecondary colorFgHover: Color.mOnSecondary - enabled: comboBox.currentKey !== "" - tooltipText: I18n.tr("tooltips.add-widget") + enabled: comboBox.currentKey !== "" && !root.isAtMaxCapacity + tooltipText: root.isAtMaxCapacity + ? I18n.tr("tooltips.max-widgets-reached") + : I18n.tr("tooltips.add-widget") Layout.alignment: Qt.AlignVCenter Layout.leftMargin: Style.marginS onClicked: { - if (comboBox.currentKey !== "") { + if (comboBox.currentKey !== "" && !root.isAtMaxCapacity) { addWidget(comboBox.currentKey, sectionId) comboBox.currentKey = "" } @@ -588,4 +603,4 @@ NBox { } } } -} +} \ No newline at end of file diff --git a/Modules/Settings/Tabs/ControlCenterTab.qml b/Modules/Settings/Tabs/ControlCenterTab.qml index e022d40b..cf683b6d 100644 --- a/Modules/Settings/Tabs/ControlCenterTab.qml +++ b/Modules/Settings/Tabs/ControlCenterTab.qml @@ -11,6 +11,34 @@ ColumnLayout { id: root spacing: Style.marginL + property list cardsModel: [] + property list cardsDefault: [{ + "id": "profile-card", + "text": "Profile", + "enabled": true, + "required": true + }, { + "id": "shortcuts-card", + "text": "Shortcuts", + "enabled": true, + "required": false + }, { + "id": "weather-card", + "text": "Weather", + "enabled": true, + "required": false + }, { + "id": "audio-card", + "text": "Audio Sliders", + "enabled": true, + "required": false + }, { + "id": "media-sysmon-card", + "text": "Media and System Monitor", + "enabled": true, + "required": false + }] + // Handler for drag start - disables panel background clicks function handleDragStart() { var panel = PanelService.getPanel("settingsPanel") @@ -27,6 +55,60 @@ ColumnLayout { } } + function saveCards() { + var toSave = [] + for (var i = 0; i < cardsModel.length; i++) { + toSave.push({ + "id": cardsModel[i].id, + "enabled": cardsModel[i].enabled + }) + } + Settings.data.controlCenter.cards = toSave + } + + Component.onCompleted: { + // Fill out availableWidgets ListModel + availableWidgets.clear() + ControlCenterWidgetRegistry.getAvailableWidgets().forEach(entry => { + availableWidgets.append({ + "key": entry, + "name": entry + }) + }) + // Starts empty + cardsModel = [] + + // Add the cards available in settings + for (var i = 0; i < Settings.data.controlCenter.cards.length; i++) { + const settingCard = Settings.data.controlCenter.cards[i] + + for (var j = 0; j < cardsDefault.length; j++) { + if (settingCard.id === cardsDefault[j].id) { + var card = cardsDefault[j] + card.enabled = settingCard.enabled + cardsModel.push(card) + } + } + } + + // Add any missing cards from default + for (var i = 0; i < cardsDefault.length; i++) { + var found = false + for (var j = 0; j < cardsModel.length; j++) { + if (cardsModel[j].id === cardsDefault[i].id) { + found = true + break + } + } + + if (!found) { + cardsModel.push(cardsDefault[i]) + } + } + + saveCards() + } + ColumnLayout { spacing: Style.marginL Layout.fillWidth: true @@ -70,11 +152,49 @@ ColumnLayout { } } - NToggle { - label: I18n.tr("settings.control-center.audio-controls.label") - description: I18n.tr("settings.control-center.audio-controls.description") - checked: Settings.data.controlCenter.audioControlsEnabled - onToggled: checked => Settings.data.controlCenter.audioControlsEnabled = checked + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL + Layout.bottomMargin: Style.marginXL + } + + // Widgets Management Section + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + + NHeader { + label: I18n.tr("settings.control-center.cards.section.label") + description: I18n.tr("settings.control-center.cards.section.description") + } + + NReorderCheckboxes { + Layout.fillWidth: true + model: cardsModel + onDragPotentialStarted: { + root.handleDragStart() + } + onDragPotentialEnded: { + root.handleDragEnd() + } + onItemToggled: function (index, enabled) { + //Logger.log("ControlCenterTab", "Item", index, "toggled to", enabled) + var newModel = cardsModel.slice() + newModel[index] = Object.assign({}, newModel[index], { + "enabled": enabled + }) + cardsModel = newModel + saveCards() + } + onItemsReordered: function (fromIndex, toIndex) { + //Logger.log("ControlCenterTab", "Item moved from", fromIndex, "to", toIndex) + var newModel = cardsModel.slice() + var item = newModel.splice(fromIndex, 1)[0] + newModel.splice(toIndex, 0, item) + cardsModel = newModel + saveCards() + } + } } NDivider { @@ -83,70 +203,66 @@ ColumnLayout { Layout.bottomMargin: Style.marginXL } - // NDivider { - // Layout.fillWidth: true - // Layout.topMargin: Style.marginXL - // Layout.bottomMargin: Style.marginXL - // } + // Widgets Management Section + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true - // // Widgets Management Section - // ColumnLayout { - // spacing: Style.marginXXS - // Layout.fillWidth: true + NHeader { + label: I18n.tr("settings.control-center.shortcuts.section.label") + description: I18n.tr("settings.control-center.shortcuts.section.description") + } - // NHeader { - // label: I18n.tr("settings.control-center.quickSettings.section.label") - // description: I18n.tr("settings.control-center.quickSettings.section.description") - // } + // Sections + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: Style.marginM + spacing: Style.marginM - // // Sections - // ColumnLayout { - // Layout.fillWidth: true - // Layout.fillHeight: true - // Layout.topMargin: Style.marginM - // spacing: Style.marginM + // Left + SectionEditor { + sectionName: I18n.tr("settings.control-center.shortcuts.sectionLeft") + sectionId: "left" + settingsDialogComponent: "" + maxWidgets: 5 + widgetRegistry: ControlCenterWidgetRegistry + widgetModel: Settings.data.controlCenter.shortcuts["left"] + 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() + } - // // Left - // SectionEditor { - // sectionName: I18n.tr("settings.control-center.quickSettings.sectionLeft") - // sectionId: "left" - // settingsDialogComponent: "" - // widgetRegistry: ControlCenterWidgetRegistry - // widgetModel: Settings.data.controlCenter.widgets["left"] - // 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() - // } + // Right + SectionEditor { + sectionName: I18n.tr("settings.control-center.shortcuts.sectionRight") + sectionId: "right" + settingsDialogComponent: "" + maxWidgets: 5 + widgetRegistry: ControlCenterWidgetRegistry + widgetModel: Settings.data.controlCenter.shortcuts["right"] + 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() + } + } + } - // // Right - // SectionEditor { - // sectionName: I18n.tr("settings.control-center.quickSettings.sectionRight") - // sectionId: "right" - // settingsDialogComponent: "" - // widgetRegistry: ControlCenterWidgetRegistry - // widgetModel: Settings.data.controlCenter.widgets["right"] - // 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 - // Layout.bottomMargin: Style.marginXL - // } + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL + Layout.bottomMargin: Style.marginXL + } // --------------------------------- // Signal functions @@ -165,55 +281,37 @@ ColumnLayout { }) } } - Settings.data.controlCenter.widgets[section].push(newWidget) + Settings.data.controlCenter.shortcuts[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() + if (index >= 0 && index < Settings.data.controlCenter.shortcuts[section].length) { + var newArray = Settings.data.controlCenter.shortcuts[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) - } + Settings.data.controlCenter.shortcuts[section] = newArray } } function _reorderWidgetInSection(section, fromIndex, toIndex) { - if (fromIndex >= 0 && fromIndex < Settings.data.controlCenter.widgets[section].length && toIndex >= 0 && toIndex < Settings.data.controlCenter.widgets[section].length) { + if (fromIndex >= 0 && fromIndex < Settings.data.controlCenter.shortcuts[section].length && toIndex >= 0 && toIndex < Settings.data.controlCenter.shortcuts[section].length) { // Create a new array to avoid modifying the original - var newArray = Settings.data.controlCenter.widgets[section].slice() + var newArray = Settings.data.controlCenter.shortcuts[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)) + Settings.data.controlCenter.shortcuts[section] = 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`) + Settings.data.controlCenter.shortcuts[section][index] = settings } // 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/Widgets/NIconButtonHot.qml b/Widgets/NIconButtonHot.qml new file mode 100644 index 00000000..ec8887b2 --- /dev/null +++ b/Widgets/NIconButtonHot.qml @@ -0,0 +1,178 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import qs.Commons +import qs.Services + +Rectangle { + id: root + + // Size properties (matching NIconButton) + property real baseSize: Style.baseWidgetSize + property bool applyUiScale: true + + // Public properties + property string icon: "" + property string tooltipText: "" + property string tooltipDirection: "auto" + property bool enabled: true + property bool allowClickWhenDisabled: false + property bool compact: false + + // Hot state (unique to NIconButtonHot) + property bool hot: false + + // Internal properties + property bool hovering: false + property bool pressed: false + + // Color properties (matching NIconButton structure) + property color colorBg: Color.mSurfaceVariant + property color colorFg: Color.mPrimary + property color colorBgHover: Color.mTertiary + property color colorFgHover: Color.mOnTertiary + property color colorBorder: Color.mOutline + property color colorBorderHover: Color.mOutline + + // Hot state colors + property color colorBgHot: Color.mPrimary + property color colorFgHot: Color.mOnPrimary + + // Signals + signal entered + signal exited + signal clicked + signal rightClicked + signal middleClicked + + // Dimensions (matching NIconButton) + implicitWidth: applyUiScale ? Math.round(baseSize * Style.uiScaleRatio) : Math.round(baseSize) + implicitHeight: applyUiScale ? Math.round(baseSize * Style.uiScaleRatio) : Math.round(baseSize) + + // Appearance (matching NIconButton) + opacity: root.enabled ? Style.opacityFull : Style.opacityMedium + color: { + if (pressed) { + return colorBgHover + } + if (hot) { + return colorBgHot + } + if (root.enabled && root.hovering) { + return colorBgHover + } + return colorBg + } + radius: width * 0.5 // Circular like NIconButton + border.color: root.enabled && root.hovering ? colorBorderHover : colorBorder + border.width: Math.max(1, Style.borderS) + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + // Icon (matching NIconButton positioning and sizing) + NIcon { + icon: root.icon + pointSize: Math.max(1, root.compact ? root.width * 0.65 : root.width * 0.48) + applyUiScale: root.applyUiScale + color: { + if (pressed) { + return colorFgHover + } + if (hot) { + return colorFgHot + } + if (root.enabled && root.hovering) { + return colorFgHover + } + return colorFg + } + // Center horizontally + x: (root.width - width) / 2 + // Center vertically accounting for font metrics + y: (root.height - height) / 2 + (height - contentHeight) / 2 + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + } + + MouseArea { + // Always enabled to allow hover/tooltip even when the button is disabled + enabled: true + anchors.fill: parent + cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + hoverEnabled: true + + onEntered: { + hovering = root.enabled ? true : false + if (tooltipText) { + TooltipService.show(Screen, parent, tooltipText, tooltipDirection) + } + root.entered() + } + + onExited: { + hovering = false + if (tooltipText) { + TooltipService.hide() + } + root.exited() + } + + onPressed: function (mouse) { + if (root.enabled) { + root.pressed = true + root.scale = 0.92 + } + if (tooltipText) { + TooltipService.hide() + } + } + + onReleased: function (mouse) { + root.scale = 1.0 + root.pressed = false + + if (!root.enabled && !allowClickWhenDisabled) { + return + } + + // Only trigger actions if released while hovering + if (root.hovering) { + 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.hovering = false + root.pressed = false + root.scale = 1.0 + if (tooltipText) { + TooltipService.hide() + } + } + } +} \ No newline at end of file diff --git a/Widgets/NQuickSetting.qml b/Widgets/NQuickSetting.qml deleted file mode 100644 index 0ddd2dfd..00000000 --- a/Widgets/NQuickSetting.qml +++ /dev/null @@ -1,160 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtQuick.Effects -import qs.Commons -import qs.Services - -Rectangle { - id: root - - // Public properties - property string icon: "" - property string tooltipText: "" - property bool enabled: true - property bool hot: false - - // Styling properties - property real iconSize: Style.fontSizeM - property real cornerRadius: Style.radiusS - - // Internal properties - property bool hovered: false - property bool pressed: false - - // Colors - property color backgroundColor: { - if (pressed) { - return Color.mTertiary - } - if (hot) { - return Color.mPrimary - } - return Color.mSurface - } - property color iconColor: { - if (pressed) { - return Color.mOnTertiary - } - if (hot) { - return Color.mOnPrimary - } - return Color.mOnSurface - } - property color hoverColor: Color.mTertiary - property color hoverIconColor: Color.mOnTertiary - - // Signals - signal clicked - signal rightClicked - signal middleClicked - - // Dimensions - implicitWidth: Style.baseWidgetSize * 0.8 - implicitHeight: Style.baseWidgetSize * 0.8 - - // Appearance - radius: cornerRadius - color: { - if (!enabled) - return Qt.lighter(Color.mSurface, 1.1) - if (hovered) - return hoverColor - return backgroundColor - } - - border.width: 0 - opacity: enabled ? 1.0 : 0.6 - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - easing.type: Easing.OutCubic - } - } - - Behavior on scale { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutCubic - } - } - - // Icon - NIcon { - anchors.centerIn: parent - 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 - } - } - } - - 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.92 - if (tooltipText) { - TooltipService.hide() - } - } - - onReleased: mouse => { - root.scale = 1.0 - root.pressed = false - - // Only trigger actions if released while hovering - if (root.hovered) { - 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() - } - } - } -} diff --git a/Widgets/NReorderCheckboxes.qml b/Widgets/NReorderCheckboxes.qml new file mode 100644 index 00000000..9a8c1abc --- /dev/null +++ b/Widgets/NReorderCheckboxes.qml @@ -0,0 +1,299 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons + +Item { + id: root + + // Public API + property var model: [] + property color activeColor: Color.mPrimary + property color activeOnColor: Color.mOnPrimary + property color dragHandleColor: Color.mOutline + property int baseSize: Style.baseWidgetSize * 0.7 + property int spacing: Style.marginM + + signal itemToggled(int index, bool enabled) + signal itemsReordered(int fromIndex, int toIndex) + signal dragPotentialStarted + signal dragPotentialEnded + + implicitHeight: listView.contentHeight + + function toggleItem(index) { + if (index < 0 || index >= root.model.length) + return + + var item = root.model[index] + if (item.required) + return + + // Create a new array to trigger binding update + var newModel = root.model.slice() + newModel[index] = Object.assign({}, item, { + "enabled": !item.enabled + }) + root.model = newModel + + root.itemToggled(index, newModel[index].enabled) + } + + function moveItem(fromIndex, toIndex) { + if (fromIndex === toIndex) + return + if (fromIndex < 0 || fromIndex >= root.model.length) + return + if (toIndex < 0 || toIndex >= root.model.length) + return + + // Create a new array with item moved + var newModel = root.model.slice() + var item = newModel.splice(fromIndex, 1)[0] + newModel.splice(toIndex, 0, item) + root.model = newModel + + root.itemsReordered(fromIndex, toIndex) + } + + ListView { + id: listView + + anchors.fill: parent + spacing: root.spacing + interactive: false + clip: true + model: root.model + + delegate: Item { + id: delegateItem + + width: listView.width + height: checkboxRow.height + + required property int index + required property var modelData + + property string text: modelData.text || "" + property bool enabled: modelData.enabled || false + property bool required: modelData.required || false + property bool dragging: false + property int dragStartY: 0 + property int dragStartIndex: -1 + property int dragTargetIndex: -1 + property int itemSpacing: root.spacing + + RowLayout { + id: checkboxRow + + width: parent.width + spacing: Style.marginS + + // Drag handle + Rectangle { + id: dragHandle + + Layout.preferredWidth: root.baseSize + Layout.preferredHeight: root.baseSize + radius: Style.radiusXS + color: dragHandleMouseArea.containsMouse ? Color.mSurfaceVariant : Color.transparent + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginS + + Repeater { + model: 3 + Rectangle { + Layout.preferredWidth: root.baseSize * 0.4 + Layout.preferredHeight: 2 + radius: 1 + color: root.dragHandleColor + } + } + } + + MouseArea { + id: dragHandleMouseArea + + anchors.fill: parent + cursorShape: Qt.SizeVerCursor + hoverEnabled: true + preventStealing: false + z: 1000 + + onPressed: mouse => { + delegateItem.dragStartIndex = delegateItem.index + delegateItem.dragTargetIndex = delegateItem.index + delegateItem.dragStartY = delegateItem.y + delegateItem.dragging = true + delegateItem.z = 999 + + // Signal that interaction started (prevents panel close) + preventStealing = true + root.dragPotentialStarted() + } + + onPositionChanged: mouse => { + if (delegateItem.dragging) { + var dy = mouse.y - dragHandle.height / 2 + var newY = delegateItem.y + dy + + // Constrain within bounds + newY = Math.max(0, Math.min(newY, listView.contentHeight - delegateItem.height)) + delegateItem.y = newY + + // Calculate target index (but don't apply yet) + var targetIndex = Math.floor((newY + delegateItem.height / 2) / (delegateItem.height + delegateItem.itemSpacing)) + targetIndex = Math.max(0, Math.min(targetIndex, listView.count - 1)) + + delegateItem.dragTargetIndex = targetIndex + } + } + + onReleased: { + // Always signal end of interaction + preventStealing = false + root.dragPotentialEnded() + + // Apply the model change now that drag is complete + if (delegateItem.dragStartIndex !== -1 && delegateItem.dragTargetIndex !== -1 && delegateItem.dragStartIndex !== delegateItem.dragTargetIndex) { + root.moveItem(delegateItem.dragStartIndex, delegateItem.dragTargetIndex) + } + + delegateItem.dragging = false + delegateItem.dragStartIndex = -1 + delegateItem.dragTargetIndex = -1 + delegateItem.z = 0 + } + + onCanceled: { + // Handle cancel (e.g., ESC key pressed during drag) + preventStealing = false + root.dragPotentialEnded() + + delegateItem.dragging = false + delegateItem.dragStartIndex = -1 + delegateItem.dragTargetIndex = -1 + delegateItem.z = 0 + } + } + } + + // Checkbox + Rectangle { + id: box + + Layout.preferredWidth: root.baseSize + Layout.preferredHeight: root.baseSize + radius: Style.radiusXS + color: delegateItem.enabled ? root.activeColor : Color.mSurface + border.color: delegateItem.required ? root.activeColor : Color.mOutline + border.width: Math.max(1, Style.borderS) + opacity: delegateItem.required ? 0.7 : 1.0 + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + NIcon { + visible: delegateItem.enabled + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + icon: "check" + color: root.activeOnColor + pointSize: Math.max(Style.fontSizeXS, root.baseSize * 0.5) + font.weight: Style.fontWeightBold + } + + MouseArea { + anchors.fill: parent + cursorShape: delegateItem.required ? Qt.ForbiddenCursor : Qt.PointingHandCursor + enabled: !delegateItem.required + + onClicked: { + root.toggleItem(delegateItem.index) + } + } + } + + // Label + NText { + Layout.fillWidth: true + text: delegateItem.text + color: Color.mOnSurface + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + // Required indicator + NText { + visible: delegateItem.required + text: "(required)" + color: Color.mOnSurfaceVariant + verticalAlignment: Text.AlignVCenter + } + } + + // Position binding for non-dragging state + y: { + if (delegateItem.dragging) { + return delegateItem.y + } + + // Check if any item is being dragged + var draggedIndex = -1 + var targetIndex = -1 + for (var i = 0; i < listView.count; i++) { + var item = listView.itemAtIndex(i) + if (item && item.dragging) { + draggedIndex = item.dragStartIndex + targetIndex = item.dragTargetIndex + break + } + } + + // If an item is being dragged, adjust positions + if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) { + var currentIndex = delegateItem.index + + if (draggedIndex < targetIndex) { + // Dragging down: shift items up between draggedIndex and targetIndex + if (currentIndex > draggedIndex && currentIndex <= targetIndex) { + return (currentIndex - 1) * (delegateItem.height + delegateItem.itemSpacing) + } + } else { + // Dragging up: shift items down between targetIndex and draggedIndex + if (currentIndex >= targetIndex && currentIndex < draggedIndex) { + return (currentIndex + 1) * (delegateItem.height + delegateItem.itemSpacing) + } + } + } + + return delegateItem.index * (delegateItem.height + delegateItem.itemSpacing) + } + + Behavior on y { + enabled: !delegateItem.dragging + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + } + } +}