Files
omarchycn/shell/plugins/bar/widgets/Tray.qml
T
dc698e5df0 Suppress LocalSend's redundant tray item (#6852)
LocalSend registers an Ayatana item with no ItemIsMenu and no Activate
handler, so its primary click is a silent no-op and the menu offers only
Open and Quit. Share > Receive already opens it, so drop the item the way
Dropbox's is dropped when its dedicated widget owns the surface.

Closes #6838

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 08:23:45 +02:00

847 lines
29 KiB
QML

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: "omarchy.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` - omarchy'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 ownedByOmarchy(item) {
var layout = root.bar && root.bar.layoutConfig ? root.bar.layoutConfig : null
return TrayModel.ownedByOmarchy(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 (ownedByOmarchy(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 || "omarchy.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 {
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 {
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 {
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 {
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
}
}