diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 2650fcc7..ca9893fc 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -474,6 +474,10 @@ "description": "Set a different wallpaper folder for each monitor.", "tooltip": "Browse for wallpaper folder" }, + "selector-position": { + "label": "Position", + "description": "Choose where the wallpaper selector panel appears." + }, "select-folder": "Select wallpaper folder", "select-monitor-folder": "Select monitor wallpaper folder" }, @@ -1500,7 +1504,8 @@ }, "launcher": { "position": { - "center": "Center (default)", + "follow_bar": "Follow bar (default)", + "center": "Center", "top_left": "Top left", "top_right": "Top right", "bottom_left": "Bottom left", diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 2dfd7cf2..8162e410 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -117,11 +117,12 @@ "transitionDuration": 1500, "transitionType": "random", "transitionEdgeSmoothness": 0.05, - "monitors": [] + "monitors": [], + "selectorPosition": "follow_bar" }, "appLauncher": { "enableClipboardHistory": false, - "position": "center", + "position": "follow_bar", "backgroundOpacity": 1, "pinnedExecs": [], "useApp2Unit": false, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 6fdb283c..68ce3b3f 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -14,7 +14,7 @@ Singleton { readonly property alias data: adapter property bool isLoaded: false property bool directoriesCreated: false - property int settingsVersion: 16 + property int settingsVersion: 17 property bool isDebug: Quickshell.env("NOCTALIA_DEBUG") === "1" // Define our app directories @@ -256,6 +256,7 @@ Singleton { property string transitionType: "random" property real transitionEdgeSmoothness: 0.05 property list monitors: [] + property string panelPosition: "folow_bar" } // applauncher diff --git a/Modules/Bar/Audio/AudioPanel.qml b/Modules/Bar/Audio/AudioPanel.qml index 7393f846..e1f240cc 100644 --- a/Modules/Bar/Audio/AudioPanel.qml +++ b/Modules/Bar/Audio/AudioPanel.qml @@ -17,7 +17,7 @@ NPanel { property bool localInputVolumeChanging: false preferredWidth: 380 * Style.uiScaleRatio - preferredHeight: 500 * Style.uiScaleRatio + preferredHeight: 420 * Style.uiScaleRatio // Connections to update local volumes when AudioService changes Connections { diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 5f1ed23b..c996a9ea 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -20,13 +20,23 @@ NPanel { panelKeyboardFocus: true // Needs Exclusive focus for text input // Positioning - readonly property string launcherPosition: Settings.data.appLauncher.position - panelAnchorHorizontalCenter: launcherPosition === "center" || launcherPosition.endsWith("_center") - panelAnchorVerticalCenter: launcherPosition === "center" - panelAnchorLeft: launcherPosition !== "center" && launcherPosition.endsWith("_left") - panelAnchorRight: launcherPosition !== "center" && launcherPosition.endsWith("_right") - panelAnchorBottom: launcherPosition.startsWith("bottom_") - panelAnchorTop: launcherPosition.startsWith("top_") + readonly property string panelPosition: { + if (Settings.data.appLauncher.position === "follow_bar") { + if (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") { + return `center_${Settings.data.bar.position}` + } else { + return `${Settings.data.bar.position}_center` + } + } else { + return Settings.data.appLauncher.position + } + } + panelAnchorHorizontalCenter: panelPosition === "center" || panelPosition.endsWith("_center") + panelAnchorVerticalCenter: panelPosition === "center" + panelAnchorLeft: panelPosition !== "center" && panelPosition.endsWith("_left") + panelAnchorRight: panelPosition !== "center" && panelPosition.endsWith("_right") + panelAnchorBottom: panelPosition.startsWith("bottom_") + panelAnchorTop: panelPosition.startsWith("top_") // Core state property string searchText: "" diff --git a/Modules/Settings/Tabs/LauncherTab.qml b/Modules/Settings/Tabs/LauncherTab.qml index dd6cee27..43eb7075 100644 --- a/Modules/Settings/Tabs/LauncherTab.qml +++ b/Modules/Settings/Tabs/LauncherTab.qml @@ -15,11 +15,13 @@ ColumnLayout { } NComboBox { - id: launcherPosition label: I18n.tr("settings.launcher.settings.position.label") description: I18n.tr("settings.launcher.settings.position.description") Layout.fillWidth: true model: [{ + "key": "follow_bar", + "name": I18n.tr("options.launcher.position.follow_bar") + }, { "key": "center", "name": I18n.tr("options.launcher.position.center") }, { diff --git a/Modules/Settings/Tabs/WallpaperTab.qml b/Modules/Settings/Tabs/WallpaperTab.qml index 002c6996..5d7175aa 100644 --- a/Modules/Settings/Tabs/WallpaperTab.qml +++ b/Modules/Settings/Tabs/WallpaperTab.qml @@ -103,6 +103,41 @@ ColumnLayout { } } } + + NComboBox { + label: I18n.tr("settings.wallpaper.settings.selector-position.label") + description: I18n.tr("settings.wallpaper.settings.selector-position.description") + Layout.fillWidth: true + model: [{ + "key": "follow_bar", + "name": I18n.tr("options.launcher.position.follow_bar") + }, { + "key": "center", + "name": I18n.tr("options.launcher.position.center") + }, { + "key": "top_center", + "name": I18n.tr("options.launcher.position.top_center") + }, { + "key": "top_left", + "name": I18n.tr("options.launcher.position.top_left") + }, { + "key": "top_right", + "name": I18n.tr("options.launcher.position.top_right") + }, { + "key": "bottom_left", + "name": I18n.tr("options.launcher.position.bottom_left") + }, { + "key": "bottom_right", + "name": I18n.tr("options.launcher.position.bottom_right") + }, { + "key": "bottom_center", + "name": I18n.tr("options.launcher.position.bottom_center") + }] + currentKey: Settings.data.wallpaper.panelPosition + onSelected: function (key) { + Settings.data.wallpaper.panelPosition = key + } + } } NDivider { diff --git a/Modules/Wallpaper/WallpaperPanel.qml b/Modules/Wallpaper/WallpaperPanel.qml index bbb2aaa8..3cfa36fc 100644 --- a/Modules/Wallpaper/WallpaperPanel.qml +++ b/Modules/Wallpaper/WallpaperPanel.qml @@ -17,17 +17,25 @@ NPanel { preferredWidthRatio: 0.5 preferredHeightRatio: 0.45 - // Positioning - Use launcher position. This saves a setting... - readonly property string launcherPosition: Settings.data.appLauncher.position - panelAnchorHorizontalCenter: launcherPosition === "center" || launcherPosition.endsWith("_center") - panelAnchorVerticalCenter: launcherPosition === "center" - panelAnchorLeft: launcherPosition !== "center" && launcherPosition.endsWith("_left") - panelAnchorRight: launcherPosition !== "center" && launcherPosition.endsWith("_right") - panelAnchorBottom: launcherPosition.startsWith("bottom_") - panelAnchorTop: launcherPosition.startsWith("top_") + // Positioning + readonly property string panelPosition: { + if (Settings.data.wallpaper.panelPosition === "follow_bar") { + if (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") { + return `center_${Settings.data.bar.position}` + } else { + return `${Settings.data.bar.position}_center` + } + } else { + return Settings.data.wallpaper.panelPosition + } + } + panelAnchorHorizontalCenter: panelPosition === "center" || panelPosition.endsWith("_center") + panelAnchorVerticalCenter: panelPosition === "center" + panelAnchorLeft: panelPosition !== "center" && panelPosition.endsWith("_left") + panelAnchorRight: panelPosition !== "center" && panelPosition.endsWith("_right") + panelAnchorBottom: panelPosition.startsWith("bottom_") + panelAnchorTop: panelPosition.startsWith("top_") - // panelAnchorHorizontalCenter: true - // panelAnchorVerticalCenter: true panelKeyboardFocus: true // Needs Exclusive focus for text input (search) // Store direct reference to content for instant access diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 3fff1d91..92a97705 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -17,6 +17,9 @@ Item { property bool forceDetached: false // Force panel to be detached regardless of settings property bool attachedToBar: (Settings.data.ui.panelsAttachedToBar && Settings.data.bar.backgroundOpacity > opacityThreshold && !forceDetached) + // Edge snapping: if panel is within this distance (in pixels) from a screen edge, snap + property real edgeSnapDistance: 40 + // Keyboard focus documentation (not currently used for focus mode) // Just for documentation: true for panels with text input // NFullScreenWindow always uses Exclusive focus when any panel is open @@ -343,6 +346,8 @@ Item { // Position the panel using explicit x/y coordinates (no anchors) // This makes coordinates clearer for the click-through mask system x: { + var calculatedX + // If useButtonPosition is enabled, align panel X with button // Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid // after the source panel (e.g., ControlCenter) closes, but we still have valid position data @@ -357,7 +362,7 @@ Item { // Panel sits right at bar edge (inverted corners curve up/down) // Slide from the bar when opening // Shift left by 1px to eliminate any gap between bar and panel - return leftBarEdge - slideOffset - 1 + calculatedX = leftBarEdge - slideOffset - 1 } else { // right // Panel to the left of right bar @@ -365,14 +370,14 @@ Item { // Panel sits right at bar edge (inverted corners curve up/down) // Slide from the bar when opening // Shift right by 1px to eliminate any gap between bar and panel - return rightBarEdge - width + slideOffset + 1 + calculatedX = rightBarEdge - width + slideOffset + 1 } } else { // Detached panels: center on button X position var panelX = root.buttonPosition.x + root.buttonWidth / 2 - width / 2 // Clamp to screen bounds with margins panelX = Math.max(Style.marginL, Math.min(panelX, parent.width - width - Style.marginL)) - return panelX + calculatedX = panelX } } else { // For horizontal bars, center panel on button X position @@ -389,56 +394,87 @@ Item { } else { panelX = Math.max(Style.marginL, Math.min(panelX, parent.width - width - Style.marginL)) } - return panelX + calculatedX = panelX } - } - - // Standard anchor positioning - Logger.d("NPanel", "Fallback to standard anchor positioning") - - if (root.panelAnchorHorizontalCenter) { - Logger.d("NPanel", " -> Horizontal center") - return (parent.width - width) / 2 - } else if (root.effectivePanelAnchorRight) { - Logger.d("NPanel", " -> Right anchor") - return parent.width - width - Style.marginL - } else if (root.effectivePanelAnchorLeft) { - Logger.d("NPanel", " -> Left anchor") - return Style.marginL } else { - // No explicit anchor: default to centering on bar - Logger.d("NPanel", " -> Default to center (no explicit anchor)") - // For horizontal bars: center horizontally - // For vertical bars: center horizontally in available space - if (root.barIsVertical) { - // Center in the space not occupied by the bar - if (root.barPosition === "left") { - var availableStart = root.barMarginH + Style.barHeight - var availableWidth = parent.width - availableStart - Style.marginL - return availableStart + (availableWidth - width) / 2 + // Standard anchor positioning + Logger.d("NPanel", "Fallback to standard anchor positioning") + + if (root.panelAnchorHorizontalCenter) { + Logger.d("NPanel", " -> Horizontal center") + calculatedX = (parent.width - width) / 2 + } else if (root.effectivePanelAnchorRight) { + Logger.d("NPanel", " -> Right anchor") + // When attached to right vertical bar, position next to bar (like useButtonPosition does) + if (root.attachedToBar && root.barIsVertical && root.barPosition === "right") { + var rightBarEdge = parent.width - root.barMarginH - Style.barHeight + calculatedX = rightBarEdge - width + 1 // +1 to eliminate gap } else { - // right - var availableWidth = parent.width - root.barMarginH - Style.barHeight - Style.marginL - return Style.marginL + (availableWidth - width) / 2 + calculatedX = parent.width - width - Style.marginL + } + } else if (root.effectivePanelAnchorLeft) { + Logger.d("NPanel", " -> Left anchor") + // When attached to left vertical bar, position next to bar (like useButtonPosition does) + if (root.attachedToBar && root.barIsVertical && root.barPosition === "left") { + var leftBarEdge = root.barMarginH + Style.barHeight + calculatedX = leftBarEdge - 1 // -1 to eliminate gap + } else { + calculatedX = Style.marginL } } else { - // For horizontal bars: center horizontally, respect bar margins if attached - if (root.attachedToBar) { - // When attached, respect bar bounds (like button position does) - var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) - var barLeftEdge = root.barMarginH + cornerInset - var barRightEdge = parent.width - root.barMarginH - cornerInset - var centeredX = (parent.width - width) / 2 - return Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - width)) + // No explicit anchor: default to centering on bar + Logger.d("NPanel", " -> Default to center (no explicit anchor)") + + // For horizontal bars: center horizontally + // For vertical bars: center horizontally in available space + if (root.barIsVertical) { + // Center in the space not occupied by the bar + if (root.barPosition === "left") { + var availableStart = root.barMarginH + Style.barHeight + var availableWidth = parent.width - availableStart - Style.marginL + calculatedX = availableStart + (availableWidth - width) / 2 + } else { + // right + var availableWidth = parent.width - root.barMarginH - Style.barHeight - Style.marginL + calculatedX = Style.marginL + (availableWidth - width) / 2 + } } else { - return (parent.width - width) / 2 + // For horizontal bars: center horizontally, respect bar margins if attached + if (root.attachedToBar) { + // When attached, respect bar bounds (like button position does) + var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) + var barLeftEdge = root.barMarginH + cornerInset + var barRightEdge = parent.width - root.barMarginH - cornerInset + var centeredX = (parent.width - width) / 2 + calculatedX = Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - width)) + } else { + calculatedX = (parent.width - width) / 2 + } } } } + + // Edge snapping: snap to screen edges if close (only when attached and bar is not floating) + if (root.attachedToBar && !root.barFloating && parent.width > 0 && width > 0) { + var leftEdgePos = root.barMarginH + var rightEdgePos = parent.width - root.barMarginH - width + + // Snap to left edge if within snap distance + if (Math.abs(calculatedX - leftEdgePos) <= root.edgeSnapDistance) { + calculatedX = leftEdgePos + } // Snap to right edge if within snap distance + else if (Math.abs(calculatedX - rightEdgePos) <= root.edgeSnapDistance) { + calculatedX = rightEdgePos + } + } + + return calculatedX } y: { + var calculatedY + // If useButtonPosition is enabled, position panel relative to bar // Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid // after the source panel (e.g., ControlCenter) closes, but we still have valid position data @@ -450,9 +486,9 @@ Item { // Panel sits right at bar edge (inverted corners curve to the sides) // Slide from the bar when opening // Shift up by 1px to eliminate any gap between bar and panel - return topBarEdge - slideOffset - 1 + calculatedY = topBarEdge - slideOffset - 1 } else { - return topBarEdge + Style.marginM + calculatedY = topBarEdge + Style.marginM } } else if (root.barPosition === "bottom") { // Panel above bottom bar @@ -461,9 +497,9 @@ Item { // Panel sits right at bar edge (inverted corners curve to the sides) // Slide from the bar when opening // Shift down by 1px to eliminate any gap between bar and panel - return bottomBarEdge - height + slideOffset + 1 + calculatedY = bottomBarEdge - height + slideOffset + 1 } else { - return bottomBarEdge - height - Style.marginM + calculatedY = bottomBarEdge - height - Style.marginM } } else if (root.barIsVertical) { // For vertical bars, center panel on button Y position @@ -481,83 +517,103 @@ Item { } else { panelY = Math.max(Style.marginL + extraPadding, Math.min(panelY, parent.height - height - Style.marginL - extraPadding)) } - return panelY - } - } - - // Standard anchor positioning - // Calculate bar offset for detached panels - they should never overlap the bar - var barOffset = 0 - if (!root.attachedToBar) { - // For detached panels, always account for bar position - if (root.barPosition === "top") { - barOffset = root.barMarginV + Style.barHeight + Style.marginM - } else if (root.barPosition === "bottom") { - barOffset = root.barMarginV + Style.barHeight + Style.marginM + calculatedY = panelY } } else { - // For attached panels with explicit anchors - if (root.effectivePanelAnchorTop && root.barPosition === "top") { - // When attached to top bar: position right at bar edge (like useButtonPosition does) - // Shift up by 1px to eliminate gap between bar and panel - return root.barMarginV + Style.barHeight - slideOffset - 1 - } else if (root.effectivePanelAnchorBottom && root.barPosition === "bottom") { - // When attached to bottom bar: position right at bar edge - // Shift down by 1px to eliminate gap between bar and panel - return parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 - } else if (!root.hasExplicitVerticalAnchor) { - // No explicit vertical anchor AND attached: default to attaching to bar edge + + // Standard anchor positioning + // Calculate bar offset for detached panels - they should never overlap the bar + var barOffset = 0 + if (!root.attachedToBar) { + // For detached panels, always account for bar position if (root.barPosition === "top") { - // Attach to top bar - return root.barMarginV + Style.barHeight - slideOffset - 1 + barOffset = root.barMarginV + Style.barHeight + Style.marginM } else if (root.barPosition === "bottom") { - // Attach to bottom bar - return parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 - } - // For vertical bars with no explicit anchor: center vertically on bar - // This is handled in the else block below - } - } - - if (root.panelAnchorVerticalCenter) { - return (parent.height - height) / 2 - } else if (root.effectivePanelAnchorTop) { - return barOffset + Style.marginL - } else if (root.effectivePanelAnchorBottom) { - return parent.height - height - barOffset - Style.marginL - } else { - // No explicit vertical anchor - if (root.barIsVertical) { - // For vertical bars: center vertically on bar - if (root.attachedToBar) { - // When attached, respect bar bounds - var cornerInset = root.barFloating ? Style.radiusL * 2 : 0 - var barTopEdge = root.barMarginV + cornerInset - var barBottomEdge = parent.height - root.barMarginV - cornerInset - var centeredY = (parent.height - height) / 2 - return Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - height)) - } else { - return (parent.height - height) / 2 + barOffset = root.barMarginV + Style.barHeight + Style.marginM } } else { - // For horizontal bars: attach to bar edge by default - if (root.attachedToBar) { + // For attached panels with explicit anchors + if (root.effectivePanelAnchorTop && root.barPosition === "top") { + // When attached to top bar: position right at bar edge (like useButtonPosition does) + // Shift up by 1px to eliminate gap between bar and panel + calculatedY = root.barMarginV + Style.barHeight - slideOffset - 1 + } else if (root.effectivePanelAnchorBottom && root.barPosition === "bottom") { + // When attached to bottom bar: position right at bar edge + // Shift down by 1px to eliminate gap between bar and panel + calculatedY = parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 + } else if (!root.hasExplicitVerticalAnchor) { + // No explicit vertical anchor AND attached: default to attaching to bar edge if (root.barPosition === "top") { - return root.barMarginV + Style.barHeight - slideOffset - 1 + // Attach to top bar + calculatedY = root.barMarginV + Style.barHeight - slideOffset - 1 } else if (root.barPosition === "bottom") { - return parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 + // Attach to bottom bar + calculatedY = parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 } + // For vertical bars with no explicit anchor: fall through to center vertically on bar } - // Detached or no bar position: use default positioning - if (root.barPosition === "top") { - return barOffset + Style.marginL - } else if (root.barPosition === "bottom") { - return Style.marginL + } + + // Continue if calculatedY was already set above, or proceed with anchor positioning + if (calculatedY === undefined) { + if (root.panelAnchorVerticalCenter) { + calculatedY = (parent.height - height) / 2 + } else if (root.effectivePanelAnchorTop) { + calculatedY = barOffset + Style.marginL + } else if (root.effectivePanelAnchorBottom) { + calculatedY = parent.height - height - barOffset - Style.marginL } else { - return Style.marginL + // No explicit vertical anchor + if (root.barIsVertical) { + // For vertical bars: center vertically on bar + if (root.attachedToBar) { + // When attached, respect bar bounds + var cornerInset = root.barFloating ? Style.radiusL * 2 : 0 + var barTopEdge = root.barMarginV + cornerInset + var barBottomEdge = parent.height - root.barMarginV - cornerInset + var centeredY = (parent.height - height) / 2 + calculatedY = Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - height)) + } else { + calculatedY = (parent.height - height) / 2 + } + } else { + // For horizontal bars: attach to bar edge by default + if (root.attachedToBar && !root.barIsVertical) { + if (root.barPosition === "top") { + calculatedY = root.barMarginV + Style.barHeight - slideOffset - 1 + } else if (root.barPosition === "bottom") { + calculatedY = parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 + } + } else { + // Detached or no bar position: use default positioning + if (root.barPosition === "top") { + calculatedY = barOffset + Style.marginL + } else if (root.barPosition === "bottom") { + calculatedY = Style.marginL + } else { + calculatedY = Style.marginL + } + } + } } } } + + // Edge snapping: snap to screen edges if close (only when attached and bar is not floating) + if (root.attachedToBar && !root.barFloating && parent.height > 0 && height > 0) { + var topEdgePos = root.barMarginV + var bottomEdgePos = parent.height - root.barMarginV - height + + // Snap to top edge if within snap distance + if (Math.abs(calculatedY - topEdgePos) <= root.edgeSnapDistance) { + calculatedY = topEdgePos + } // Snap to bottom edge if within snap distance + else if (Math.abs(calculatedY - bottomEdgePos) <= root.edgeSnapDistance) { + calculatedY = bottomEdgePos + } + } + + return calculatedY } // MouseArea to catch clicks on the panel and prevent them from reaching the background