From bdf7bf76cb88192a6be739dc90278326739b5533 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 20 May 2026 19:11:00 +0200 Subject: [PATCH] Extract clock and tray bar widgets --- shell/plugins/bar/Bar.qml | 497 +---------------------- shell/plugins/bar/FirstPartyWidgets.qml | 2 + shell/plugins/bar/README.md | 6 +- shell/plugins/bar/widgets/clock.qml | 44 ++ shell/plugins/bar/widgets/tray.qml | 448 ++++++++++++++++++++ shell/plugins/settings/SettingsPanel.qml | 9 +- 6 files changed, 499 insertions(+), 507 deletions(-) create mode 100644 shell/plugins/bar/widgets/clock.qml create mode 100644 shell/plugins/bar/widgets/tray.qml diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index 88f3664a..acb6adfd 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -1,8 +1,6 @@ import Quickshell import Quickshell.Io -import Quickshell.Services.SystemTray import Quickshell.Wayland -import Quickshell.Widgets import QtQuick import QtQuick.Layouts import qs.Commons @@ -54,7 +52,6 @@ Item { Behavior on foreground { ColorAnimation { duration: 420; easing.type: Easing.InOutCubic } } Behavior on background { ColorAnimation { duration: 420; easing.type: Easing.InOutCubic } } Behavior on urgent { ColorAnimation { duration: 420; easing.type: Easing.InOutCubic } } - property bool clockAlt: false property var tooltipTarget: null property var pendingTooltipTarget: null property string tooltipText: "" @@ -214,14 +211,6 @@ Item { } } - function builtinModuleComponent(name) { - switch (String(name)) { - case "clock": return clockModuleComponent - case "tray": return trayModuleComponent - default: return null - } - } - function expandPath(path) { var value = String(path || "") if (value === "") return "" @@ -322,48 +311,6 @@ Item { clearTooltip() } - function trayIconSource(icon) { - var value = String(icon || "") - var marker = "?path=" - var markerIndex = value.indexOf(marker) - if (markerIndex === -1) return value - - var name = value.substring(0, markerIndex).split("/").pop() - var iconPath = value.substring(markerIndex + marker.length).split("&")[0] - return Util.fileUrl(iconPath + "/hicolor/16x16/status/" + name + ".png") - } - - function trayTooltip(item) { - return item.tooltipTitle || item.title || item.id || "" - } - - function clockEntry() { - var serial = barConfigSerial - var sections = ["left", "center", "right"] - for (var i = 0; i < sections.length; i++) { - var entries = layoutEntries(sections[i]) - var idx = entryIndex(entries, "clock") - if (idx !== -1) return entries[idx] - } - return null - } - - function clockText() { - var entry = clockEntry() - if (clockAlt) - return Qt.formatDateTime(systemClock.date, moduleString(entry, "formatAlt", "dd MMMM 'W'ww yyyy")) - - if (vertical) - return Qt.formatDateTime(systemClock.date, moduleString(entry, "verticalFormat", "HH\n—\nmm")) - - return Qt.formatDateTime(systemClock.date, moduleString(entry, "format", "dddd HH:mm")) - } - - SystemClock { - id: systemClock - precision: SystemClock.Minutes - } - Process { id: launcher } Timer { @@ -543,8 +490,6 @@ Item { } Component { id: emptyModuleComponent; Item { implicitWidth: 0; implicitHeight: 0; visible: false } } - Component { id: clockModuleComponent; ClockModule {} } - Component { id: trayModuleComponent; TrayModule {} } function findCenterAnchorEntry() { var entries = root.layoutEntries("center") @@ -717,13 +662,12 @@ Item { readonly property string moduleName: root.entryId(entry) readonly property var moduleSettings: root.entrySettings(entry) readonly property string customType: root.customModuleType(entry) - readonly property var builtinComponent: customType ? null : root.builtinModuleComponent(moduleName) // Re-evaluate when the registry mutates (Component reference changes, // plugin enabled/disabled, etc.). Reading the `widgets` property creates // the binding dependency — the wrapped function call alone wouldn't. readonly property var registryComponent: { var w = root.barWidgetRegistry.widgets - if (customType || builtinComponent) return null + if (customType) return null var registryName = root.canonicalWidgetId(moduleName) return w[registryName] ? w[registryName].component : null } @@ -744,7 +688,7 @@ Item { Loader { id: componentLoader active: !slot.qmlCustom && !slot.registered - sourceComponent: slot.builtinComponent || (slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent) + sourceComponent: slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent anchors.fill: parent onLoaded: { slot.injectProps() @@ -913,441 +857,4 @@ Item { } } - component ClockModule: ModuleButton { - text: root.clockText() - horizontalMargin: 8.75 - verticalPadding: 8.75 - onPressed: function(button) { - if (button === Qt.RightButton) root.run("omarchy-launch-floating-terminal-with-presentation omarchy-tz-select") - else root.clockAlt = !root.clockAlt - } - } - - component TrayModule: Item { - id: trayRoot - - property bool expanded: false - property bool managePopupOpen: false - function close() { managePopupOpen = false } - - // Re-resolve the tray's own entry settings whenever the bar layout reloads. - readonly property var trayEntry: { - var serial = root.barConfigSerial - var sections = ["left", "center", "right"] - for (var s = 0; s < sections.length; s++) { - var arr = root.layoutEntries(sections[s]) - for (var i = 0; i < arr.length; i++) { - if (root.entryId(arr[i]) === "tray") return arr[i] - } - } - return ({}) - } - readonly property var pinnedIds: Array.isArray(trayEntry.pinned) ? trayEntry.pinned : [] - readonly property var hiddenIds: Array.isArray(trayEntry.hidden) ? trayEntry.hidden : [] - - function classifyItem(item) { - var iid = String(item.id || "") - if (hiddenIds.indexOf(iid) !== -1) return "hidden" - if (pinnedIds.indexOf(iid) !== -1) return "pinned" - return "drawer" - } - - function bucket(category) { - var values = SystemTray.items.values - var result = [] - for (var i = 0; i < values.length; i++) { - var item = values[i] - if (item.status === Status.Passive) continue - if (category === "all") { result.push(item); continue } - if (classifyItem(item) === category) result.push(item) - } - return result - } - - readonly property var pinnedItems: bucket("pinned") - readonly property var drawerItems: bucket("drawer") - readonly property var allItems: bucket("all") - readonly property int drawerCount: drawerItems.length - readonly property int drawerExtent: drawerCount > 0 ? drawerCount * 16 + (drawerCount - 1) * 17 : 0 - // Match Waybar's group/tray-expander drawer transition-duration. - readonly property int animationDuration: 600 - property real revealProgress: expanded ? 1 : 0 - readonly property real revealExtent: drawerExtent * revealProgress - - Behavior on revealProgress { - NumberAnimation { duration: trayRoot.animationDuration; easing.type: Easing.OutCubic } - } - - function persistTrayState(pinned, hidden) { - if (!root.shell || typeof root.shell.updateEntryInline !== "function") return - root.shell.updateEntryInline("tray", { id: "tray", pinned: pinned, hidden: hidden }) - } - - function togglePin(iid) { - var p = pinnedIds.slice(), h = hiddenIds.slice() - var idx = p.indexOf(iid) - if (idx !== -1) p.splice(idx, 1) - else { - p.push(iid) - var hi = h.indexOf(iid); if (hi !== -1) h.splice(hi, 1) - } - persistTrayState(p, h) - } - - function toggleHide(iid) { - var p = pinnedIds.slice(), h = hiddenIds.slice() - var idx = h.indexOf(iid) - if (idx !== -1) h.splice(idx, 1) - else { - h.push(iid) - var pi = p.indexOf(iid); if (pi !== -1) p.splice(pi, 1) - } - persistTrayState(p, h) - } - - visible: pinnedItems.length > 0 || drawerCount > 0 - clip: false - implicitWidth: root.vertical ? root.barSize : trayContent.implicitWidth - implicitHeight: root.vertical ? trayContent.implicitHeight : root.barSize - - Loader { - id: trayContent - anchors.fill: parent - sourceComponent: root.vertical ? verticalTray : horizontalTray - } - - Component { - id: horizontalTray - - Item { - id: horizontalTrayRoot - - readonly property int pinnedWidth: pinnedRow.implicitWidth - readonly property int drawerBlockWidth: trayRoot.allItems.length > 0 ? expandIcon.implicitWidth + trayRoot.drawerExtent : 0 - - implicitWidth: pinnedWidth + drawerBlockWidth - implicitHeight: root.barSize - - // Mask out the empty area the collapsed drawer reserves for its slide-in, - // so hovering it doesn't trigger expand and clicks pass through. - containmentMask: QtObject { - function contains(point: point): bool { - if (point.y < 0 || point.y > horizontalTrayRoot.height) return false - // Drawer reveals leftward; chevron sits at the right end when collapsed - // and slides left as it opens. The visible region starts at the chevron. - var chevronX = trayRoot.drawerExtent - trayRoot.revealExtent - if (point.x >= chevronX && point.x <= horizontalTrayRoot.drawerBlockWidth) return true - // Pinned items, placed to the right of the drawer block. - var pinnedStart = horizontalTrayRoot.drawerBlockWidth - return point.x >= pinnedStart && point.x <= horizontalTrayRoot.implicitWidth - } - } - - Item { - id: drawerArea - x: 0 - width: horizontalTrayRoot.drawerBlockWidth - height: root.barSize - visible: trayRoot.allItems.length > 0 - - HoverHandler { - onHoveredChanged: trayRoot.expanded = hovered - } - - ModuleButton { - id: expandIcon - width: implicitWidth - height: implicitHeight - x: trayRoot.drawerExtent - trayRoot.revealExtent - text: "" - horizontalMargin: 9 - verticalPadding: 6 - onPressed: function(button) { - if (button === Qt.RightButton) trayRoot.managePopupOpen = !trayRoot.managePopupOpen - } - } - - Item { - id: trayClip - x: expandIcon.width - anchors.verticalCenter: parent.verticalCenter - width: trayRoot.drawerExtent - height: root.barSize - clip: true - - Row { - id: trayIcons - x: trayRoot.drawerExtent - trayRoot.revealExtent - anchors.verticalCenter: parent.verticalCenter - spacing: Style.space(17) - layer.enabled: true - - Repeater { - model: trayRoot.drawerItems - TrayItem {} - } - } - } - } - - Row { - id: pinnedRow - x: drawerArea.x + horizontalTrayRoot.drawerBlockWidth - anchors.verticalCenter: parent.verticalCenter - spacing: Style.space(17) - leftPadding: trayRoot.pinnedItems.length > 0 && trayRoot.allItems.length > 0 ? Style.space(6) : 0 - Repeater { - model: trayRoot.pinnedItems - TrayItem {} - } - } - } - } - - Component { - id: verticalTray - - Item { - id: verticalTrayRoot - - readonly property int pinnedHeight: pinnedCol.implicitHeight - readonly property int drawerBlockHeight: trayRoot.allItems.length > 0 ? expandIcon.implicitHeight + trayRoot.drawerExtent : 0 - - implicitWidth: root.barSize - implicitHeight: pinnedHeight + drawerBlockHeight - - containmentMask: QtObject { - function contains(point: point): bool { - if (point.x < 0 || point.x > verticalTrayRoot.width) return false - var chevronY = trayRoot.drawerExtent - trayRoot.revealExtent - if (point.y >= chevronY && point.y <= verticalTrayRoot.drawerBlockHeight) return true - var pinnedStart = verticalTrayRoot.drawerBlockHeight - return point.y >= pinnedStart && point.y <= verticalTrayRoot.implicitHeight - } - } - - Item { - id: drawerArea - y: 0 - width: root.barSize - height: verticalTrayRoot.drawerBlockHeight - visible: trayRoot.allItems.length > 0 - - HoverHandler { - onHoveredChanged: trayRoot.expanded = hovered - } - - ModuleButton { - id: expandIcon - width: implicitWidth - height: implicitHeight - y: trayRoot.drawerExtent - trayRoot.revealExtent - text: "" - textRotation: 90 - horizontalMargin: 9 - verticalPadding: 6 - onPressed: function(button) { - if (button === Qt.RightButton) trayRoot.managePopupOpen = !trayRoot.managePopupOpen - } - } - - Item { - id: trayClip - y: expandIcon.height - anchors.horizontalCenter: parent.horizontalCenter - width: root.barSize - height: trayRoot.drawerExtent - clip: true - - Column { - id: trayIcons - y: trayRoot.drawerExtent - trayRoot.revealExtent - anchors.horizontalCenter: parent.horizontalCenter - spacing: Style.space(17) - layer.enabled: true - - Repeater { - model: trayRoot.drawerItems - TrayItem {} - } - } - } - } - - Column { - id: pinnedCol - y: drawerArea.y + verticalTrayRoot.drawerBlockHeight - anchors.horizontalCenter: parent.horizontalCenter - spacing: Style.space(17) - topPadding: trayRoot.pinnedItems.length > 0 && trayRoot.allItems.length > 0 ? Style.space(6) : 0 - Repeater { - model: trayRoot.pinnedItems - TrayItem {} - } - } - } - } - - PopupCard { - id: managePopup - anchorItem: trayRoot - owner: trayRoot - bar: root - open: trayRoot.managePopupOpen - contentWidth: managePopup.fittedContentWidth(Style.space(300)) - contentHeight: managePopup.fittedContentHeight(manageColumn.implicitHeight) - - Column { - id: manageColumn - anchors.fill: parent - spacing: Style.space(8) - - Text { - text: "Tray icons" - color: root.foreground - font.family: root.fontFamily - font.pixelSize: Style.font.body - font.bold: true - } - - Text { - text: "Pinned icons stay visible. Hidden icons never show." - color: Qt.darker(root.foreground, 1.4) - font.family: root.fontFamily - font.pixelSize: Style.font.caption - wrapMode: Text.WordWrap - width: parent.width - } - - Text { - visible: trayRoot.allItems.length === 0 - text: "No tray items reporting." - color: Qt.darker(root.foreground, 1.5) - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - font.italic: true - } - - Repeater { - model: trayRoot.allItems - delegate: Item { - id: rowRoot - required property var modelData - required property int index - width: manageColumn.width - implicitHeight: 28 - - readonly property string itemId: String(modelData.id || "") - readonly property string displayName: { - var t = String(modelData.title || "").trim() - if (t) return t - var tt = String(modelData.tooltipTitle || "").trim() - if (tt) return tt - var id = String(modelData.id || "") - var slash = id.lastIndexOf("/") - return slash !== -1 ? id.substring(slash + 1) : (id || "Unknown") - } - readonly property bool isPinned: trayRoot.pinnedIds.indexOf(itemId) !== -1 - readonly property bool isHidden: trayRoot.hiddenIds.indexOf(itemId) !== -1 - - IconImage { - id: rowIcon - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - implicitSize: 16 - width: 16 - height: 16 - source: root.trayIconSource(rowRoot.modelData.icon) - } - - Text { - anchors.verticalCenter: parent.verticalCenter - anchors.left: rowIcon.right - anchors.leftMargin: Style.space(10) - anchors.right: rowHideBtn.left - anchors.rightMargin: Style.space(8) - text: rowRoot.displayName - color: root.foreground - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - elide: Text.ElideRight - } - - Button { - id: rowPinBtn - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - iconText: "" - text: rowRoot.isPinned ? "Unpin" : "Pin" - foreground: root.foreground - horizontalPadding: 8 - verticalPadding: 3 - iconSize: Style.font.bodySmall - fontSize: Style.font.bodySmall - onClicked: trayRoot.togglePin(rowRoot.itemId) - } - - Button { - id: rowHideBtn - anchors.verticalCenter: parent.verticalCenter - anchors.right: rowPinBtn.left - anchors.rightMargin: Style.space(6) - iconText: "" - text: rowRoot.isHidden ? "Show" : "Hide" - foreground: root.foreground - horizontalPadding: 8 - verticalPadding: 3 - iconSize: Style.font.bodySmall - fontSize: Style.font.bodySmall - onClicked: trayRoot.toggleHide(rowRoot.itemId) - } - } - } - } - } - } - - component TrayItem: Item { - id: trayItemRoot - - required property var modelData - - visible: modelData.status !== Status.Passive - implicitWidth: visible ? 16 : 0 - implicitHeight: visible ? 16 : 0 - - IconImage { - anchors.centerIn: parent - implicitSize: 12 - width: 12 - height: 12 - source: root.trayIconSource(trayItemRoot.modelData.icon) - } - - MouseArea { - id: mouseArea - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: root.showTooltip(trayItemRoot, root.trayTooltip(modelData)) - onExited: root.hideTooltip(trayItemRoot) - onClicked: function(mouse) { - if (mouse.button === Qt.RightButton && trayItemRoot.modelData.hasMenu) { - var point = trayItemRoot.QsWindow.contentItem.mapFromItem(trayItemRoot, mouse.x, mouse.y) - trayItemRoot.modelData.display(trayItemRoot.QsWindow.window, point.x, point.y) - } else if (mouse.button === Qt.MiddleButton) { - trayItemRoot.modelData.secondaryActivate() - } else { - trayItemRoot.modelData.activate() - } - } - onWheel: function(wheel) { - trayItemRoot.modelData.scroll(wheel.angleDelta.y, false) - } - } - - readonly property bool tooltipHovered: visible && opacity > 0 && mouseArea.containsMouse - } - } diff --git a/shell/plugins/bar/FirstPartyWidgets.qml b/shell/plugins/bar/FirstPartyWidgets.qml index 7291864e..164d0e60 100644 --- a/shell/plugins/bar/FirstPartyWidgets.qml +++ b/shell/plugins/bar/FirstPartyWidgets.qml @@ -14,11 +14,13 @@ QtObject { "networkPanel": { displayName: "Network", description: "Wi-Fi list and connection state", category: "Network", allowMultiple: false, sourceDir: "../panels", sourceName: "Network" }, "powerPanel": { displayName: "Power", description: "Battery, power profile, and system stats", category: "System", allowMultiple: false, sourceDir: "../panels", sourceName: "Power" }, "bluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false, sourceDir: "../panels", sourceName: "Bluetooth" }, + "clock": { displayName: "Clock", description: "Date / time text", category: "Time", allowMultiple: false }, "daytime": { displayName: "Daytime", description: "Day/time label with date tooltip", category: "Time", allowMultiple: false, settingsForm: "daytimeSettings" }, "indicators": { displayName: "Indicators", description: "Manual state indicators", category: "Status", allowMultiple: false }, "notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND", category: "Status", allowMultiple: false }, "systemUpdate": { displayName: "System update", description: "Indicates available system updates", category: "System", allowMultiple: false }, "systemStats": { displayName: "System stats", description: "CPU icon — hover for graphs, click to open btop", category: "System", allowMultiple: false }, + "tray": { displayName: "System tray", description: "Status notifier items", category: "Status", allowMultiple: false }, "weather": { displayName: "Weather", description: "Weather pill with detail popup", category: "Info", allowMultiple: false, settingsForm: "weatherSettings" }, "microphone": { displayName: "Microphone", description: "Mic input state and mute toggle", category: "Audio", allowMultiple: false }, "activeWindow": { displayName: "Active window", description: "Title of the focused window", category: "Compositor", allowMultiple: false }, diff --git a/shell/plugins/bar/README.md b/shell/plugins/bar/README.md index fce962fb..155aab56 100644 --- a/shell/plugins/bar/README.md +++ b/shell/plugins/bar/README.md @@ -56,12 +56,14 @@ Example `shell.json` (bar subtree only shown): |---|---|---| | `omarchy` | Omarchy menu launcher | left = menu · right = terminal | | `workspaces` | Hyprland workspace switcher | left = focus workspace | +| `clock` | Date/time label | left = alternate format · right = timezone selector | | `media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup | | `daytime` | Day/time label with date tooltip | hover = date | | `indicators` | Manual state indicators | left = indicator action | | `notificationCenter` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND | | `systemUpdate` | Available update indicator | left = update | | `systemStats` | Inline CPU + memory sparklines, popup with detail | left = popup · right = terminal | +| `tray` | System tray | hover = reveal drawer · right on chevron = manage | | `weather` | Weather icon + popup with forecast | left = popup · right = full notification | | `microphone` | Mic icon + scroll volume | left = mute toggle · middle = audio panel · scroll = source volume | @@ -75,10 +77,6 @@ Example `shell.json` (bar subtree only shown): | `panels.bluetooth` | Bluetooth icon + popup with device list, connect/disconnect, battery | left = popup · right = toggle radio · middle = bluetoothctl TUI | | `panels.monitor` | Brightness and laptop display controls | left = popup | -### Built-in base modules (in `Bar.qml`) - -`clock`, `tray`. - The `indicators` widget loads individual bar indicators from `indicators/`, ordered by its `items` array in `shell.json`. Rich panels such as `powerPanel`, `networkPanel`, and `audioPanel` live in `../panels/` above. ## Orientation diff --git a/shell/plugins/bar/widgets/clock.qml b/shell/plugins/bar/widgets/clock.qml new file mode 100644 index 00000000..2820bb3f --- /dev/null +++ b/shell/plugins/bar/widgets/clock.qml @@ -0,0 +1,44 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Ui + +BarWidget { + id: root + moduleName: "clock" + + property bool alt: false + + function setting(name, fallback) { + var value = settings ? settings[name] : undefined + return value === undefined || value === null ? fallback : value + } + + function label() { + if (alt) return Qt.formatDateTime(clock.date, String(setting("formatAlt", "dd MMMM 'W'ww yyyy"))) + if (bar && bar.vertical) return Qt.formatDateTime(clock.date, String(setting("verticalFormat", "HH\n—\nmm"))) + return Qt.formatDateTime(clock.date, String(setting("format", "dddd HH:mm"))) + } + + implicitWidth: button.implicitWidth + implicitHeight: button.implicitHeight + + SystemClock { + id: clock + precision: SystemClock.Minutes + } + + WidgetButton { + id: button + anchors.fill: parent + bar: root.bar + text: root.label() + horizontalMargin: 8.75 + verticalPadding: 8.75 + onPressed: function(button) { + if (!root.bar) return + if (button === Qt.RightButton) root.bar.run("omarchy-launch-floating-terminal-with-presentation omarchy-tz-select") + else root.alt = !root.alt + } + } +} diff --git a/shell/plugins/bar/widgets/tray.qml b/shell/plugins/bar/widgets/tray.qml new file mode 100644 index 00000000..d53f2ba6 --- /dev/null +++ b/shell/plugins/bar/widgets/tray.qml @@ -0,0 +1,448 @@ +import QtQuick +import Quickshell.Services.SystemTray +import Quickshell.Widgets +import qs.Commons +import qs.Ui + +BarWidget { + id: root + moduleName: "tray" + + property bool expanded: false + property bool managePopupOpen: false + readonly property bool vertical: bar ? bar.vertical : false + readonly property int barSize: bar ? bar.barSize : Style.bar.sizeHorizontal + readonly property color foreground: bar ? bar.foreground : Color.foreground + readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family + readonly property var pinnedIds: Array.isArray(settings.pinned) ? settings.pinned : [] + readonly property var hiddenIds: Array.isArray(settings.hidden) ? settings.hidden : [] + readonly property var pinnedItems: bucket("pinned") + readonly property var drawerItems: bucket("drawer") + readonly property var allItems: bucket("all") + readonly property int drawerCount: drawerItems.length + readonly property int drawerExtent: drawerCount > 0 ? drawerCount * 16 + (drawerCount - 1) * 17 : 0 + // Match Waybar's group/tray-expander drawer transition-duration. + readonly property int animationDuration: 600 + property real revealProgress: expanded ? 1 : 0 + readonly property real revealExtent: drawerExtent * revealProgress + + function close() { + managePopupOpen = false + } + + function trayIconSource(icon) { + var value = String(icon || "") + var marker = "?path=" + var markerIndex = value.indexOf(marker) + if (markerIndex === -1) return value + + var name = value.substring(0, markerIndex).split("/").pop() + var iconPath = value.substring(markerIndex + marker.length).split("&")[0] + return Util.fileUrl(iconPath + "/hicolor/16x16/status/" + name + ".png") + } + + function trayTooltip(item) { + return item.tooltipTitle || item.title || item.id || "" + } + + function classifyItem(item) { + var iid = String(item.id || "") + if (hiddenIds.indexOf(iid) !== -1) return "hidden" + if (pinnedIds.indexOf(iid) !== -1) return "pinned" + return "drawer" + } + + function bucket(category) { + var values = SystemTray.items.values + var result = [] + for (var i = 0; i < values.length; i++) { + var item = values[i] + if (item.status === Status.Passive) continue + if (category === "all") { + result.push(item) + continue + } + if (classifyItem(item) === category) result.push(item) + } + return result + } + + function persistTrayState(pinned, hidden) { + if (!root.bar || !root.bar.shell || typeof root.bar.shell.updateEntryInline !== "function") return + root.bar.shell.updateEntryInline("tray", { id: "tray", pinned: pinned, hidden: hidden }) + } + + function togglePin(iid) { + var p = pinnedIds.slice(), h = hiddenIds.slice() + var idx = p.indexOf(iid) + if (idx !== -1) p.splice(idx, 1) + else { + p.push(iid) + var hi = h.indexOf(iid) + if (hi !== -1) h.splice(hi, 1) + } + persistTrayState(p, h) + } + + function toggleHide(iid) { + var p = pinnedIds.slice(), h = hiddenIds.slice() + var idx = h.indexOf(iid) + if (idx !== -1) h.splice(idx, 1) + else { + h.push(iid) + var pi = p.indexOf(iid) + if (pi !== -1) p.splice(pi, 1) + } + persistTrayState(p, h) + } + + visible: pinnedItems.length > 0 || drawerCount > 0 + clip: false + implicitWidth: root.vertical ? root.barSize : trayContent.implicitWidth + implicitHeight: root.vertical ? trayContent.implicitHeight : root.barSize + + Behavior on revealProgress { + NumberAnimation { duration: root.animationDuration; easing.type: Easing.OutCubic } + } + + Loader { + id: trayContent + anchors.fill: parent + sourceComponent: root.vertical ? verticalTray : horizontalTray + } + + Component { + id: horizontalTray + + Item { + id: horizontalTrayRoot + + readonly property int pinnedWidth: pinnedRow.implicitWidth + readonly property int drawerBlockWidth: root.allItems.length > 0 ? expandIcon.implicitWidth + root.drawerExtent : 0 + + implicitWidth: pinnedWidth + drawerBlockWidth + implicitHeight: root.barSize + + // Mask out the empty area the collapsed drawer reserves for its slide-in, + // so hovering it doesn't trigger expand and clicks pass through. + containmentMask: QtObject { + function contains(point: point): bool { + if (point.y < 0 || point.y > horizontalTrayRoot.height) return false + // Drawer reveals leftward; chevron sits at the right end when collapsed + // and slides left as it opens. The visible region starts at the chevron. + var chevronX = root.drawerExtent - root.revealExtent + if (point.x >= chevronX && point.x <= horizontalTrayRoot.drawerBlockWidth) return true + // Pinned items, placed to the right of the drawer block. + var pinnedStart = horizontalTrayRoot.drawerBlockWidth + return point.x >= pinnedStart && point.x <= horizontalTrayRoot.implicitWidth + } + } + + Item { + id: drawerArea + x: 0 + width: horizontalTrayRoot.drawerBlockWidth + height: root.barSize + visible: root.allItems.length > 0 + + HoverHandler { + onHoveredChanged: root.expanded = hovered + } + + WidgetButton { + id: expandIcon + bar: root.bar + width: implicitWidth + height: implicitHeight + x: root.drawerExtent - root.revealExtent + text: "\uf053" + horizontalMargin: 9 + verticalPadding: 6 + onPressed: function(button) { + if (button === Qt.RightButton) root.managePopupOpen = !root.managePopupOpen + } + } + + Item { + id: trayClip + x: expandIcon.width + anchors.verticalCenter: parent.verticalCenter + width: root.drawerExtent + height: root.barSize + clip: true + + Row { + id: trayIcons + x: root.drawerExtent - root.revealExtent + anchors.verticalCenter: parent.verticalCenter + spacing: Style.space(17) + layer.enabled: true + + Repeater { + model: root.drawerItems + TrayItem {} + } + } + } + } + + Row { + id: pinnedRow + x: drawerArea.x + horizontalTrayRoot.drawerBlockWidth + anchors.verticalCenter: parent.verticalCenter + spacing: Style.space(17) + leftPadding: root.pinnedItems.length > 0 && root.allItems.length > 0 ? Style.space(6) : 0 + Repeater { + model: root.pinnedItems + TrayItem {} + } + } + } + } + + Component { + id: verticalTray + + Item { + id: verticalTrayRoot + + readonly property int pinnedHeight: pinnedCol.implicitHeight + readonly property int drawerBlockHeight: root.allItems.length > 0 ? expandIcon.implicitHeight + root.drawerExtent : 0 + + implicitWidth: root.barSize + implicitHeight: pinnedHeight + drawerBlockHeight + + containmentMask: QtObject { + function contains(point: point): bool { + if (point.x < 0 || point.x > verticalTrayRoot.width) return false + var chevronY = root.drawerExtent - root.revealExtent + if (point.y >= chevronY && point.y <= verticalTrayRoot.drawerBlockHeight) return true + var pinnedStart = verticalTrayRoot.drawerBlockHeight + return point.y >= pinnedStart && point.y <= verticalTrayRoot.implicitHeight + } + } + + Item { + id: drawerArea + y: 0 + width: root.barSize + height: verticalTrayRoot.drawerBlockHeight + visible: root.allItems.length > 0 + + HoverHandler { + onHoveredChanged: root.expanded = hovered + } + + WidgetButton { + id: expandIcon + bar: root.bar + width: implicitWidth + height: implicitHeight + y: root.drawerExtent - root.revealExtent + text: "\uf053" + textRotation: 90 + horizontalMargin: 9 + verticalPadding: 6 + onPressed: function(button) { + if (button === Qt.RightButton) root.managePopupOpen = !root.managePopupOpen + } + } + + Item { + id: trayClip + y: expandIcon.height + anchors.horizontalCenter: parent.horizontalCenter + width: root.barSize + height: root.drawerExtent + clip: true + + Column { + id: trayIcons + y: root.drawerExtent - root.revealExtent + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.space(17) + layer.enabled: true + + Repeater { + model: root.drawerItems + TrayItem {} + } + } + } + } + + Column { + id: pinnedCol + y: drawerArea.y + verticalTrayRoot.drawerBlockHeight + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.space(17) + topPadding: root.pinnedItems.length > 0 && root.allItems.length > 0 ? Style.space(6) : 0 + Repeater { + model: root.pinnedItems + TrayItem {} + } + } + } + } + + PopupCard { + id: managePopup + anchorItem: root + owner: root + bar: root.bar + open: root.managePopupOpen + contentWidth: managePopup.fittedContentWidth(Style.space(300)) + contentHeight: managePopup.fittedContentHeight(manageColumn.implicitHeight) + + Column { + id: manageColumn + anchors.fill: parent + spacing: Style.space(8) + + Text { + text: "Tray icons" + color: root.foreground + font.family: root.fontFamily + font.pixelSize: Style.font.body + font.bold: true + } + + Text { + text: "Pinned icons stay visible. Hidden icons never show." + color: Qt.darker(root.foreground, 1.4) + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + width: parent.width + } + + Text { + visible: root.allItems.length === 0 + text: "No tray items reporting." + color: Qt.darker(root.foreground, 1.5) + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + font.italic: true + } + + Repeater { + model: root.allItems + delegate: Item { + id: rowRoot + required property var modelData + required property int index + width: manageColumn.width + implicitHeight: 28 + + readonly property string itemId: String(modelData.id || "") + readonly property string displayName: { + var t = String(modelData.title || "").trim() + if (t) return t + var tt = String(modelData.tooltipTitle || "").trim() + if (tt) return tt + var id = String(modelData.id || "") + var slash = id.lastIndexOf("/") + return slash !== -1 ? id.substring(slash + 1) : (id || "Unknown") + } + readonly property bool isPinned: root.pinnedIds.indexOf(itemId) !== -1 + readonly property bool isHidden: root.hiddenIds.indexOf(itemId) !== -1 + + IconImage { + id: rowIcon + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + implicitSize: 16 + width: 16 + height: 16 + source: root.trayIconSource(rowRoot.modelData.icon) + } + + Text { + anchors.verticalCenter: parent.verticalCenter + anchors.left: rowIcon.right + anchors.leftMargin: Style.space(10) + anchors.right: rowHideBtn.left + anchors.rightMargin: Style.space(8) + text: rowRoot.displayName + color: root.foreground + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + elide: Text.ElideRight + } + + Button { + id: rowPinBtn + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + iconText: "\uf08d" + text: rowRoot.isPinned ? "Unpin" : "Pin" + foreground: root.foreground + horizontalPadding: 8 + verticalPadding: 3 + iconSize: Style.font.bodySmall + fontSize: Style.font.bodySmall + onClicked: root.togglePin(rowRoot.itemId) + } + + Button { + id: rowHideBtn + anchors.verticalCenter: parent.verticalCenter + anchors.right: rowPinBtn.left + anchors.rightMargin: Style.space(6) + iconText: "\uf06e" + text: rowRoot.isHidden ? "Show" : "Hide" + foreground: root.foreground + horizontalPadding: 8 + verticalPadding: 3 + iconSize: Style.font.bodySmall + fontSize: Style.font.bodySmall + onClicked: root.toggleHide(rowRoot.itemId) + } + } + } + } + } + + component TrayItem: Item { + id: trayItemRoot + + required property var modelData + + visible: modelData.status !== Status.Passive + implicitWidth: visible ? 16 : 0 + implicitHeight: visible ? 16 : 0 + + IconImage { + anchors.centerIn: parent + implicitSize: 12 + width: 12 + height: 12 + source: root.trayIconSource(trayItemRoot.modelData.icon) + } + + MouseArea { + id: mouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: if (root.bar) root.bar.showTooltip(trayItemRoot, root.trayTooltip(modelData)) + onExited: if (root.bar) root.bar.hideTooltip(trayItemRoot) + onClicked: function(mouse) { + if (mouse.button === Qt.RightButton && trayItemRoot.modelData.hasMenu) { + var point = trayItemRoot.QsWindow.contentItem.mapFromItem(trayItemRoot, mouse.x, mouse.y) + trayItemRoot.modelData.display(trayItemRoot.QsWindow.window, point.x, point.y) + } else if (mouse.button === Qt.MiddleButton) { + trayItemRoot.modelData.secondaryActivate() + } else { + trayItemRoot.modelData.activate() + } + } + onWheel: function(wheel) { + trayItemRoot.modelData.scroll(wheel.angleDelta.y, false) + } + } + + readonly property bool tooltipHovered: visible && opacity > 0 && mouseArea.containsMouse + } +} diff --git a/shell/plugins/settings/SettingsPanel.qml b/shell/plugins/settings/SettingsPanel.qml index 69c32ac3..86db77e2 100644 --- a/shell/plugins/settings/SettingsPanel.qml +++ b/shell/plugins/settings/SettingsPanel.qml @@ -290,11 +290,6 @@ Item { } // ---------------- widget catalog ----------------------------------------- - readonly property var builtinWidgetMeta: ({ - "clock": { name: "Clock", description: "Date / time text", category: "Time" }, - "tray": { name: "System tray", description: "Status notifier items", category: "Status" } - }) - property int catalogRevision: 0 onBarWidgetRegistryChanged: { catalogRevision++ @@ -323,7 +318,6 @@ Item { var canonicalKey = canonicalWidgetId(key) if (root.barWidgetRegistry && root.barWidgetRegistry.has(canonicalKey)) return root.barWidgetRegistry.metadataFor(canonicalKey) || {} - if (builtinWidgetMeta[canonicalKey]) return builtinWidgetMeta[canonicalKey] var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[key] : null if (manifest) { @@ -388,7 +382,6 @@ Item { ids[pid] = true } } - for (var key in builtinWidgetMeta) ids[key] = true return Object.keys(ids) } @@ -411,7 +404,7 @@ Item { var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null var manifestIsBarWidget = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1 var isBarWidget = !!(meta && meta.source !== "plugin") || manifestIsBarWidget - if (!isBarWidget && !builtinWidgetMeta[id]) continue + if (!isBarWidget) continue var inSection = sectionArray(section) var existsHere = false