diff --git a/Assets/Matugen/templates/ghostty.conf b/Assets/Matugen/templates/ghostty.conf index b2b38446..08ce7ef1 100644 --- a/Assets/Matugen/templates/ghostty.conf +++ b/Assets/Matugen/templates/ghostty.conf @@ -1,22 +1,25 @@ -palette = 0={{colors.shadow.default.hex}} -palette = 1={{colors.red.default.hex}} -palette = 2={{colors.green_source.default.hex}} -palette = 3={{colors.yellow_source.default.hex}} -palette = 4={{colors.primary.default.hex}} -palette = 5={{colors.purple_source.default.hex}} -palette = 6={{colors.blue_source.default.hex}} -palette = 7=#f0f0f0 -palette = 8={{colors.outline.default.hex}} -palette = 9={{colors.red_value.default.hex}} -palette = 10={{colors.green_value.default.hex}} -palette = 11={{colors.yellow_value.default.hex}} -palette = 12={{colors.blue_value.default.hex}} -palette = 13={{colors.purple_value.default.hex}} -palette = 14={{colors.blue_value.default.hex}} -palette = 15={{colors.primary.default.hex}} +palette = 0= {{colors.shadow.default.hex}} +palette = 1= {{colors.error.default.hex}} +palette = 2= {{colors.tertiary.default.hex}} +palette = 3= {{colors.secondary.default.hex}} +palette = 4= {{colors.primary.default.hex}} +palette = 5= {{colors.primary.default.hex}} +palette = 6= {{colors.secondary.default.hex}} +palette = 7= {{colors.on_background.default.hex}} +palette = 8= {{colors.outline.default.hex}} +palette = 9= {{colors.secondary_fixed_dim.default.hex}} +palette = 10= {{colors.tertiary_container.default.hex}} +palette = 11= {{colors.surface_container.default.hex}} +palette = 12= {{colors.primary_container.default.hex}} +palette = 13= {{colors.on_primary_container.default.hex}} +palette = 14= {{colors.surface_variant.default.hex}} +palette = 15= {{colors.primary.default.hex}} + foreground={{colors.on_surface.default.hex}} background={{colors.surface.default.hex}} + cursor-color = {{colors.on_surface.default.hex}} cursor-text = {{colors.on_surface.default.hex}} + selection-background = {{colors.on_secondary.default.hex}} selection-foreground = {{colors.secondary_fixed_dim.default.hex}} diff --git a/Assets/settings-default.json b/Assets/settings-default.json index b14d05c7..37d85a50 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -1,5 +1,5 @@ { - "settingsVersion": 3, + "settingsVersion": 4, "bar": { "position": "top", "backgroundOpacity": 1, @@ -116,7 +116,8 @@ "exclusive": false, "backgroundOpacity": 1, "floatingRatio": 1, - "monitors": [] + "monitors": [], + "pinnedApps": [] }, "network": { "wifiEnabled": true, @@ -125,6 +126,7 @@ "notifications": { "doNotDisturb": false, "monitors": [], + "location": "top_right", "lastSeenTs": 0, "lowUrgencyDuration": 3, "normalUrgencyDuration": 8, @@ -149,7 +151,7 @@ }, "colorSchemes": { "useWallpaperColors": false, - "predefinedScheme": "", + "predefinedScheme": "Noctalia (default)", "darkMode": true }, "matugen": { diff --git a/Commons/Settings.qml b/Commons/Settings.qml index f4c463bb..1339f952 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -113,7 +113,7 @@ Singleton { JsonAdapter { id: adapter - property int settingsVersion: 3 + property int settingsVersion: 4 // bar property JsonObject bar: JsonObject { @@ -233,6 +233,8 @@ Singleton { property real backgroundOpacity: 1.0 property real floatingRatio: 1.0 property list monitors: [] + // Desktop entry IDs pinned to the dock (e.g., "org.kde.konsole", "firefox.desktop") + property list pinnedApps: [] } // network @@ -245,9 +247,8 @@ Singleton { property JsonObject notifications: JsonObject { property bool doNotDisturb: false property list monitors: [] - // Last time the user opened the notification history (ms since e899999999999998poch) + property string location: "top_right" property real lastSeenTs: 0 - // Duration settings for different urgency levels (in seconds) property int lowUrgencyDuration: 3 property int normalUrgencyDuration: 8 property int criticalUrgencyDuration: 15 diff --git a/Modules/Bar/Widgets/SidePanelToggle.qml b/Modules/Bar/Widgets/SidePanelToggle.qml index 037de875..f2336f69 100644 --- a/Modules/Bar/Widgets/SidePanelToggle.qml +++ b/Modules/Bar/Widgets/SidePanelToggle.qml @@ -31,8 +31,10 @@ NIconButton { readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon readonly property bool useDistroLogo: (widgetSettings.useDistroLogo !== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo + readonly property string customIconPath: widgetSettings.customIconPath || "" - icon: useDistroLogo ? "" : customIcon + // If we have a custom path or distro logo, don't use the theme icon. + icon: (customIconPath === "" && !useDistroLogo) ? customIcon : "" tooltipText: "Open side panel" baseSize: Style.capsuleHeight compact: (Settings.data.bar.density === "compact") @@ -45,12 +47,16 @@ NIconButton { onRightClicked: PanelService.getPanel("settingsPanel")?.toggle() IconImage { - id: logo + id: customOrDistroLogo anchors.centerIn: parent width: root.width * 0.8 height: width - source: useDistroLogo ? DistroLogoService.osLogo : "" - visible: useDistroLogo && source !== "" + source: { + if (customIconPath !== "") return customIconPath; + if (useDistroLogo) return DistroLogoService.osLogo; + return ""; + } + visible: source !== "" smooth: true asynchronous: true } diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index 5bc5f863..9bd1d1fb 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -120,7 +120,7 @@ Rectangle { columnSpacing: Style.marginXXS * scaling NText { - text: isVertical ? `${SystemStatService.cpuTemp}°` : `${SystemStatService.cpuTemp}°C` + text: `${SystemStatService.cpuTemp}°C` font.family: Settings.data.ui.fontFixed font.pointSize: textSize font.weight: Style.fontWeightMedium @@ -128,6 +128,7 @@ Rectangle { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter color: Color.mPrimary + scale: isVertical ? Math.min(1.0, cpuTempContent.width / implicitWidth) : 1.0 Layout.row: isVertical ? 0 : 0 Layout.column: isVertical ? 0 : 1 } diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index cec00ca3..11f633f5 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -13,6 +13,7 @@ Variants { model: Quickshell.screens delegate: Item { + id: root required property ShellScreen modelData property real scaling: ScalingService.getScreenScale(modelData) @@ -25,6 +26,45 @@ Variants { } } + // Update dock apps when toplevels change + Connections { + target: ToplevelManager ? ToplevelManager.toplevels : null + function onValuesChanged() { + updateDockApps() + } + } + + // Also listen to model changes (for ObjectModel) + Connections { + target: ToplevelManager ? ToplevelManager.toplevels : null + function onCountChanged() { + updateDockApps() + } + } + + // Update dock apps when pinned apps change + Connections { + target: Settings.data.dock + function onPinnedAppsChanged() { + updateDockApps() + } + } + + // Initial update when component is ready + Component.onCompleted: { + if (Settings.isLoaded && ToplevelManager) { + updateDockApps() + } + } + + // Update when Settings are loaded + Connections { + target: Settings + function onSettingsLoaded() { + updateDockApps() + } + } + // Shared properties between peek and dock windows readonly property bool autoHide: Settings.data.dock.autoHide readonly property int hideDelay: 500 @@ -43,12 +83,72 @@ Variants { // Shared state between windows property bool dockHovered: false property bool anyAppHovered: false + property bool menuHovered: false property bool hidden: autoHide property bool peekHovered: false // Separate property to control Loader - stays true during animations property bool dockLoaded: !autoHide // Start loaded if autoHide is off + // Track the currently open context menu + property var currentContextMenu: null + + // Combined model of running apps and pinned apps + property var dockApps: [] + + // Function to close any open context menu + function closeAllContextMenus() { + if (currentContextMenu && currentContextMenu.visible) { + currentContextMenu.hide() + } + } + + // Function to update the combined dock apps model + function updateDockApps() { + const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : [] + const pinnedApps = Settings.data.dock.pinnedApps || [] + const combined = [] + + // First, add pinned apps (both running and non-running) in their pinned order + pinnedApps.forEach(pinnedAppId => { + const runningApp = runningApps.find(toplevel => toplevel && toplevel.appId === pinnedAppId) + if (runningApp) { + // Pinned app that is currently running + combined.push({ + "type": "pinned-running", + "toplevel": runningApp, + "appId": runningApp.appId, + "title": runningApp.title + }) + } else { + // Pinned app that is not running + combined.push({ + "type": "pinned", + "toplevel": null, + "appId": pinnedAppId, + "title": pinnedAppId + }) + } + }) + + // Then, add running apps that are not pinned + runningApps.forEach(toplevel => { + if (toplevel && toplevel.appId) { + const isPinned = pinnedApps.includes(toplevel.appId) + if (!isPinned) { + combined.push({ + "type": "running", + "toplevel": toplevel, + "appId": toplevel.appId, + "title": toplevel.title + }) + } + } + }) + + dockApps = combined + } + // Timer to unload dock after hide animation completes Timer { id: unloadTimer @@ -65,7 +165,7 @@ Variants { id: hideTimer interval: hideDelay onTriggered: { - if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) { + if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) { hidden = true unloadTimer.restart() // Start unload timer when hiding } @@ -137,7 +237,7 @@ Variants { onExited: { peekHovered = false - if (!hidden && !dockHovered && !anyAppHovered) { + if (!hidden && !dockHovered && !anyAppHovered && !menuHovered) { hideTimer.restart() } } @@ -147,7 +247,7 @@ Variants { // DOCK WINDOW Loader { - active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (ToplevelManager.toplevels.values.length > 0) + active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (dockApps.length > 0) sourceComponent: PanelWindow { id: dockWindow @@ -235,10 +335,15 @@ Variants { onExited: { dockHovered = false - if (autoHide && !anyAppHovered && !peekHovered) { + if (autoHide && !anyAppHovered && !peekHovered && !menuHovered) { hideTimer.restart() } } + + onClicked: { + // Close any open context menu when clicking on the dock background + closeAllContextMenus() + } } Item { @@ -247,10 +352,10 @@ Variants { height: parent.height - (Style.marginM * 2 * scaling) anchors.centerIn: parent - function getAppIcon(toplevel: Toplevel): string { - if (!toplevel) + function getAppIcon(appData): string { + if (!appData || !appData.appId) return "" - return AppIcons.iconForAppId(toplevel.appId?.toLowerCase()) + return AppIcons.iconForAppId(appData.appId?.toLowerCase()) } RowLayout { @@ -260,7 +365,7 @@ Variants { anchors.centerIn: parent Repeater { - model: ToplevelManager ? ToplevelManager.toplevels : null + model: dockApps delegate: Item { id: appButton @@ -268,10 +373,19 @@ Variants { Layout.preferredHeight: iconSize Layout.alignment: Qt.AlignCenter - property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData + property bool isActive: modelData.toplevel && ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData.toplevel property bool hovered: appMouseArea.containsMouse property string appId: modelData ? modelData.appId : "" - property string appTitle: modelData ? modelData.title : "" + property string appTitle: modelData ? (modelData.title || modelData.appId) : "" + property bool isRunning: modelData && (modelData.type === "running" || modelData.type === "pinned-running") + + // Listen for the toplevel being closed + Connections { + target: modelData?.toplevel + function onClosed() { + Qt.callLater(root.updateDockApps) + } + } // Individual tooltip for this app NTooltip { @@ -296,6 +410,9 @@ Variants { fillMode: Image.PreserveAspectFit cache: true + // Dim pinned apps that aren't running + opacity: appButton.isRunning ? 1.0 : 0.6 + scale: appButton.hovered ? 1.15 : 1.0 Behavior on scale { @@ -305,6 +422,13 @@ Variants { easing.overshoot: 1.2 } } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } } // Fall back if no icon @@ -314,6 +438,7 @@ Variants { icon: "question-mark" font.pointSize: iconSize * 0.7 color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant + opacity: appButton.isRunning ? 1.0 : 0.6 scale: appButton.hovered ? 1.15 : 1.0 Behavior on scale { @@ -323,6 +448,29 @@ Variants { easing.overshoot: 1.2 } } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } + } + + // Context menu popup + DockMenu { + id: contextMenu + scaling: root.scaling + onHoveredChanged: menuHovered = hovered + onRequestClose: contextMenu.hide() + onAppClosed: root.updateDockApps // Force immediate dock update when app is closed + onVisibleChanged: { + if (visible) { + root.currentContextMenu = contextMenu + } else if (root.currentContextMenu === contextMenu) { + root.currentContextMenu = null + } + } } MouseArea { @@ -330,7 +478,7 @@ Variants { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.MiddleButton + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton onEntered: { anyAppHovered = true @@ -347,17 +495,35 @@ Variants { onExited: { anyAppHovered = false appTooltip.hide() - if (autoHide && !dockHovered && !peekHovered) { + if (autoHide && !dockHovered && !peekHovered && !menuHovered) { hideTimer.restart() } } onClicked: function (mouse) { - if (mouse.button === Qt.MiddleButton && modelData?.close) { - modelData.close() + // Close any existing context menu first + if (mouse.button !== Qt.RightButton || root.currentContextMenu !== contextMenu) { + root.closeAllContextMenus() } - if (mouse.button === Qt.LeftButton && modelData?.activate) { - modelData.activate() + + // Check if toplevel is still valid (not a stale reference) + const isValidToplevel = modelData?.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(modelData.toplevel) + + if (mouse.button === Qt.MiddleButton && isValidToplevel && modelData.toplevel.close) { + modelData.toplevel.close() + Qt.callLater(root.updateDockApps) // Force immediate dock update + } else if (mouse.button === Qt.LeftButton) { + if (isValidToplevel && modelData.toplevel.activate) { + // Running app - activate it + modelData.toplevel.activate() + } else if (modelData?.appId) { + // Pinned app not running - launch it + Quickshell.execDetached(["gtk-launch", modelData.appId]) + } + } else if (mouse.button === Qt.RightButton) { + // Hide tooltip when showing context menu + appTooltip.hide() + contextMenu.show(appButton, modelData.toplevel || modelData) } } } diff --git a/Modules/Dock/DockMenu.qml b/Modules/Dock/DockMenu.qml new file mode 100644 index 00000000..f4b78ae5 --- /dev/null +++ b/Modules/Dock/DockMenu.qml @@ -0,0 +1,261 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons +import qs.Services +import qs.Widgets + +PopupWindow { + id: root + + property var toplevel: null + property Item anchorItem: null + property real scaling: 1.0 + property bool hovered: menuMouseArea.containsMouse || activateMouseArea.containsMouse || pinMouseArea.containsMouse || closeMouseArea.containsMouse + property var onAppClosed: null // Callback function for when an app is closed + + signal requestClose + + implicitWidth: 160 * scaling + implicitHeight: contextMenuColumn.implicitHeight + (Style.marginM * scaling * 2) + color: Color.transparent + visible: false + + // Helper functions for pin/unpin functionality + function isAppPinned(appId) { + if (!appId) + return false + const pinnedApps = Settings.data.dock.pinnedApps || [] + return pinnedApps.includes(appId) + } + + function toggleAppPin(appId) { + if (!appId) + return + + let pinnedApps = (Settings.data.dock.pinnedApps || []).slice() // Create a copy + const isPinned = pinnedApps.includes(appId) + + if (isPinned) { + // Unpin: remove from array + pinnedApps = pinnedApps.filter(id => id !== appId) + } else { + // Pin: add to array + pinnedApps.push(appId) + } + + // Update the settings + Settings.data.dock.pinnedApps = pinnedApps + } + + anchor.item: anchorItem + anchor.rect.x: anchorItem ? (anchorItem.width - implicitWidth) / 2 : 0 + anchor.rect.y: anchorItem ? -implicitHeight - (Style.marginM * scaling) : 0 + + function show(item, toplevelData) { + if (!item) { + Logger.warn("DockMenu", "anchorItem is undefined, won't show menu.") + return + } + + anchorItem = item + toplevel = toplevelData + visible = true + } + + function hide() { + visible = false + } + + // Close menu when clicking on background, track hover for the whole menu area + MouseArea { + id: menuMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: root.hide() // Close when clicking on the background (outside menu content) + } + + Shortcut { + sequences: ["Escape"] + enabled: root.visible + onActivated: root.hide() + } + + Rectangle { + anchors.fill: parent + color: Color.mSurface + radius: Style.radiusS * scaling + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + // Prevent clicks inside the menu from closing it + MouseArea { + anchors.fill: parent + onClicked: { + + } // Do nothing, just consume the click + } + + Column { + id: contextMenuColumn + anchors.fill: parent + anchors.margins: Style.marginM * scaling + spacing: 0 + + // Activate/Focus item + Rectangle { + width: parent.width + height: 32 * scaling + color: activateMouseArea.containsMouse ? Qt.alpha(Color.mSecondary, 0.2) : Color.transparent + radius: Style.radiusXS * scaling + + Row { + anchors.left: parent.left + anchors.leftMargin: Style.marginS * scaling + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS * scaling + + NIcon { + icon: "eye" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + anchors.verticalCenter: parent.verticalCenter + } + + NText { + text: { + if (!root.toplevel) + return "Activate" + // Check if this toplevel is active by comparing with ToplevelManager.activeToplevel + const isActive = ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === root.toplevel + return isActive ? "Focus" : "Activate" + } + font.pointSize: Style.fontSizeS * scaling + color: Color.mOnSurface + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: activateMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (root.toplevel?.activate) { + root.toplevel.activate() + } + root.hide() + } + } + } + + // Pin/Unpin item + Rectangle { + width: parent.width + height: 32 * scaling + color: pinMouseArea.containsMouse ? Qt.alpha(Color.mTertiary, 0.2) : Color.transparent + radius: Style.radiusXS * scaling + + Row { + anchors.left: parent.left + anchors.leftMargin: Style.marginS * scaling + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS * scaling + + NIcon { + icon: { + if (!root.toplevel) + return "pin" + return root.isAppPinned(root.toplevel.appId) ? "pinned-off" : "pin" + } + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + anchors.verticalCenter: parent.verticalCenter + } + + NText { + text: { + if (!root.toplevel) + return "Pin" + return root.isAppPinned(root.toplevel.appId) ? "Unpin" : "Pin" + } + font.pointSize: Style.fontSizeS * scaling + color: Color.mOnSurface + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: pinMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (root.toplevel?.appId) { + root.toggleAppPin(root.toplevel.appId) + } + root.hide() + } + } + } + + // Close item + Rectangle { + width: parent.width + height: 32 * scaling + color: closeMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.2) : Color.transparent + radius: Style.radiusXS * scaling + + Row { + anchors.left: parent.left + anchors.leftMargin: Style.marginS * scaling + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS * scaling + + NIcon { + icon: "x" + font.pointSize: Style.fontSizeL * scaling + color: closeMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurface + anchors.verticalCenter: parent.verticalCenter + } + + NText { + text: "Close" + font.pointSize: Style.fontSizeS * scaling + color: closeMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurface + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: closeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + // Check if toplevel is still valid before trying to close it + const isValidToplevel = root.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(root.toplevel) + + if (isValidToplevel && root.toplevel.close) { + root.toplevel.close() + // Trigger immediate dock update callback if provided + if (root.onAppClosed && typeof root.onAppClosed === "function") { + Qt.callLater(root.onAppClosed) + } + } else { + Logger.warn("DockMenu", "Cannot close app - invalid toplevel reference") + } + root.hide() + } + } + } + } + } +} diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 2257b027..48bfe37a 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -16,7 +16,7 @@ Variants { id: root required property ShellScreen modelData - readonly property real scaling: ScalingService.getScreenScale(modelData) + property real scaling: ScalingService.getScreenScale(modelData) // Access the notification model from the service - UPDATED NAME property ListModel notificationModel: NotificationService.activeList @@ -26,52 +26,75 @@ Variants { visible: (notificationModel.count > 0) + Connections { + target: ScalingService + function onScaleChanged(screenName, scale) { + if (root.modelData && screenName === root.modelData.name) { + root.scaling = scale + } + } + } + sourceComponent: PanelWindow { screen: modelData color: Color.transparent - // Position based on bar location - always at top - anchors.top: true - anchors.right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom" - anchors.left: Settings.data.bar.position === "left" + readonly property string location: (Settings.isLoaded && Settings.data && Settings.data.notifications && Settings.data.notifications.location) ? Settings.data.notifications.location : "top_right" + readonly property bool isTop: (location === "top") || (location.length >= 3 && location.substring(0, 3) === "top") + readonly property bool isBottom: (location === "bottom") || (location.length >= 6 && location.substring(0, 6) === "bottom") + readonly property bool isLeft: location.indexOf("_left") >= 0 + readonly property bool isRight: location.indexOf("_right") >= 0 + readonly property bool isCentered: (location === "top" || location === "bottom") + // Anchor selection based on location (window edges) + anchors.top: isTop + anchors.bottom: isBottom + anchors.left: isLeft + anchors.right: isRight + + // Margins depending on bar position and chosen location margins.top: { - switch (Settings.data.bar.position) { - case "top": - return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0) - default: - return Style.marginM * scaling + if (!(anchors.top)) + return 0 + var base = Style.marginM * scaling + if (Settings.data.bar.position === "top") { + var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0 + return (Style.barHeight * scaling) + base + floatExtraV } + return base } margins.bottom: { - switch (Settings.data.bar.position) { - case "bottom": - return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0) - default: + if (!(anchors.bottom)) return 0 + var base = Style.marginM * scaling + if (Settings.data.bar.position === "bottom") { + var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0 + return (Style.barHeight * scaling) + base + floatExtraV } + return base } margins.left: { - switch (Settings.data.bar.position) { - case "left": - return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0) - default: + if (!(anchors.left)) return 0 + var base = Style.marginM * scaling + if (Settings.data.bar.position === "left") { + var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0 + return (Style.barHeight * scaling) + base + floatExtraH } + return base } margins.right: { - switch (Settings.data.bar.position) { - case "right": - return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0) - case "top": - case "bottom": - return Style.marginM * scaling - default: + if (!(anchors.right)) return 0 + var base = Style.marginM * scaling + if (Settings.data.bar.position === "right") { + var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0 + return (Style.barHeight * scaling) + base + floatExtraH } + return base } implicitWidth: 360 * scaling @@ -110,9 +133,12 @@ Variants { // Main notification container ColumnLayout { id: notificationStack - anchors.top: parent.top - anchors.right: (Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom") ? parent.right : undefined - anchors.left: Settings.data.bar.position === "left" ? parent.left : undefined + // Anchor the stack inside the window based on chosen location + anchors.top: parent.isTop ? parent.top : undefined + anchors.bottom: parent.isBottom ? parent.bottom : undefined + anchors.left: parent.isLeft ? parent.left : undefined + anchors.right: parent.isRight ? parent.right : undefined + anchors.horizontalCenter: parent.isCentered ? parent.horizontalCenter : undefined spacing: Style.marginS * scaling width: 360 * scaling visible: true diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml index ef87e177..df48506c 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml @@ -16,18 +16,34 @@ ColumnLayout { // Local state property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo + property string valueCustomIconPath: widgetData.customIconPath !== undefined ? widgetData.customIconPath : "" function saveSettings() { var settings = Object.assign({}, widgetData || {}) settings.icon = valueIcon settings.useDistroLogo = valueUseDistroLogo + settings.customIconPath = valueCustomIconPath return settings } NToggle { label: "Use distro logo instead of icon" checked: valueUseDistroLogo - onToggled: checked => valueUseDistroLogo = checked + onToggled: { + valueUseDistroLogo = checked + if (checked) { + valueCustomIconPath = "" + valueIcon = "" + } + } + } + + NFilePicker { + id: filePicker + title: "Select a custom icon" + onFileSelected: function (filePath) { + valueCustomIconPath = "file://" + filePath + } } RowLayout { @@ -35,21 +51,35 @@ ColumnLayout { NLabel { label: "Icon" - description: "Select an icon from the library." + description: "Select an icon from the library or a custom file." + } + + NImageCircled { + Layout.alignment: Qt.AlignVCenter + imagePath: valueCustomIconPath + visible: valueCustomIconPath !== "" + width: Style.fontSizeXL * 2 * scaling + height: Style.fontSizeXL * 2 * scaling } NIcon { Layout.alignment: Qt.AlignVCenter icon: valueIcon font.pointSize: Style.fontSizeXL * scaling - visible: valueIcon !== "" + visible: valueIcon !== "" && valueCustomIconPath === "" } NButton { enabled: !valueUseDistroLogo - text: "Browse" + text: "Browse Library" onClicked: iconPicker.open() } + + NButton { + enabled: !valueUseDistroLogo + text: "Browse File" + onClicked: filePicker.open() + } } NIconPicker { @@ -57,6 +87,7 @@ ColumnLayout { initialIcon: valueIcon onIconSelected: function (iconName) { valueIcon = iconName + valueCustomIconPath = "" } } -} +} \ No newline at end of file diff --git a/Modules/SettingsPanel/Tabs/NotificationsTab.qml b/Modules/SettingsPanel/Tabs/NotificationsTab.qml index 68a56a61..64093d49 100644 --- a/Modules/SettingsPanel/Tabs/NotificationsTab.qml +++ b/Modules/SettingsPanel/Tabs/NotificationsTab.qml @@ -38,6 +38,39 @@ ColumnLayout { checked: Settings.data.notifications.doNotDisturb onToggled: checked => Settings.data.notifications.doNotDisturb = checked } + + NComboBox { + label: "Location" + description: "Where notifications appear on screen." + model: ListModel { + ListElement { + key: "top" + name: "Top" + } + ListElement { + key: "top_left" + name: "Top left" + } + ListElement { + key: "top_right" + name: "Top right" + } + ListElement { + key: "bottom" + name: "Bottom" + } + ListElement { + key: "bottom_left" + name: "Bottom left" + } + ListElement { + key: "bottom_right" + name: "Bottom right" + } + } + currentKey: Settings.data.notifications.location || "top_right" + onSelected: key => Settings.data.notifications.location = key + } } NDivider { diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index d8df0bdb..d4594468 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -103,7 +103,8 @@ Singleton { "SidePanelToggle": { "allowUserSettings": true, "useDistroLogo": false, - "icon": "noctalia" + "icon": "noctalia", + "customIconPath": "" }, "Volume": { "allowUserSettings": true,