diff --git a/Common/Prefs.qml b/Common/Prefs.qml index 28adce76..77375df0 100644 --- a/Common/Prefs.qml +++ b/Common/Prefs.qml @@ -24,14 +24,43 @@ Singleton { property string profileImage: "" property string weatherLocation: "New York, NY" property string weatherCoordinates: "40.7128,-74.0060" + property bool showLauncherButton: true + property bool showWorkspaceSwitcher: true property bool showFocusedWindow: true property bool showWeather: true property bool showMusic: true property bool showClipboard: true property bool showSystemResources: true property bool showSystemTray: true + property bool showClock: true + property bool showNotificationButton: true + property bool showBattery: true + property bool showControlCenterButton: true property bool showWorkspaceIndex: false property bool showWorkspacePadding: false + property var topBarLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"] + property var topBarCenterWidgets: ["clock", "music", "weather"] + property var topBarRightWidgets: ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"] + + // Reactive ListModel properties for TopBar + property alias topBarLeftWidgetsModel: leftWidgetsModel + property alias topBarCenterWidgetsModel: centerWidgetsModel + property alias topBarRightWidgetsModel: rightWidgetsModel + + // Signal to force immediate TopBar layout refresh + signal forceTopBarLayoutRefresh() + + ListModel { + id: leftWidgetsModel + } + + ListModel { + id: centerWidgetsModel + } + + ListModel { + id: rightWidgetsModel + } property string appLauncherViewMode: "list" property string spotlightModalViewMode: "list" property string networkPreference: "auto" @@ -85,7 +114,14 @@ Singleton { Component.onCompleted: { loadSettings(); - fontCheckTimer.start() + fontCheckTimer.start(); + initializeListModels(); + } + + function initializeListModels() { + updateListModel(leftWidgetsModel, topBarLeftWidgets); + updateListModel(centerWidgetsModel, topBarCenterWidgets); + updateListModel(rightWidgetsModel, topBarRightWidgets); } function loadSettings() { @@ -109,14 +145,38 @@ Singleton { profileImage = settings.profileImage !== undefined ? settings.profileImage : ""; weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY"; weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060"; + showLauncherButton = settings.showLauncherButton !== undefined ? settings.showLauncherButton : true; + showWorkspaceSwitcher = settings.showWorkspaceSwitcher !== undefined ? settings.showWorkspaceSwitcher : true; showFocusedWindow = settings.showFocusedWindow !== undefined ? settings.showFocusedWindow : true; showWeather = settings.showWeather !== undefined ? settings.showWeather : true; showMusic = settings.showMusic !== undefined ? settings.showMusic : true; showClipboard = settings.showClipboard !== undefined ? settings.showClipboard : true; showSystemResources = settings.showSystemResources !== undefined ? settings.showSystemResources : true; showSystemTray = settings.showSystemTray !== undefined ? settings.showSystemTray : true; + showClock = settings.showClock !== undefined ? settings.showClock : true; + showNotificationButton = settings.showNotificationButton !== undefined ? settings.showNotificationButton : true; + showBattery = settings.showBattery !== undefined ? settings.showBattery : true; + showControlCenterButton = settings.showControlCenterButton !== undefined ? settings.showControlCenterButton : true; showWorkspaceIndex = settings.showWorkspaceIndex !== undefined ? settings.showWorkspaceIndex : false; showWorkspacePadding = settings.showWorkspacePadding !== undefined ? settings.showWorkspacePadding : false; + if (settings.topBarWidgetOrder) { + // Migrate from old single list to new three-list system + topBarLeftWidgets = settings.topBarWidgetOrder.filter(w => ["launcherButton", "workspaceSwitcher", "focusedWindow"].includes(w)); + topBarCenterWidgets = settings.topBarWidgetOrder.filter(w => ["clock", "music", "weather"].includes(w)); + topBarRightWidgets = settings.topBarWidgetOrder.filter(w => ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"].includes(w)); + } else { + var leftWidgets = settings.topBarLeftWidgets !== undefined ? settings.topBarLeftWidgets : ["launcherButton", "workspaceSwitcher", "focusedWindow"]; + var centerWidgets = settings.topBarCenterWidgets !== undefined ? settings.topBarCenterWidgets : ["clock", "music", "weather"]; + var rightWidgets = settings.topBarRightWidgets !== undefined ? settings.topBarRightWidgets : ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"]; + + topBarLeftWidgets = leftWidgets; + topBarCenterWidgets = centerWidgets; + topBarRightWidgets = rightWidgets; + + updateListModel(leftWidgetsModel, leftWidgets); + updateListModel(centerWidgetsModel, centerWidgets); + updateListModel(rightWidgetsModel, rightWidgets); + } appLauncherViewMode = settings.appLauncherViewMode !== undefined ? settings.appLauncherViewMode : "list"; spotlightModalViewMode = settings.spotlightModalViewMode !== undefined ? settings.spotlightModalViewMode : "list"; networkPreference = settings.networkPreference !== undefined ? settings.networkPreference : "auto"; @@ -163,14 +223,23 @@ Singleton { "profileImage": profileImage, "weatherLocation": weatherLocation, "weatherCoordinates": weatherCoordinates, + "showLauncherButton": showLauncherButton, + "showWorkspaceSwitcher": showWorkspaceSwitcher, "showFocusedWindow": showFocusedWindow, "showWeather": showWeather, "showMusic": showMusic, "showClipboard": showClipboard, "showSystemResources": showSystemResources, "showSystemTray": showSystemTray, + "showClock": showClock, + "showNotificationButton": showNotificationButton, + "showBattery": showBattery, + "showControlCenterButton": showControlCenterButton, "showWorkspaceIndex": showWorkspaceIndex, "showWorkspacePadding": showWorkspacePadding, + "topBarLeftWidgets": topBarLeftWidgets, + "topBarCenterWidgets": topBarCenterWidgets, + "topBarRightWidgets": topBarRightWidgets, "appLauncherViewMode": appLauncherViewMode, "spotlightModalViewMode": spotlightModalViewMode, "networkPreference": networkPreference, @@ -340,6 +409,16 @@ Singleton { } // Widget visibility setters + function setShowLauncherButton(enabled) { + showLauncherButton = enabled; + saveSettings(); + } + + function setShowWorkspaceSwitcher(enabled) { + showWorkspaceSwitcher = enabled; + saveSettings(); + } + function setShowFocusedWindow(enabled) { showFocusedWindow = enabled; saveSettings(); @@ -370,6 +449,84 @@ Singleton { saveSettings(); } + function setShowClock(enabled) { + showClock = enabled; + saveSettings(); + } + + function setShowNotificationButton(enabled) { + showNotificationButton = enabled; + saveSettings(); + } + + function setShowBattery(enabled) { + showBattery = enabled; + saveSettings(); + } + + function setShowControlCenterButton(enabled) { + showControlCenterButton = enabled; + saveSettings(); + } + + function setTopBarWidgetOrder(order) { + topBarWidgetOrder = order; + saveSettings(); + } + + function setTopBarLeftWidgets(order) { + topBarLeftWidgets = order; + updateListModel(leftWidgetsModel, order); + saveSettings(); + } + + function setTopBarCenterWidgets(order) { + topBarCenterWidgets = order; + updateListModel(centerWidgetsModel, order); + saveSettings(); + } + + function setTopBarRightWidgets(order) { + topBarRightWidgets = order; + updateListModel(rightWidgetsModel, order); + saveSettings(); + } + + function updateListModel(listModel, order) { + listModel.clear(); + for (var i = 0; i < order.length; i++) { + listModel.append({"widgetId": order[i]}); + } + } + + function resetTopBarWidgetsToDefault() { + var defaultLeft = ["launcherButton", "workspaceSwitcher", "focusedWindow"]; + var defaultCenter = ["clock", "music", "weather"]; + var defaultRight = ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"]; + + topBarLeftWidgets = defaultLeft; + topBarCenterWidgets = defaultCenter; + topBarRightWidgets = defaultRight; + + updateListModel(leftWidgetsModel, defaultLeft); + updateListModel(centerWidgetsModel, defaultCenter); + updateListModel(rightWidgetsModel, defaultRight); + + showLauncherButton = true; + showWorkspaceSwitcher = true; + showFocusedWindow = true; + showWeather = true; + showMusic = true; + showClipboard = true; + showSystemResources = true; + showSystemTray = true; + showClock = true; + showNotificationButton = true; + showBattery = true; + showControlCenterButton = true; + saveSettings(); + } + // View mode setters function setAppLauncherViewMode(mode) { appLauncherViewMode = mode; diff --git a/Modules/Settings/WidgetsTab.qml b/Modules/Settings/WidgetsTab.qml index 59c667fe..4ca31ca6 100644 --- a/Modules/Settings/WidgetsTab.qml +++ b/Modules/Settings/WidgetsTab.qml @@ -10,6 +10,249 @@ ScrollView { contentHeight: column.implicitHeight + Theme.spacingXL clip: true + property var baseWidgetDefinitions: [ + { + id: "launcherButton", + text: "App Launcher", + description: "Quick access to application launcher", + icon: "apps", + enabled: true + }, + { + id: "workspaceSwitcher", + text: "Workspace Switcher", + description: "Shows current workspace and allows switching", + icon: "view_module", + enabled: true + }, + { + id: "focusedWindow", + text: "Focused Window", + description: "Display currently focused application title", + icon: "window", + enabled: true + }, + { + id: "clock", + text: "Clock", + description: "Current time and date display", + icon: "schedule", + enabled: true + }, + { + id: "weather", + text: "Weather Widget", + description: "Current weather conditions and temperature", + icon: "wb_sunny", + enabled: true + }, + { + id: "music", + text: "Media Controls", + description: "Control currently playing media", + icon: "music_note", + enabled: true + }, + { + id: "clipboard", + text: "Clipboard Manager", + description: "Access clipboard history", + icon: "content_paste", + enabled: true + }, + { + id: "systemResources", + text: "System Resources", + description: "CPU and memory usage indicators", + icon: "memory", + enabled: true + }, + { + id: "systemTray", + text: "System Tray", + description: "System notification area icons", + icon: "notifications", + enabled: true + }, + { + id: "controlCenterButton", + text: "Control Center", + description: "Access to system controls and settings", + icon: "settings", + enabled: true + }, + { + id: "notificationButton", + text: "Notification Center", + description: "Access to notifications and do not disturb", + icon: "notifications", + enabled: true + }, + { + id: "battery", + text: "Battery", + description: "Battery level and power management", + icon: "battery_std", + enabled: true + }, + { + id: "spacer", + text: "Spacer", + description: "Empty space to separate widgets", + icon: "more_horiz", + enabled: true + }, + { + id: "separator", + text: "Separator", + description: "Visual divider between widgets", + icon: "remove", + enabled: true + } + ] + + // Default widget configurations for each section + property var defaultLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"] + property var defaultCenterWidgets: ["music", "clock", "weather"] + property var defaultRightWidgets: ["clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"] + + Component.onCompleted: { + // Initialize sections with defaults if they're empty + if (!Prefs.topBarLeftWidgets || Prefs.topBarLeftWidgets.length === 0) { + Prefs.setTopBarLeftWidgets(defaultLeftWidgets) + } + if (!Prefs.topBarCenterWidgets || Prefs.topBarCenterWidgets.length === 0) { + Prefs.setTopBarCenterWidgets(defaultCenterWidgets) + } + if (!Prefs.topBarRightWidgets || Prefs.topBarRightWidgets.length === 0) { + Prefs.setTopBarRightWidgets(defaultRightWidgets) + } + } + + function addWidgetToSection(widgetId, targetSection) { + var leftWidgets = Prefs.topBarLeftWidgets.slice() + var centerWidgets = Prefs.topBarCenterWidgets.slice() + var rightWidgets = Prefs.topBarRightWidgets.slice() + + if (targetSection === "left") { + leftWidgets.push(widgetId) + Prefs.setTopBarLeftWidgets(leftWidgets) + } else if (targetSection === "center") { + centerWidgets.push(widgetId) + Prefs.setTopBarCenterWidgets(centerWidgets) + } else if (targetSection === "right") { + rightWidgets.push(widgetId) + Prefs.setTopBarRightWidgets(rightWidgets) + } + } + + function removeLastWidgetFromSection(sectionId) { + var leftWidgets = Prefs.topBarLeftWidgets.slice() + var centerWidgets = Prefs.topBarCenterWidgets.slice() + var rightWidgets = Prefs.topBarRightWidgets.slice() + + if (sectionId === "left" && leftWidgets.length > 0) { + leftWidgets.pop() + Prefs.setTopBarLeftWidgets(leftWidgets) + } else if (sectionId === "center" && centerWidgets.length > 0) { + centerWidgets.pop() + Prefs.setTopBarCenterWidgets(centerWidgets) + } else if (sectionId === "right" && rightWidgets.length > 0) { + rightWidgets.pop() + Prefs.setTopBarRightWidgets(rightWidgets) + } + } + + function handleItemEnabledChanged(itemId, enabled) { + // Update the widget's enabled state in preferences + if (itemId === "focusedWindow") { + Prefs.setShowFocusedWindow(enabled) + } else if (itemId === "weather") { + Prefs.setShowWeather(enabled) + } else if (itemId === "music") { + Prefs.setShowMusic(enabled) + } else if (itemId === "clipboard") { + Prefs.setShowClipboard(enabled) + } else if (itemId === "systemResources") { + Prefs.setShowSystemResources(enabled) + } else if (itemId === "systemTray") { + Prefs.setShowSystemTray(enabled) + } else if (itemId === "clock") { + Prefs.setShowClock(enabled) + } else if (itemId === "notificationButton") { + Prefs.setShowNotificationButton(enabled) + } else if (itemId === "controlCenterButton") { + Prefs.setShowControlCenterButton(enabled) + } else if (itemId === "battery") { + Prefs.setShowBattery(enabled) + } else if (itemId === "launcherButton") { + Prefs.setShowLauncherButton(enabled) + } else if (itemId === "workspaceSwitcher") { + Prefs.setShowWorkspaceSwitcher(enabled) + } + // Note: spacer and separator don't need preference handling as they're always enabled + } + + function handleItemOrderChanged(sectionId, newOrder) { + if (sectionId === "left") { + Prefs.setTopBarLeftWidgets(newOrder) + } else if (sectionId === "center") { + Prefs.setTopBarCenterWidgets(newOrder) + } else if (sectionId === "right") { + Prefs.setTopBarRightWidgets(newOrder) + } + } + + function getItemsForSection(sectionId) { + var widgets = [] + var widgetIds = [] + + if (sectionId === "left") { + widgetIds = Prefs.topBarLeftWidgets || [] + } else if (sectionId === "center") { + widgetIds = Prefs.topBarCenterWidgets || [] + } else if (sectionId === "right") { + widgetIds = Prefs.topBarRightWidgets || [] + } + + widgetIds.forEach(widgetId => { + var widgetDef = baseWidgetDefinitions.find(w => w.id === widgetId) + if (widgetDef) { + var item = Object.assign({}, widgetDef) + // Set enabled state based on preferences + if (widgetId === "focusedWindow") { + item.enabled = Prefs.showFocusedWindow + } else if (widgetId === "weather") { + item.enabled = Prefs.showWeather + } else if (widgetId === "music") { + item.enabled = Prefs.showMusic + } else if (widgetId === "clipboard") { + item.enabled = Prefs.showClipboard + } else if (widgetId === "systemResources") { + item.enabled = Prefs.showSystemResources + } else if (widgetId === "systemTray") { + item.enabled = Prefs.showSystemTray + } else if (widgetId === "clock") { + item.enabled = Prefs.showClock + } else if (widgetId === "notificationButton") { + item.enabled = Prefs.showNotificationButton + } else if (widgetId === "controlCenterButton") { + item.enabled = Prefs.showControlCenterButton + } else if (widgetId === "battery") { + item.enabled = Prefs.showBattery + } else if (widgetId === "launcherButton") { + item.enabled = Prefs.showLauncherButton + } else if (widgetId === "workspaceSwitcher") { + item.enabled = Prefs.showWorkspaceSwitcher + } + // spacer and separator are always enabled (no preference toggle needed) + widgets.push(item) + } + }) + + return widgets + } + Column { id: column @@ -18,105 +261,213 @@ ScrollView { topPadding: Theme.spacingL bottomPadding: Theme.spacingXL - // Top Bar Widgets Section - StyledRect { + // Header section + Row { width: parent.width - height: topBarSection.implicitHeight + Theme.spacingL * 2 - radius: Theme.cornerRadiusLarge - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) - border.width: 1 + spacing: Theme.spacingM - Column { - id: topBarSection + DankIcon { + name: "widgets" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } - anchors.fill: parent - anchors.margins: Theme.spacingL - spacing: Theme.spacingM + StyledText { + text: "Top Bar Widget Management" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + Item { + width: parent.width - 400 + height: 1 + } + + Rectangle { + width: 80 + height: 28 + radius: Theme.cornerRadius + color: resetArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariant + anchors.verticalCenter: parent.verticalCenter + border.width: 1 + border.color: resetArea.containsMouse ? Theme.outline : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5) + Row { - width: parent.width - spacing: Theme.spacingM - + anchors.centerIn: parent + spacing: Theme.spacingXS + DankIcon { - name: "widgets" - size: Theme.iconSize - color: Theme.primary + name: "refresh" + size: 14 + color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } - + StyledText { - text: "Top Bar Widgets" - font.pixelSize: Theme.fontSizeLarge + text: "Reset" + font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } - } - - DankToggle { - width: parent.width - text: "Focused Window" - description: "Show the currently focused application in the top bar" - checked: Prefs.showFocusedWindow - onToggled: (checked) => { - return Prefs.setShowFocusedWindow(checked); + + MouseArea { + id: resetArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + // Reset all sections to defaults + Prefs.setTopBarLeftWidgets(defaultLeftWidgets) + Prefs.setTopBarCenterWidgets(defaultCenterWidgets) + Prefs.setTopBarRightWidgets(defaultRightWidgets) + + // Reset all widget enabled states to defaults (all enabled) + Prefs.setShowFocusedWindow(true) + Prefs.setShowWeather(true) + Prefs.setShowMusic(true) + Prefs.setShowClipboard(true) + Prefs.setShowSystemResources(true) + Prefs.setShowSystemTray(true) + Prefs.setShowClock(true) + Prefs.setShowNotificationButton(true) + Prefs.setShowControlCenterButton(true) + Prefs.setShowBattery(true) + Prefs.setShowLauncherButton(true) + Prefs.setShowWorkspaceSwitcher(true) } } - - DankToggle { - width: parent.width - text: "Weather Widget" - description: "Display weather information in the top bar" - checked: Prefs.showWeather - onToggled: (checked) => { - return Prefs.setShowWeather(checked); + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing } } - - DankToggle { - width: parent.width - text: "Media Controls" - description: "Show currently playing media in the top bar" - checked: Prefs.showMusic - onToggled: (checked) => { - return Prefs.setShowMusic(checked); + + Behavior on border.color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing } } + } + } - DankToggle { - width: parent.width - text: "Clipboard Button" - description: "Show clipboard access button in the top bar" - checked: Prefs.showClipboard - onToggled: (checked) => { - return Prefs.setShowClipboard(checked); - } + Rectangle { + width: parent.width + height: messageText.contentHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + visible: true + opacity: 1.0 + z: 1 + + StyledText { + id: messageText + anchors.centerIn: parent + text: "Drag widgets to reorder within sections. Use + to add widgets and - to remove the last widget from each section." + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + width: parent.width - Theme.spacingM * 2 + wrapMode: Text.WordWrap + } + } + + // Widget sections + Column { + width: parent.width + spacing: Theme.spacingL + + // Left Section + DankSections { + width: parent.width + title: "Left Section" + titleIcon: "format_align_left" + sectionId: "left" + allWidgets: widgetsTab.baseWidgetDefinitions + items: widgetsTab.getItemsForSection("left") + + onItemEnabledChanged: (itemId, enabled) => { + widgetsTab.handleItemEnabledChanged(itemId, enabled) } - DankToggle { - width: parent.width - text: "System Resources" - description: "Display CPU and RAM usage indicators" - checked: Prefs.showSystemResources - onToggled: (checked) => { - return Prefs.setShowSystemResources(checked); - } + onItemOrderChanged: (newOrder) => { + widgetsTab.handleItemOrderChanged("left", newOrder) } - DankToggle { - width: parent.width - text: "System Tray" - description: "Show system tray icons in the top bar" - checked: Prefs.showSystemTray - onToggled: (checked) => { - return Prefs.setShowSystemTray(checked); - } + onAddWidget: (sectionId) => { + widgetSelectionPopup.allWidgets = widgetsTab.baseWidgetDefinitions + widgetSelectionPopup.targetSection = sectionId + widgetSelectionPopup.safeOpen() } + onRemoveLastWidget: (sectionId) => { + widgetsTab.removeLastWidgetFromSection(sectionId) + } } + // Center Section + DankSections { + width: parent.width + title: "Center Section" + titleIcon: "format_align_center" + sectionId: "center" + allWidgets: widgetsTab.baseWidgetDefinitions + items: widgetsTab.getItemsForSection("center") + + onItemEnabledChanged: (itemId, enabled) => { + widgetsTab.handleItemEnabledChanged(itemId, enabled) + } + + onItemOrderChanged: (newOrder) => { + widgetsTab.handleItemOrderChanged("center", newOrder) + } + + onAddWidget: (sectionId) => { + widgetSelectionPopup.allWidgets = widgetsTab.baseWidgetDefinitions + widgetSelectionPopup.targetSection = sectionId + widgetSelectionPopup.safeOpen() + } + + onRemoveLastWidget: (sectionId) => { + widgetsTab.removeLastWidgetFromSection(sectionId) + } + } + + // Right Section + DankSections { + width: parent.width + title: "Right Section" + titleIcon: "format_align_right" + sectionId: "right" + allWidgets: widgetsTab.baseWidgetDefinitions + items: widgetsTab.getItemsForSection("right") + + onItemEnabledChanged: (itemId, enabled) => { + widgetsTab.handleItemEnabledChanged(itemId, enabled) + } + + onItemOrderChanged: (newOrder) => { + widgetsTab.handleItemOrderChanged("right", newOrder) + } + + onAddWidget: (sectionId) => { + widgetSelectionPopup.allWidgets = widgetsTab.baseWidgetDefinitions + widgetSelectionPopup.targetSection = sectionId + widgetSelectionPopup.safeOpen() + } + + onRemoveLastWidget: (sectionId) => { + widgetsTab.removeLastWidgetFromSection(sectionId) + } + } } // Workspace Section @@ -153,7 +504,6 @@ ScrollView { color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } - } DankToggle { @@ -175,11 +525,47 @@ ScrollView { return Prefs.setShowWorkspacePadding(checked); } } - } - } - } -} + // Tooltip for reset button (positioned above the button) + Rectangle { + width: tooltipText.contentWidth + Theme.spacingM * 2 + height: tooltipText.contentHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + visible: resetArea.containsMouse + opacity: resetArea.containsMouse ? 1 : 0 + y: column.y + 48 // Position above the reset button in the header + x: parent.width - width - Theme.spacingM + z: 100 + + StyledText { + id: tooltipText + anchors.centerIn: parent + text: "Reset widget layout to defaults" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + // Widget selection popup + DankWidgetSelectionPopup { + id: widgetSelectionPopup + anchors.centerIn: parent + + onWidgetSelected: (widgetId, targetSection) => { + widgetsTab.addWidgetToSection(widgetId, targetSection) + } + } +} \ No newline at end of file diff --git a/Modules/TopBar/TopBar.qml b/Modules/TopBar/TopBar.qml index 533087aa..93f7c1b3 100644 --- a/Modules/TopBar/TopBar.qml +++ b/Modules/TopBar/TopBar.qml @@ -28,7 +28,23 @@ PanelWindow { let fonts = Qt.fontFamilies(); if (fonts.indexOf("Material Symbols Rounded") === -1) ToastService.showError("Please install Material Symbols Rounded and Restart your Shell. See README.md for instructions"); - + + // Connect to the force refresh signal + Prefs.forceTopBarLayoutRefresh.connect(function() { + console.log("TopBar: Forcing layout refresh"); + // Force layout recalculation by toggling visibility briefly + Qt.callLater(() => { + leftSection.visible = false; + centerSection.visible = false; + rightSection.visible = false; + Qt.callLater(() => { + leftSection.visible = true; + centerSection.visible = true; + rightSection.visible = true; + console.log("TopBar: Layout refresh completed"); + }); + }); + }); } Connections { @@ -36,6 +52,8 @@ PanelWindow { root.backgroundTransparency = Prefs.topBarTransparency; } + // Remove old manual refresh handlers - ListModel updates are automatic + target: Prefs } @@ -116,12 +134,12 @@ PanelWindow { // Use estimated fixed widths to break circular dependencies readonly property int launcherButtonWidth: 40 readonly property int workspaceSwitcherWidth: 120 // Approximate - readonly property int focusedAppMaxWidth: focusedApp.visible ? 456 : 0 + readonly property int focusedAppMaxWidth: 456 // Fixed width since we don't have focusedApp reference readonly property int estimatedLeftSectionWidth: launcherButtonWidth + workspaceSwitcherWidth + focusedAppMaxWidth + (Theme.spacingXS * 2) readonly property int rightSectionWidth: rightSection.width - readonly property int clockWidth: clock.width - readonly property int mediaMaxWidth: media.visible ? 280 : 0 // Normal max width - readonly property int weatherWidth: weather.visible ? weather.width : 0 + readonly property int clockWidth: 120 // Approximate clock width + readonly property int mediaMaxWidth: 280 // Normal max width + readonly property int weatherWidth: 80 // Approximate weather width readonly property bool validLayout: availableWidth > 100 && estimatedLeftSectionWidth > 0 && rightSectionWidth > 0 readonly property int clockLeftEdge: (availableWidth - clockWidth) / 2 readonly property int clockRightEdge: clockLeftEdge + clockWidth @@ -135,6 +153,79 @@ PanelWindow { readonly property bool spacingTight: validLayout && (leftToMediaGap < 150 || clockToRightGap < 100) readonly property bool overlapping: validLayout && (leftToMediaGap < 100 || clockToRightGap < 50) + // Helper functions + function getWidgetEnabled(widgetId) { + switch (widgetId) { + case "launcherButton": return Prefs.showLauncherButton + case "workspaceSwitcher": return Prefs.showWorkspaceSwitcher + case "focusedWindow": return Prefs.showFocusedWindow + case "clock": return Prefs.showClock + case "music": return Prefs.showMusic + case "weather": return Prefs.showWeather + case "systemTray": return Prefs.showSystemTray + case "clipboard": return Prefs.showClipboard + case "systemResources": return Prefs.showSystemResources + case "notificationButton": return Prefs.showNotificationButton + case "battery": return Prefs.showBattery + case "controlCenterButton": return Prefs.showControlCenterButton + default: return false + } + } + + function getWidgetVisible(widgetId) { + // Some widgets have additional visibility conditions + switch (widgetId) { + case "launcherButton": return true + case "workspaceSwitcher": return true // Simplified - was NiriService.niriAvailable + case "focusedWindow": return true + case "clock": return true + case "music": return true // Simplified - was MprisController.activePlayer + case "weather": return true // Simplified - was complex weather condition + case "systemTray": return true + case "clipboard": return true + case "systemResources": return true + case "notificationButton": return true + case "battery": return true + case "controlCenterButton": return true + default: return false + } + } + + function getWidgetComponent(widgetId) { + switch (widgetId) { + case "launcherButton": + return launcherButtonComponent + case "workspaceSwitcher": + return workspaceSwitcherComponent + case "focusedWindow": + return focusedWindowComponent + case "clock": + return clockComponent + case "music": + return mediaComponent + case "weather": + return weatherComponent + case "systemTray": + return systemTrayComponent + case "clipboard": + return clipboardComponent + case "systemResources": + return systemResourcesComponent + case "notificationButton": + return notificationButtonComponent + case "battery": + return batteryComponent + case "controlCenterButton": + return controlCenterButtonComponent + case "spacer": + return spacerComponent + case "separator": + return separatorComponent + default: + return null + } + } + anchors.fill: parent anchors.leftMargin: Theme.spacingM anchors.rightMargin: Theme.spacingM @@ -142,6 +233,7 @@ PanelWindow { anchors.bottomMargin: Theme.spacingXS clip: true + // Dynamic left section Row { id: leftSection @@ -150,67 +242,45 @@ PanelWindow { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - LauncherButton { - anchors.verticalCenter: parent.verticalCenter - isActive: appDrawerPopout ? appDrawerPopout.isVisible : false - onClicked: { - if (appDrawerPopout) - appDrawerPopout.toggle(); - + Repeater { + model: Prefs.topBarLeftWidgetsModel + + Loader { + anchors.verticalCenter: parent ? parent.verticalCenter : undefined + active: topBarContent.getWidgetEnabled(model.widgetId) && topBarContent.getWidgetVisible(model.widgetId) + sourceComponent: topBarContent.getWidgetComponent(model.widgetId) + + property string widgetId: model.widgetId } } - - WorkspaceSwitcher { - anchors.verticalCenter: parent.verticalCenter - screenName: root.screenName - } - - FocusedApp { - id: focusedApp - - anchors.verticalCenter: parent.verticalCenter - visible: Prefs.showFocusedWindow - compactMode: topBarContent.spacingTight - availableWidth: topBarContent.leftToMediaGap - } - } - Clock { - id: clock + // Dynamic center section + Row { + id: centerSection + height: parent.height + spacing: Theme.spacingS anchors.centerIn: parent - compactMode: topBarContent.overlapping - onClockClicked: { - centcomPopout.calendarVisible = !centcomPopout.calendarVisible; - } - } - - Media { - id: media - - anchors.verticalCenter: parent.verticalCenter - anchors.right: clock.left - anchors.rightMargin: Theme.spacingS - visible: Prefs.showMusic && MprisController.activePlayer - compactMode: topBarContent.spacingTight || topBarContent.overlapping - onClicked: { - centcomPopout.calendarVisible = !centcomPopout.calendarVisible; - } - } - - Weather { - id: weather - - anchors.verticalCenter: parent.verticalCenter - anchors.left: clock.right - anchors.leftMargin: Theme.spacingS - visible: Prefs.showWeather && WeatherService.weather.available && WeatherService.weather.temp > 0 && WeatherService.weather.tempF > 0 - onClicked: { - centcomPopout.calendarVisible = !centcomPopout.calendarVisible; + + Component.onCompleted: { + console.log("Center widgets model count:", Prefs.topBarCenterWidgetsModel.count) + } + + Repeater { + model: Prefs.topBarCenterWidgetsModel + + Loader { + anchors.verticalCenter: parent ? parent.verticalCenter : undefined + active: topBarContent.getWidgetEnabled(model.widgetId) && topBarContent.getWidgetVisible(model.widgetId) + sourceComponent: topBarContent.getWidgetComponent(model.widgetId) + + property string widgetId: model.widgetId + } } } + // Dynamic right section Row { id: rightSection @@ -219,9 +289,82 @@ PanelWindow { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + Component.onCompleted: { + console.log("Right widgets model count:", Prefs.topBarRightWidgetsModel.count) + } + + Repeater { + model: Prefs.topBarRightWidgetsModel + + Loader { + anchors.verticalCenter: parent ? parent.verticalCenter : undefined + active: topBarContent.getWidgetEnabled(model.widgetId) && topBarContent.getWidgetVisible(model.widgetId) + sourceComponent: topBarContent.getWidgetComponent(model.widgetId) + + property string widgetId: model.widgetId + } + } + } + + // Widget Components + Component { + id: launcherButtonComponent + LauncherButton { + isActive: appDrawerPopout ? appDrawerPopout.isVisible : false + onClicked: { + if (appDrawerPopout) + appDrawerPopout.toggle(); + } + } + } + + Component { + id: workspaceSwitcherComponent + WorkspaceSwitcher { + screenName: root.screenName + } + } + + Component { + id: focusedWindowComponent + FocusedApp { + compactMode: topBarContent.spacingTight + availableWidth: topBarContent.leftToMediaGap + } + } + + Component { + id: clockComponent + Clock { + compactMode: topBarContent.overlapping + onClockClicked: { + centcomPopout.calendarVisible = !centcomPopout.calendarVisible; + } + } + } + + Component { + id: mediaComponent + Media { + compactMode: topBarContent.spacingTight || topBarContent.overlapping + onClicked: { + centcomPopout.calendarVisible = !centcomPopout.calendarVisible; + } + } + } + + Component { + id: weatherComponent + Weather { + onClicked: { + centcomPopout.calendarVisible = !centcomPopout.calendarVisible; + } + } + } + + Component { + id: systemTrayComponent SystemTrayBar { - anchors.verticalCenter: parent.verticalCenter - visible: Prefs.showSystemTray onMenuRequested: (menu, item, x, y) => { systemTrayContextMenu.currentTrayMenu = menu; systemTrayContextMenu.currentTrayItem = item; @@ -231,7 +374,10 @@ PanelWindow { menu.menuVisible = true; } } + } + Component { + id: clipboardComponent Rectangle { width: 40 height: 30 @@ -240,8 +386,6 @@ PanelWindow { const baseColor = clipboardArea.containsMouse ? Theme.primaryHover : Theme.secondaryHover; return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); } - anchors.verticalCenter: parent.verticalCenter - visible: Prefs.showClipboard DankIcon { anchors.centerIn: parent @@ -252,7 +396,6 @@ PanelWindow { MouseArea { id: clipboardArea - anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -266,75 +409,84 @@ PanelWindow { duration: Theme.shortDuration easing.type: Theme.standardEasing } - } - } + } - Loader { - anchors.verticalCenter: parent.verticalCenter - active: Prefs.showSystemResources - - sourceComponent: Component { - CpuMonitor { - toggleProcessList: () => { - return processListPopout.toggle(); - } + Component { + id: systemResourcesComponent + Row { + spacing: Theme.spacingXS + + CpuMonitor { + toggleProcessList: () => { + return processListPopout.toggle(); } - } - } - - Loader { - anchors.verticalCenter: parent.verticalCenter - active: Prefs.showSystemResources - - sourceComponent: Component { - RamMonitor { - toggleProcessList: () => { - return processListPopout.toggle(); - } + RamMonitor { + toggleProcessList: () => { + return processListPopout.toggle(); } - } - } + } + Component { + id: notificationButtonComponent NotificationCenterButton { - anchors.verticalCenter: parent.verticalCenter hasUnread: root.notificationCount > 0 isActive: notificationCenter.notificationHistoryVisible onClicked: { notificationCenter.notificationHistoryVisible = !notificationCenter.notificationHistoryVisible; } } + } + Component { + id: batteryComponent Battery { - anchors.verticalCenter: parent.verticalCenter batteryPopupVisible: batteryPopout.batteryPopupVisible onToggleBatteryPopup: { batteryPopout.batteryPopupVisible = !batteryPopout.batteryPopupVisible; } } + } + Component { + id: controlCenterButtonComponent ControlCenterButton { - anchors.verticalCenter: parent.verticalCenter isActive: controlCenterPopout.controlCenterVisible onClicked: { controlCenterPopout.controlCenterVisible = !controlCenterPopout.controlCenterVisible; if (controlCenterPopout.controlCenterVisible) { if (NetworkService.wifiEnabled) NetworkService.scanWifi(); - } } } + } + Component { + id: spacerComponent + Item { + width: 20 + height: 30 + } + } + + Component { + id: separatorComponent + Rectangle { + width: 1 + height: 20 + color: Theme.outline + opacity: 0.3 + } } } } -} +} \ No newline at end of file diff --git a/Widgets/DankActionButton.qml b/Widgets/DankActionButton.qml index 73c71dcb..b570ce61 100644 --- a/Widgets/DankActionButton.qml +++ b/Widgets/DankActionButton.qml @@ -30,7 +30,10 @@ StyledRect { StateLayer { stateColor: Theme.primary cornerRadius: root.radius - onClicked: root.clicked() + onClicked: { + console.log("StateLayer clicked for button:", root.iconName); + root.clicked(); + } } } diff --git a/Widgets/DankDropdown.qml b/Widgets/DankDropdown.qml index f555af7d..1b81c883 100644 --- a/Widgets/DankDropdown.qml +++ b/Widgets/DankDropdown.qml @@ -208,7 +208,7 @@ Rectangle { if (selectedIndex >= 0 && selectedIndex < filteredOptions.length) { root.currentValue = filteredOptions[selectedIndex]; root.valueChanged(filteredOptions[selectedIndex]); - dropdownMenu.close(); + close(); } } @@ -256,17 +256,17 @@ Rectangle { anchors.fill: parent anchors.margins: 1 placeholderText: "Search..." - text: dropdownMenu.searchQuery + text: searchQuery topPadding: Theme.spacingS bottomPadding: Theme.spacingS onTextChanged: { - dropdownMenu.searchQuery = text; - dropdownMenu.updateFilteredOptions(); + searchQuery = text; + updateFilteredOptions(); } - Keys.onDownPressed: dropdownMenu.selectNext() - Keys.onUpPressed: dropdownMenu.selectPrevious() - Keys.onReturnPressed: dropdownMenu.selectCurrent() - Keys.onEnterPressed: dropdownMenu.selectCurrent() + Keys.onDownPressed: selectNext() + Keys.onUpPressed: selectPrevious() + Keys.onReturnPressed: selectCurrent() + Keys.onEnterPressed: selectCurrent() } } @@ -286,7 +286,7 @@ Rectangle { width: parent.width height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0) clip: true - model: dropdownMenu.filteredOptions + model: filteredOptions spacing: 2 WheelHandler { @@ -311,7 +311,7 @@ Rectangle { } delegate: Rectangle { - property bool isSelected: dropdownMenu.selectedIndex === index + property bool isSelected: selectedIndex === index property bool isCurrentValue: root.currentValue === modelData property int optionIndex: root.options.indexOf(modelData) @@ -354,7 +354,7 @@ Rectangle { onClicked: { root.currentValue = modelData; root.valueChanged(modelData); - dropdownMenu.close(); + close(); } } diff --git a/Widgets/DankSections.qml b/Widgets/DankSections.qml new file mode 100644 index 00000000..8ba3b3fc --- /dev/null +++ b/Widgets/DankSections.qml @@ -0,0 +1,263 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets + +Column { + id: root + + property var items: [] + property var allWidgets: [] + property string title: "" + property string titleIcon: "widgets" + property string sectionId: "" + + signal itemEnabledChanged(string itemId, bool enabled) + signal itemOrderChanged(var newOrder) + signal addWidget(string sectionId) + signal removeLastWidget(string sectionId) + + width: parent.width + spacing: Theme.spacingM + + // Header + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: root.titleIcon + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: root.title + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - 60 + height: 1 + } + } + + // Widget Items + Column { + id: itemsList + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: root.items + + delegate: Item { + id: delegateItem + width: itemsList.width + height: 70 + + property int visualIndex: index + property bool held: dragArea.pressed + property string itemId: modelData.id + + z: held ? 2 : 1 + + Rectangle { + id: itemBackground + anchors.fill: parent + anchors.margins: 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + // Drag handle + Rectangle { + width: 40 + height: parent.height + color: "transparent" + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: "drag_indicator" + size: Theme.iconSize - 4 + color: Theme.outline + anchors.centerIn: parent + opacity: 0.8 + } + } + + // Widget icon + DankIcon { + name: modelData.icon + size: Theme.iconSize + color: modelData.enabled ? Theme.primary : Theme.outline + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + 40 + Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + } + + // Widget info + Column { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + 40 + Theme.spacingM + Theme.iconSize + Theme.spacingM + anchors.right: toggle.left + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.text + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: modelData.enabled ? Theme.surfaceText : Theme.outline + elide: Text.ElideRight + width: parent.width + } + + StyledText { + text: modelData.description + font.pixelSize: Theme.fontSizeSmall + color: modelData.enabled ? Theme.outline : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) + elide: Text.ElideRight + width: parent.width + wrapMode: Text.WordWrap + } + } + + // Toggle - positioned at right edge + DankToggle { + id: toggle + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + width: 48 + height: 24 + hideText: true + checked: modelData.enabled + onToggled: (checked) => { + root.itemEnabledChanged(modelData.id, checked) + } + } + + // Drag functionality + MouseArea { + id: dragArea + anchors.fill: parent + hoverEnabled: true + + property bool validDragStart: false + + drag.target: held && validDragStart ? delegateItem : undefined + drag.axis: Drag.YAxis + drag.minimumY: -delegateItem.height + drag.maximumY: itemsList.height + + onPressed: (mouse) => { + // Only allow dragging from the drag handle area (first 60px) + if (mouse.x <= 60) { + validDragStart = true + delegateItem.z = 2 + } else { + validDragStart = false + mouse.accepted = false + } + } + + onReleased: { + delegateItem.z = 1 + + if (drag.active && validDragStart) { + // Calculate new index based on position + var newIndex = Math.round(delegateItem.y / (delegateItem.height + itemsList.spacing)) + newIndex = Math.max(0, Math.min(newIndex, root.items.length - 1)) + + if (newIndex !== index) { + var newItems = root.items.slice() + var draggedItem = newItems.splice(index, 1)[0] + newItems.splice(newIndex, 0, draggedItem) + + root.itemOrderChanged(newItems.map(item => item.id)) + } + } + + // Reset position + delegateItem.x = 0 + delegateItem.y = 0 + validDragStart = false + } + } + + // Animations for drag + Behavior on y { + enabled: !dragArea.held && !dragArea.drag.active + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + + // Add/Remove Controls + Rectangle { + width: parent.width * 0.5 + height: 40 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + anchors.horizontalCenter: parent.horizontalCenter + + Row { + anchors.centerIn: parent + spacing: Theme.spacingL + + StyledText { + text: "Add or remove widgets" + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + anchors.verticalCenter: parent.verticalCenter + } + + Row { + spacing: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + + // Add button + DankActionButton { + iconName: "add" + iconSize: Theme.iconSize - 4 + iconColor: Theme.primary + hoverColor: Theme.primaryContainer + onClicked: { + root.addWidget(root.sectionId); + } + } + + // Remove button + DankActionButton { + iconName: "remove" + iconSize: Theme.iconSize - 4 + iconColor: Theme.error + hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1) + enabled: root.items.length > 0 + opacity: root.items.length > 0 ? 1.0 : 0.5 + onClicked: { + if (root.items.length > 0) { + root.removeLastWidget(root.sectionId); + } + } + } + } + } + } +} diff --git a/Widgets/DankSections_backup.qml b/Widgets/DankSections_backup.qml new file mode 100644 index 00000000..e2f174b5 --- /dev/null +++ b/Widgets/DankSections_backup.qml @@ -0,0 +1,265 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets + +Column { + id: root + + property var items: [] + property var allWidgets: [] + property string title: "" + property string titleIcon: "widgets" + property string sectionId: "" + + signal itemEnabledChanged(string itemId, bool enabled) + signal itemOrderChanged(var newOrder) + signal addWidget(string sectionId) + signal removeLastWidget(string sectionId) + + width: parent.width + spacing: Theme.spacingM + + // Header + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: root.titleIcon + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: root.title + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - 60 + height: 1 + } + } + + // Widget Items + Column { + id: itemsList + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: root.items + + delegate: Item { + id: delegateItem + width: itemsList.width + height: 70 + + property int visualIndex: index + property bool held: dragArea.pressed + property string itemId: modelData.id + + z: held ? 2 : 1 + + Rectangle { + id: itemBackground + anchors.fill: parent + anchors.margins: 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + // Drag handle + Rectangle { + width: 40 + height: parent.height + color: "transparent" + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: "drag_indicator" + size: Theme.iconSize - 4 + color: Theme.outline + anchors.centerIn: parent + opacity: 0.8 + } + } + + // Widget icon + DankIcon { + name: modelData.icon + size: Theme.iconSize + color: modelData.enabled ? Theme.primary : Theme.outline + anchors.verticalCenter: parent.verticalCenter + } + + // Widget info + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: parent.width - 200 // Leave space for toggle + + StyledText { + text: modelData.text + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: modelData.enabled ? Theme.surfaceText : Theme.outline + elide: Text.ElideRight + width: parent.width + } + + StyledText { + text: modelData.description + font.pixelSize: Theme.fontSizeSmall + color: modelData.enabled ? Theme.outline : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) + elide: Text.ElideRight + width: parent.width + wrapMode: Text.WordWrap + } + } + + // Spacer to push toggle to right + Item { + width: parent.width - 280 // Dynamic width + height: 1 + } + + // Toggle - positioned at right edge + DankToggle { + anchors.verticalCenter: parent.verticalCenter + width: 48 + height: 24 + hideText: true + checked: modelData.enabled + onToggled: (checked) => { + root.itemEnabledChanged(modelData.id, checked) + } + } + } + + // Drag functionality + MouseArea { + id: dragArea + anchors.fill: parent + hoverEnabled: true + + property bool validDragStart: false + + drag.target: held && validDragStart ? delegateItem : undefined + drag.axis: Drag.YAxis + drag.minimumY: -delegateItem.height + drag.maximumY: itemsList.height + + onPressed: (mouse) => { + // Only allow dragging from the drag handle area (first 60px) + if (mouse.x <= 60) { + validDragStart = true + delegateItem.z = 2 + } else { + validDragStart = false + mouse.accepted = false + } + } + + onReleased: { + delegateItem.z = 1 + + if (drag.active && validDragStart) { + // Calculate new index based on position + var newIndex = Math.round(delegateItem.y / (delegateItem.height + itemsList.spacing)) + newIndex = Math.max(0, Math.min(newIndex, root.items.length - 1)) + + if (newIndex !== index) { + var newItems = root.items.slice() + var draggedItem = newItems.splice(index, 1)[0] + newItems.splice(newIndex, 0, draggedItem) + + root.itemOrderChanged(newItems.map(item => item.id)) + } + } + + // Reset position + delegateItem.x = 0 + delegateItem.y = 0 + validDragStart = false + } + } + + // Animations for drag + Behavior on y { + enabled: !dragArea.held && !dragArea.drag.active + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + + // Add/Remove Controls + Rectangle { + width: parent.width * 0.5 + height: 40 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + anchors.horizontalCenter: parent.horizontalCenter + + Row { + anchors.centerIn: parent + spacing: Theme.spacingL + + StyledText { + text: "Add or remove widgets" + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + anchors.verticalCenter: parent.verticalCenter + } + + Row { + spacing: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + + // Add button + DankActionButton { + iconName: "add" + iconSize: Theme.iconSize - 4 + iconColor: Theme.primary + hoverColor: Theme.primaryContainer + onClicked: { + root.addWidget(root.sectionId); + } + } + + // Remove button + DankActionButton { + iconName: "remove" + iconSize: Theme.iconSize - 4 + iconColor: Theme.error + hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1) + enabled: root.items.length > 0 + opacity: root.items.length > 0 ? 1.0 : 0.5 + onClicked: { + if (root.items.length > 0) { + root.removeLastWidget(root.sectionId); + } + } + } + } + } + } +} diff --git a/Widgets/DankToggle.qml b/Widgets/DankToggle.qml index 8b92e61f..a73860e3 100644 --- a/Widgets/DankToggle.qml +++ b/Widgets/DankToggle.qml @@ -10,23 +10,24 @@ Item { property bool toggling: false property string text: "" property string description: "" + property bool hideText: false signal clicked() signal toggled(bool checked) - width: text ? parent.width : 48 - height: text ? 60 : 24 + width: (text && !hideText) ? parent.width : 48 + height: (text && !hideText) ? 60 : 24 StyledRect { id: background anchors.fill: parent - radius: toggle.text ? Theme.cornerRadius : 0 - color: toggle.text ? Theme.surfaceHover : "transparent" - visible: toggle.text + radius: (toggle.text && !toggle.hideText) ? Theme.cornerRadius : 0 + color: (toggle.text && !toggle.hideText) ? Theme.surfaceHover : "transparent" + visible: (toggle.text && !toggle.hideText) StateLayer { - visible: toggle.text + visible: (toggle.text && !toggle.hideText) disabled: !toggle.enabled stateColor: Theme.primary cornerRadius: parent.radius @@ -50,7 +51,7 @@ Item { anchors.leftMargin: Theme.spacingM anchors.rightMargin: Theme.spacingM spacing: Theme.spacingXS - visible: toggle.text + visible: (toggle.text && !toggle.hideText) Column { anchors.verticalCenter: parent.verticalCenter diff --git a/Widgets/DankWidgetSelectionPopup.qml b/Widgets/DankWidgetSelectionPopup.qml new file mode 100644 index 00000000..79746c73 --- /dev/null +++ b/Widgets/DankWidgetSelectionPopup.qml @@ -0,0 +1,187 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets + +Popup { + id: root + + property var allWidgets: [] + property string targetSection: "" + property bool isOpening: false + + signal widgetSelected(string widgetId, string targetSection) + + width: 400 + height: 450 + modal: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + // Prevent multiple openings + function safeOpen() { + if (!isOpening && !visible) { + isOpening = true + open() + } + } + + onOpened: { + isOpening = false + } + + onClosed: { + isOpening = false + // Clear references to prevent memory leaks + allWidgets = [] + targetSection = "" + } + + background: Rectangle { + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1) + border.color: Theme.primarySelected + border.width: 1 + radius: Theme.cornerRadiusLarge + } + + contentItem: Item { + anchors.fill: parent + + // Close button in top-right + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 2 + iconColor: Theme.outline + hoverColor: Theme.primaryContainer + anchors.top: parent.top + anchors.topMargin: Theme.spacingM + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + onClicked: root.close() + } + + Column { + id: contentColumn + spacing: Theme.spacingM + anchors.fill: parent + anchors.margins: Theme.spacingL + anchors.topMargin: Theme.spacingL + 30 // Space for close button + + // Header + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "add_circle" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Add Widget to " + root.targetSection + " Section" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + text: "Select a widget to add to the " + root.targetSection.toLowerCase() + " section of the top bar. You can add multiple instances of the same widget if needed." + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + width: parent.width + wrapMode: Text.WordWrap + } + + // Widget List + ScrollView { + width: parent.width + height: parent.height - 120 // Leave space for header and description + clip: true + + ListView { + id: widgetList + spacing: Theme.spacingS + model: root.allWidgets + + delegate: Rectangle { + width: widgetList.width + height: 60 + radius: Theme.cornerRadius + color: widgetArea.containsMouse ? Theme.primaryHover : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + // Widget icon + DankIcon { + name: modelData.icon + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + // Widget info + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: parent.width - Theme.iconSize - Theme.spacingM * 3 + + StyledText { + text: modelData.text + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + elide: Text.ElideRight + width: parent.width + } + + StyledText { + text: modelData.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.outline + elide: Text.ElideRight + width: parent.width + wrapMode: Text.WordWrap + } + } + + // Add icon + DankIcon { + name: "add" + size: Theme.iconSize - 4 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: widgetArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.widgetSelected(modelData.id, root.targetSection) + root.close() + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + } +} \ No newline at end of file