Fork the desktop off Omarchy as a self-contained system
This commit is contained in:
@@ -0,0 +1,850 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell.Services.SystemTray
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
import "TrayModel.js" as TrayModel
|
||||
|
||||
BarWidget {
|
||||
id: root
|
||||
moduleName: "blob.tray"
|
||||
|
||||
property bool expanded: false
|
||||
property bool managePopupOpen: false
|
||||
property bool trayMenuOpen: false
|
||||
property var activeTrayItem: null
|
||||
property var activeTrayAnchor: null
|
||||
readonly property color foreground: bar ? bar.foreground : Color.foreground
|
||||
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
|
||||
readonly property var pinnedIds: settings.pinned instanceof Array ? settings.pinned : []
|
||||
readonly property var hiddenIds: settings.hidden instanceof Array ? 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 trayItemExtent: Style.bar.iconSlot
|
||||
readonly property int trayItemGap: 0
|
||||
readonly property int trayJoinGap: 0
|
||||
readonly property int drawerExtent: drawerCount > 0 ? drawerCount * trayItemExtent + (drawerCount - 1) * trayItemGap : 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
|
||||
|
||||
// Submenu drill-down state. QsMenuEntry.display() renders a *platform* menu,
|
||||
// which Quickshell refuses unless the shell root sets `//@ pragma
|
||||
// UseQApplication` - blob's shell.qml does not, so every submenu click was
|
||||
// a silent no-op ("Cannot display PlatformMenuEntry as quickshell was not
|
||||
// started in QApplication mode" in the shell log) and apps whose whole UI is
|
||||
// submenus, e.g. radiotray-ng's station list, were unusable. QsMenuEntry
|
||||
// inherits QsMenuHandle, so a child entry can feed a nested QsMenuOpener and
|
||||
// render inside this popup instead of going through the platform. Each level
|
||||
// keeps its own live opener: a child entry is owned by its parent opener's
|
||||
// model, so collapsing the stack to a single opener would destroy the very
|
||||
// entry being displayed (submenu turns up empty).
|
||||
property var submenuStack: []
|
||||
readonly property int submenuDepth: submenuStack.length
|
||||
readonly property string currentTitle: submenuDepth > 0 ? submenuStack[submenuDepth - 1].title : ""
|
||||
readonly property var currentChildren: submenuDepth > 0
|
||||
? submenuStack[submenuDepth - 1].opener.children
|
||||
: trayMenuOpener.children
|
||||
|
||||
// Changing level rebuilds the row delegates synchronously, so the next
|
||||
// row lands under a cursor that hasn't moved. Submenu clicks used to be
|
||||
// silent no-ops, which trained users to click them twice, and that second
|
||||
// click would now fire whatever entry took the spot. Ignore row clicks for
|
||||
// a beat after each level change; a deliberate follow-up click is slower.
|
||||
property bool menuLevelSettling: false
|
||||
|
||||
Component {
|
||||
id: submenuOpenerComponent
|
||||
QsMenuOpener {}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: menuLevelSettleTimer
|
||||
interval: 250
|
||||
onTriggered: root.menuLevelSettling = false
|
||||
}
|
||||
|
||||
function settleMenuLevel() {
|
||||
menuLevelSettling = true
|
||||
menuLevelSettleTimer.restart()
|
||||
}
|
||||
|
||||
function resetTrayMenu() {
|
||||
menuLevelSettling = false
|
||||
menuLevelSettleTimer.stop()
|
||||
// Flickable keeps its offset across a model swap whenever the new content
|
||||
// is still tall enough to hold it, so a menu dismissed while scrolled
|
||||
// would otherwise reopen part-way down with its first entries off screen.
|
||||
trayMenuFlick.contentY = 0
|
||||
// Clear the reactive stack before tearing anything down, so no binding can
|
||||
// read a partially-destroyed opener while this runs. Then destroy deepest
|
||||
// first: an inner opener's menu entry is owned by its parent's children
|
||||
// model, so destroying a parent first would invalidate an entry a still-
|
||||
// live child opener references.
|
||||
var openers = submenuStack
|
||||
submenuStack = []
|
||||
for (var i = openers.length - 1; i >= 0; i--) openers[i].opener.destroy()
|
||||
}
|
||||
|
||||
function enterSubmenu(entry, title) {
|
||||
var opener = submenuOpenerComponent.createObject(root, { menu: entry })
|
||||
if (!opener) return
|
||||
var stack = submenuStack.slice()
|
||||
stack.push({ opener: opener, title: title })
|
||||
submenuStack = stack
|
||||
settleMenuLevel()
|
||||
}
|
||||
|
||||
function leaveSubmenu() {
|
||||
if (submenuStack.length === 0) return
|
||||
var stack = submenuStack.slice()
|
||||
var top = stack.pop()
|
||||
submenuStack = stack
|
||||
top.opener.destroy()
|
||||
settleMenuLevel()
|
||||
}
|
||||
|
||||
function close() {
|
||||
managePopupOpen = false
|
||||
trayMenuOpen = false
|
||||
}
|
||||
|
||||
function openTrayMenu(item, anchorItem, mouse) {
|
||||
if (!item || !item.menu) {
|
||||
var point = anchorItem.QsWindow.contentItem.mapFromItem(anchorItem, mouse.x, mouse.y)
|
||||
item.display(anchorItem.QsWindow.window, point.x, point.y)
|
||||
return
|
||||
}
|
||||
|
||||
// Reset before switching items: trayMenuOpener.menu binds to
|
||||
// activeTrayItem.menu, so assigning a new item invalidates the old root's
|
||||
// children immediately, before any nested opener referencing them would
|
||||
// otherwise get torn down.
|
||||
resetTrayMenu()
|
||||
activeTrayItem = item
|
||||
activeTrayAnchor = anchorItem
|
||||
trayMenuOpen = true
|
||||
}
|
||||
|
||||
function trayIconSource(icon) {
|
||||
// Quickshell already resolves the tray icon into a ready-to-use image://
|
||||
// URL, including a "?path=" fallback search dir for apps that ship their
|
||||
// tray icon outside a standard theme (e.g. Steam's flat public/ dir). Hand
|
||||
// it straight to IconImage; guessing a theme sub-directory here only broke
|
||||
// apps whose layout didn't match the guess.
|
||||
return String(icon || "")
|
||||
}
|
||||
|
||||
// Symbolic icons ship a fixed fill (often near-white) that the host is meant
|
||||
// to recolor to its foreground; detect them by the freedesktop "-symbolic"
|
||||
// name suffix so they can be tinted instead of rendered as-is.
|
||||
function iconIsSymbolic(icon) {
|
||||
var name = String(icon || "").split("?")[0]
|
||||
return name.slice(-9) === "-symbolic"
|
||||
}
|
||||
|
||||
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 ownedByBlob(item) {
|
||||
var layout = root.bar && root.bar.layoutConfig ? root.bar.layoutConfig : null
|
||||
return TrayModel.ownedByBlob(item, layout)
|
||||
}
|
||||
|
||||
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 (ownedByBlob(item)) 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
|
||||
var id = root.moduleName || "blob.tray"
|
||||
root.bar.shell.updateEntryInline(id, { id: id, 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
|
||||
}
|
||||
|
||||
BarIconButton {
|
||||
id: expandIcon
|
||||
bar: root.bar
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
x: root.drawerExtent - root.revealExtent
|
||||
text: "\uf053"
|
||||
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: root.trayItemGap
|
||||
layer.enabled: true
|
||||
|
||||
Repeater {
|
||||
model: root.drawerItems
|
||||
TrayItem {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: pinnedRow
|
||||
x: drawerArea.x + horizontalTrayRoot.drawerBlockWidth
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: root.trayItemGap
|
||||
leftPadding: root.pinnedItems.length > 0 && root.allItems.length > 0 ? root.trayJoinGap : 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
|
||||
}
|
||||
|
||||
BarIconButton {
|
||||
id: expandIcon
|
||||
bar: root.bar
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
y: root.drawerExtent - root.revealExtent
|
||||
text: "\uf053"
|
||||
textRotation: 90
|
||||
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: root.trayItemGap
|
||||
layer.enabled: true
|
||||
|
||||
Repeater {
|
||||
model: root.drawerItems
|
||||
TrayItem {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: pinnedCol
|
||||
y: drawerArea.y + verticalTrayRoot.drawerBlockHeight
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: root.trayItemGap
|
||||
topPadding: root.pinnedItems.length > 0 && root.allItems.length > 0 ? root.trayJoinGap : 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
|
||||
|
||||
TrayIcon {
|
||||
id: rowIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
width: 16
|
||||
height: 16
|
||||
icon: rowRoot.modelData.icon
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QsMenuOpener {
|
||||
id: trayMenuOpener
|
||||
menu: root.activeTrayItem ? root.activeTrayItem.menu : null
|
||||
}
|
||||
|
||||
PopupCard {
|
||||
id: trayMenuPopup
|
||||
anchorItem: root.activeTrayAnchor || root
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.trayMenuOpen
|
||||
// The card fades out over 140ms (visible stays true for that whole time --
|
||||
// see PopupCard's own visible: open || card.opacity > 0), so resetting on
|
||||
// "open" would swap a live submenu for the root menu mid-fade: a visible
|
||||
// flash, and a resize/reposition if the two have different geometry. Wait
|
||||
// for the fade to actually finish. Switching to a different tray item
|
||||
// still resets immediately, from openTrayMenu() itself.
|
||||
onVisibleChanged: if (!visible) root.resetTrayMenu()
|
||||
padding: Style.space(8)
|
||||
borderColor: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.45)
|
||||
contentWidth: trayMenuPopup.fittedContentWidth(Style.space(232))
|
||||
contentHeight: trayMenuPopup.fittedContentHeight(menuHeaderHeight + trayMenuColumn.implicitHeight, Style.space(420))
|
||||
|
||||
// Column skips invisible children but keeps reporting their height, so
|
||||
// read the header's extent through its own visibility.
|
||||
readonly property int menuHeaderHeight: menuHeader.visible ? menuHeader.implicitHeight : 0
|
||||
|
||||
Column {
|
||||
id: trayMenuLayout
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// Header for a drilled-into submenu: names where we are and walks back
|
||||
// out. Pinned above the Flickable rather than scrolling with the rows,
|
||||
// so the way back stays reachable in a submenu taller than the card.
|
||||
// Only present below the root level, so the root menu is unchanged.
|
||||
Column {
|
||||
id: menuHeader
|
||||
visible: root.submenuDepth > 0
|
||||
width: trayMenuLayout.width
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
id: menuBackRow
|
||||
width: menuHeader.width
|
||||
implicitHeight: Style.space(30)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Math.max(2, Style.cornerRadius)
|
||||
color: backMouse.containsMouse ? Style.hoverFillFor(root.foreground, root.foreground) : "transparent"
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
width: Style.space(22)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "\u2039"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.space(28)
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.space(10)
|
||||
text: root.currentTitle
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.menuLevelSettling) return
|
||||
// Reset before the model swap so the parent level shows from
|
||||
// the top (same ordering as the row delegate below).
|
||||
trayMenuFlick.contentY = 0
|
||||
root.leaveSubmenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: menuHeader.width
|
||||
implicitHeight: Style.space(11)
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.space(10)
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.space(10)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 1
|
||||
color: Color.popups.border
|
||||
opacity: 0.45
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: trayMenuFlick
|
||||
width: trayMenuLayout.width
|
||||
height: trayMenuLayout.height - trayMenuPopup.menuHeaderHeight
|
||||
contentWidth: width
|
||||
contentHeight: trayMenuColumn.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
interactive: contentHeight > height
|
||||
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
|
||||
Column {
|
||||
id: trayMenuColumn
|
||||
width: trayMenuFlick.width
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: root.currentChildren
|
||||
|
||||
delegate: Item {
|
||||
id: menuRow
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property string rowText: String(modelData.text || "")
|
||||
readonly property string activeTitle: root.activeTrayItem ? String(root.activeTrayItem.title || root.activeTrayItem.id || "") : ""
|
||||
// Both only ever describe the root menu; inside a submenu the first
|
||||
// rows are real entries and must not be swallowed.
|
||||
readonly property bool atRoot: root.submenuDepth === 0
|
||||
readonly property bool rootTitleEntry: atRoot && index === 0 && modelData.hasChildren && rowText.toLowerCase() === activeTitle.toLowerCase()
|
||||
readonly property bool leadingSeparator: atRoot && modelData.isSeparator && index <= 1
|
||||
readonly property bool hiddenRow: rootTitleEntry || leadingSeparator
|
||||
|
||||
visible: !hiddenRow
|
||||
width: trayMenuColumn.width
|
||||
implicitHeight: hiddenRow ? 0 : (modelData.isSeparator ? Style.space(11) : Style.space(30))
|
||||
opacity: modelData.enabled ? 1.0 : 0.45
|
||||
|
||||
Rectangle {
|
||||
visible: menuRow.modelData.isSeparator
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.space(10)
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.space(10)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 1
|
||||
color: Color.popups.border
|
||||
opacity: 0.45
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: !menuRow.modelData.isSeparator
|
||||
anchors.fill: parent
|
||||
radius: Math.max(2, Style.cornerRadius)
|
||||
color: rowMouse.containsMouse && menuRow.modelData.enabled ? Style.hoverFillFor(root.foreground, root.foreground) : "transparent"
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: !menuRow.modelData.isSeparator && menuRow.modelData.buttonType !== QsMenuButtonType.None
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
width: Style.space(22)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: menuRow.modelData.checkState === Qt.Checked ? "\uf00c" : ""
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
|
||||
Image {
|
||||
id: menuIcon
|
||||
visible: !menuRow.modelData.isSeparator && String(menuRow.modelData.icon || "") !== ""
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.space(24)
|
||||
width: Style.space(16)
|
||||
height: Style.space(16)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
// Decode at physical pixels: IconImage uses the logical size,
|
||||
// which leaves PNG icons upscaled and blurry on HiDPI displays.
|
||||
sourceSize.width: width * Screen.devicePixelRatio
|
||||
sourceSize.height: height * Screen.devicePixelRatio
|
||||
source: menuRow.modelData.icon
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: !menuRow.modelData.isSeparator
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: menuIcon.visible ? Style.space(46) : Style.space(28)
|
||||
anchors.right: submenuGlyph.left
|
||||
anchors.rightMargin: Style.space(8)
|
||||
text: menuRow.rowText
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
id: submenuGlyph
|
||||
visible: !menuRow.modelData.isSeparator && menuRow.modelData.hasChildren
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.space(10)
|
||||
text: "\u203a"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rowMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: !menuRow.modelData.isSeparator && menuRow.modelData.enabled
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
if (root.menuLevelSettling) return
|
||||
if (menuRow.modelData.hasChildren) {
|
||||
// Reset scroll BEFORE swapping the model: the swap destroys
|
||||
// this delegate synchronously and ids stop resolving after.
|
||||
trayMenuFlick.contentY = 0
|
||||
root.enterSubmenu(menuRow.modelData, menuRow.rowText)
|
||||
} else {
|
||||
menuRow.modelData.triggered()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Renders a tray icon, recoloring symbolic icons to the bar foreground so
|
||||
// they stay visible on any theme (a raw symbolic icon keeps its baked-in
|
||||
// fill and disappears against a matching background).
|
||||
component TrayIcon: Item {
|
||||
id: trayIconRoot
|
||||
required property var icon
|
||||
readonly property bool symbolic: root.iconIsSymbolic(icon)
|
||||
|
||||
Image {
|
||||
id: trayIconImage
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectFit
|
||||
// Decode at physical pixels: IconImage uses the logical size,
|
||||
// which leaves PNG icons upscaled and blurry on HiDPI displays.
|
||||
sourceSize.width: Math.round(Math.min(width, height) * Screen.devicePixelRatio)
|
||||
sourceSize.height: Math.round(Math.min(width, height) * Screen.devicePixelRatio)
|
||||
source: root.trayIconSource(trayIconRoot.icon)
|
||||
// Kept as a hidden layer so the effect can sample it as a texture.
|
||||
visible: !trayIconRoot.symbolic
|
||||
layer.enabled: trayIconRoot.symbolic
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: trayIconImage
|
||||
source: trayIconImage
|
||||
visible: trayIconRoot.symbolic
|
||||
colorization: 1.0
|
||||
colorizationColor: root.foreground
|
||||
}
|
||||
}
|
||||
|
||||
component TrayItem: Item {
|
||||
id: trayItemRoot
|
||||
|
||||
required property var modelData
|
||||
|
||||
visible: modelData.status !== Status.Passive
|
||||
implicitWidth: visible ? root.trayItemExtent : 0
|
||||
implicitHeight: visible ? root.trayItemExtent : 0
|
||||
|
||||
function displayMenu(mouse) {
|
||||
root.openTrayMenu(trayItemRoot.modelData, trayItemRoot, mouse)
|
||||
}
|
||||
|
||||
TrayIcon {
|
||||
anchors.centerIn: parent
|
||||
width: Style.space(12)
|
||||
height: Style.space(12)
|
||||
icon: 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)
|
||||
onPressed: function(mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
trayItemRoot.displayMenu(mouse)
|
||||
mouse.accepted = true
|
||||
}
|
||||
}
|
||||
onClicked: function(mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
mouse.accepted = true
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
trayItemRoot.modelData.secondaryActivate()
|
||||
} else if (trayItemRoot.modelData.onlyMenu) {
|
||||
trayItemRoot.displayMenu(mouse)
|
||||
} else {
|
||||
trayItemRoot.modelData.activate()
|
||||
}
|
||||
}
|
||||
onWheel: function(wheel) {
|
||||
trayItemRoot.modelData.scroll(wheel.angleDelta.y, false)
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool tooltipHovered: visible && opacity > 0 && mouseArea.containsMouse
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user