Extract clock and tray bar widgets

This commit is contained in:
David Heinemeier Hansson
2026-05-20 19:11:00 +02:00
parent d9ec5b82f2
commit bdf7bf76cb
6 changed files with 499 additions and 507 deletions
+2 -495
View File
@@ -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
}
}
+2
View File
@@ -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 },
+2 -4
View File
@@ -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
+44
View File
@@ -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
}
}
}
+448
View File
@@ -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
}
}
+1 -8
View File
@@ -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