diff --git a/Commons/Settings.qml b/Commons/Settings.qml index feebe5d..c01c5fe 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -100,6 +100,31 @@ Singleton { Logger.log("Settings", "Settings loaded successfully") isLoaded = true + for (var i = 0; i < adapter.bar.widgets.left.length; i++) { + var obj = adapter.bar.widgets.left[i] + if (typeof obj === "string") { + adapter.bar.widgets.left[i] = { + "id": obj + } + } + } + for (var i = 0; i < adapter.bar.widgets.center.length; i++) { + var obj = adapter.bar.widgets.center[i] + if (typeof obj === "string") { + adapter.bar.widgets.center[i] = { + "id": obj + } + } + } + for (var i = 0; i < adapter.bar.widgets.right.length; i++) { + var obj = adapter.bar.widgets.right[i] + if (typeof obj === "string") { + adapter.bar.widgets.right[i] = { + "id": obj + } + } + } + // Emit the signal root.settingsLoaded() @@ -126,6 +151,8 @@ Singleton { JsonAdapter { id: adapter + property int settingsVersion: 1 + // bar property JsonObject bar: JsonObject { property string position: "top" // Possible values: "top", "bottom" @@ -140,9 +167,39 @@ Singleton { // Widget configuration for modular bar system property JsonObject widgets widgets: JsonObject { - property list left: ["SystemMonitor", "ActiveWindow", "MediaMini"] - property list center: ["Workspace"] - property list right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "NightLight", "Clock", "SidePanelToggle"] + property list left: [{ + "id": "SystemMonitor" + }, { + "id": "ActiveWindow" + }, { + "id": "MediaMini" + }] + property list center: [{ + "id": "Workspace" + }] + property list right: [{ + "id": "ScreenRecorderIndicator" + }, { + "id": "Tray" + }, { + "id": "NotificationHistory" + }, { + "id": "WiFi" + }, { + "id": "Bluetooth" + }, { + "id": "Battery" + }, { + "id": "Volume" + }, { + "id": "Brightness" + }, { + "id": "NightLight" + }, { + "id": "Clock" + }, { + "id": "SidePanelToggle" + }] } } diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 59e6655..7365227 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -73,7 +73,7 @@ Variants { Repeater { model: Settings.data.bar.widgets.left delegate: NWidgetLoader { - widgetName: modelData + widgetId: (modelData.id !== undefined ? modelData.id : "") widgetProps: { "screen": root.modelData || null, "scaling": ScalingService.getScreenScale(screen), @@ -100,8 +100,7 @@ Variants { Repeater { model: Settings.data.bar.widgets.center delegate: NWidgetLoader { - - widgetName: modelData + widgetId: (modelData.id !== undefined ? modelData.id : "") widgetProps: { "screen": root.modelData || null, "scaling": ScalingService.getScreenScale(screen), @@ -129,7 +128,7 @@ Variants { Repeater { model: Settings.data.bar.widgets.right delegate: NWidgetLoader { - widgetName: modelData + widgetId: (modelData.id !== undefined ? modelData.id : "") widgetProps: { "screen": root.modelData || null, "scaling": ScalingService.getScreenScale(screen), diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml new file mode 100644 index 0000000..8e8252b --- /dev/null +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -0,0 +1,87 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets +import qs.Modules.SettingsPanel + +NIconButton { + id: root + + // Widget properties passed from Bar.qml + property var screen + property real scaling: 1.0 + + property string barSection: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + // Get user settings from Settings data + property var widgetSettings: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + // Use settings or defaults from BarWidgetRegistry + readonly property string userIcon: widgetSettings.icon || BarWidgetRegistry.widgetMetadata["CustomButton"].icon + readonly property string userLeftClickExec: widgetSettings.leftClickExec + || BarWidgetRegistry.widgetMetadata["CustomButton"].leftClickExec + readonly property string userRightClickExec: widgetSettings.rightClickExec + || BarWidgetRegistry.widgetMetadata["CustomButton"].rightClickExec + readonly property string userMiddleClickExec: widgetSettings.middleClickExec + || BarWidgetRegistry.widgetMetadata["CustomButton"].middleClickExec + readonly property bool hasExec: (userLeftClickExec || userRightClickExec || userMiddleClickExec) + + icon: userIcon + tooltipText: { + if (!hasExec) { + return "Custom Button - Configure in settings" + } else { + var lines = [] + if (userLeftClickExec !== "") { + lines.push(`Left click: ${userLeftClickExec}`) + } + if (userRightClickExec !== "") { + lines.push(`Right click: ${userRightClickExec}`) + } + if (userLeftClickExec !== "") { + lines.push(`Middle click: ${userMiddleClickExec}`) + } + return lines.join("
") + } + } + opacity: hasExec ? Style.opacityFull : Style.opacityMedium + + onClicked: { + if (userLeftClickExec) { + Quickshell.execDetached(userLeftClickExec.split(" ")) + Logger.log("CustomButton", `Executing command: ${userLeftClickExec}`) + } else if (!hasExec) { + // No script was defined, open settings + var settingsPanel = PanelService.getPanel("settingsPanel") + settingsPanel.requestedTab = SettingsPanel.Tab.Bar + settingsPanel.open(screen) + } + } + + onRightClicked: { + if (userRightClickExec) { + Quickshell.execDetached(userRightClickExec.split(" ")) + Logger.log("CustomButton", `Executing command: ${userRightClickExec}`) + } + } + + onMiddleClicked: { + if (userMiddleClickExec) { + Quickshell.execDetached(userMiddleClickExec.split(" ")) + Logger.log("CustomButton", `Executing command: ${userMiddleClickExec}`) + } + } +} diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index b9761a3..5aa7ba9 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -93,7 +93,7 @@ Item { } onClicked: { var settingsPanel = PanelService.getPanel("settingsPanel") - settingsPanel.requestedTab = SettingsPanel.Tab.AudioService + settingsPanel.requestedTab = SettingsPanel.Tab.Audio settingsPanel.open(screen) } onRightClicked: { diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 36e396b..9d98eea 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -78,7 +78,7 @@ Item { } onClicked: { var settingsPanel = PanelService.getPanel("settingsPanel") - settingsPanel.requestedTab = SettingsPanel.Tab.AudioService + settingsPanel.requestedTab = SettingsPanel.Tab.Audio settingsPanel.open(screen) } onRightClicked: { diff --git a/Modules/SettingsPanel/Extras/BarSectionEditor.qml b/Modules/SettingsPanel/Extras/BarSectionEditor.qml new file mode 100644 index 0000000..830ee46 --- /dev/null +++ b/Modules/SettingsPanel/Extras/BarSectionEditor.qml @@ -0,0 +1,432 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +NBox { + id: root + + property string sectionName: "" + property string sectionId: "" + property var widgetModel: [] + property var availableWidgets: [] + + signal addWidget(string widgetId, string section) + signal removeWidget(string section, int index) + signal reorderWidget(string section, int fromIndex, int toIndex) + signal updateWidgetSettings(string section, int index, var settings) + + color: Color.mSurface + Layout.fillWidth: true + Layout.minimumHeight: { + var widgetCount = widgetModel.length + if (widgetCount === 0) + return 140 * scaling + + var availableWidth = parent.width + var avgWidgetWidth = 150 * scaling + var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth)) + var rows = Math.ceil(widgetCount / widgetsPerRow) + + return (50 + 20 + (rows * 48) + ((rows - 1) * Style.marginS) + 20) * scaling + } + + // Generate widget color from name checksum + function getWidgetColor(widget) { + const totalSum = JSON.stringify(widget).split('').reduce((acc, character) => { + return acc + character.charCodeAt(0) + }, 0) + switch (totalSum % 10) { + case 0: + return Color.mPrimary + case 1: + return Color.mSecondary + case 2: + return Color.mTertiary + case 3: + return Color.mError + case 4: + return Color.mOnSurface + case 5: + return Qt.darker(Color.mPrimary, 1.3) + case 6: + return Qt.darker(Color.mSecondary, 1.3) + case 7: + return Qt.darker(Color.mTertiary, 1.3) + case 8: + return Qt.darker(Color.mError, 1.3) + case 9: + return Qt.darker(Color.mOnSurface, 1.3) + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginM * scaling + + RowLayout { + Layout.fillWidth: true + + NText { + text: sectionName + " Section" + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + color: Color.mSecondary + Layout.alignment: Qt.AlignVCenter + } + + Item { + Layout.fillWidth: true + } + NComboBox { + id: comboBox + model: availableWidgets + label: "" + description: "" + placeholder: "Select a widget to add..." + onSelected: key => comboBox.currentKey = key + popupHeight: 240 * scaling + + Layout.alignment: Qt.AlignVCenter + } + + NIconButton { + icon: "add" + + colorBg: Color.mPrimary + colorFg: Color.mOnPrimary + colorBgHover: Color.mSecondary + colorFgHover: Color.mOnSecondary + enabled: comboBox.currentKey !== "" + tooltipText: "Add widget to section" + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Style.marginS * scaling + onClicked: { + if (comboBox.currentKey !== "") { + addWidget(comboBox.currentKey, sectionId) + comboBox.currentKey = "" + } + } + } + } + + // Drag and Drop Widget Area + // Replace your Flow section with this: + + // Drag and Drop Widget Area - use Item container + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 65 * scaling + + Flow { + id: widgetFlow + anchors.fill: parent + spacing: Style.marginS * scaling + flow: Flow.LeftToRight + + Repeater { + model: widgetModel + delegate: Rectangle { + id: widgetItem + required property int index + required property var modelData + + width: widgetContent.implicitWidth + Style.marginL * scaling + height: Style.baseWidgetSize * 1.15 * scaling + radius: Style.radiusL * scaling + color: root.getWidgetColor(modelData) + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + // Store the widget index for drag operations + property int widgetIndex: index + readonly property int buttonsWidth: Math.round(20 * scaling) + readonly property int buttonsCount: 1 + BarWidgetRegistry.widgetHasUserSettings(modelData.id) + + // Visual feedback during drag + states: State { + when: flowDragArea.draggedIndex === index + PropertyChanges { + target: widgetItem + scale: 1.1 + opacity: 0.9 + z: 1000 + } + } + + RowLayout { + id: widgetContent + anchors.centerIn: parent + spacing: Style.marginXXS * scaling + + NText { + text: modelData.id + font.pointSize: Style.fontSizeS * scaling + color: Color.mOnPrimary + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + Layout.preferredWidth: 80 * scaling + } + + RowLayout { + spacing: 0 + Layout.preferredWidth: buttonsCount * buttonsWidth + + Loader { + active: BarWidgetRegistry.widgetHasUserSettings(modelData.id) + sourceComponent: NIconButton { + icon: "settings" + sizeRatio: 0.6 + colorBorder: Color.applyOpacity(Color.mOutline, "40") + colorBg: Color.mOnSurface + colorFg: Color.mOnPrimary + colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40") + colorFgHover: Color.mOnPrimary + onClicked: { + var dialog = Qt.createComponent("BarWidgetSettingsDialog.qml").createObject(root, { + "widgetIndex": index, + "widgetData": modelData, + "widgetId": modelData.id, + "parent": Overlay.overlay + }) + dialog.open() + } + } + } + + NIconButton { + icon: "close" + sizeRatio: 0.6 + colorBorder: Color.applyOpacity(Color.mOutline, "40") + colorBg: Color.mOnSurface + colorFg: Color.mOnPrimary + colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40") + colorFgHover: Color.mOnPrimary + onClicked: { + removeWidget(sectionId, index) + } + } + } + } + } + } + } + + // MouseArea outside Flow, covering the same area + MouseArea { + id: flowDragArea + anchors.fill: parent + z: 999 // Above all widgets to ensure it gets events first + + // Critical properties for proper event handling + acceptedButtons: Qt.LeftButton + preventStealing: false // Prevent child items from stealing events + propagateComposedEvents: draggedIndex != -1 // Don't propagate to children during drag + hoverEnabled: draggedIndex != -1 + + property point startPos: Qt.point(0, 0) + property bool dragStarted: false + property int draggedIndex: -1 + property real dragThreshold: 15 * scaling + property Item draggedWidget: null + property point clickOffsetInWidget: Qt.point(0, 0) + property point originalWidgetPos: Qt.point(0, 0) // ADD THIS: Store original position + + onPressed: mouse => { + startPos = Qt.point(mouse.x, mouse.y) + dragStarted = false + draggedIndex = -1 + draggedWidget = null + + // Find which widget was clicked + for (var i = 0; i < widgetModel.length; i++) { + const widget = widgetFlow.children[i] + if (widget && widget.widgetIndex !== undefined) { + if (mouse.x >= widget.x && mouse.x <= widget.x + widget.width && mouse.y >= widget.y + && mouse.y <= widget.y + widget.height) { + + const localX = mouse.x - widget.x + const buttonsStartX = widget.width - (widget.buttonsCount * widget.buttonsWidth) + + if (localX < buttonsStartX) { + draggedIndex = widget.widgetIndex + draggedWidget = widget + + // Calculate and store where within the widget the user clicked + const clickOffsetX = mouse.x - widget.x + const clickOffsetY = mouse.y - widget.y + clickOffsetInWidget = Qt.point(clickOffsetX, clickOffsetY) + + // STORE ORIGINAL POSITION + originalWidgetPos = Qt.point(widget.x, widget.y) + + // Immediately set prevent stealing to true when drag candidate is found + preventStealing = true + break + } else { + // Click was on buttons - allow event propagation + mouse.accepted = false + return + } + } + } + } + } + + onPositionChanged: mouse => { + if (draggedIndex !== -1) { + const deltaX = mouse.x - startPos.x + const deltaY = mouse.y - startPos.y + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY) + + if (!dragStarted && distance > dragThreshold) { + dragStarted = true + //Logger.log("BarSectionEditor", "Drag started") + + // Enable visual feedback + if (draggedWidget) { + draggedWidget.z = 1000 + } + } + + if (dragStarted && draggedWidget) { + // Adjust position to account for where within the widget the user clicked + draggedWidget.x = mouse.x - clickOffsetInWidget.x + draggedWidget.y = mouse.y - clickOffsetInWidget.y + } + } + } + + onReleased: mouse => { + if (dragStarted && draggedWidget) { + // Find drop target using improved logic + let targetIndex = -1 + let minDistance = Infinity + const mouseX = mouse.x + const mouseY = mouse.y + + // Check if we should insert at the beginning + let insertAtBeginning = true + let insertAtEnd = true + + // Check if the dragged item is already the last item + let isLastItem = true + for (var k = 0; k < widgetModel.length; k++) { + if (k !== draggedIndex && k > draggedIndex) { + isLastItem = false + break + } + } + + for (var i = 0; i < widgetModel.length; i++) { + if (i !== draggedIndex) { + const widget = widgetFlow.children[i] + if (widget && widget.widgetIndex !== undefined) { + const centerX = widget.x + widget.width / 2 + const centerY = widget.y + widget.height / 2 + const distance = Math.sqrt(Math.pow(mouseX - centerX, 2) + Math.pow(mouseY - centerY, 2)) + + // Check if mouse is to the right of this widget + if (mouseX > widget.x + widget.width / 2) { + insertAtBeginning = false + } + // Check if mouse is to the left of this widget + if (mouseX < widget.x + widget.width / 2) { + insertAtEnd = false + } + + if (distance < minDistance) { + minDistance = distance + targetIndex = widget.widgetIndex + } + } + } + } + + // If dragging the last item to the right, don't reorder + if (isLastItem && insertAtEnd) { + insertAtEnd = false + targetIndex = -1 + //Logger.log("BarSectionEditor", "Last item dropped to right - no reordering needed") + } + + // Determine final target index based on position + let finalTargetIndex = targetIndex + + if (insertAtBeginning && widgetModel.length > 1) { + // Insert at the very beginning (position 0) + finalTargetIndex = 0 + //Logger.log("BarSectionEditor", "Inserting at beginning") + } else if (insertAtEnd && widgetModel.length > 1) { + // Insert at the very end + let maxIndex = -1 + for (var j = 0; j < widgetModel.length; j++) { + if (j !== draggedIndex) { + maxIndex = Math.max(maxIndex, j) + } + } + finalTargetIndex = maxIndex + //Logger.log("BarSectionEditor", "Inserting at end, target:", finalTargetIndex) + } else if (targetIndex !== -1) { + // Normal case - determine if we should insert before or after the target + const targetWidget = widgetFlow.children[targetIndex] + if (targetWidget) { + const targetCenterX = targetWidget.x + targetWidget.width / 2 + if (mouseX > targetCenterX) { + // Mouse is to the right of target center, insert after + //Logger.log("BarSectionEditor", "Inserting after widget at index:", targetIndex) + } else { + // Mouse is to the left of target center, insert before + finalTargetIndex = targetIndex + //Logger.log("BarSectionEditor", "Inserting before widget at index:", targetIndex) + } + } + } + + //Logger.log("BarSectionEditor", "Final drop target index:", finalTargetIndex) + + // Check if reordering is needed + if (finalTargetIndex !== -1 && finalTargetIndex !== draggedIndex) { + // Reordering will happen - reset position for the Flow to handle + draggedWidget.x = 0 + draggedWidget.y = 0 + draggedWidget.z = 0 + reorderWidget(sectionId, draggedIndex, finalTargetIndex) + } else { + // No reordering - restore original position + draggedWidget.x = originalWidgetPos.x + draggedWidget.y = originalWidgetPos.y + draggedWidget.z = 0 + //Logger.log("BarSectionEditor", "No reordering - restoring original position") + } + } else if (draggedIndex !== -1 && !dragStarted) { + + // This was a click without drag - could add click handling here if needed + } + + // Reset everything + dragStarted = false + draggedIndex = -1 + draggedWidget = null + preventStealing = false // Allow normal event propagation again + originalWidgetPos = Qt.point(0, 0) // Reset stored position + } + + // Handle case where mouse leaves the area during drag + onExited: { + if (dragStarted && draggedWidget) { + // Restore original position when mouse leaves area + draggedWidget.x = originalWidgetPos.x + draggedWidget.y = originalWidgetPos.y + draggedWidget.z = 0 + } + } + } + } + } +} diff --git a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml new file mode 100644 index 0000000..cccacbb --- /dev/null +++ b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml @@ -0,0 +1,160 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +// Widget Settings Dialog Component +Popup { + id: settingsPopup + + property int widgetIndex: -1 + property var widgetData: null + property string widgetId: "" + + // Center popup in parent + x: (parent.width - width) * 0.5 + y: (parent.height - height) * 0.5 + + width: 420 * scaling + height: content.implicitHeight + padding * 2 + padding: Style.marginXL * scaling + modal: true + + background: Rectangle { + id: bgRect + color: Color.mSurface + radius: Style.radiusL * scaling + border.color: Color.mPrimary + border.width: Style.borderM * scaling + } + + ColumnLayout { + id: content + width: parent.width + spacing: Style.marginM * scaling + + // Title + RowLayout { + Layout.fillWidth: true + + NText { + text: "Widget Settings: " + settingsPopup.widgetId + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + onClicked: settingsPopup.close() + } + } + + // Separator + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + } + + // Settings based on widget type + Loader { + id: settingsLoader + Layout.fillWidth: true + sourceComponent: { + if (settingsPopup.widgetId === "CustomButton") { + return customButtonSettings + } + // Add more widget settings components here as needed + return null + } + } + + // Action buttons + RowLayout { + Layout.fillWidth: true + Layout.topMargin: Style.marginM * scaling + + Item { + Layout.fillWidth: true + } + + NButton { + text: "Cancel" + outlined: true + onClicked: settingsPopup.close() + } + + NButton { + text: "Save" + onClicked: { + if (settingsLoader.item && settingsLoader.item.saveSettings) { + var newSettings = settingsLoader.item.saveSettings() + root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings) + settingsPopup.close() + } + } + } + } + } + + // CustomButton settings component + Component { + id: customButtonSettings + + ColumnLayout { + spacing: Style.marginM * scaling + + function saveSettings() { + var settings = Object.assign({}, settingsPopup.widgetData) + settings.icon = iconInput.text + settings.leftClickExec = leftClickExecInput.text + settings.rightClickExec = rightClickExecInput.text + settings.middleClickExec = middleClickExecInput.text + return settings + } + + // Icon setting + NTextInput { + id: iconInput + Layout.fillWidth: true + Layout.bottomMargin: Style.marginXL * scaling + label: "Icon Name" + description: "Use Material Icon names from the icon set." + text: settingsPopup.widgetData.icon || "" + placeholderText: "Enter icon name (e.g., favorite, home, settings)" + } + + NTextInput { + id: leftClickExecInput + Layout.fillWidth: true + label: "Left Click Command" + description: "Command or application to run when left clicked." + text: settingsPopup.widgetData.leftClickExec || "" + placeholderText: "Enter command to execute (app or custom script)" + } + + NTextInput { + id: rightClickExecInput + Layout.fillWidth: true + label: "Right Click Command" + description: "Command or application to run when right clicked." + text: settingsPopup.widgetData.rightClickExec || "" + placeholderText: "Enter command to execute (app or custom script)" + } + + NTextInput { + id: middleClickExecInput + Layout.fillWidth: true + label: "Middle Click Command" + description: "Command or application to run when middle clicked." + text: settingsPopup.widgetData.middleClickExec || "" + placeholderText: "Enter command to execute (app or custom script)" + } + } + } +} diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 4dfcfad..65e742d 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -24,13 +24,12 @@ NPanel { panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true - // Enable keyboard focus for settings panel panelKeyboardFocus: true // Tabs enumeration, order is NOT relevant enum Tab { About, - AudioService, + Audio, Bar, Launcher, Brightness, @@ -131,7 +130,7 @@ NPanel { "icon": "apps", "source": launcherTab }, { - "id": SettingsPanel.Tab.AudioService, + "id": SettingsPanel.Tab.Audio, "label": "Audio", "icon": "volume_up", "source": audioTab diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index e927456..e1bf63a 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import qs.Commons import qs.Services import qs.Widgets +import qs.Modules.SettingsPanel.Extras ColumnLayout { id: root @@ -157,36 +158,39 @@ ColumnLayout { spacing: Style.marginM * scaling // Left Section - NSectionEditor { + BarSectionEditor { sectionName: "Left" sectionId: "left" widgetModel: Settings.data.bar.widgets.left availableWidgets: availableWidgets - onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section) - onRemoveWidget: (section, index) => removeWidgetFromSection(section, index) - onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex) + 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) } // Center Section - NSectionEditor { + BarSectionEditor { sectionName: "Center" sectionId: "center" widgetModel: Settings.data.bar.widgets.center availableWidgets: availableWidgets - onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section) - onRemoveWidget: (section, index) => removeWidgetFromSection(section, index) - onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex) + 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) } // Right Section - NSectionEditor { + BarSectionEditor { sectionName: "Right" sectionId: "right" widgetModel: Settings.data.bar.widgets.right availableWidgets: availableWidgets - onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section) - onRemoveWidget: (section, index) => removeWidgetFromSection(section, index) - onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex) + 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) } } } @@ -197,60 +201,55 @@ ColumnLayout { Layout.bottomMargin: Style.marginXL * scaling } - // Helper functions - function addWidgetToSection(widgetName, section) { - //Logger.log("BarTab", "Adding widget", widgetName, "to section", section) - var sectionArray = Settings.data.bar.widgets[section] - - if (sectionArray) { - // Create a new array to avoid modifying the original - var newArray = sectionArray.slice() - newArray.push(widgetName) - //Logger.log("BarTab", "Widget added. New array:", JSON.stringify(newArray)) - - // Assign the new array - Settings.data.bar.widgets[section] = newArray + // --------------------------------- + // Signal functions + // --------------------------------- + function _addWidgetToSection(widgetId, section) { + var newWidget = { + "id": widgetId } + if (BarWidgetRegistry.widgetHasUserSettings(widgetId)) { + var metadata = BarWidgetRegistry.widgetMetadata[widgetId] + if (metadata) { + Object.keys(metadata).forEach(function (key) { + if (key !== "allowUserSettings") { + newWidget[key] = metadata[key] + } + }) + } + } + Settings.data.bar.widgets[section].push(newWidget) } - function removeWidgetFromSection(section, index) { - // Logger.log("BarTab", "Removing widget from section", section, "at index", index) - var sectionArray = Settings.data.bar.widgets[section] - - //Logger.log("BarTab", "Current section array:", JSON.stringify(sectionArray)) - if (sectionArray && index >= 0 && index < sectionArray.length) { - // Create a new array to avoid modifying the original - var newArray = sectionArray.slice() + function _removeWidgetFromSection(section, index) { + if (index >= 0 && index < Settings.data.bar.widgets[section].length) { + var newArray = Settings.data.bar.widgets[section].slice() newArray.splice(index, 1) - //Logger.log("BarTab", "Widget removed. New array:", JSON.stringify(newArray)) - - // Assign the new array Settings.data.bar.widgets[section] = newArray - } else { - - //Logger.log("BarTab", "Invalid section or index:", section, index, "array length:", - // sectionArray ? sectionArray.length : "null") } } - function reorderWidgetInSection(section, fromIndex, toIndex) { - //Logger.log("BarTab", "Reordering widget in section", section, "from", fromIndex, "to", toIndex) - var sectionArray = Settings.data.bar.widgets[section] - if (sectionArray && fromIndex >= 0 && fromIndex < sectionArray.length && toIndex >= 0 - && toIndex < sectionArray.length) { + function _reorderWidgetInSection(section, fromIndex, toIndex) { + if (fromIndex >= 0 && fromIndex < Settings.data.bar.widgets[section].length && toIndex >= 0 + && toIndex < Settings.data.bar.widgets[section].length) { // Create a new array to avoid modifying the original - var newArray = sectionArray.slice() + var newArray = Settings.data.bar.widgets[section].slice() var item = newArray[fromIndex] newArray.splice(fromIndex, 1) newArray.splice(toIndex, 0, item) - Logger.log("BarTab", "Widget reordered. New array:", JSON.stringify(newArray)) - // Assign the new array Settings.data.bar.widgets[section] = newArray + //Logger.log("BarTab", "Widget reordered. New array:", JSON.stringify(newArray)) } } + function _updateWidgetSettingsInSection(section, index, settings) { + // Update the widget settings in the Settings data + Settings.data.bar.widgets[section][index] = settings + //Logger.log("BarTab", `Updated widget settings for ${settings.id} in ${section} section`) + } + // Base list model for all combo boxes ListModel { id: availableWidgets diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index e0e8189..ebdbf95 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -8,7 +8,7 @@ import qs.Services import qs.Widgets NPanel { - id: panel + id: root panelWidth: 460 * scaling panelHeight: 708 * scaling diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index fc7016a..b2f2b5a 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -16,6 +16,7 @@ Singleton { "Bluetooth": bluetoothComponent, "Brightness": brightnessComponent, "Clock": clockComponent, + "CustomButton": customButtonComponent, "DarkModeToggle": darkModeToggle, "KeyboardLayout": keyboardLayoutComponent, "MediaMini": mediaMiniComponent, @@ -33,6 +34,16 @@ Singleton { "Workspace": workspaceComponent }) + property var widgetMetadata: ({ + "CustomButton": { + "allowUserSettings": true, + "icon": "favorite", + "leftClickExec": "", + "rightClickExec": "", + "middleClickExec": "" + } + }) + // Component definitions - these are loaded once at startup property Component activeWindowComponent: Component { ActiveWindow {} @@ -52,6 +63,9 @@ Singleton { property Component clockComponent: Component { Clock {} } + property Component customButtonComponent: Component { + CustomButton {} + } property Component darkModeToggle: Component { DarkModeToggle {} } @@ -100,20 +114,25 @@ Singleton { // ------------------------------ // Helper function to get widget component by name - function getWidget(name) { - return widgets[name] || null + function getWidget(id) { + return widgets[id] || null } // Helper function to check if widget exists - function hasWidget(name) { - return name in widgets + function hasWidget(id) { + return id in widgets } - // Get list of available widget names + // Get list of available widget id function getAvailableWidgets() { return Object.keys(widgets) } + // Helper function to check if widget has user settings + function widgetHasUserSettings(id) { + return (widgetMetadata[id] !== undefined) && (widgetMetadata[id].allowUserSettings === true) + } + function getNPillDirection(widget) { try { if (widget.barSection === "leftSection") { diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 9955ecc..0650839 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -28,6 +28,7 @@ Rectangle { signal exited signal clicked signal rightClicked + signal middleClicked implicitWidth: size implicitHeight: size @@ -59,7 +60,7 @@ Rectangle { enabled: root.enabled anchors.fill: parent cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton hoverEnabled: true onEntered: { hovering = true @@ -83,6 +84,8 @@ Rectangle { root.clicked() } else if (mouse.button === Qt.RightButton) { root.rightClicked() + } else if (mouse.button === Qt.MiddleButton) { + root.middleClicked() } } } diff --git a/Widgets/NSectionEditor.qml b/Widgets/NSectionEditor.qml deleted file mode 100644 index a0a38a9..0000000 --- a/Widgets/NSectionEditor.qml +++ /dev/null @@ -1,321 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import qs.Commons -import qs.Widgets - -NBox { - id: root - - property string sectionName: "" - property string sectionId: "" - property var widgetModel: [] - property var availableWidgets: [] - - signal addWidget(string widgetName, string section) - signal removeWidget(string section, int index) - signal reorderWidget(string section, int fromIndex, int toIndex) - - color: Color.mSurface - Layout.fillWidth: true - Layout.minimumHeight: { - var widgetCount = widgetModel.length - if (widgetCount === 0) - return 140 * scaling - - var availableWidth = parent.width - var avgWidgetWidth = 150 * scaling - var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth)) - var rows = Math.ceil(widgetCount / widgetsPerRow) - - return (50 + 20 + (rows * 48) + ((rows - 1) * Style.marginS) + 20) * scaling - } - - // Generate widget color from name checksum - function getWidgetColor(name) { - const totalSum = name.split('').reduce((acc, character) => { - return acc + character.charCodeAt(0) - }, 0) - switch (totalSum % 5) { - case 0: - return Color.mPrimary - case 1: - return Color.mSecondary - case 2: - return Color.mTertiary - case 3: - return Color.mError - case 4: - return Color.mOnSurface - } - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginM * scaling - - RowLayout { - Layout.fillWidth: true - - NText { - text: sectionName + " Section" - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - Layout.alignment: Qt.AlignVCenter - } - - Item { - Layout.fillWidth: true - } - NComboBox { - id: comboBox - model: availableWidgets - label: "" - description: "" - placeholder: "Select a widget to add..." - onSelected: key => comboBox.currentKey = key - popupHeight: 240 * scaling - - Layout.alignment: Qt.AlignVCenter - } - - NIconButton { - icon: "add" - - colorBg: Color.mPrimary - colorFg: Color.mOnPrimary - colorBgHover: Color.mSecondary - colorFgHover: Color.mOnSecondary - enabled: comboBox.currentKey !== "" - tooltipText: "Add widget to section" - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: Style.marginS * scaling - onClicked: { - if (comboBox.currentKey !== "") { - addWidget(comboBox.currentKey, sectionId) - comboBox.currentKey = "" - } - } - } - } - - // Drag and Drop Widget Area - Flow { - id: widgetFlow - Layout.fillWidth: true - Layout.fillHeight: true - Layout.minimumHeight: 65 * scaling - spacing: Style.marginS * scaling - flow: Flow.LeftToRight - - Repeater { - model: widgetModel - delegate: Rectangle { - id: widgetItem - required property int index - required property string modelData - - width: widgetContent.implicitWidth + Style.marginL * scaling - height: 40 * scaling - radius: Style.radiusL * scaling - color: root.getWidgetColor(modelData) - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - - // Drag properties - Drag.keys: ["widget"] - Drag.active: mouseArea.drag.active - Drag.hotSpot.x: width / 2 - Drag.hotSpot.y: height / 2 - - // Store the widget index for drag operations - property int widgetIndex: index - - // Visual feedback during drag - states: State { - when: mouseArea.drag.active - PropertyChanges { - target: widgetItem - scale: 1.1 - opacity: 0.9 - z: 1000 - } - } - - RowLayout { - id: widgetContent - - anchors.centerIn: parent - spacing: Style.marginXS * scaling - - NText { - text: modelData - font.pointSize: Style.fontSizeS * scaling - color: Color.mOnPrimary - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - Layout.preferredWidth: 80 * scaling - } - - NIconButton { - icon: "close" - sizeRatio: 0.6 - colorBorder: Color.applyOpacity(Color.mOutline, "40") - colorBg: Color.mOnSurface - colorFg: Color.mOnPrimary - colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40") - colorFgHover: Color.mOnPrimary - onClicked: { - removeWidget(sectionId, index) - } - } - } - - // Mouse area for drag and drop - MouseArea { - id: mouseArea - anchors.fill: parent - drag.target: parent - - onPressed: mouse => { - // Check if the click is on the close button area - const closeButtonX = widgetContent.x + widgetContent.width - 20 * scaling - const closeButtonY = widgetContent.y - const closeButtonWidth = 20 * scaling - const closeButtonHeight = 20 * scaling - - if (mouseX >= closeButtonX && mouseX <= closeButtonX + closeButtonWidth - && mouseY >= closeButtonY && mouseY <= closeButtonY + closeButtonHeight) { - // Click is on the close button, don't start drag - mouse.accepted = false - return - } - - //Logger.log("NSectionEditor", `Started dragging widget: ${modelData} at index ${index}`) - // Bring to front when starting drag - widgetItem.z = 1000 - } - - onReleased: { - //Logger.log("NSectionEditor", `Released widget: ${modelData} at index ${index}`) - // Reset z-index when drag ends - widgetItem.z = 0 - - // Get the global mouse position - const globalDropX = mouseArea.mouseX + widgetItem.x + widgetFlow.x - const globalDropY = mouseArea.mouseY + widgetItem.y + widgetFlow.y - - // Find which widget the drop position is closest to - let targetIndex = -1 - let minDistance = Infinity - - for (var i = 0; i < widgetModel.length; i++) { - if (i !== index) { - // Get the position of other widgets - const otherWidget = widgetFlow.children[i] - if (otherWidget && otherWidget.widgetIndex !== undefined) { - // Calculate the center of the other widget - const otherCenterX = otherWidget.x + otherWidget.width / 2 + widgetFlow.x - const otherCenterY = otherWidget.y + otherWidget.height / 2 + widgetFlow.y - - // Calculate distance to the center of this widget - const distance = Math.sqrt(Math.pow(globalDropX - otherCenterX, - 2) + Math.pow(globalDropY - otherCenterY, 2)) - - if (distance < minDistance) { - minDistance = distance - targetIndex = otherWidget.widgetIndex - } - } - } - } - - // Only reorder if we found a valid target and it's different from current position - if (targetIndex !== -1 && targetIndex !== index) { - const fromIndex = index - const toIndex = targetIndex - // Logger.log( - // "NSectionEditor", - // `Dropped widget from index ${fromIndex} to position ${toIndex} (distance: ${minDistance.toFixed( - // 2)})`) - reorderWidget(sectionId, fromIndex, toIndex) - } else { - Logger.warn("NSectionEditor", `No valid drop target found for widget at index ${index}`) - } - } - } - } - } - } - - // Drop zone at the beginning (positioned absolutely) - DropArea { - id: startDropZone - width: 40 * scaling - height: 40 * scaling - x: widgetFlow.x - y: widgetFlow.y + (widgetFlow.height - height) / 2 - keys: ["widget"] - z: 1001 // Above the Flow - - Rectangle { - anchors.fill: parent - color: startDropZone.containsDrag ? Color.applyOpacity(Color.mPrimary, "20") : Color.transparent - border.color: startDropZone.containsDrag ? Color.mPrimary : Color.transparent - border.width: startDropZone.containsDrag ? 2 : 0 - radius: Style.radiusS * scaling - } - - onEntered: function (drag) {//Logger.log("NSectionEditor", "Entered start drop zone") - } - - onDropped: function (drop) { - //Logger.log("NSectionEditor", "Dropped on start zone") - if (drop.source && drop.source.widgetIndex !== undefined) { - const fromIndex = drop.source.widgetIndex - const toIndex = 0 // Insert at the beginning - if (fromIndex !== toIndex) { - //Logger.log("NSectionEditor", `Dropped widget from index ${fromIndex} to beginning`) - reorderWidget(sectionId, fromIndex, toIndex) - } - } - } - } - - // Drop zone at the end (positioned absolutely) - DropArea { - id: endDropZone - width: 40 * scaling - height: 40 * scaling - x: widgetFlow.x + widgetFlow.width - width - y: widgetFlow.y + (widgetFlow.height - height) / 2 - keys: ["widget"] - z: 1001 // Above the Flow - - Rectangle { - anchors.fill: parent - color: endDropZone.containsDrag ? Color.applyOpacity(Color.mPrimary, "20") : Color.transparent - border.color: endDropZone.containsDrag ? Color.mPrimary : Color.transparent - border.width: endDropZone.containsDrag ? 2 : 0 - radius: Style.radiusS * scaling - } - - onEntered: function (drag) {//Logger.log("NSectionEditor", "Entered end drop zone") - } - - onDropped: function (drop) { - //Logger.log("NSectionEditor", "Dropped on end zone") - if (drop.source && drop.source.widgetIndex !== undefined) { - const fromIndex = drop.source.widgetIndex - const toIndex = widgetModel.length // Insert at the end - if (fromIndex !== toIndex) { - //Logger.log("NSectionEditor", `Dropped widget from index ${fromIndex} to end`) - reorderWidget(sectionId, fromIndex, toIndex) - } - } - } - } - } -} diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 829c92a..1cb7141 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -39,10 +39,13 @@ ColumnLayout { // Container Rectangle { id: frame - implicitWidth: parent.width - implicitHeight: Style.baseWidgetSize * 1.1 * scaling + + Layout.fillWidth: true Layout.minimumWidth: 80 * scaling Layout.maximumWidth: root.inputMaxWidth + + implicitWidth: parent.width + implicitHeight: Style.baseWidgetSize * 1.1 * scaling radius: Style.radiusM * scaling color: Color.mSurface border.color: Color.mOutline @@ -76,7 +79,7 @@ ColumnLayout { readOnly: root.readOnly enabled: root.enabled color: Color.mOnSurface - placeholderTextColor: Color.mOnSurfaceVariant + placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6) background: null font.family: fontFamily font.pointSize: fontSize diff --git a/Widgets/NWidgetLoader.qml b/Widgets/NWidgetLoader.qml index 1f4046c..17bf887 100644 --- a/Widgets/NWidgetLoader.qml +++ b/Widgets/NWidgetLoader.qml @@ -6,7 +6,7 @@ import qs.Commons Item { id: root - property string widgetName: "" + property string widgetId: "" property var widgetProps: ({}) property bool enabled: true @@ -27,12 +27,12 @@ Item { id: loader anchors.fill: parent - active: Settings.isLoaded && enabled && widgetName !== "" + active: Settings.isLoaded && enabled && widgetId !== "" sourceComponent: { if (!active) { return null } - return BarWidgetRegistry.getWidget(widgetName) + return BarWidgetRegistry.getWidget(widgetId) } onLoaded: { @@ -49,14 +49,14 @@ Item { item.onLoaded() } - //Logger.log("NWidgetLoader", "Loaded", widgetName, "on screen", item.screen.name) + //Logger.log("NWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name) } } // Error handling - onWidgetNameChanged: { - if (widgetName && !BarWidgetRegistry.hasWidget(widgetName)) { - Logger.warn("WidgetLoader", "Widget not found in registry:", widgetName) + onWidgetIdChanged: { + if (widgetId && !BarWidgetRegistry.hasWidget(widgetId)) { + Logger.warn("WidgetLoader", "Widget not found in registry:", widgetId) } } }