diff --git a/config/Config.qml b/config/Config.qml index 1d7cc321..0c70e2c5 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -22,6 +22,7 @@ import "defaults/system.js" as SystemDefaults import "defaults/dock.js" as DockDefaults import "defaults/ai.js" as AiDefaults import "ConfigValidator.js" as ConfigValidator +import "ScreenPositions.js" as ScreenPositions Singleton { id: root @@ -527,6 +528,7 @@ Singleton { adapter: JsonAdapter { property string position: "top" + property var screenPositions: ({}) property string launcherIcon: "" property bool launcherIconTint: true property bool launcherIconFullTint: true @@ -672,6 +674,7 @@ Singleton { adapter: JsonAdapter { property string theme: "default" property string position: "top" + property var screenPositions: ({}) property int hoverRegionHeight: 8 property bool keepHidden: false property string noMediaDisplay: "userHost" @@ -1088,6 +1091,7 @@ Singleton { property bool enabled: false property string theme: "default" property string position: "bottom" + property var screenPositions: ({}) property int height: 56 property int iconSize: 40 property int spacing: 4 @@ -3254,6 +3258,20 @@ Singleton { // Bar configuration property QtObject bar: barLoader.adapter property bool showBackground: theme.srBarBg.opacity > 0 + readonly property var shellEdgePositions: ["top", "bottom", "left", "right"] + readonly property var notchEdgePositions: ["top", "bottom"] + + function barPositionForScreen(screenName) { + return ScreenPositions.positionForScreen(bar, screenName, "top", shellEdgePositions); + } + + function notchPositionForScreen(screenName) { + return ScreenPositions.positionForScreen(notch, screenName, "top", notchEdgePositions); + } + + function dockPositionForScreen(screenName) { + return ScreenPositions.positionForScreen(dock, screenName, "bottom", shellEdgePositions); + } // Workspace configuration property QtObject workspaces: workspacesLoader.adapter diff --git a/config/ConfigValidator.js b/config/ConfigValidator.js index 5cb593af..61c5a174 100644 --- a/config/ConfigValidator.js +++ b/config/ConfigValidator.js @@ -9,6 +9,13 @@ function validate(current, defaults, keyName) { return clone(defaults); } + if (keyName === "screenPositions") { + if (typeof current !== 'object' || Array.isArray(current)) { + return clone(defaults); + } + return clone(current); + } + if (Array.isArray(defaults)) { if (!Array.isArray(current)) { return clone(defaults); diff --git a/config/ScreenPositions.js b/config/ScreenPositions.js new file mode 100644 index 00000000..16f22242 --- /dev/null +++ b/config/ScreenPositions.js @@ -0,0 +1,20 @@ +.pragma library + +function isValidPosition(value, allowedPositions) { + return typeof value === "string" && allowedPositions.indexOf(value) !== -1; +} + +function positionForScreen(config, screenName, fallbackPosition, allowedPositions) { + var globalPosition = fallbackPosition; + + if (config && isValidPosition(config.position, allowedPositions)) { + globalPosition = config.position; + } + + if (!config || !screenName || !config.screenPositions || typeof config.screenPositions !== "object" || Array.isArray(config.screenPositions)) { + return globalPosition; + } + + var screenPosition = config.screenPositions[screenName]; + return isValidPosition(screenPosition, allowedPositions) ? screenPosition : globalPosition; +} diff --git a/config/defaults/bar.js b/config/defaults/bar.js index 7962cdde..2dda51e9 100644 --- a/config/defaults/bar.js +++ b/config/defaults/bar.js @@ -2,6 +2,7 @@ var data = { "position": "top", + "screenPositions": {}, "launcherIcon": "", "launcherIconTint": true, "launcherIconFullTint": true, diff --git a/config/defaults/dock.js b/config/defaults/dock.js index ffde30ec..fc949d52 100644 --- a/config/defaults/dock.js +++ b/config/defaults/dock.js @@ -4,6 +4,7 @@ var data = { "enabled": true, "theme": "default", "position": "bottom", + "screenPositions": {}, "height": 48, "iconSize": 24, "spacing": 4, diff --git a/config/defaults/notch.js b/config/defaults/notch.js index decb7159..f7908e4d 100644 --- a/config/defaults/notch.js +++ b/config/defaults/notch.js @@ -3,6 +3,7 @@ var data = { "theme": "default", "position": "top", + "screenPositions": {}, "hoverRegionHeight": 8, "keepHidden": false, "noMediaDisplay": "userHost", diff --git a/modules/bar/BarContent.qml b/modules/bar/BarContent.qml index b9e57ca7..ba4d7598 100644 --- a/modules/bar/BarContent.qml +++ b/modules/bar/BarContent.qml @@ -25,7 +25,7 @@ Item { required property ShellScreen screen - property string barPosition: (Config.bar && Config.bar.position !== undefined && ["top", "bottom", "left", "right"].includes(Config.bar.position) ? Config.bar.position : "top") + property string barPosition: Config.barPositionForScreen(screen.name) property string orientation: barPosition === "left" || barPosition === "right" ? "vertical" : "horizontal" // Auto-hide properties @@ -69,7 +69,7 @@ Item { // Check if notch hover is active (for synchronized reveal when bar is at same side) // NOTE: We access Visibilities.notchPanels directly because UnifiedShellPanel registers itself as the panel ref readonly property var notchPanelRef: Visibilities.notchPanels[screen.name] - readonly property string notchPosition: (Config.notchPosition !== undefined ? Config.notchPosition : "top") + readonly property string notchPosition: Config.notchPositionForScreen(screen.name) readonly property bool notchHoverActive: { if (barPosition !== notchPosition) return false; @@ -145,7 +145,7 @@ Item { readonly property bool integratedDockEnabled: (Config.dock && Config.dock.enabled !== undefined ? Config.dock.enabled : false) && (Config.dock && Config.dock.theme !== undefined ? Config.dock.theme : "default") === "integrated" // Map dock position for integrated based on orientation readonly property string integratedDockPosition: { - const pos = (Config.dock && Config.dock.position !== undefined ? Config.dock.position : "center"); + const pos = Config.dockPositionForScreen(screen.name); if (root.orientation === "horizontal") { if (pos === "left" || pos === "start") diff --git a/modules/bar/IntegratedDock.qml b/modules/bar/IntegratedDock.qml index 201644db..600be43e 100644 --- a/modules/bar/IntegratedDock.qml +++ b/modules/bar/IntegratedDock.qml @@ -19,7 +19,7 @@ StyledRect { readonly property bool isVertical: orientation === "vertical" readonly property bool isIntegrated: (Config.dock?.theme ?? "default") === "integrated" - readonly property string dockPosition: Config.dock?.position ?? "center" + readonly property string dockPosition: Config.dockPositionForScreen(bar.screen.name) // Compact sizing for integrated dock readonly property int iconSize: 18 diff --git a/modules/desktop/Desktop.qml b/modules/desktop/Desktop.qml index 7444afa8..3b0ed58d 100644 --- a/modules/desktop/Desktop.qml +++ b/modules/desktop/Desktop.qml @@ -12,7 +12,7 @@ PanelWindow { property int barSize: Config.showBackground ? 44 : 40 property int bottomTextMargin: 32 - property string barPosition: ["top", "bottom", "left", "right"].includes(Config.bar.position) ? Config.bar.position : "top" + property string barPosition: Config.barPositionForScreen(screen?.name ?? "") anchors { top: true diff --git a/modules/dock/DockContent.qml b/modules/dock/DockContent.qml index a6b31db3..5406c6f9 100644 --- a/modules/dock/DockContent.qml +++ b/modules/dock/DockContent.qml @@ -29,9 +29,9 @@ Item { readonly property bool isDefault: theme === "default" // Position configuration with fallback logic to avoid bar collision - readonly property string userPosition: Config.dock?.position ?? "bottom" - readonly property string barPosition: Config.bar?.position ?? "top" - readonly property string notchPosition: Config.notchPosition ?? "top" + readonly property string userPosition: Config.dockPositionForScreen(screen.name) + readonly property string barPosition: Config.barPositionForScreen(screen.name) + readonly property string notchPosition: Config.notchPositionForScreen(screen.name) // Effective position readonly property string position: { diff --git a/modules/frame/ScreenFrame.qml b/modules/frame/ScreenFrame.qml index c6d02b09..deba7342 100644 --- a/modules/frame/ScreenFrame.qml +++ b/modules/frame/ScreenFrame.qml @@ -50,7 +50,7 @@ Item { readonly property int sidebarMargin: 4 readonly property int sidebarExpansion: sidebarPinned ? (sidebarWidth + thickness) : 0 - readonly property string barPos: Config.bar?.position ?? "top" + readonly property string barPos: Config.barPositionForScreen(targetScreen.name) // Bar height is 44. Total size = Thickness (Outer) + Bar (44) + Thickness (Inner) readonly property int barExpansion: 44 + thickness readonly property int topThickness: hasFullscreenWindow ? 0 : (thickness + ((containBar && barPos === "top") ? barExpansion : 0)) diff --git a/modules/frame/ScreenFrameContent.qml b/modules/frame/ScreenFrameContent.qml index a4c725b8..e41423f2 100644 --- a/modules/frame/ScreenFrameContent.qml +++ b/modules/frame/ScreenFrameContent.qml @@ -18,8 +18,8 @@ Item { // State source: Singletons and Registry readonly property bool frameEnabled: Config.bar?.frameEnabled ?? false readonly property bool configContainBar: Config.bar?.containBar ?? false - readonly property string barPos: Config.bar?.position ?? "top" - readonly property string notchPos: Config.notchPosition ?? "top" + readonly property string barPos: Config.barPositionForScreen(targetScreen.name) + readonly property string notchPos: Config.notchPositionForScreen(targetScreen.name) readonly property var barPanel: Visibilities.barPanels[targetScreen.name] readonly property var dockPanel: Visibilities.dockPanels[targetScreen.name] diff --git a/modules/globals/GlobalStates.qml b/modules/globals/GlobalStates.qml index 0a22f221..fa9e11e1 100644 --- a/modules/globals/GlobalStates.qml +++ b/modules/globals/GlobalStates.qml @@ -345,11 +345,11 @@ Singleton { // Shell config sections and their properties readonly property var _shellSections: { - "bar": ["position", "launcherIcon", "launcherIconTint", "launcherIconFullTint", "launcherIconSize", "enableFirefoxPlayer", "screenList", "frameEnabled", "frameThickness", "pinnedOnStartup", "hoverToReveal", "hoverRegionHeight", "showPinButton", "availableOnFullscreen", "pillStyle", "use12hFormat", "containBar", "keepBarShadow", "keepBarBorder"], - "notch": ["theme", "position", "hoverRegionHeight", "keepHidden"], + "bar": ["position", "screenPositions", "launcherIcon", "launcherIconTint", "launcherIconFullTint", "launcherIconSize", "enableFirefoxPlayer", "screenList", "frameEnabled", "frameThickness", "pinnedOnStartup", "hoverToReveal", "hoverRegionHeight", "showPinButton", "availableOnFullscreen", "pillStyle", "use12hFormat", "containBar", "keepBarShadow", "keepBarBorder"], + "notch": ["theme", "position", "screenPositions", "hoverRegionHeight", "keepHidden"], "workspaces": ["shown", "showAppIcons", "alwaysShowNumbers", "showNumbers", "dynamic"], "overview": ["rows", "columns", "scale", "workspaceSpacing"], - "dock": ["enabled", "theme", "position", "height", "iconSize", "spacing", "margin", "hoverRegionHeight", "pinnedOnStartup", "hoverToReveal", "availableOnFullscreen", "showRunningIndicators", "showPinButton", "showOverviewButton", "screenList", "keepHidden"], + "dock": ["enabled", "theme", "position", "screenPositions", "height", "iconSize", "spacing", "margin", "hoverRegionHeight", "pinnedOnStartup", "hoverToReveal", "availableOnFullscreen", "showRunningIndicators", "showPinButton", "showOverviewButton", "screenList", "keepHidden"], "lockscreen": ["position"], "desktop": ["enabled", "iconSize", "spacingVertical", "textColor"], "system": ["idle", "ocr"] diff --git a/modules/notch/Notch.qml b/modules/notch/Notch.qml index 557c3261..a1191513 100644 --- a/modules/notch/Notch.qml +++ b/modules/notch/Notch.qml @@ -48,7 +48,7 @@ Item { property int defaultHeight: Config.showBackground ? (screenNotchOpen || hasActiveNotifications ? Math.max(stackContainer.height, 44) : 44) : (screenNotchOpen || hasActiveNotifications ? Math.max(stackContainer.height, 40) : 40) property int islandHeight: screenNotchOpen || hasActiveNotifications ? Math.max(stackContainer.height, 36) : 36 - readonly property string position: Config.notchPosition ?? "top" + property string position: Config.notchPosition ?? "top" // Corner size calculation for dynamic width (only for default theme) readonly property int cornerSize: Config.roundness > 0 ? Config.roundness + 4 : 0 diff --git a/modules/notch/NotchContent.qml b/modules/notch/NotchContent.qml index ca69bd49..fdc7baf1 100644 --- a/modules/notch/NotchContent.qml +++ b/modules/notch/NotchContent.qml @@ -34,8 +34,8 @@ Item { readonly property bool hasWindows: toplevels.length > 0 // Get the bar position for this screen - readonly property string barPosition: (Config.bar && Config.bar.position !== undefined) ? Config.bar.position : "top" - readonly property string notchPosition: Config.notchPosition !== undefined ? Config.notchPosition : "top" + readonly property string barPosition: Config.barPositionForScreen(screen.name) + readonly property string notchPosition: Config.notchPositionForScreen(screen.name) // Get the bar panel for this screen to check its state readonly property var barPanelRef: Visibilities.barPanels[screen.name] @@ -153,7 +153,9 @@ Item { // Default view component - user@host text Component { id: defaultViewComponent - DefaultView {} + DefaultView { + screenName: root.screen.name + } } // Persistent views to avoid creation lag when opening the notch @@ -273,6 +275,7 @@ Item { id: notchContainer unifiedEffectActive: root.unifiedEffectActive parentHovered: root.isMouseOverNotch + position: root.notchPosition anchors.horizontalCenter: parent.horizontalCenter anchors.top: root.notchPosition === "top" ? parent.top : undefined anchors.bottom: root.notchPosition === "bottom" ? parent.bottom : undefined diff --git a/modules/shell/UnifiedShellPanel.qml b/modules/shell/UnifiedShellPanel.qml index 028c2dce..ba2cc7da 100644 --- a/modules/shell/UnifiedShellPanel.qml +++ b/modules/shell/UnifiedShellPanel.qml @@ -90,7 +90,7 @@ PanelWindow { readonly property var compositorMonitor: AxctlService.monitorFor(targetScreen) readonly property bool hasFullscreenWindow: { - if (!compositorMonitor) + if (!compositorMonitor || !compositorMonitor.activeWorkspace) return false; const activeWorkspaceId = compositorMonitor.activeWorkspace.id; @@ -98,7 +98,7 @@ PanelWindow { // Check active toplevel first (fast path) const toplevel = ToplevelManager.activeToplevel; - if (toplevel && toplevel.fullscreen && AxctlService.focusedMonitor.id === monId) { + if (toplevel && toplevel.fullscreen && AxctlService.focusedMonitor && AxctlService.focusedMonitor.id === monId) { return true; } @@ -264,7 +264,7 @@ PanelWindow { let margin = (frameOn && !frameWrapped) ? (Config.bar?.frameThickness ?? 6) : 0; if (unifiedPanel.barEnabled && unifiedPanel.barPosition === "bottom" && unifiedPanel.barPinned) { margin += unifiedPanel.barTargetHeight + unifiedPanel.barOuterMargin + (unifiedPanel.containBar ? Config.bar.frameThickness : 0); - } else if (unifiedPanel.dockEnabled && dockContent.dockPosition === "bottom" && dockContent.pinned) { + } else if (unifiedPanel.dockEnabled && dockContent.position === "bottom" && dockContent.pinned) { margin += dockContent.dockHeight; } return margin; diff --git a/modules/widgets/dashboard/controls/SettingsIndex.qml b/modules/widgets/dashboard/controls/SettingsIndex.qml index 6a606409..a3f5c073 100644 --- a/modules/widgets/dashboard/controls/SettingsIndex.qml +++ b/modules/widgets/dashboard/controls/SettingsIndex.qml @@ -150,6 +150,7 @@ QtObject { // Ambxst > Bar { label: "Bar", keywords: "panel taskbar top bottom", section: 8, subSection: "bar", subLabel: "Ambxst > Bar", icon: Icons.layout, isIcon: true }, { label: "Bar Position", keywords: "top bottom left right edge", section: 8, subSection: "bar", subLabel: "Ambxst > Bar", icon: Icons.layout, isIcon: true }, + { label: "Bar Per-Monitor Position", keywords: "screen monitor display edge override", section: 8, subSection: "bar", subLabel: "Ambxst > Bar", icon: Icons.layout, isIcon: true }, { label: "Launcher Icon", keywords: "logo symbol path", section: 8, subSection: "bar", subLabel: "Ambxst > Bar", icon: Icons.layout, isIcon: true }, { label: "Launcher Icon Tint", keywords: "color theme", section: 8, subSection: "bar", subLabel: "Ambxst > Bar", icon: Icons.palette, isIcon: true }, { label: "Launcher Icon Full Tint", keywords: "monochrome color", section: 8, subSection: "bar", subLabel: "Ambxst > Bar", icon: Icons.palette, isIcon: true }, @@ -168,6 +169,8 @@ QtObject { // Ambxst > Notch { label: "Notch", keywords: "island dynamic island center", section: 8, subSection: "notch", subLabel: "Ambxst > Notch", icon: Icons.layout, isIcon: true }, + { label: "Notch Position", keywords: "top bottom edge", section: 8, subSection: "notch", subLabel: "Ambxst > Notch", icon: Icons.layout, isIcon: true }, + { label: "Notch Per-Monitor Position", keywords: "screen monitor display edge override", section: 8, subSection: "notch", subLabel: "Ambxst > Notch", icon: Icons.layout, isIcon: true }, // Ambxst > Workspaces { label: "Workspaces", keywords: "virtual desktop spaces", section: 8, subSection: "workspaces", subLabel: "Ambxst > Workspaces", icon: Icons.squaresFour, isIcon: true }, @@ -189,6 +192,7 @@ QtObject { { label: "Dock Enabled", keywords: "show hide toggle", section: 8, subSection: "dock", subLabel: "Ambxst > Dock", icon: Icons.layout, isIcon: true }, { label: "Dock Mode", keywords: "default floating integrated style", section: 8, subSection: "dock", subLabel: "Ambxst > Dock", icon: Icons.layout, isIcon: true }, { label: "Dock Position", keywords: "left bottom right edge", section: 8, subSection: "dock", subLabel: "Ambxst > Dock", icon: Icons.layout, isIcon: true }, + { label: "Dock Per-Monitor Position", keywords: "screen monitor display edge override", section: 8, subSection: "dock", subLabel: "Ambxst > Dock", icon: Icons.layout, isIcon: true }, { label: "Dock Height", keywords: "size thickness pixels", section: 8, subSection: "dock", subLabel: "Ambxst > Dock", icon: Icons.layout, isIcon: true }, { label: "Dock Icon Size", keywords: "width height pixels apps", section: 8, subSection: "dock", subLabel: "Ambxst > Dock", icon: Icons.layout, isIcon: true }, { label: "Dock Spacing", keywords: "gap between icons", section: 8, subSection: "dock", subLabel: "Ambxst > Dock", icon: Icons.layout, isIcon: true }, diff --git a/modules/widgets/dashboard/controls/ShellPanel.qml b/modules/widgets/dashboard/controls/ShellPanel.qml index d02bf8cf..fb750998 100644 --- a/modules/widgets/dashboard/controls/ShellPanel.qml +++ b/modules/widgets/dashboard/controls/ShellPanel.qml @@ -48,6 +48,60 @@ Item { property string currentSection: "" + readonly property var shellEdgeOptions: [ + { + label: "Top", + value: "top", + icon: Icons.arrowUp + }, + { + label: "Bottom", + value: "bottom", + icon: Icons.arrowDown + }, + { + label: "Left", + value: "left", + icon: Icons.arrowLeft + }, + { + label: "Right", + value: "right", + icon: Icons.arrowRight + } + ] + readonly property var notchEdgeOptions: [ + { + label: "Top", + value: "top", + icon: Icons.arrowUp + }, + { + label: "Bottom", + value: "bottom", + icon: Icons.arrowDown + } + ] + + function setScreenPositionOverride(configObject, screenName, newValue) { + if (!configObject || !screenName) + return; + + let nextPositions = configObject.screenPositions ? JSON.parse(JSON.stringify(configObject.screenPositions)) : {}; + const currentValue = nextPositions[screenName] ?? "__default"; + if (newValue === "__default") { + delete nextPositions[screenName]; + } else { + nextPositions[screenName] = newValue; + } + + const resolvedValue = nextPositions[screenName] ?? "__default"; + if (resolvedValue !== currentValue) { + GlobalStates.markShellChanged(); + configObject.screenPositions = nextPositions; + } + } + component SectionButton: StyledRect { id: sectionBtn required property string text @@ -439,6 +493,77 @@ Item { } } + component ScreenPositionOverrideEditor: ColumnLayout { + id: screenPositionRoot + + property string label: "Per-Monitor Position" + property var screenPositions: ({}) + property var options: [] + property string selectedScreenName: Quickshell.screens.length > 0 ? Quickshell.screens[0].name : "" + signal overrideSelected(string screenName, string newValue) + + readonly property var screenOptions: { + let result = []; + for (let i = 0; i < Quickshell.screens.length; i++) { + result.push({ + label: Quickshell.screens[i].name, + value: Quickshell.screens[i].name + }); + } + return result; + } + + readonly property var overrideOptions: { + let result = [ + { + label: "Default", + value: "__default" + } + ]; + for (let i = 0; i < options.length; i++) { + result.push(options[i]); + } + return result; + } + + function currentOverrideValue() { + if (selectedScreenName === "") + return "__default"; + const positions = screenPositions || {}; + const value = positions[selectedScreenName]; + return value !== undefined ? value : "__default"; + } + + Layout.fillWidth: true + spacing: 8 + + Text { + text: screenPositionRoot.label + font.family: Config.theme.font + font.pixelSize: Styling.fontSize(-1) + font.weight: Font.Medium + color: Colors.overSurfaceVariant + } + + SelectorRow { + label: "Monitor" + options: screenPositionRoot.screenOptions + value: screenPositionRoot.selectedScreenName + onValueSelected: newValue => { + screenPositionRoot.selectedScreenName = newValue; + } + } + + SelectorRow { + label: "Override" + options: screenPositionRoot.overrideOptions + value: screenPositionRoot.currentOverrideValue() + onValueSelected: newValue => { + screenPositionRoot.overrideSelected(screenPositionRoot.selectedScreenName, newValue); + } + } + } + // Inline component for screen list selection component ScreenListRow: ColumnLayout { id: screenListRowRoot @@ -688,29 +813,8 @@ Item { } SelectorRow { - label: "" - options: [ - { - label: "Top", - value: "top", - icon: Icons.arrowUp - }, - { - label: "Bottom", - value: "bottom", - icon: Icons.arrowDown - }, - { - label: "Left", - value: "left", - icon: Icons.arrowLeft - }, - { - label: "Right", - value: "right", - icon: Icons.arrowRight - } - ] + label: "Global Position" + options: root.shellEdgeOptions value: Config.bar.position ?? "top" onValueSelected: newValue => { if (newValue !== Config.bar.position) { @@ -720,6 +824,15 @@ Item { } } + ScreenPositionOverrideEditor { + label: "Per-Monitor Position" + screenPositions: Config.bar.screenPositions ?? ({}) + options: root.shellEdgeOptions + onOverrideSelected: (screenName, newValue) => { + root.setScreenPositionOverride(Config.bar, screenName, newValue); + } + } + TextInputRow { label: "Launcher Icon" value: Config.bar.launcherIcon ?? "" @@ -993,19 +1106,8 @@ Item { } SelectorRow { - label: "" - options: [ - { - label: "Top", - value: "top", - icon: Icons.arrowUp - }, - { - label: "Bottom", - value: "bottom", - icon: Icons.arrowDown - } - ] + label: "Global Position" + options: root.notchEdgeOptions value: Config.notch.position ?? "top" onValueSelected: newValue => { if (newValue !== Config.notch.position) { @@ -1015,6 +1117,15 @@ Item { } } + ScreenPositionOverrideEditor { + label: "Per-Monitor Position" + screenPositions: Config.notch.screenPositions ?? ({}) + options: root.notchEdgeOptions + onOverrideSelected: (screenName, newValue) => { + root.setScreenPositionOverride(Config.notch, screenName, newValue); + } + } + SelectorRow { label: "" options: [ @@ -1355,29 +1466,8 @@ Item { } SelectorRow { - label: "Position" - options: [ - { - label: "Top", - value: "top", - icon: Icons.arrowUp - }, - { - label: "Bottom", - value: "bottom", - icon: Icons.arrowDown - }, - { - label: "Left", - value: "left", - icon: Icons.arrowLeft - }, - { - label: "Right", - value: "right", - icon: Icons.arrowRight - } - ] + label: "Global Position" + options: root.shellEdgeOptions value: Config.dock.position ?? "bottom" onValueSelected: newValue => { if (newValue !== Config.dock.position) { @@ -1387,6 +1477,15 @@ Item { } } + ScreenPositionOverrideEditor { + label: "Per-Monitor Position" + screenPositions: Config.dock.screenPositions ?? ({}) + options: root.shellEdgeOptions + onOverrideSelected: (screenName, newValue) => { + root.setScreenPositionOverride(Config.dock, screenName, newValue); + } + } + SelectorRow { label: "Theme" options: [ diff --git a/modules/widgets/defaultview/CompactPlayer.qml b/modules/widgets/defaultview/CompactPlayer.qml index 265b87e8..dd861c19 100644 --- a/modules/widgets/defaultview/CompactPlayer.qml +++ b/modules/widgets/defaultview/CompactPlayer.qml @@ -10,6 +10,7 @@ import qs.modules.theme import qs.modules.bar.workspaces import qs.modules.services import qs.modules.components +import qs.modules.globals import qs.config Item { @@ -17,6 +18,7 @@ Item { required property var player required property bool notchHovered + property string screenName: "" onPlayerChanged: { if (!player) { @@ -596,7 +598,8 @@ Item { id: playerPopup anchorItem: playerIcon bar: ({ - position: Config.bar?.position ?? "top" + position: Config.barPositionForScreen(compactPlayer.screenName), + barPosition: Config.barPositionForScreen(compactPlayer.screenName) }) contentWidth: 250 diff --git a/modules/widgets/defaultview/DefaultView.qml b/modules/widgets/defaultview/DefaultView.qml index c75a7ad2..2ca98ac2 100644 --- a/modules/widgets/defaultview/DefaultView.qml +++ b/modules/widgets/defaultview/DefaultView.qml @@ -10,6 +10,7 @@ Item { id: root anchors.top: parent.top focus: false + property string screenName: "" // Layout constants readonly property int notificationPadding: 16 @@ -24,7 +25,7 @@ Item { property bool isNavigating: false // Position detection - readonly property string notchPosition: Config.notchPosition ?? "top" + readonly property string notchPosition: Config.notchPositionForScreen(screenName) readonly property bool isBottom: notchPosition === "bottom" HoverHandler { @@ -157,6 +158,7 @@ Item { height: 32 player: activePlayer notchHovered: expandedState + screenName: root.screenName } Separator { diff --git a/modules/widgets/overview/Overview.qml b/modules/widgets/overview/Overview.qml index 6dad0e66..d226490d 100644 --- a/modules/widgets/overview/Overview.qml +++ b/modules/widgets/overview/Overview.qml @@ -36,7 +36,7 @@ Item { readonly property int monitorId: monitor?.id ?? -1 readonly property var monitorData: monitors.find(m => m.id === monitorId) ?? null - readonly property string barPosition: Config.bar.position + readonly property string barPosition: monitor ? Config.barPositionForScreen(monitor.name) : Config.bar.position readonly property var barPanel: monitor ? Visibilities.getBarPanelForScreen(monitor.name) : null readonly property bool isBarPinned: barPanel ? barPanel.pinned : (Config.bar.pinnedOnStartup ?? true) readonly property int barReserved: isBarPinned ? (Config.showBackground ? 44 : 40) : 0 diff --git a/modules/widgets/overview/ScrollingOverview.qml b/modules/widgets/overview/ScrollingOverview.qml index 66b6a465..ba99cd8d 100644 --- a/modules/widgets/overview/ScrollingOverview.qml +++ b/modules/widgets/overview/ScrollingOverview.qml @@ -28,7 +28,7 @@ Item { readonly property var monitors: CompositorData.monitors readonly property var monitorData: monitors.find(m => m.id === monitorId) ?? null - readonly property string barPosition: Config.bar.position + readonly property string barPosition: monitor ? Config.barPositionForScreen(monitor.name) : Config.bar.position readonly property var barPanel: monitor ? Visibilities.getBarPanelForScreen(monitor.name) : null readonly property bool isBarPinned: barPanel ? barPanel.pinned : (Config.bar.pinnedOnStartup ?? true) readonly property int barReserved: isBarPinned ? (Config.showBackground ? 44 : 40) : 0 diff --git a/tests/config-screen-positions.test.mjs b/tests/config-screen-positions.test.mjs new file mode 100644 index 00000000..6a529ae5 --- /dev/null +++ b/tests/config-screen-positions.test.mjs @@ -0,0 +1,55 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import vm from "node:vm"; + +const repoRoot = path.resolve(import.meta.dirname, ".."); + +function loadQmlLibrary(relativePath) { + const filePath = path.join(repoRoot, relativePath); + const source = fs.readFileSync(filePath, "utf8").replace(/^\.pragma library\s*$/gm, ""); + const sandbox = {}; + vm.createContext(sandbox); + vm.runInContext(source, sandbox, { filename: filePath }); + return sandbox; +} + +function plain(value) { + return JSON.parse(JSON.stringify(value)); +} + +test("ConfigValidator preserves arbitrary monitor keys inside screenPositions maps", () => { + const validator = loadQmlLibrary("config/ConfigValidator.js"); + + const current = { + position: "bottom", + screenPositions: { + "eDP-1": "top", + "DP-3": "bottom" + } + }; + const defaults = { + position: "top", + screenPositions: {} + }; + + assert.deepEqual(plain(validator.validate(current, defaults)), current); +}); + +test("ScreenPositions resolves a per-screen override before falling back to global position", () => { + const screenPositions = loadQmlLibrary("config/ScreenPositions.js"); + const allowed = ["top", "bottom", "left", "right"]; + const config = { + position: "top", + screenPositions: { + "eDP-1": "bottom", + "DP-3": "sideways" + } + }; + + assert.equal(screenPositions.positionForScreen(config, "eDP-1", "top", allowed), "bottom"); + assert.equal(screenPositions.positionForScreen(config, "HDMI-A-1", "top", allowed), "top"); + assert.equal(screenPositions.positionForScreen(config, "DP-3", "top", allowed), "top"); + assert.equal(screenPositions.positionForScreen({ position: "sideways" }, "DP-3", "top", allowed), "top"); +});