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
}
}