From 0628c9c36c102feadbabcfef012c6d422c9e1fc3 Mon Sep 17 00:00:00 2001 From: 0xffash <212138419+0xffash@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:17:39 +0200 Subject: [PATCH] feat(ui): ability to reorder and delete bar items --- AGENTS.md | 73 -- config/Config.qml | 6 + config/defaults/bar.js | 9 +- modules/bar/BarContent.qml | 782 ++++++++++++------ modules/bar/BarItemRegistry.js | 41 + modules/components/Separator.qml | 4 +- modules/globals/GlobalStates.qml | 2 +- modules/notch/Notch.qml | 3 + modules/notch/NotchContent.qml | 22 + modules/services/Visibilities.qml | 14 + modules/shell/UnifiedShellPanel.qml | 23 +- .../widgets/dashboard/controls/ShellPanel.qml | 497 +++++++++++ 12 files changed, 1117 insertions(+), 359 deletions(-) delete mode 100644 AGENTS.md create mode 100644 modules/bar/BarItemRegistry.js diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index eeaccd2b..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,73 +0,0 @@ -# PROJECT KNOWLEDGE BASE - -**Generated:** 2026-01-28 -**Framework:** QtQuick / Quickshell -**Language:** QML / JavaScript - -## OVERVIEW -Ambxst is a highly customizable Wayland shell built with Quickshell. It provides a unified panel (bar, dock, notch), dashboard, lockscreen, and desktop widgets, driven by a reactive JSON configuration system. - -## STRUCTURE -``` -./ -├── config/ # Configuration logic & defaults (Singleton) -├── modules/ -│ ├── bar/ # Panels, dock, and status indicators -│ ├── widgets/ # Dashboard, launcher, and overlay tools -│ ├── services/ # Backend logic (Battery, Network, AI) - Singletons -│ ├── theme/ # Style definitions (Colors, Icons) - Singletons -│ ├── components/ # Reusable UI primitives (StyledRect, Buttons) -│ └── globals/ # Shared transient state (GlobalStates.qml) -├── assets/ # Wallpapers, presets, icons -├── scripts/ # Helper utilities (install, run) -└── shell.qml # Entry point: Loads root window & layers -``` - -## WHERE TO LOOK -| Task | Location | Notes | -|------|----------|-------| -| **Entry Point** | `shell.qml` | Initializes `UnifiedShellPanel`, `Desktop`, `LockScreen` | -| **Config Logic** | `config/Config.qml` | HUGE Singleton. Handles `FileView` + `JsonAdapter` persistence | -| **State** | `modules/globals/GlobalStates.qml` | Transient UI state (window visibility, modes) | -| **Services** | `modules/services/*.qml` | `pragma Singleton` services (Battery, Hyprland, AI) | -| **Theme** | `modules/theme/*.qml` | Colors, fonts, radius definitions | -| **Dashboard** | `modules/widgets/dashboard/` | Main overlay UI, tabs, and controls | - -## CODE MAP - -| Symbol | Type | Location | Role | -|--------|------|----------|------| -| `Config` | Singleton | `config/Config.qml` | Central config store. Reactive to JSON file changes. | -| `GlobalStates` | Singleton | `modules/globals/GlobalStates.qml` | Shared runtime state (non-persistent). | -| `UnifiedShellPanel` | Component | `modules/shell/UnifiedShellPanel.qml` | Container for Bar, Notch, Dock. | -| `ShellRoot` | Component | `shell.qml` | Root Quickshell window. | -| `Ai` | Singleton | `modules/services/Ai.qml` | AI Assistant service interface. | - -## CONVENTIONS -- **Singletons**: Use `pragma Singleton` for all services and global state. -- **Imports**: `qs.modules.*` namespace used for internal modules. -- **Persistence**: `FileView` watches JSON files; `JsonAdapter` maps JSON keys to QML properties. -- **Formatting**: 4-space indent. -- **Defaults**: Default config values live in `config/defaults/*.js`. - -## ANTI-PATTERNS (THIS PROJECT) -- **Hardcoding**: NEVER hardcode colors/sizes. Use `Config.theme.*` or `Config.bar.*`. -- **Direct Props**: AVOID modifying `Config` properties directly; they are bound to `JsonAdapter`. -- **Global Pollution**: Do not add random properties to `root` in `shell.qml`. Use `GlobalStates`. - -## COMMANDS -```bash -# Run shell (requires Quickshell) -qs -p shell.qml - -# Test specific component -qs -p modules/widgets/dashboard/Dashboard.qml - -# Reload config (auto-reloads on save usually) -# (File watchers in Config.qml handle this) -``` - -## NOTES -- `Config.qml` is >3500 lines. Be careful when modifying. -- The project supports "Presets" which bulk-update config files. -- `ReservationWindows` handles exclusive zones (bar/dock space reservation). diff --git a/config/Config.qml b/config/Config.qml index 7eb3bd49..a78b378c 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -605,6 +605,12 @@ Singleton { property bool containBar: false property bool keepBarShadow: false property bool keepBarBorder: false + property var itemsLeft: ["launcher", "workspaces", "layout", "pin"] + property var itemsCenter: ["dock"] + property var itemsRight: ["presets", "tools", "systray", "controls", "battery", "clock", "power"] + property var itemsLeftVertical: ["launcher", "systray", "tools", "presets"] + property var itemsCenterVertical: ["layout", "workspaces", "pin", "dock"] + property var itemsRightVertical: ["controls", "battery", "clock", "power"] } } diff --git a/config/defaults/bar.js b/config/defaults/bar.js index 7962cdde..1f3e4172 100644 --- a/config/defaults/bar.js +++ b/config/defaults/bar.js @@ -20,5 +20,12 @@ var data = { "use12hFormat": false, "containBar": false, "keepBarShadow": false, - "keepBarBorder": false + "keepBarBorder": false, + "itemsLeft": ["launcher", "workspaces", "layout", "pin"], + "itemsCenter": [], + "centerItemsSplitByNotch": false, + "itemsRight": ["presets", "tools", "systray", "controls", "battery", "clock", "power"], + "itemsLeftVertical": ["launcher", "systray", "tools", "presets"], + "itemsCenterVertical": ["layout", "workspaces", "pin"], + "itemsRightVertical": ["controls", "battery", "clock", "power"] } diff --git a/modules/bar/BarContent.qml b/modules/bar/BarContent.qml index daf05fb7..a4af8be0 100644 --- a/modules/bar/BarContent.qml +++ b/modules/bar/BarContent.qml @@ -19,6 +19,7 @@ import qs.modules.globals import qs.modules.bar import qs.config import "." as Bar +import "BarItemRegistry.js" as BarItems Item { id: root @@ -60,6 +61,7 @@ Item { // 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 ?? "top" + readonly property var notchContainerRef: Visibilities.getNotchForScreen(screen.name) readonly property bool notchHoverActive: { if (barPosition !== notchPosition) return false; @@ -183,6 +185,140 @@ Item { // Shadow logic for bar components readonly property bool shadowsEnabled: Config.showBackground && (!actualContainBar || Config.bar.keepBarShadow) + readonly property var barItemIds: BarItems.itemIds + + function normalizeBarItemList(list, fallback) { + if (list === undefined || list === null || typeof list.length === "undefined") + return fallback ? fallback.slice() : []; + + var normalized = []; + for (var i = 0; i < list.length; i++) { + var itemId = list[i]; + if (barItemIds.includes(itemId)) + normalized.push(itemId); + } + return normalized; + } + + function asArray(list) { + var out = []; + if (!list || typeof list.length === "undefined") + return out; + for (var i = 0; i < list.length; i++) + out.push(list[i]); + return out; + } + + readonly property var barItemsLeft: normalizeBarItemList(Config.bar?.itemsLeft, BarItems.defaultLeft) + readonly property var barItemsCenter: normalizeBarItemList(Config.bar?.itemsCenter, BarItems.defaultCenter) + readonly property var barItemsRight: normalizeBarItemList(Config.bar?.itemsRight, BarItems.defaultRight) + readonly property var barItemsCenterNonDock: barItemsCenter.filter(itemId => itemId !== "dock") + readonly property var barItemsLeftVertical: normalizeBarItemList(Config.bar?.itemsLeftVertical, ["launcher", "systray", "tools", "presets"]) + readonly property var barItemsCenterVertical: normalizeBarItemList(Config.bar?.itemsCenterVertical, ["layout", "workspaces", "pin"]).filter(itemId => itemId !== "notch") + readonly property var barItemsRightVertical: normalizeBarItemList(Config.bar?.itemsRightVertical, ["controls", "battery", "clock", "power"]) + readonly property bool barCenterDockEnabled: barItemsCenter.includes("dock") + readonly property bool notchBlocksCenter: root.orientation === "horizontal" + && (root.barPosition === "top" || root.barPosition === "bottom") + && root.barPosition === notchPosition + && (Config.bar?.centerItemsSplitByNotch ?? true) + function computeNotchGap() { + if (!notchBlocksCenter) + return 0; + const notchWidth = Math.max( + Visibilities.getNotchWidth(screen.name), + notchPanelRef?.notchWidth ?? 0, + notchPanelRef?.notchHitboxWidth ?? 0, + notchContainerRef?.implicitWidth ?? 0, + notchContainerRef?.width ?? 0 + ); + const rawGap = Math.max(0, Math.round(notchWidth + 16)); + return rawGap; + } + + readonly property int notchGapDebug: computeNotchGap() + readonly property var barItemsCenterDisplay: { + var items = asArray(barItemsCenterNonDock).filter(id => id !== "notch"); + if (!notchBlocksCenter) + return items; + var mid = Math.ceil(items.length / 2); + items.splice(mid, 0, "notch"); + return items; + } + readonly property int centerNotchIndex: notchBlocksCenter ? barItemsCenterDisplay.indexOf("notch") : -1 + readonly property var barItemsCenterLeft: centerNotchIndex >= 0 ? barItemsCenterDisplay.slice(0, centerNotchIndex) : barItemsCenterDisplay + readonly property var barItemsCenterRight: centerNotchIndex >= 0 ? barItemsCenterDisplay.slice(centerNotchIndex + 1) : [] + + + function componentForBarItem(itemId, groupRole) { + switch (itemId) { + case "launcher": return launcherItemComponent; + case "workspaces": return workspacesItemComponent; + case "layout": return layoutSelectorItemComponent; + case "pin": return pinButtonItemComponent; + case "dock": return (groupRole === "center") ? dockCenteredItemComponent : dockInlineItemComponent; + case "presets": return presetsItemComponent; + case "tools": return toolsItemComponent; + case "systray": return systrayItemComponent; + case "controls": return controlsItemComponent; + case "battery": return batteryItemComponent; + case "clock": return clockItemComponent; + case "power": return powerItemComponent; + case "separator": return separatorItemComponent; + case "notch": return notchSpacerComponent; + default: return null; + } + } + + function barItemsForRole(role) { + if (root.orientation === "vertical") { + if (role === "left") + return barItemsLeftVertical; + if (role === "center") + return barItemsCenterVertical; + return barItemsRightVertical; + } + if (role === "left") + return barItemsLeft; + if (role === "center") + return barItemsCenterNonDock; + return barItemsRight; + } + + function barItemIndex(role, itemId) { + var items = barItemsForRole(role); + if (!items || typeof items.indexOf !== "function") + return -1; + return items.indexOf(itemId); + } + + function barItemCount(role) { + var items = barItemsForRole(role); + return items ? items.length : 0; + } + + function configureBarItem(itemId, item, startRadius, endRadius, groupRole) { + if (!item) + return; + + if (typeof item.startRadius !== "undefined") + item.startRadius = startRadius; + if (typeof item.endRadius !== "undefined") + item.endRadius = endRadius; + + if (typeof item.enableShadow !== "undefined") + item.enableShadow = root.shadowsEnabled; + if (typeof item.layerEnabled !== "undefined") + item.layerEnabled = root.shadowsEnabled; + if (typeof item.bar !== "undefined") + item.bar = root; + if (typeof item.orientation !== "undefined") + item.orientation = Qt.binding(() => root.orientation); + if (typeof item.vertical !== "undefined") + item.vertical = Qt.binding(() => root.orientation === "vertical"); + if (typeof item.groupRole !== "undefined") + item.groupRole = groupRole; + } + // The hitbox for the mask property alias barHitbox: barMouseArea @@ -342,116 +478,191 @@ Item { anchors.fill: parent position: root.barPosition - RowLayout { - id: horizontalLayout - visible: root.orientation === "horizontal" - anchors.fill: parent - spacing: 4 + component BarItemLoader: Item { + id: barItemRoot + property string itemKey: "" + property int itemIndexCtx: -1 + property int itemCountCtx: -1 + property int itemIndexChainCtx: -1 + property int itemCountChainCtx: -1 + property string groupRole: "left" + + Layout.alignment: Qt.AlignVCenter + + readonly property string itemId: itemKey + readonly property int itemIndex: itemIndexCtx >= 0 ? itemIndexCtx : root.barItemIndex(groupRole, itemId) + readonly property int itemCount: itemCountCtx >= 0 ? itemCountCtx : root.barItemCount(groupRole) + readonly property bool useChainRadii: (Config.bar?.pillStyle ?? "default") === "squished" && itemIndexChainCtx >= 0 && itemCountChainCtx >= 0 + readonly property int effectiveIndex: useChainRadii ? itemIndexChainCtx : itemIndex + readonly property int effectiveCount: useChainRadii ? itemCountChainCtx : itemCount + readonly property int chainIndex: effectiveIndex + readonly property real startRadius: { + if (effectiveCount <= 1) + return root.outerRadius; + return chainIndex <= 0 ? root.outerRadius : root.innerRadius; + } + readonly property real endRadius: { + if (effectiveCount <= 1) + return root.outerRadius; + return (chainIndex >= 0 && chainIndex === (effectiveCount - 1)) ? root.outerRadius : root.innerRadius; + } - // Obtener referencia al notch de esta pantalla - readonly property var notchContainer: Visibilities.getNotchForScreen(root.screen.name) + implicitWidth: { + if (!itemLoader.item) + return 0; + if (itemLoader.item.implicitWidth > 0) + return itemLoader.item.implicitWidth; + if (itemLoader.item.Layout && itemLoader.item.Layout.preferredWidth > 0) + return itemLoader.item.Layout.preferredWidth; + return 36; + } + implicitHeight: { + if (!itemLoader.item) + return 0; + if (itemLoader.item.implicitHeight > 0) + return itemLoader.item.implicitHeight; + if (itemLoader.item.Layout && itemLoader.item.Layout.preferredHeight > 0) + return itemLoader.item.Layout.preferredHeight; + return 36; + } + Loader { + id: itemLoader + anchors.fill: barItemRoot + sourceComponent: root.componentForBarItem(itemId, groupRole) + onLoaded: { + root.configureBarItem(itemId, item, startRadius, endRadius, groupRole); + if (item && typeof item.startRadius !== "undefined") + item.startRadius = Qt.binding(() => barItemRoot.startRadius); + if (item && typeof item.endRadius !== "undefined") + item.endRadius = Qt.binding(() => barItemRoot.endRadius); + if (item && item.implicitWidth <= 0 && item.implicitHeight <= 0 && item.width <= 0 && item.height <= 0) { + item.anchors.fill = itemLoader; + } + } + } + } + + Component { + id: launcherItemComponent LauncherButton { - id: launcherButton - startRadius: root.outerRadius - endRadius: root.innerRadius + startRadius: 0 + endRadius: 0 enableShadow: root.shadowsEnabled } + } + Component { + id: workspacesItemComponent Workspaces { orientation: root.orientation bar: QtObject { property var screen: root.screen } - startRadius: root.innerRadius - endRadius: root.innerRadius + startRadius: 0 + endRadius: 0 } + } + Component { + id: layoutSelectorItemComponent LayoutSelectorButton { - id: layoutSelectorButton bar: root layerEnabled: root.shadowsEnabled - startRadius: root.innerRadius - endRadius: (root.pinButtonVisible) ? root.innerRadius : (root.dockAtStart ? root.innerRadius : root.outerRadius) + startRadius: 0 + endRadius: 0 } + } - // Pin button (horizontal) - Loader { - active: Config.bar?.showPinButton ?? true - visible: active - Layout.alignment: Qt.AlignVCenter - - sourceComponent: Button { - id: pinButton - implicitWidth: 36 - implicitHeight: 36 - - background: StyledRect { - id: pinButtonBg - variant: root.pinned ? "primary" : "bg" - enableShadow: root.shadowsEnabled - - // PinButton is typically last in group 1 (unless IntegratedDock follows at start) - property real startRadius: root.innerRadius - property real endRadius: root.dockAtStart ? root.innerRadius : root.outerRadius - - topLeftRadius: startRadius - bottomLeftRadius: startRadius - topRightRadius: endRadius - bottomRightRadius: endRadius - - Rectangle { - anchors.fill: parent - color: Styling.srItem("overprimary") - opacity: root.pinned ? 0 : (pinButton.pressed ? 0.5 : (pinButton.hovered ? 0.25 : 0)) - radius: parent.radius ?? 0 - - Behavior on opacity { - enabled: (Config.animDuration ?? 0) > 0 - NumberAnimation { - duration: (Config.animDuration ?? 0) / 2 + Component { + id: pinButtonItemComponent + Item { + id: pinButtonItemRoot + property real startRadius: 0 + property real endRadius: 0 + + implicitWidth: pinButtonLoader.item ? pinButtonLoader.item.implicitWidth : 0 + implicitHeight: pinButtonLoader.item ? pinButtonLoader.item.implicitHeight : 0 + + Loader { + id: pinButtonLoader + active: root.pinButtonVisible + visible: active + + sourceComponent: Button { + id: pinButton + implicitWidth: 36 + implicitHeight: 36 + + background: StyledRect { + id: pinButtonBg + variant: root.pinned ? "primary" : "bg" + enableShadow: root.shadowsEnabled + + topLeftRadius: pinButtonItemRoot.startRadius + bottomLeftRadius: pinButtonItemRoot.startRadius + topRightRadius: pinButtonItemRoot.endRadius + bottomRightRadius: pinButtonItemRoot.endRadius + + Rectangle { + anchors.fill: parent + color: Styling.srItem("overprimary") + opacity: root.pinned ? 0 : (pinButton.pressed ? 0.5 : (pinButton.hovered ? 0.25 : 0)) + radius: parent.radius ?? 0 + + Behavior on opacity { + enabled: (Config.animDuration ?? 0) > 0 + NumberAnimation { + duration: (Config.animDuration ?? 0) / 2 + } } } } - } - contentItem: Text { - text: Icons.pin - font.family: Icons.font - font.pixelSize: 18 - color: root.pinned ? pinButtonBg.item : (pinButton.pressed ? Colors.background : (Styling.srItem("overprimary") || Colors.foreground)) - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - - rotation: root.pinned ? 0 : 45 - Behavior on rotation { - enabled: Config.animDuration > 0 - NumberAnimation { - duration: Config.animDuration / 2 + contentItem: Text { + text: Icons.pin + font.family: Icons.font + font.pixelSize: 18 + color: root.pinned ? pinButtonBg.item : (pinButton.pressed ? Colors.background : (Styling.srItem("overprimary") || Colors.foreground)) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + rotation: root.pinned ? 0 : 45 + Behavior on rotation { + enabled: Config.animDuration > 0 + NumberAnimation { + duration: Config.animDuration / 2 + } } - } - Behavior on color { - enabled: Config.animDuration > 0 - ColorAnimation { - duration: Config.animDuration / 2 + Behavior on color { + enabled: Config.animDuration > 0 + ColorAnimation { + duration: Config.animDuration / 2 + } } } - } - onClicked: root.pinned = !root.pinned + onClicked: root.pinned = !root.pinned - StyledToolTip { - show: pinButton.hovered - tooltipText: root.pinned ? "Unpin bar" : "Pin bar" + StyledToolTip { + show: pinButton.hovered + tooltipText: root.pinned ? "Unpin bar" : "Pin bar" + } } } } + } + Component { + id: dockCenteredItemComponent Item { - Layout.fillWidth: true - Layout.fillHeight: true - visible: root.orientation === "horizontal" && integratedDockEnabled + id: dockCenteredRoot + property real startRadius: 0 + property real endRadius: 0 + + visible: root.integratedDockEnabled + anchors.fill: parent Bar.IntegratedDock { bar: root @@ -459,124 +670,267 @@ Item { anchors.verticalCenter: parent.verticalCenter enableShadow: root.shadowsEnabled - // Connect to left/right groups if at start/end - startRadius: root.dockAtStart ? root.innerRadius : root.outerRadius - endRadius: root.dockAtEnd ? root.innerRadius : root.outerRadius + startRadius: dockCenteredRoot.startRadius + endRadius: dockCenteredRoot.endRadius - // Calculate target position based on config property real targetX: { - if (integratedDockPosition === "start") + if (root.integratedDockPosition === "start") return 0; - if (integratedDockPosition === "end") + if (root.integratedDockPosition === "end") return parent.width - width; - // Center logic (reactive using parent.x + margin offset) - // RowLayout has anchors.margins: 4, so offset is 4 return (bar.width - width) / 2 - (parent.x + 4); } - // Clamp the x position so it never leaves the container (preventing overlap) x: Math.max(0, Math.min(parent.width - width, targetX)) width: Math.min(implicitWidth, parent.width) height: implicitHeight } } + } - Item { - Layout.fillWidth: true - visible: !(root.orientation === "horizontal" && integratedDockEnabled) + Component { + id: dockInlineItemComponent + Bar.IntegratedDock { + bar: root + orientation: root.orientation + enableShadow: root.shadowsEnabled + visible: root.integratedDockEnabled + startRadius: 0 + endRadius: 0 } + } + Component { + id: presetsItemComponent PresetsButton { - id: presetsButton - startRadius: root.dockAtEnd ? root.innerRadius : root.outerRadius - endRadius: root.innerRadius + startRadius: 0 + endRadius: 0 enableShadow: root.shadowsEnabled } + } + Component { + id: toolsItemComponent ToolsButton { - id: toolsButton - startRadius: root.innerRadius - endRadius: root.innerRadius + startRadius: 0 + endRadius: 0 enableShadow: root.shadowsEnabled } + } + Component { + id: systrayItemComponent SysTray { bar: root enableShadow: root.shadowsEnabled - startRadius: root.innerRadius - endRadius: root.innerRadius + startRadius: 0 + endRadius: 0 } + } + Component { + id: controlsItemComponent ControlsButton { - id: controlsButton bar: root layerEnabled: root.shadowsEnabled - startRadius: root.innerRadius - endRadius: root.innerRadius + startRadius: 0 + endRadius: 0 } + } + Component { + id: batteryItemComponent Bar.BatteryIndicator { - id: batteryIndicator bar: root layerEnabled: root.shadowsEnabled - startRadius: root.innerRadius - endRadius: root.innerRadius + startRadius: 0 + endRadius: 0 } + } + Component { + id: clockItemComponent Clock { - id: clockComponent bar: root layerEnabled: root.shadowsEnabled - startRadius: root.innerRadius - endRadius: root.innerRadius + startRadius: 0 + endRadius: 0 } + } + Component { + id: powerItemComponent PowerButton { - id: powerButton - startRadius: root.innerRadius - endRadius: root.outerRadius + startRadius: 0 + endRadius: 0 enableShadow: root.shadowsEnabled } } - ColumnLayout { - id: verticalLayout - visible: root.orientation === "vertical" + Component { + id: separatorItemComponent + Separator { + vert: root.orientation === "horizontal" + implicitWidth: root.orientation === "horizontal" + ? Math.max(3, (Config.theme.srBg.border[1] ?? 1) + 1) + : Math.max(12, root.contentImplicitWidth - 10) + implicitHeight: root.orientation === "horizontal" + ? Math.max(12, root.contentImplicitHeight - 10) + : Math.max(3, (Config.theme.srBg.border[1] ?? 1) + 1) + } + } + + Component { + id: notchSpacerComponent + Item { + implicitWidth: centerOverlay?.effectiveNotchGap ?? 0 + width: centerOverlay?.effectiveNotchGap ?? 0 + implicitHeight: 36 + visible: root.notchBlocksCenter + } + } + + RowLayout { + id: horizontalLayout + visible: root.orientation === "horizontal" anchors.fill: parent spacing: 4 - LauncherButton { - id: launcherButtonVert - Layout.preferredHeight: 36 - startRadius: root.outerRadius - endRadius: root.innerRadius - vertical: true - enableShadow: root.shadowsEnabled + RowLayout { + id: leftGroup + spacing: 4 + Layout.alignment: Qt.AlignVCenter + + Repeater { + model: root.asArray(root.barItemsLeft) + delegate: BarItemLoader { + itemKey: modelData + itemIndexCtx: index + itemCountCtx: root.asArray(root.barItemsLeft).length + groupRole: "left" + } + } } - SysTray { - bar: root - enableShadow: root.shadowsEnabled - startRadius: root.innerRadius - endRadius: root.innerRadius + Item { + Layout.fillWidth: true + Layout.fillHeight: true } - ToolsButton { - id: toolsButtonVert - startRadius: root.innerRadius - endRadius: root.innerRadius - vertical: true - enableShadow: root.shadowsEnabled + RowLayout { + id: rightGroup + spacing: 4 + Layout.alignment: Qt.AlignVCenter + + Repeater { + model: root.asArray(root.barItemsRight) + delegate: BarItemLoader { + itemKey: modelData + itemIndexCtx: index + itemCountCtx: root.asArray(root.barItemsRight).length + groupRole: "right" + } + } } + } - PresetsButton { - id: presetsButtonVert - startRadius: root.innerRadius - endRadius: root.outerRadius - vertical: true - enableShadow: root.shadowsEnabled + Item { + id: centerOverlay + anchors.fill: parent + visible: root.orientation === "horizontal" && (root.barItemsCenterDisplay.length > 0 || root.barCenterDockEnabled) + property int effectiveNotchGap: root.computeNotchGap() + property int notchHalfGap: Math.round(effectiveNotchGap / 2) + + BarItemLoader { + anchors.fill: parent + visible: root.barCenterDockEnabled + itemKey: "dock" + itemIndexCtx: 0 + groupRole: "center" + } + + RowLayout { + id: centerLeftGroup + anchors.right: parent.horizontalCenter + anchors.rightMargin: root.notchBlocksCenter ? centerOverlay.notchHalfGap : 0 + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + visible: root.notchBlocksCenter && root.barItemsCenterLeft.length > 0 + + Repeater { + model: root.asArray(root.barItemsCenterLeft) + delegate: BarItemLoader { + itemKey: modelData + itemIndexCtx: index + itemCountCtx: root.asArray(root.barItemsCenterLeft).length + itemIndexChainCtx: root.notchBlocksCenter ? index : -1 + itemCountChainCtx: root.notchBlocksCenter ? (root.asArray(root.barItemsCenterLeft).length + 1) : -1 + groupRole: "center" + } + } + } + + RowLayout { + id: centerRightGroup + anchors.left: parent.horizontalCenter + anchors.leftMargin: root.notchBlocksCenter ? centerOverlay.notchHalfGap : 0 + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + visible: root.notchBlocksCenter && root.barItemsCenterRight.length > 0 + + Repeater { + model: root.asArray(root.barItemsCenterRight) + delegate: BarItemLoader { + itemKey: modelData + itemIndexCtx: index + itemCountCtx: root.asArray(root.barItemsCenterRight).length + itemIndexChainCtx: root.notchBlocksCenter ? (index + 1) : -1 + itemCountChainCtx: root.notchBlocksCenter ? (root.asArray(root.barItemsCenterRight).length + 1) : -1 + groupRole: "center" + } + } + } + } + + RowLayout { + id: centerUnifiedGroup + anchors.centerIn: parent + spacing: 4 + visible: !root.notchBlocksCenter && root.barItemsCenterDisplay.length > 0 + + Repeater { + model: root.asArray(root.barItemsCenterDisplay) + delegate: BarItemLoader { + itemKey: modelData + itemIndexCtx: index + itemCountCtx: root.asArray(root.barItemsCenterDisplay).length + groupRole: "center" + } + } + } + + ColumnLayout { + id: verticalLayout + visible: root.orientation === "vertical" + anchors.fill: parent + spacing: 4 + + ColumnLayout { + id: verticalTopGroup + spacing: 4 + Layout.alignment: Qt.AlignHCenter + + Repeater { + model: root.asArray(root.barItemsLeftVertical) + delegate: BarItemLoader { + itemKey: modelData + itemIndexCtx: index + itemCountCtx: root.asArray(root.barItemsLeftVertical).length + groupRole: "left" + } + } } // Center Group Container @@ -586,6 +940,7 @@ Item { ColumnLayout { anchors.horizontalCenter: parent.horizontalCenter + visible: root.barItemsCenterVertical.length > 0 // Calculate target position to be absolutely centered in the bar (vertically) property real targetY: { @@ -606,145 +961,32 @@ Item { width: parent.width spacing: 4 - LayoutSelectorButton { - id: layoutSelectorButtonVert - bar: root - layerEnabled: root.shadowsEnabled - Layout.alignment: Qt.AlignHCenter - startRadius: root.outerRadius - endRadius: root.innerRadius - vertical: true - } - - Workspaces { - id: workspacesVert - orientation: root.orientation - bar: QtObject { - property var screen: root.screen - } - Layout.alignment: Qt.AlignHCenter - startRadius: root.innerRadius - endRadius: root.innerRadius - } - - // Pin button (vertical) - Loader { - active: Config.bar?.showPinButton ?? true - visible: active - Layout.alignment: Qt.AlignHCenter - - sourceComponent: Button { - id: pinButtonV - implicitWidth: 36 - implicitHeight: 36 - - background: StyledRect { - id: pinButtonVBg - variant: root.pinned ? "primary" : "bg" - enableShadow: root.shadowsEnabled - - property real startRadius: root.innerRadius - // In vertical, dock is always appended to this group if enabled - property real endRadius: root.integratedDockEnabled ? root.innerRadius : root.outerRadius - - topLeftRadius: startRadius - topRightRadius: startRadius - bottomLeftRadius: endRadius - bottomRightRadius: endRadius - - Rectangle { - anchors.fill: parent - color: Styling.srItem("overprimary") - opacity: root.pinned ? 0 : (pinButtonV.pressed ? 0.5 : (pinButtonV.hovered ? 0.25 : 0)) - radius: parent.radius ?? 0 - - Behavior on opacity { - enabled: (Config.animDuration ?? 0) > 0 - NumberAnimation { - duration: (Config.animDuration ?? 0) / 2 - } - } - } - } - - contentItem: Text { - text: Icons.pin - font.family: Icons.font - font.pixelSize: 18 - color: root.pinned ? pinButtonVBg.item : (pinButtonV.pressed ? Colors.background : (Styling.srItem("overprimary") || Colors.foreground)) - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - - rotation: root.pinned ? 0 : 45 - Behavior on rotation { - enabled: Config.animDuration > 0 - NumberAnimation { - duration: Config.animDuration / 2 - } - } - - Behavior on color { - enabled: Config.animDuration > 0 - ColorAnimation { - duration: Config.animDuration / 2 - } - } - } - - onClicked: root.pinned = !root.pinned - - StyledToolTip { - show: pinButtonV.hovered - tooltipText: root.pinned ? "Unpin bar" : "Pin bar" - } + Repeater { + model: root.asArray(root.barItemsCenterVertical) + delegate: BarItemLoader { + itemKey: modelData + itemIndexCtx: index + itemCountCtx: root.asArray(root.barItemsCenterVertical).length + groupRole: "center" } } - - Bar.IntegratedDock { - bar: root - orientation: root.orientation - visible: integratedDockEnabled - Layout.fillHeight: true - Layout.fillWidth: true - enableShadow: root.shadowsEnabled - - startRadius: root.innerRadius - endRadius: root.outerRadius - } } } - ControlsButton { - id: controlsButtonVert - bar: root - layerEnabled: root.shadowsEnabled - startRadius: root.outerRadius - endRadius: root.innerRadius - } - - Bar.BatteryIndicator { - id: batteryIndicatorVert - bar: root - layerEnabled: root.shadowsEnabled - startRadius: root.innerRadius - endRadius: root.innerRadius - } - - Clock { - id: clockComponentVert - bar: root - layerEnabled: root.shadowsEnabled - startRadius: root.innerRadius - endRadius: root.innerRadius - } - - PowerButton { - id: powerButtonVert - Layout.preferredHeight: 36 - startRadius: root.innerRadius - endRadius: root.outerRadius - vertical: true - enableShadow: root.shadowsEnabled + ColumnLayout { + id: verticalBottomGroup + spacing: 4 + Layout.alignment: Qt.AlignHCenter + + Repeater { + model: root.asArray(root.barItemsRightVertical) + delegate: BarItemLoader { + itemKey: modelData + itemIndexCtx: index + itemCountCtx: root.asArray(root.barItemsRightVertical).length + groupRole: "right" + } + } } } } diff --git a/modules/bar/BarItemRegistry.js b/modules/bar/BarItemRegistry.js new file mode 100644 index 00000000..69b14c80 --- /dev/null +++ b/modules/bar/BarItemRegistry.js @@ -0,0 +1,41 @@ +.pragma library + +var items = [ + { "id": "launcher", "label": "Launcher" }, + { "id": "workspaces", "label": "Workspaces" }, + { "id": "layout", "label": "Layout Selector" }, + { "id": "pin", "label": "Pin Button" }, + { "id": "notch", "label": "Notch Spacer" }, + { "id": "presets", "label": "Presets" }, + { "id": "tools", "label": "Tools" }, + { "id": "systray", "label": "System Tray" }, + { "id": "controls", "label": "Controls" }, + { "id": "battery", "label": "Battery" }, + { "id": "clock", "label": "Clock" }, + { "id": "power", "label": "Power" }, + { "id": "separator", "label": "Separator" } +]; + +var itemIds = []; +for (var i = 0; i < items.length; i++) { + itemIds.push(items[i].id); +} + +// Items supported in vertical bar layout +var verticalItemIds = [ + "launcher", + "systray", + "tools", + "presets", + "layout", + "workspaces", + "pin", + "controls", + "battery", + "clock", + "power" +]; + +var defaultLeft = ["launcher", "workspaces", "layout", "pin"]; +var defaultCenter = []; +var defaultRight = ["presets", "tools", "systray", "controls", "battery", "clock", "power"]; diff --git a/modules/components/Separator.qml b/modules/components/Separator.qml index 5de068bb..cf6a614f 100644 --- a/modules/components/Separator.qml +++ b/modules/components/Separator.qml @@ -10,8 +10,8 @@ Rectangle { opacity: 0.1 radius: Styling.radius(0) - implicitWidth: vert ? 2 : 20 - implicitHeight: vert ? 20 : 2 + implicitWidth: vert ? 3 : 20 + implicitHeight: vert ? 5 : 2 Layout.fillWidth: !vert Layout.fillHeight: vert diff --git a/modules/globals/GlobalStates.qml b/modules/globals/GlobalStates.qml index fcdcdf34..fa4d3d73 100644 --- a/modules/globals/GlobalStates.qml +++ b/modules/globals/GlobalStates.qml @@ -340,7 +340,7 @@ 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"], + "bar": ["position", "launcherIcon", "launcherIconTint", "launcherIconFullTint", "launcherIconSize", "enableFirefoxPlayer", "screenList", "frameEnabled", "frameThickness", "pinnedOnStartup", "hoverToReveal", "hoverRegionHeight", "showPinButton", "availableOnFullscreen", "pillStyle", "use12hFormat", "containBar", "keepBarShadow", "keepBarBorder", "itemsLeft", "itemsCenter", "itemsRight"], "notch": ["theme", "position", "hoverRegionHeight", "keepHidden"], "workspaces": ["shown", "showAppIcons", "alwaysShowNumbers", "showNumbers", "dynamic"], "overview": ["rows", "columns", "scale", "workspaceSpacing"], diff --git a/modules/notch/Notch.qml b/modules/notch/Notch.qml index 557c3261..fd4c85e5 100644 --- a/modules/notch/Notch.qml +++ b/modules/notch/Notch.qml @@ -14,6 +14,9 @@ Item { property bool unifiedEffectActive: false z: 1000 + width: implicitWidth + height: implicitHeight + property Component defaultViewComponent property Component launcherViewComponent property Component dashboardViewComponent diff --git a/modules/notch/NotchContent.qml b/modules/notch/NotchContent.qml index 2add932c..7079b9b7 100644 --- a/modules/notch/NotchContent.qml +++ b/modules/notch/NotchContent.qml @@ -120,6 +120,17 @@ Item { // Track if mouse is over any notch-related area readonly property bool isMouseOverNotch: notchMouseAreaHover.hovered || notchRegionHover.hovered + readonly property real reportedNotchWidth: Math.max( + notchContainer.implicitWidth ?? 0, + notchContainer.width ?? 0, + notchRegionContainer.width ?? 0 + ) + + onReportedNotchWidthChanged: { + if (root.screen && root.screen.name) + Visibilities.setNotchWidth(root.screen.name, reportedNotchWidth); + } + // Reveal logic: readonly property bool reveal: { // If keepHidden is true, ONLY show on interaction @@ -499,4 +510,15 @@ Item { // Export some internal items for Visibilities property alias notchContainerRef: notchContainer + property alias notchRegionWidth: notchRegionContainer.width + + Component.onCompleted: { + if (root.screen && root.screen.name) + Visibilities.setNotchWidth(root.screen.name, reportedNotchWidth); + } + + Component.onDestruction: { + if (root.screen && root.screen.name) + Visibilities.clearNotchWidth(root.screen.name); + } } diff --git a/modules/services/Visibilities.qml b/modules/services/Visibilities.qml index b34f863c..9b835dfa 100644 --- a/modules/services/Visibilities.qml +++ b/modules/services/Visibilities.qml @@ -14,6 +14,7 @@ Singleton { property var barPanels: ({}) property var notches: ({}) property var notchPanels: ({}) + property var notchWidths: ({}) property var docks: ({}) property var dockPanels: ({}) property string currentActiveModule: "" @@ -100,6 +101,19 @@ Singleton { return notches[screenName] || null; } + function setNotchWidth(screenName, width) { + const safeWidth = Math.max(0, Math.round(width ?? 0)); + notchWidths = _updateMap(notchWidths, screenName, safeWidth); + } + + function clearNotchWidth(screenName) { + notchWidths = _updateMap(notchWidths, screenName, null); + } + + function getNotchWidth(screenName) { + return notchWidths[screenName] ?? 0; + } + function registerNotchPanel(screenName, notchPanel) { notchPanels = _updateMap(notchPanels, screenName, notchPanel); } diff --git a/modules/shell/UnifiedShellPanel.qml b/modules/shell/UnifiedShellPanel.qml index 285d8cb8..765915c1 100644 --- a/modules/shell/UnifiedShellPanel.qml +++ b/modules/shell/UnifiedShellPanel.qml @@ -52,6 +52,15 @@ PanelWindow { readonly property alias notchHoverActive: notchContent.hoverActive readonly property alias notchOpen: notchContent.screenNotchOpen readonly property alias notchReveal: notchContent.reveal + readonly property real notchWidth: notchContent.notchContainerRef ? Math.max( + notchContent.notchContainerRef.implicitWidth ?? 0, + notchContent.notchContainerRef.width ?? 0, + notchContent.notchRegionWidth ?? 0 + ) : 0 + readonly property real notchHitboxWidth: notchContent.notchHitbox ? Math.max( + notchContent.notchHitbox.width ?? 0, + notchWidth + ) : notchWidth // Generic names for external compatibility (Visibilities expects these on the panel object) readonly property alias pinned: barContent.pinned @@ -192,18 +201,8 @@ PanelWindow { screen: unifiedPanel.targetScreen z: 2 - // Keep the masking logic to cut out the notch area from the bar - layer.enabled: true - layer.effect: MultiEffect { - maskEnabled: true - maskInverted: true - maskThresholdMin: 0.3 - maskSpreadAtMin: 0.5 - maskSource: ShaderEffectSource { - sourceItem: notchContent - hideSource: false - } - } + // Do not mask the entire bar; only center items should avoid the notch. + layer.enabled: false } DockContent { diff --git a/modules/widgets/dashboard/controls/ShellPanel.qml b/modules/widgets/dashboard/controls/ShellPanel.qml index 90c9bb8d..16ebf5a1 100644 --- a/modules/widgets/dashboard/controls/ShellPanel.qml +++ b/modules/widgets/dashboard/controls/ShellPanel.qml @@ -8,6 +8,7 @@ import qs.modules.theme import qs.modules.components import qs.modules.globals import qs.config +import "../../../bar/BarItemRegistry.js" as BarItems Item { id: root @@ -47,6 +48,160 @@ Item { } property string currentSection: "" + readonly property var barItemDefs: BarItems.items + readonly property var barItemIds: BarItems.itemIds + + Component.onCompleted: { + var barPos = Config.bar.position ?? "top"; + if (barPos === "left" || barPos === "right") + ensureVerticalBarLists(); + } + + function sanitizeBarItems(list, allowedIds) { + if (list === undefined || list === null || typeof list.length === "undefined") + return []; + + var seen = {}; + var sanitized = []; + for (var i = 0; i < list.length; i++) { + var itemId = list[i]; + if (!barItemIds.includes(itemId)) + continue; + if (allowedIds && !allowedIds.includes(itemId)) + continue; + if (seen[itemId]) + continue; + seen[itemId] = true; + sanitized.push(itemId); + } + return sanitized; + } + + function ensureVerticalBarLists() { + var changed = false; + if (Config.bar.itemsLeftVertical === undefined || Config.bar.itemsLeftVertical.length === 0) { + Config.bar.itemsLeftVertical = ["launcher", "systray", "tools", "presets"]; + changed = true; + } + if (Config.bar.itemsCenterVertical === undefined || Config.bar.itemsCenterVertical.length === 0) { + Config.bar.itemsCenterVertical = ["layout", "workspaces", "pin"]; + changed = true; + } + if (Config.bar.itemsRightVertical === undefined || Config.bar.itemsRightVertical.length === 0) { + Config.bar.itemsRightVertical = ["controls", "battery", "clock", "power"]; + changed = true; + } + if (changed) + GlobalStates.markShellChanged(); + } + + function getBarItems(groupId) { + var barPos = Config.bar.position ?? "top"; + var isVertical = (barPos === "left" || barPos === "right"); + var verticalIds = BarItems.verticalItemIds || barItemIds; + if (isVertical) + ensureVerticalBarLists(); + if (groupId === "left") + return isVertical + ? sanitizeBarItems(Config.bar.itemsLeftVertical ?? ["launcher", "systray", "tools", "presets"], verticalIds) + : sanitizeBarItems(Config.bar.itemsLeft); + if (groupId === "center") { + if (isVertical) + return sanitizeBarItems(Config.bar.itemsCenterVertical ?? ["layout", "workspaces", "pin"], verticalIds); + var centerItems = sanitizeBarItems(Config.bar.itemsCenter); + var notchPos = Config.notchPosition ?? "top"; + if ((barPos === "top" && notchPos === "top") || (barPos === "bottom" && notchPos === "bottom")) { + if (Config.bar.centerItemsSplitByNotch === false) + return centerItems; + if (!centerItems.includes("notch")) + centerItems.push("notch"); + } + return centerItems; + } + return isVertical + ? sanitizeBarItems(Config.bar.itemsRightVertical ?? ["controls", "battery", "clock", "power"], verticalIds) + : sanitizeBarItems(Config.bar.itemsRight); + } + + function availableBarItems(groupId) { + var current = getBarItems(groupId); + var available = []; + var barPos = Config.bar.position ?? "top"; + var isVertical = (barPos === "left" || barPos === "right"); + var verticalIds = BarItems.verticalItemIds || barItemIds; + if (isVertical) + ensureVerticalBarLists(); + for (var i = 0; i < barItemDefs.length; i++) { + var def = barItemDefs[i]; + if (def.id === "notch" && (isVertical || groupId !== "center")) + continue; + if (isVertical && !verticalIds.includes(def.id)) + continue; + if (!current.includes(def.id)) + available.push(def); + } + return available; + } + + function applyBarItemsUpdate(groupId, newItems) { + var barPos = Config.bar.position ?? "top"; + var isVertical = (barPos === "left" || barPos === "right"); + var verticalIds = BarItems.verticalItemIds || barItemIds; + if (isVertical) + ensureVerticalBarLists(); + var cleaned = sanitizeBarItems(newItems, isVertical ? verticalIds : null); + var left = isVertical + ? sanitizeBarItems(Config.bar.itemsLeftVertical ?? [], verticalIds) + : sanitizeBarItems(Config.bar.itemsLeft); + var center = isVertical + ? sanitizeBarItems(Config.bar.itemsCenterVertical ?? [], verticalIds) + : sanitizeBarItems(Config.bar.itemsCenter); + var right = isVertical + ? sanitizeBarItems(Config.bar.itemsRightVertical ?? [], verticalIds) + : sanitizeBarItems(Config.bar.itemsRight); + + if (groupId === "left") { + center = center.filter(itemId => !cleaned.includes(itemId)); + right = right.filter(itemId => !cleaned.includes(itemId)); + left = cleaned; + } else if (groupId === "center") { + left = left.filter(itemId => !cleaned.includes(itemId)); + right = right.filter(itemId => !cleaned.includes(itemId)); + center = cleaned; + } else { + left = left.filter(itemId => !cleaned.includes(itemId)); + center = center.filter(itemId => !cleaned.includes(itemId)); + right = cleaned; + } + + if (!isVertical) { + var notchPos = Config.notchPosition ?? "top"; + if ((barPos === "top" && notchPos === "top") || (barPos === "bottom" && notchPos === "bottom")) { + if (Config.bar.centerItemsSplitByNotch === false) { + center = center.filter(itemId => itemId !== "notch"); + } else if (!center.includes("notch")) { + center.push("notch"); + } + } else { + center = center.filter(itemId => itemId !== "notch"); + } + } else { + center = center.filter(itemId => itemId !== "notch"); + } + + GlobalStates.markShellChanged(); + if (isVertical) { + Config.bar.itemsLeftVertical = left; + Config.bar.itemsCenterVertical = center; + Config.bar.itemsRightVertical = right; + } else { + Config.bar.itemsLeft = left; + Config.bar.itemsCenter = center; + Config.bar.itemsRight = right; + } + if (typeof Config.saveBar === "function") + Config.saveBar(); + } component SectionButton: StyledRect { id: sectionBtn @@ -523,6 +678,309 @@ Item { } } + component StyledSelect: ComboBox { + id: styledSelectRoot + property string placeholder: "" + + Layout.fillWidth: true + Layout.preferredHeight: 32 + + background: StyledRect { + variant: styledSelectRoot.hovered ? "focus" : "common" + radius: Styling.radius(-2) + enableShadow: true + } + + contentItem: Text { + text: { + if (styledSelectRoot.currentIndex < 0 || styledSelectRoot.currentText === "") + return styledSelectRoot.placeholder; + return styledSelectRoot.currentText; + } + font.family: Config.theme.font + font.pixelSize: Styling.fontSize(-1) + color: Colors.overBackground + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.right: parent.right + anchors.rightMargin: 24 + } + + indicator: Text { + x: styledSelectRoot.width - width - 10 + anchors.verticalCenter: parent.verticalCenter + text: Icons.caretDown + font.family: Icons.font + font.pixelSize: 16 + color: Colors.overSurfaceVariant + } + + popup: Popup { + y: styledSelectRoot.height + 4 + width: Math.max(styledSelectRoot.width, popupList.contentWidth + 8) + implicitHeight: popupList.contentHeight > 240 ? 240 : popupList.contentHeight + padding: 4 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: StyledRect { + variant: "pane" + radius: Styling.radius(-1) + enableShadow: true + } + + contentItem: ListView { + id: popupList + implicitWidth: styledSelectRoot.width + model: styledSelectRoot.model + clip: true + spacing: 4 + + delegate: StyledRect { + id: optionRect + required property var modelData + required property int index + + property bool isHovered: false + + variant: styledSelectRoot.currentIndex === index ? "primary" : (optionRect.isHovered ? "focus" : "common") + radius: Styling.radius(-2) + height: 32 + width: popupList.width + enableShadow: false + + Text { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 10 + text: (typeof optionRect.modelData === "string") ? optionRect.modelData : (optionRect.modelData && optionRect.modelData.text ? optionRect.modelData.text : "") + font.family: Config.theme.font + font.pixelSize: Styling.fontSize(-1) + color: optionRect.item + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: optionRect.isHovered = true + onExited: optionRect.isHovered = false + onClicked: { + styledSelectRoot.currentIndex = optionRect.index; + styledSelectRoot.popup.close(); + styledSelectRoot.activated(optionRect.index); + } + } + } + } + } + } + + component BarItemsList: ColumnLayout { + id: barItemsListRoot + required property string label + required property string groupId + required property var items + required property var availableItems + property bool readOnly: false + signal itemsUpdated(var newList) + + property int rowHeight: 40 + + Layout.fillWidth: true + spacing: 8 + + Text { + text: barItemsListRoot.label + font.family: Config.theme.font + font.pixelSize: Styling.fontSize(-1) + font.weight: Font.Medium + color: Colors.overSurfaceVariant + } + + ListView { + id: itemsList + Layout.fillWidth: true + Layout.preferredHeight: Math.max(barItemsListRoot.rowHeight, contentHeight) + Layout.minimumHeight: barItemsListRoot.rowHeight + spacing: 6 + interactive: false + clip: true + model: barItemsListRoot.items ?? [] + + delegate: StyledRect { + id: delegateItem + required property string modelData + required property int index + + property real originalY: 0 + property bool held: dragArea.pressed + + width: itemsList.width + height: barItemsListRoot.rowHeight + variant: "pane" + radius: Styling.radius(-2) + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 + + Text { + text: Icons.dotsNine + font.family: Icons.font + font.pixelSize: 14 + color: Colors.overSurfaceVariant + Layout.alignment: Qt.AlignVCenter + } + + Text { + text: { + for (var i = 0; i < barItemDefs.length; i++) { + if (barItemDefs[i].id === delegateItem.modelData) + return barItemDefs[i].label; + } + return delegateItem.modelData; + } + font.family: Config.theme.font + font.pixelSize: Styling.fontSize(0) + color: Colors.overBackground + Layout.fillWidth: true + elide: Text.ElideRight + } + + StyledRect { + id: removeButton + width: 28 + height: 28 + radius: Styling.radius(-2) + variant: "common" + visible: !barItemsListRoot.readOnly && delegateItem.modelData !== "notch" + + Text { + anchors.centerIn: parent + text: Icons.trash + font.family: Icons.font + font.pixelSize: 14 + color: Colors.overSurfaceVariant + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + var newItems = barItemsListRoot.items.slice(); + newItems.splice(delegateItem.index, 1); + barItemsListRoot.itemsUpdated(newItems); + } + } + } + } + + MouseArea { + id: dragArea + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 48 + hoverEnabled: true + cursorShape: Qt.SizeVerCursor + enabled: !barItemsListRoot.readOnly + drag.target: held ? delegateItem : undefined + drag.axis: Drag.YAxis + drag.minimumY: -delegateItem.height + drag.maximumY: itemsList.height + preventStealing: true + + onPressed: { + delegateItem.z = 2; + delegateItem.originalY = delegateItem.y; + } + onReleased: { + delegateItem.z = 1; + if (drag.active) { + var newIndex = Math.round(delegateItem.y / (delegateItem.height + itemsList.spacing)); + newIndex = Math.max(0, Math.min(newIndex, barItemsListRoot.items.length - 1)); + if (newIndex !== delegateItem.index) { + var newItems = barItemsListRoot.items.slice(); + var draggedItem = newItems.splice(delegateItem.index, 1)[0]; + newItems.splice(newIndex, 0, draggedItem); + barItemsListRoot.itemsUpdated(newItems); + } + } + delegateItem.x = 0; + delegateItem.y = delegateItem.originalY; + } + } + + Behavior on y { + enabled: !dragArea.held && !dragArea.drag.active + NumberAnimation { + duration: Config.animDuration > 0 ? Config.animDuration / 2 : 0 + easing.type: Easing.OutCubic + } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: 32 + spacing: 8 + visible: !barItemsListRoot.readOnly + + StyledSelect { + id: addComboBox + Layout.fillWidth: true + model: barItemsListRoot.availableItems.map(item => item.label) + currentIndex: 0 + enabled: barItemsListRoot.availableItems.length > 0 + placeholder: "Select item" + } + + StyledRect { + id: addButton + Layout.preferredWidth: 80 + Layout.preferredHeight: 32 + radius: Styling.radius(-2) + variant: barItemsListRoot.availableItems.length > 0 ? "primary" : "common" + enableShadow: barItemsListRoot.availableItems.length > 0 + + Text { + anchors.centerIn: parent + text: "Add" + font.family: Config.theme.font + font.pixelSize: Styling.fontSize(-1) + font.weight: Font.Medium + color: addButton.item + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + enabled: barItemsListRoot.availableItems.length > 0 + cursorShape: Qt.PointingHandCursor + onClicked: { + if (barItemsListRoot.availableItems.length === 0) + return; + var selectedItem = barItemsListRoot.availableItems[addComboBox.currentIndex]; + if (!selectedItem) + return; + var newItems = (barItemsListRoot.items ?? []).slice(); + newItems.push(selectedItem.id); + barItemsListRoot.itemsUpdated(newItems); + } + } + } + } + } + // Main content Flickable { id: mainFlickable @@ -716,6 +1174,45 @@ Item { } } + Text { + text: "Bar Items" + font.family: Config.theme.font + font.pixelSize: Styling.fontSize(-1) + font.weight: Font.Medium + color: Colors.overSurfaceVariant + Layout.topMargin: 8 + } + + BarItemsList { + label: "Left Group" + groupId: "left" + items: root.getBarItems("left") + availableItems: root.availableBarItems("left") + onItemsUpdated: newList => { + root.applyBarItemsUpdate("left", newList); + } + } + + BarItemsList { + label: "Center Group" + groupId: "center" + items: root.getBarItems("center") + availableItems: root.availableBarItems("center") + onItemsUpdated: newList => { + root.applyBarItemsUpdate("center", newList); + } + } + + BarItemsList { + label: "Right Group" + groupId: "right" + items: root.getBarItems("right") + availableItems: root.availableBarItems("right") + onItemsUpdated: newList => { + root.applyBarItemsUpdate("right", newList); + } + } + TextInputRow { label: "Launcher Icon" value: Config.bar.launcherIcon ?? ""