From db28879185a1bd3e15ef7b3aa8c2f195a60fcf03 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 25 Sep 2025 21:29:14 -0400 Subject: [PATCH] Reparations for drag and drop control center edits --- .../Components/DragDropDetailHost.qml | 17 +- .../ControlCenter/Components/DragDropGrid.qml | 393 +++++----- .../Components/DragDropWidgetWrapper.qml | 147 ++-- .../ControlCenter/Components/WidgetGrid.qml | 719 ------------------ Widgets/PieChartSizeControl.qml | 90 +-- 5 files changed, 312 insertions(+), 1054 deletions(-) delete mode 100644 Modules/ControlCenter/Components/WidgetGrid.qml diff --git a/Modules/ControlCenter/Components/DragDropDetailHost.qml b/Modules/ControlCenter/Components/DragDropDetailHost.qml index 7492198c..5e54242c 100644 --- a/Modules/ControlCenter/Components/DragDropDetailHost.qml +++ b/Modules/ControlCenter/Components/DragDropDetailHost.qml @@ -69,8 +69,23 @@ Item { Component { id: diskUsageDetailComponent DiskUsageDetail { - currentMountPath: root.expandedWidgetData?.currentMountPath || "/" + currentMountPath: root.expandedWidgetData?.mountPath || "/" instanceId: root.expandedWidgetData?.instanceId || "" + + onMountPathChanged: (newMountPath) => { + if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") { + const widgets = SettingsData.controlCenterWidgets || [] + const newWidgets = widgets.map(w => { + if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) { + const updatedWidget = Object.assign({}, w) + updatedWidget.mountPath = newMountPath + return updatedWidget + } + return w + }) + SettingsData.setControlCenterWidgets(newWidgets) + } + } } } } \ No newline at end of file diff --git a/Modules/ControlCenter/Components/DragDropGrid.qml b/Modules/ControlCenter/Components/DragDropGrid.qml index 2de74827..85baae6a 100644 --- a/Modules/ControlCenter/Components/DragDropGrid.qml +++ b/Modules/ControlCenter/Components/DragDropGrid.qml @@ -3,8 +3,9 @@ import qs.Common import qs.Services import qs.Modules.ControlCenter.Widgets import qs.Modules.ControlCenter.Components +import "../utils/layout.js" as LayoutUtils -Item { +Column { id: root property bool editMode: false @@ -18,66 +19,23 @@ Item { signal moveWidget(int fromIndex, int toIndex) signal toggleWidgetSize(int index) - readonly property int gridColumns: 4 - readonly property real cellWidth: (width - (gridSpacing + 1) * (gridColumns - 1)) / gridColumns - readonly property real cellHeight: 60 - readonly property real gridSpacing: 4 + spacing: editMode ? Theme.spacingL : Theme.spacingS - height: { - const dummy = [SettingsData.controlCenterWidgets?.length, widgetPositions.length] - return calculateGridHeight() + (detailHost.active ? detailHost.height + Theme.spacingL : 0) + property var currentRowWidgets: [] + property real currentRowWidth: 0 + property int expandedRowIndex: -1 + + function calculateRowsAndWidgets() { + return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex) } - function calculateGridHeight() { - const widgets = SettingsData.controlCenterWidgets || [] - if (widgets.length === 0) - return 0 - - let rows = [] - let currentRow = [] - let currentWidth = 0 - const spacing = gridSpacing - const baseWidth = width - - for (var i = 0; i < widgets.length; i++) { - const widget = widgets[i] - const widgetWidth = widget.width || 50 - - let itemWidth - if (widgetWidth <= 25) { - itemWidth = (baseWidth - spacing * 3) / 4 - } else if (widgetWidth <= 50) { - itemWidth = (baseWidth - spacing) / 2 - } else if (widgetWidth <= 75) { - itemWidth = (baseWidth - spacing * 2) * 0.75 - } else { - itemWidth = baseWidth - } - - if (currentRow.length > 0 && (currentWidth + spacing + itemWidth > baseWidth)) { - rows.push([...currentRow]) - currentRow = [widget] - currentWidth = itemWidth - } else { - currentRow.push(widget) - currentWidth += (currentRow.length > 1 ? spacing : 0) + itemWidth - } - } - - if (currentRow.length > 0) { - rows.push(currentRow) - } - - return rows.length * cellHeight + (rows.length > 0 ? (rows.length - 1) * spacing : 0) + property var layoutResult: { + const dummy = [expandedSection, expandedWidgetIndex, model?.controlCenterWidgets] + return calculateRowsAndWidgets() } - DragDropDetailHost { - id: detailHost - y: calculateGridHeight() - anchors.left: parent.left - anchors.right: parent.right - expandedSection: root.expandedSection - expandedWidgetData: root.expandedWidgetData + onLayoutResultChanged: { + expandedRowIndex = layoutResult.expandedRowIndex } function moveToTop(item) { @@ -92,131 +50,115 @@ Item { } Repeater { - id: widgetRepeater - model: SettingsData.controlCenterWidgets || [] + model: root.layoutResult.rows - DragDropWidgetWrapper { - id: widgetWrapper - - editMode: root.editMode - widgetData: modelData - widgetIndex: index - gridCellWidth: root.cellWidth - gridCellHeight: root.cellHeight - gridColumns: root.gridColumns - gridLayout: root - isSlider: { - const id = modelData.id || "" - return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider" + Column { + width: root.width + spacing: 0 + property int rowIndex: index + property var rowWidgets: modelData + property bool isSliderOnlyRow: { + const widgets = rowWidgets || [] + if (widgets.length === 0) return false + return widgets.every(w => w.id === "volumeSlider" || w.id === "brightnessSlider" || w.id === "inputVolumeSlider") } + topPadding: isSliderOnlyRow ? (root.editMode ? 4 : -6) : 0 + bottomPadding: isSliderOnlyRow ? (root.editMode ? 4 : -6) : 0 - widgetComponent: { - const id = modelData.id || "" - if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") { - return compoundPillComponent - } else if (id === "volumeSlider") { - return audioSliderComponent - } else if (id === "brightnessSlider") { - return brightnessSliderComponent - } else if (id === "inputVolumeSlider") { - return inputAudioSliderComponent - } else if (id === "battery") { - const widgetWidth = modelData.width || 50 - return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent - } else if (id === "diskUsage") { - return diskUsagePillComponent - } else { - const widgetWidth = modelData.width || 50 - return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent + Flow { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: rowWidgets || [] + + DragDropWidgetWrapper { + widgetData: modelData + property int globalWidgetIndex: { + const widgets = SettingsData.controlCenterWidgets || [] + for (var i = 0; i < widgets.length; i++) { + if (widgets[i].id === modelData.id) { + if (modelData.id === "diskUsage") { + if (widgets[i].instanceId === modelData.instanceId) { + return i + } + } else { + return i + } + } + } + return -1 + } + property int widgetWidth: modelData.width || 50 + width: { + const baseWidth = root.width + const spacing = Theme.spacingS + if (widgetWidth <= 25) { + return (baseWidth - spacing * 3) / 4 + } else if (widgetWidth <= 50) { + return (baseWidth - spacing) / 2 + } else if (widgetWidth <= 75) { + return (baseWidth - spacing * 2) * 0.75 + } else { + return baseWidth + } + } + height: isSliderOnlyRow ? 48 : 60 + + editMode: root.editMode + widgetIndex: globalWidgetIndex + gridCellWidth: width + gridCellHeight: height + gridColumns: 4 + gridLayout: root + isSlider: { + const id = modelData.id || "" + return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider" + } + + widgetComponent: { + const id = modelData.id || "" + if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") { + return compoundPillComponent + } else if (id === "volumeSlider") { + return audioSliderComponent + } else if (id === "brightnessSlider") { + return brightnessSliderComponent + } else if (id === "inputVolumeSlider") { + return inputAudioSliderComponent + } else if (id === "battery") { + return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent + } else if (id === "diskUsage") { + return diskUsagePillComponent + } else { + return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent + } + } + + onWidgetMoved: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) + onRemoveWidget: index => root.removeWidget(index) + onToggleWidgetSize: index => root.toggleWidgetSize(index) + } } } - x: calculateWidgetX(index) - y: calculateWidgetY(index) + DetailHost { + width: parent.width + height: active ? (250 + Theme.spacingS) : 0 + property bool active: { + if (root.expandedSection === "") return false - onWidgetMoved: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) - onRemoveWidget: index => root.removeWidget(index) - onToggleWidgetSize: index => root.toggleWidgetSize(index) + if (root.expandedSection.startsWith("diskUsage_") && root.expandedWidgetData) { + const expandedInstanceId = root.expandedWidgetData.instanceId + return rowWidgets.some(w => w.id === "diskUsage" && w.instanceId === expandedInstanceId) + } - Behavior on x { - enabled: !editMode - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Easing.OutCubic + return rowIndex === root.expandedRowIndex } + visible: active + expandedSection: root.expandedSection + expandedWidgetData: root.expandedWidgetData } - - Behavior on y { - enabled: !editMode - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Easing.OutCubic - } - } - } - } - - property var widgetPositions: calculateAllWidgetPositions() - - function calculateAllWidgetPositions() { - const widgets = SettingsData.controlCenterWidgets || [] - let positions = [] - let currentX = 0 - let currentY = 0 - - for (var i = 0; i < widgets.length; i++) { - const widget = widgets[i] - const widgetWidth = widget.width || 50 - let cellsNeeded = 1 - - if (widgetWidth <= 25) - cellsNeeded = 1 - else if (widgetWidth <= 50) - cellsNeeded = 2 - else if (widgetWidth <= 75) - cellsNeeded = 3 - else - cellsNeeded = 4 - - if (currentX + cellsNeeded > gridColumns) { - currentX = 0 - currentY++ - } - - const horizontalSpacing = gridSpacing - positions[i] = { - "x": currentX * cellWidth + (currentX > 0 ? currentX * horizontalSpacing : 0), - "y": currentY * cellHeight + (currentY > 0 ? currentY * gridSpacing : 0), - "cellsUsed": cellsNeeded - } - - currentX += cellsNeeded - - if (currentX >= gridColumns) { - currentX = 0 - currentY++ - } - } - - return positions - } - - function calculateWidgetX(widgetIndex) { - if (widgetIndex < 0 || widgetIndex >= widgetPositions.length) - return 0 - return widgetPositions[widgetIndex].x - } - - function calculateWidgetY(widgetIndex) { - if (widgetIndex < 0 || widgetIndex >= widgetPositions.length) - return 0 - return widgetPositions[widgetIndex].y - } - - Connections { - target: SettingsData - function onControlCenterWidgetsChanged() { - widgetPositions = calculateAllWidgetPositions() } } @@ -227,7 +169,7 @@ Item { property int widgetIndex: parent.widgetIndex || 0 property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") width: parent.width - height: cellHeight + height: 60 iconName: { switch (widgetData.id || "") { case "wifi": @@ -395,10 +337,9 @@ Item { return false } } - enabled: !root.editMode && (widgetDef?.enabled ?? true) + enabled: widgetDef?.enabled ?? true onToggled: { - if (root.editMode) - return + if (root.editMode) return switch (widgetData.id || "") { case "wifi": { @@ -431,12 +372,11 @@ Item { } } onExpandClicked: { - if (!root.editMode) - root.expandClicked(widgetData, widgetIndex) + if (root.editMode) return + root.expandClicked(widgetData, widgetIndex) } onWheelEvent: function (wheelEvent) { - if (root.editMode) - return + if (root.editMode) return const id = widgetData.id || "" if (id === "audioOutput") { if (!AudioService.sink || !AudioService.sink.audio) @@ -471,43 +411,67 @@ Item { Component { id: audioSliderComponent - AudioSliderRow { + Item { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 width: parent.width - height: 14 - enabled: !root.editMode - property color sliderTrackColor: Theme.surfaceContainerHigh + height: 16 + + AudioSliderRow { + anchors.centerIn: parent + width: parent.width + height: 14 + property color sliderTrackColor: Theme.surfaceContainerHigh + } } } Component { id: brightnessSliderComponent - BrightnessSliderRow { + Item { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 width: parent.width - height: 14 - enabled: !root.editMode - property color sliderTrackColor: Theme.surfaceContainerHigh + height: 16 + + BrightnessSliderRow { + anchors.centerIn: parent + width: parent.width + height: 14 + property color sliderTrackColor: Theme.surfaceContainerHigh + } } } Component { id: inputAudioSliderComponent - InputAudioSliderRow { + Item { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 width: parent.width - height: 14 - enabled: !root.editMode - property color sliderTrackColor: Theme.surfaceContainerHigh + height: 16 + + InputAudioSliderRow { + anchors.centerIn: parent + width: parent.width + height: 14 + property color sliderTrackColor: Theme.surfaceContainerHigh + } } } Component { id: batteryPillComponent BatteryPill { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 width: parent.width - height: cellHeight - enabled: !root.editMode + height: 60 + onExpandClicked: { - if (!root.editMode) - root.expandClicked(parent.widgetData, parent.widgetIndex) + if (!root.editMode) { + root.expandClicked(widgetData, widgetIndex) + } } } } @@ -515,12 +479,15 @@ Item { Component { id: smallBatteryComponent SmallBatteryButton { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 width: parent.width height: 48 - enabled: !root.editMode + onClicked: { - if (!root.editMode) - root.expandClicked(parent.widgetData, parent.widgetIndex) + if (!root.editMode) { + root.expandClicked(widgetData, widgetIndex) + } } } } @@ -530,9 +497,8 @@ Item { ToggleButton { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 - property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") width: parent.width - height: cellHeight + height: 60 iconName: { switch (widgetData.id || "") { @@ -545,7 +511,7 @@ Item { case "idleInhibitor": return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle" default: - return widgetDef?.icon || "help" + return "help" } } @@ -560,7 +526,7 @@ Item { case "idleInhibitor": return SessionService.idleInhibited ? "Keeping Awake" : "Keep Awake" default: - return widgetDef?.text || "Unknown" + return "Unknown" } } @@ -581,7 +547,7 @@ Item { } } - enabled: !root.editMode && (widgetDef?.enabled ?? true) +enabled: !root.editMode onClicked: { if (root.editMode) @@ -618,7 +584,6 @@ Item { SmallToggleButton { property var widgetData: parent.widgetData || {} property int widgetIndex: parent.widgetIndex || 0 - property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") width: parent.width height: 48 @@ -633,7 +598,7 @@ Item { case "idleInhibitor": return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle" default: - return widgetDef?.icon || "help" + return "help" } } @@ -654,7 +619,7 @@ Item { } } - enabled: !root.editMode && (widgetDef?.enabled ?? true) +enabled: !root.editMode onClicked: { if (root.editMode) @@ -689,14 +654,18 @@ Item { Component { id: diskUsagePillComponent DiskUsagePill { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 width: parent.width - height: cellHeight - enabled: !root.editMode - mountPath: parent.widgetData?.mountPath || "/" - instanceId: parent.widgetData?.instanceId || "" + height: 60 + + mountPath: widgetData.mountPath || "/" + instanceId: widgetData.instanceId || "" + onExpandClicked: { - if (!root.editMode) - root.expandClicked(parent.widgetData, parent.widgetIndex) + if (!root.editMode) { + root.expandClicked(widgetData, widgetIndex) + } } } } diff --git a/Modules/ControlCenter/Components/DragDropWidgetWrapper.qml b/Modules/ControlCenter/Components/DragDropWidgetWrapper.qml index 78f7e048..cc65a4cb 100644 --- a/Modules/ControlCenter/Components/DragDropWidgetWrapper.qml +++ b/Modules/ControlCenter/Components/DragDropWidgetWrapper.qml @@ -16,6 +16,8 @@ Item { property int gridColumns: 4 property var gridLayout: null + z: dragArea.drag.active ? 10000 : 1 + signal widgetMoved(int fromIndex, int toIndex) signal removeWidget(int index) signal toggleWidgetSize(int index) @@ -27,7 +29,7 @@ Item { else if (widgetWidth <= 75) return gridCellWidth * 3 else return gridCellWidth * 4 } - height: gridCellHeight + height: isSlider ? 16 : gridCellHeight Rectangle { id: dragIndicator @@ -37,7 +39,7 @@ Item { border.width: dragArea.drag.active ? 2 : 0 radius: Theme.cornerRadius opacity: dragArea.drag.active ? 0.8 : 1.0 - z: dragArea.drag.active ? 1000 : 1 + z: dragArea.drag.active ? 10000 : 1 Behavior on border.width { NumberAnimation { duration: 150 } @@ -56,13 +58,14 @@ Item { property int globalWidgetIndex: root.widgetIndex property int widgetWidth: root.widgetData?.width || 50 + MouseArea { id: editModeBlocker anchors.fill: parent enabled: root.editMode acceptedButtons: Qt.AllButtons - onPressed: mouse.accepted = true - onWheel: wheel.accepted = true + onPressed: function(mouse) { mouse.accepted = true } + onWheel: function(wheel) { wheel.accepted = true } z: 100 } } @@ -71,23 +74,23 @@ Item { id: dragArea anchors.fill: parent enabled: editMode - cursorShape: editMode ? Qt.OpenHandCursor : Qt.ArrowCursor + cursorShape: editMode ? Qt.OpenHandCursor : Qt.PointingHandCursor drag.target: editMode ? root : null drag.axis: Drag.XAndYAxis drag.smoothed: true - onPressed: { + onPressed: function(mouse) { if (editMode) { cursorShape = Qt.ClosedHandCursor - root.z = 1000 - root.parent.moveToTop(root) + if (root.gridLayout && root.gridLayout.moveToTop) { + root.gridLayout.moveToTop(root) + } } } - onReleased: { + onReleased: function(mouse) { if (editMode) { cursorShape = Qt.OpenHandCursor - root.z = 1 root.snapToGrid() } } @@ -97,6 +100,19 @@ Item { Drag.hotSpot.x: width / 2 Drag.hotSpot.y: height / 2 + function swapIndices(i, j) { + if (i === j) return; + const arr = SettingsData.controlCenterWidgets; + if (!arr || i < 0 || j < 0 || i >= arr.length || j >= arr.length) return; + + const copy = arr.slice(); + const tmp = copy[i]; + copy[i] = copy[j]; + copy[j] = tmp; + + SettingsData.setControlCenterWidgets(copy); + } + function snapToGrid() { if (!editMode || !gridLayout) return @@ -104,80 +120,90 @@ Item { const cellWidth = gridLayout.width / gridColumns const cellHeight = gridCellHeight + Theme.spacingS - let targetCol = Math.max(0, Math.round(globalPos.x / cellWidth)) - let targetRow = Math.max(0, Math.round(globalPos.y / cellHeight)) + const centerX = globalPos.x + (root.width / 2) + const centerY = globalPos.y + (root.height / 2) - const widgetCells = Math.ceil(root.width / cellWidth) + let targetCol = Math.max(0, Math.floor(centerX / cellWidth)) + let targetRow = Math.max(0, Math.floor(centerY / cellHeight)) - if (targetCol + widgetCells > gridColumns) { - targetCol = Math.max(0, gridColumns - widgetCells) - } + targetCol = Math.min(targetCol, gridColumns - 1) const newIndex = findBestInsertionIndex(targetRow, targetCol) if (newIndex !== widgetIndex && newIndex >= 0 && newIndex < (SettingsData.controlCenterWidgets?.length || 0)) { - widgetMoved(widgetIndex, newIndex) + swapIndices(widgetIndex, newIndex) } } function findBestInsertionIndex(targetRow, targetCol) { - const widgets = SettingsData.controlCenterWidgets || [] - if (!widgets.length) return 0 + const widgets = SettingsData.controlCenterWidgets || []; + const n = widgets.length; + if (!n || widgetIndex < 0 || widgetIndex >= n) return -1; - const targetPosition = targetRow * gridColumns + targetCol + function spanFor(width) { + const w = width ?? 50; + if (w <= 25) return 1; + if (w <= 50) return 2; + if (w <= 75) return 3; + return 4; + } - // Find the widget position closest to our target - let bestIndex = 0 - let bestDistance = Infinity + const cols = gridColumns || 4; - for (let i = 0; i <= widgets.length; i++) { - if (i === widgetIndex) continue + let row = 0, col = 0; + let draggedOrigKey = null; - let currentPos = calculatePositionForIndex(i) - let distance = Math.abs(currentPos - targetPosition) + const pos = []; - if (distance < bestDistance) { - bestDistance = distance - bestIndex = i > widgetIndex ? i - 1 : i + for (let i = 0; i < n; i++) { + const span = Math.min(spanFor(widgets[i].width), cols); + + if (col + span > cols) { + row++; + col = 0; + } + + const startCol = col; + const centerKey = row * cols + (startCol + (span - 1) / 2); + + if (i === widgetIndex) { + draggedOrigKey = centerKey; + } else { + pos.push({ index: i, row, startCol, span, centerKey }); + } + + col += span; + if (col >= cols) { + row++; + col = 0; } } - return Math.max(0, Math.min(bestIndex, widgets.length - 1)) - } + if (pos.length === 0) return -1; - function calculatePositionForIndex(index) { - const widgets = SettingsData.controlCenterWidgets || [] - let currentX = 0 - let currentY = 0 + const centerColCoord = targetCol + 0.5; + const targetKey = targetRow * cols + centerColCoord; - for (let i = 0; i < index && i < widgets.length; i++) { - if (i === widgetIndex) continue - - const widget = widgets[i] - const widgetWidth = widget.width || 50 - let cellsNeeded = widgetWidth <= 25 ? 1 : widgetWidth <= 50 ? 2 : widgetWidth <= 75 ? 3 : 4 - - if (currentX + cellsNeeded > gridColumns) { - currentX = 0 - currentY++ - } - - currentX += cellsNeeded - if (currentX >= gridColumns) { - currentX = 0 - currentY++ + for (let k = 0; k < pos.length; k++) { + const p = pos[k]; + if (p.row === targetRow && centerColCoord >= p.startCol && centerColCoord < (p.startCol + p.span)) { + return p.index; } } - return currentY * gridColumns + currentX - } + let lo = 0, hi = pos.length - 1; + if (targetKey <= pos[0].centerKey) return pos[0].index; + if (targetKey >= pos[hi].centerKey) return pos[hi].index; - function getWidgetWidth(widgetWidth) { - const cellWidth = gridLayout ? gridLayout.width / gridColumns : gridCellWidth - if (widgetWidth <= 25) return cellWidth - else if (widgetWidth <= 50) return cellWidth * 2 - else if (widgetWidth <= 75) return cellWidth * 3 - else return cellWidth * 4 + while (lo <= hi) { + const mid = (lo + hi) >> 1; + const mk = pos[mid].centerKey; + if (targetKey < mk) hi = mid - 1; + else if (targetKey > mk) lo = mid + 1; + else return pos[mid].index; + } + const movingUp = (draggedOrigKey != null) ? (targetKey < draggedOrigKey) : false; + return (movingUp ? pos[lo].index : pos[hi].index); } Rectangle { @@ -200,6 +226,7 @@ Item { MouseArea { anchors.fill: parent + cursorShape: Qt.PointingHandCursor onClicked: removeWidget(widgetIndex) } } diff --git a/Modules/ControlCenter/Components/WidgetGrid.qml b/Modules/ControlCenter/Components/WidgetGrid.qml deleted file mode 100644 index 86a8a45c..00000000 --- a/Modules/ControlCenter/Components/WidgetGrid.qml +++ /dev/null @@ -1,719 +0,0 @@ -import QtQuick -import qs.Common -import qs.Services -import qs.Modules.ControlCenter.Widgets -import qs.Modules.ControlCenter.Components -import "../utils/layout.js" as LayoutUtils - -Column { - id: root - - property bool editMode: false - property string expandedSection: "" - property int expandedWidgetIndex: -1 - property var model: null - property var expandedWidgetData: null - - signal expandClicked(var widgetData, int globalIndex) - signal removeWidget(int index) - signal moveWidget(int fromIndex, int toIndex) - signal toggleWidgetSize(int index) - - spacing: editMode ? Theme.spacingL : Theme.spacingS - - property var currentRowWidgets: [] - property real currentRowWidth: 0 - property int expandedRowIndex: -1 - - function calculateRowsAndWidgets() { - return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex) - } - - property var layoutResult: { - const dummy = [expandedSection, expandedWidgetIndex, model?.controlCenterWidgets] - return calculateRowsAndWidgets() - } - - onLayoutResultChanged: { - expandedRowIndex = layoutResult.expandedRowIndex - } - - Repeater { - model: root.layoutResult.rows - - Column { - width: root.width - spacing: 0 - property int rowIndex: index - property var rowWidgets: modelData - property bool isSliderOnlyRow: { - const widgets = rowWidgets || [] - if (widgets.length === 0) return false - return widgets.every(w => w.id === "volumeSlider" || w.id === "brightnessSlider" || w.id === "inputVolumeSlider") - } - topPadding: isSliderOnlyRow ? (root.editMode ? 4 : -12) : 0 - bottomPadding: isSliderOnlyRow ? (root.editMode ? 4 : -12) : 0 - - Flow { - width: parent.width - spacing: Theme.spacingS - - Repeater { - model: rowWidgets || [] - - Item { - property var widgetData: modelData - property int globalWidgetIndex: { - const widgets = SettingsData.controlCenterWidgets || [] - for (var i = 0; i < widgets.length; i++) { - if (widgets[i].id === modelData.id) { - if (modelData.id === "diskUsage") { - if (widgets[i].instanceId === modelData.instanceId) { - return i - } - } else { - return i - } - } - } - return -1 - } - property int widgetWidth: modelData.width || 50 - width: { - const baseWidth = root.width - const spacing = Theme.spacingS - if (widgetWidth <= 25) { - return (baseWidth - spacing * 3) / 4 - } else if (widgetWidth <= 50) { - return (baseWidth - spacing) / 2 - } else if (widgetWidth <= 75) { - return (baseWidth - spacing * 2) * 0.75 - } else { - return baseWidth - } - } - height: 60 - - Loader { - id: widgetLoader - anchors.fill: parent - property var widgetData: parent.widgetData - property int widgetIndex: parent.globalWidgetIndex - property int globalWidgetIndex: parent.globalWidgetIndex - property int widgetWidth: parent.widgetWidth - - sourceComponent: { - const id = modelData.id || "" - if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") { - return compoundPillComponent - } else if (id === "volumeSlider") { - return audioSliderComponent - } else if (id === "brightnessSlider") { - return brightnessSliderComponent - } else if (id === "inputVolumeSlider") { - return inputAudioSliderComponent - } else if (id === "battery") { - return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent - } else if (id === "diskUsage") { - return diskUsagePillComponent - } else { - return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent - } - } - - } - } - } - } - - DetailHost { - width: parent.width - height: active ? (250 + Theme.spacingS) : 0 - property bool active: { - if (root.expandedSection === "") return false - - if (root.expandedSection.startsWith("diskUsage_") && root.expandedWidgetData) { - const expandedInstanceId = root.expandedWidgetData.instanceId - return rowWidgets.some(w => w.id === "diskUsage" && w.instanceId === expandedInstanceId) - } - - return rowIndex === root.expandedRowIndex - } - visible: active - expandedSection: root.expandedSection - expandedWidgetData: root.expandedWidgetData - } - } - } - - Component { - id: compoundPillComponent - CompoundPill { - property var widgetData: parent.widgetData || {} - property int widgetIndex: parent.widgetIndex || 0 - property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") - width: parent.width - height: 60 - iconName: { - switch (widgetData.id || "") { - case "wifi": { - if (NetworkService.wifiToggling) { - return "sync" - } - if (NetworkService.networkStatus === "ethernet") { - return "settings_ethernet" - } - if (NetworkService.networkStatus === "wifi") { - return NetworkService.wifiSignalIcon - } - if (NetworkService.wifiEnabled) { - return "wifi_off" - } - return "wifi_off" - } - case "bluetooth": { - if (!BluetoothService.available) { - return "bluetooth_disabled" - } - if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) { - return "bluetooth_disabled" - } - return "bluetooth" - } - case "audioOutput": { - if (!AudioService.sink) return "volume_off" - let volume = AudioService.sink.audio.volume - let muted = AudioService.sink.audio.muted - if (muted || volume === 0.0) return "volume_off" - if (volume <= 0.33) return "volume_down" - if (volume <= 0.66) return "volume_up" - return "volume_up" - } - case "audioInput": { - if (!AudioService.source) return "mic_off" - let muted = AudioService.source.audio.muted - return muted ? "mic_off" : "mic" - } - default: return widgetDef?.icon || "help" - } - } - primaryText: { - switch (widgetData.id || "") { - case "wifi": { - if (NetworkService.wifiToggling) { - return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..." - } - if (NetworkService.networkStatus === "ethernet") { - return "Ethernet" - } - if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) { - return NetworkService.currentWifiSSID - } - if (NetworkService.wifiEnabled) { - return "Not connected" - } - return "WiFi off" - } - case "bluetooth": { - if (!BluetoothService.available) { - return "Bluetooth" - } - if (!BluetoothService.adapter) { - return "No adapter" - } - if (!BluetoothService.adapter.enabled) { - return "Disabled" - } - return "Enabled" - } - case "audioOutput": return AudioService.sink?.description || "No output device" - case "audioInput": return AudioService.source?.description || "No input device" - default: return widgetDef?.text || "Unknown" - } - } - secondaryText: { - switch (widgetData.id || "") { - case "wifi": { - if (NetworkService.wifiToggling) { - return "Please wait..." - } - if (NetworkService.networkStatus === "ethernet") { - return "Connected" - } - if (NetworkService.networkStatus === "wifi") { - return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected" - } - if (NetworkService.wifiEnabled) { - return "Select network" - } - return "" - } - case "bluetooth": { - if (!BluetoothService.available) { - return "No adapters" - } - if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) { - return "Off" - } - const primaryDevice = (() => { - if (!BluetoothService.adapter || !BluetoothService.adapter.devices) { - return null - } - let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))] - for (let device of devices) { - if (device && device.connected) { - return device - } - } - return null - })() - if (primaryDevice) { - return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device" - } - return "No devices" - } - case "audioOutput": { - if (!AudioService.sink) { - return "Select device" - } - if (AudioService.sink.audio.muted) { - return "Muted" - } - return Math.round(AudioService.sink.audio.volume * 100) + "%" - } - case "audioInput": { - if (!AudioService.source) { - return "Select device" - } - if (AudioService.source.audio.muted) { - return "Muted" - } - return Math.round(AudioService.source.audio.volume * 100) + "%" - } - default: return widgetDef?.description || "" - } - } - isActive: { - switch (widgetData.id || "") { - case "wifi": { - if (NetworkService.wifiToggling) { - return false - } - if (NetworkService.networkStatus === "ethernet") { - return true - } - if (NetworkService.networkStatus === "wifi") { - return true - } - return NetworkService.wifiEnabled - } - case "bluetooth": return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled) - case "audioOutput": return !!(AudioService.sink && !AudioService.sink.audio.muted) - case "audioInput": return !!(AudioService.source && !AudioService.source.audio.muted) - default: return false - } - } - enabled: (widgetDef?.enabled ?? true) - onToggled: { - if (root.editMode) return - switch (widgetData.id || "") { - case "wifi": { - if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) { - NetworkService.toggleWifiRadio() - } - break - } - case "bluetooth": { - if (BluetoothService.available && BluetoothService.adapter) { - BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled - } - break - } - case "audioOutput": { - if (AudioService.sink && AudioService.sink.audio) { - AudioService.sink.audio.muted = !AudioService.sink.audio.muted - } - break - } - case "audioInput": { - if (AudioService.source && AudioService.source.audio) { - AudioService.source.audio.muted = !AudioService.source.audio.muted - } - break - } - } - } - onExpandClicked: { - if (root.editMode) return - root.expandClicked(widgetData, widgetIndex) - } - onWheelEvent: function (wheelEvent) { - const id = widgetData.id || "" - if (id === "audioOutput") { - if (!AudioService.sink || !AudioService.sink.audio) return - let delta = wheelEvent.angleDelta.y - let currentVolume = AudioService.sink.audio.volume * 100 - let newVolume - if (delta > 0) - newVolume = Math.min(100, currentVolume + 5) - else - newVolume = Math.max(0, currentVolume - 5) - AudioService.sink.audio.muted = false - AudioService.sink.audio.volume = newVolume / 100 - wheelEvent.accepted = true - } else if (id === "audioInput") { - if (!AudioService.source || !AudioService.source.audio) return - let delta = wheelEvent.angleDelta.y - let currentVolume = AudioService.source.audio.volume * 100 - let newVolume - if (delta > 0) - newVolume = Math.min(100, currentVolume + 5) - else - newVolume = Math.max(0, currentVolume - 5) - AudioService.source.audio.muted = false - AudioService.source.audio.volume = newVolume / 100 - wheelEvent.accepted = true - } - } - - EditModeOverlay { - anchors.fill: parent - editMode: root.editMode - widgetData: parent.widgetData - widgetIndex: parent.widgetIndex - showSizeControls: true - isSlider: false - onRemoveWidget: (index) => root.removeWidget(index) - onToggleWidgetSize: (index) => root.toggleWidgetSize(index) - onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) - } - } - } - - Component { - id: audioSliderComponent - Item { - property var widgetData: parent.widgetData || {} - property int widgetIndex: parent.widgetIndex || 0 - property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") - width: parent.width - height: 16 - - AudioSliderRow { - anchors.centerIn: parent - width: parent.width - height: 14 - property color sliderTrackColor: Theme.surfaceContainerHigh - } - - EditModeOverlay { - anchors.fill: parent - editMode: root.editMode - widgetData: parent.widgetData - widgetIndex: parent.widgetIndex - showSizeControls: true - isSlider: true - onRemoveWidget: (index) => root.removeWidget(index) - onToggleWidgetSize: (index) => root.toggleWidgetSize(index) - onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) - } - } - } - - Component { - id: brightnessSliderComponent - Item { - property var widgetData: parent.widgetData || {} - property int widgetIndex: parent.widgetIndex || 0 - width: parent.width - height: 16 - - BrightnessSliderRow { - anchors.centerIn: parent - width: parent.width - height: 14 - property color sliderTrackColor: Theme.surfaceContainerHigh - } - - EditModeOverlay { - anchors.fill: parent - editMode: root.editMode - widgetData: parent.widgetData - widgetIndex: parent.widgetIndex - showSizeControls: true - isSlider: true - onRemoveWidget: (index) => root.removeWidget(index) - onToggleWidgetSize: (index) => root.toggleWidgetSize(index) - onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) - } - } - } - - Component { - id: inputAudioSliderComponent - Item { - property var widgetData: parent.widgetData || {} - property int widgetIndex: parent.widgetIndex || 0 - width: parent.width - height: 16 - - InputAudioSliderRow { - anchors.centerIn: parent - width: parent.width - height: 14 - property color sliderTrackColor: Theme.surfaceContainerHigh - } - - EditModeOverlay { - anchors.fill: parent - editMode: root.editMode - widgetData: parent.widgetData - widgetIndex: parent.widgetIndex - showSizeControls: true - isSlider: true - onRemoveWidget: (index) => root.removeWidget(index) - onToggleWidgetSize: (index) => root.toggleWidgetSize(index) - onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) - } - } - } - - Component { - id: batteryPillComponent - BatteryPill { - property var widgetData: parent.widgetData || {} - property int widgetIndex: parent.widgetIndex || 0 - width: parent.width - height: 60 - - onExpandClicked: { - if (!root.editMode) { - root.expandClicked(widgetData, widgetIndex) - } - } - - EditModeOverlay { - anchors.fill: parent - editMode: root.editMode - widgetData: parent.widgetData - widgetIndex: parent.widgetIndex - showSizeControls: true - isSlider: false - onRemoveWidget: (index) => root.removeWidget(index) - onToggleWidgetSize: (index) => root.toggleWidgetSize(index) - onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) - } - } - } - - Component { - id: smallBatteryComponent - SmallBatteryButton { - property var widgetData: parent.widgetData || {} - property int widgetIndex: parent.widgetIndex || 0 - width: parent.width - height: 48 - - onClicked: { - if (!root.editMode) { - root.expandClicked(widgetData, widgetIndex) - } - } - - EditModeOverlay { - anchors.fill: parent - editMode: root.editMode - widgetData: parent.widgetData - widgetIndex: parent.widgetIndex - showSizeControls: true - isSlider: false - onRemoveWidget: (index) => root.removeWidget(index) - onToggleWidgetSize: (index) => root.toggleWidgetSize(index) - onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) - } - } - } - - Component { - id: toggleButtonComponent - ToggleButton { - property var widgetData: parent.widgetData || {} - property int widgetIndex: parent.widgetIndex || 0 - property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") - width: parent.width - height: 60 - - iconName: { - switch (widgetData.id || "") { - case "nightMode": return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode" - case "darkMode": return "contrast" - case "doNotDisturb": return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off" - case "idleInhibitor": return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle" - default: return widgetDef?.icon || "help" - } - } - - text: { - switch (widgetData.id || "") { - case "nightMode": return "Night Mode" - case "darkMode": return SessionData.isLightMode ? "Light Mode" : "Dark Mode" - case "doNotDisturb": return "Do Not Disturb" - case "idleInhibitor": return SessionService.idleInhibited ? "Keeping Awake" : "Keep Awake" - default: return widgetDef?.text || "Unknown" - } - } - - secondaryText: "" - - iconRotation: widgetData.id === "darkMode" && SessionData.isLightMode ? 180 : 0 - - isActive: { - switch (widgetData.id || "") { - case "nightMode": return DisplayService.nightModeEnabled || false - case "darkMode": return !SessionData.isLightMode - case "doNotDisturb": return SessionData.doNotDisturb || false - case "idleInhibitor": return SessionService.idleInhibited || false - default: return false - } - } - - enabled: (widgetDef?.enabled ?? true) && !root.editMode - - onClicked: { - switch (widgetData.id || "") { - case "nightMode": { - if (DisplayService.automationAvailable) { - DisplayService.toggleNightMode() - } - break - } - case "darkMode": { - Theme.toggleLightMode() - break - } - case "doNotDisturb": { - SessionData.setDoNotDisturb(!SessionData.doNotDisturb) - break - } - case "idleInhibitor": { - SessionService.toggleIdleInhibit() - break - } - } - } - - EditModeOverlay { - anchors.fill: parent - editMode: root.editMode - widgetData: parent.widgetData - widgetIndex: parent.widgetIndex - showSizeControls: true - isSlider: false - onRemoveWidget: (index) => root.removeWidget(index) - onToggleWidgetSize: (index) => root.toggleWidgetSize(index) - onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) - } - } - } - - Component { - id: smallToggleComponent - SmallToggleButton { - property var widgetData: parent.widgetData || {} - property int widgetIndex: parent.widgetIndex || 0 - property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") - width: parent.width - height: 48 - - iconName: { - switch (widgetData.id || "") { - case "nightMode": return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode" - case "darkMode": return "contrast" - case "doNotDisturb": return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off" - case "idleInhibitor": return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle" - default: return widgetDef?.icon || "help" - } - } - - iconRotation: widgetData.id === "darkMode" && SessionData.isLightMode ? 180 : 0 - - isActive: { - switch (widgetData.id || "") { - case "nightMode": return DisplayService.nightModeEnabled || false - case "darkMode": return !SessionData.isLightMode - case "doNotDisturb": return SessionData.doNotDisturb || false - case "idleInhibitor": return SessionService.idleInhibited || false - default: return false - } - } - - enabled: (widgetDef?.enabled ?? true) && !root.editMode - - onClicked: { - switch (widgetData.id || "") { - case "nightMode": { - if (DisplayService.automationAvailable) { - DisplayService.toggleNightMode() - } - break - } - case "darkMode": { - Theme.toggleLightMode() - break - } - case "doNotDisturb": { - SessionData.setDoNotDisturb(!SessionData.doNotDisturb) - break - } - case "idleInhibitor": { - SessionService.toggleIdleInhibit() - break - } - } - } - - EditModeOverlay { - anchors.fill: parent - editMode: root.editMode - widgetData: parent.widgetData - widgetIndex: parent.widgetIndex - showSizeControls: true - isSlider: false - onRemoveWidget: (index) => root.removeWidget(index) - onToggleWidgetSize: (index) => root.toggleWidgetSize(index) - onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) - } - } - } - - Component { - id: diskUsagePillComponent - DiskUsagePill { - property var widgetData: parent.widgetData || {} - property int widgetIndex: parent.widgetIndex || 0 - width: parent.width - height: 60 - - mountPath: widgetData.mountPath || "/" - instanceId: widgetData.instanceId || "" - - onExpandClicked: { - if (!root.editMode) { - root.expandClicked(widgetData, widgetIndex) - } - } - - EditModeOverlay { - anchors.fill: parent - editMode: root.editMode - widgetData: parent.widgetData - widgetIndex: parent.widgetIndex - showSizeControls: true - isSlider: false - onRemoveWidget: (index) => root.removeWidget(index) - onToggleWidgetSize: (index) => root.toggleWidgetSize(index) - onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) - } - } - } -} \ No newline at end of file diff --git a/Widgets/PieChartSizeControl.qml b/Widgets/PieChartSizeControl.qml index 12eebd89..45ebe1f8 100644 --- a/Widgets/PieChartSizeControl.qml +++ b/Widgets/PieChartSizeControl.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Controls import qs.Common -Item { +Row { id: root property int currentSize: 50 @@ -11,75 +11,41 @@ Item { signal sizeChanged(int newSize) - width: 28 - height: 28 - readonly property var availableSizes: isSlider ? [50, 100] : [25, 50, 75, 100] - readonly property int currentSizeIndex: availableSizes.indexOf(currentSize) - Canvas { - id: pieCanvas - anchors.fill: parent + spacing: 2 - onPaint: { - const ctx = getContext("2d") - const centerX = width / 2 - const centerY = height / 2 - const radius = Math.min(width, height) / 2 - 2 + Repeater { + model: root.availableSizes - ctx.clearRect(0, 0, width, height) + Rectangle { + width: 16 + height: 16 + radius: 3 + color: modelData === root.currentSize ? Theme.primary : Theme.surfaceContainer + border.color: modelData === root.currentSize ? Theme.primary : Theme.outline + border.width: 1 - ctx.strokeStyle = Theme.primary - ctx.lineWidth = 1.5 - ctx.beginPath() - ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI) - ctx.stroke() + StyledText { + anchors.centerIn: parent + text: modelData.toString() + font.pixelSize: 8 + font.weight: Font.Medium + color: modelData === root.currentSize ? Theme.primaryContainer : Theme.surfaceText + } - if (availableSizes.length > 0 && currentSizeIndex >= 0) { - const segmentAngle = (2 * Math.PI) / availableSizes.length - const startAngle = -Math.PI / 2 - const endAngle = startAngle + segmentAngle * (currentSizeIndex + 1) + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + root.currentSize = modelData + root.sizeChanged(modelData) + } + } - ctx.fillStyle = Theme.primary - ctx.beginPath() - ctx.moveTo(centerX, centerY) - ctx.arc(centerX, centerY, radius - 1, startAngle, endAngle) - ctx.closePath() - ctx.fill() + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } } } } - - Rectangle { - anchors.centerIn: parent - width: 12 - height: 12 - radius: 6 - color: Theme.surfaceContainer - border.color: Theme.outline - border.width: 1 - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - const nextIndex = (currentSizeIndex + 1) % availableSizes.length - const newSize = availableSizes[nextIndex] - currentSize = newSize - pieCanvas.requestPaint() - sizeChanged(newSize) - } - } - - onCurrentSizeChanged: { - pieCanvas.requestPaint() - } - - onIsSliderChanged: { - if (isSlider && currentSize !== 50 && currentSize !== 100) { - currentSize = 50 - } - pieCanvas.requestPaint() - } } \ No newline at end of file