Merge pull request #6348 from InfeCtlll3/agent/scroll-long-tray-menus

Make long tray menus scrollable
This commit is contained in:
David Heinemeier Hansson
2026-07-24 09:09:17 -07:00
committed by GitHub
+112 -98
View File
@@ -1,5 +1,6 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import qs.Commons import qs.Commons
@@ -446,116 +447,129 @@ BarWidget {
contentWidth: trayMenuPopup.fittedContentWidth(Style.space(232)) contentWidth: trayMenuPopup.fittedContentWidth(Style.space(232))
contentHeight: trayMenuPopup.fittedContentHeight(trayMenuColumn.implicitHeight, Style.space(420)) contentHeight: trayMenuPopup.fittedContentHeight(trayMenuColumn.implicitHeight, Style.space(420))
Column { Flickable {
id: trayMenuColumn id: trayMenuFlick
anchors.fill: parent anchors.fill: parent
spacing: 0 contentWidth: width
contentHeight: trayMenuColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
flickableDirection: Flickable.VerticalFlick
interactive: contentHeight > height
Repeater { ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
model: trayMenuOpener.children
delegate: Item { Column {
id: menuRow id: trayMenuColumn
required property var modelData width: trayMenuFlick.width
required property int index spacing: 0
readonly property string rowText: String(modelData.text || "") Repeater {
readonly property string activeTitle: root.activeTrayItem ? String(root.activeTrayItem.title || root.activeTrayItem.id || "") : "" model: trayMenuOpener.children
readonly property bool rootTitleEntry: index === 0 && modelData.hasChildren && rowText.toLowerCase() === activeTitle.toLowerCase()
readonly property bool leadingSeparator: modelData.isSeparator && index <= 1
readonly property bool hiddenRow: rootTitleEntry || leadingSeparator
visible: !hiddenRow delegate: Item {
width: trayMenuColumn.width id: menuRow
implicitHeight: hiddenRow ? 0 : (modelData.isSeparator ? Style.space(11) : Style.space(30)) required property var modelData
opacity: modelData.enabled ? 1.0 : 0.45 required property int index
Rectangle { readonly property string rowText: String(modelData.text || "")
visible: menuRow.modelData.isSeparator readonly property string activeTitle: root.activeTrayItem ? String(root.activeTrayItem.title || root.activeTrayItem.id || "") : ""
anchors.left: parent.left readonly property bool rootTitleEntry: index === 0 && modelData.hasChildren && rowText.toLowerCase() === activeTitle.toLowerCase()
anchors.leftMargin: Style.space(10) readonly property bool leadingSeparator: modelData.isSeparator && index <= 1
anchors.right: parent.right readonly property bool hiddenRow: rootTitleEntry || leadingSeparator
anchors.rightMargin: Style.space(10)
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Color.popups.border
opacity: 0.45
}
Rectangle { visible: !hiddenRow
visible: !menuRow.modelData.isSeparator width: trayMenuColumn.width
anchors.fill: parent implicitHeight: hiddenRow ? 0 : (modelData.isSeparator ? Style.space(11) : Style.space(30))
radius: Math.max(2, Style.cornerRadius) opacity: modelData.enabled ? 1.0 : 0.45
color: rowMouse.containsMouse && menuRow.modelData.enabled ? Style.hoverFillFor(root.foreground, root.foreground) : "transparent"
}
Text { Rectangle {
visible: !menuRow.modelData.isSeparator && menuRow.modelData.buttonType !== QsMenuButtonType.None visible: menuRow.modelData.isSeparator
anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left
anchors.left: parent.left anchors.leftMargin: Style.space(10)
width: Style.space(22) anchors.right: parent.right
horizontalAlignment: Text.AlignHCenter anchors.rightMargin: Style.space(10)
text: menuRow.modelData.checkState === Qt.Checked ? "\uf00c" : "" anchors.verticalCenter: parent.verticalCenter
color: root.foreground height: 1
font.family: root.fontFamily color: Color.popups.border
font.pixelSize: Style.font.bodySmall opacity: 0.45
} }
Image { Rectangle {
id: menuIcon visible: !menuRow.modelData.isSeparator
visible: !menuRow.modelData.isSeparator && String(menuRow.modelData.icon || "") !== "" anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter radius: Math.max(2, Style.cornerRadius)
anchors.left: parent.left color: rowMouse.containsMouse && menuRow.modelData.enabled ? Style.hoverFillFor(root.foreground, root.foreground) : "transparent"
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 { Text {
visible: !menuRow.modelData.isSeparator visible: !menuRow.modelData.isSeparator && menuRow.modelData.buttonType !== QsMenuButtonType.None
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: menuIcon.visible ? Style.space(46) : Style.space(28) width: Style.space(22)
anchors.right: submenuGlyph.left horizontalAlignment: Text.AlignHCenter
anchors.rightMargin: Style.space(8) text: menuRow.modelData.checkState === Qt.Checked ? "\uf00c" : ""
text: menuRow.rowText color: root.foreground
color: root.foreground font.family: root.fontFamily
font.family: root.fontFamily font.pixelSize: Style.font.bodySmall
font.pixelSize: Style.font.bodySmall }
elide: Text.ElideRight
}
Text { Image {
id: submenuGlyph id: menuIcon
visible: !menuRow.modelData.isSeparator && menuRow.modelData.hasChildren visible: !menuRow.modelData.isSeparator && String(menuRow.modelData.icon || "") !== ""
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right anchors.left: parent.left
anchors.rightMargin: Style.space(10) anchors.leftMargin: Style.space(24)
text: "\u203a" width: Style.space(16)
color: root.foreground height: Style.space(16)
font.family: root.fontFamily fillMode: Image.PreserveAspectFit
font.pixelSize: Style.font.bodySmall // 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
}
MouseArea { Text {
id: rowMouse visible: !menuRow.modelData.isSeparator
anchors.fill: parent anchors.verticalCenter: parent.verticalCenter
hoverEnabled: true anchors.left: parent.left
enabled: !menuRow.modelData.isSeparator && menuRow.modelData.enabled anchors.leftMargin: menuIcon.visible ? Style.space(46) : Style.space(28)
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor anchors.right: submenuGlyph.left
onClicked: { anchors.rightMargin: Style.space(8)
if (menuRow.modelData.hasChildren) { text: menuRow.rowText
var point = menuRow.QsWindow.contentItem.mapFromItem(menuRow, menuRow.width, menuRow.height / 2) color: root.foreground
menuRow.modelData.display(menuRow.QsWindow.window, point.x, point.y) font.family: root.fontFamily
} else { font.pixelSize: Style.font.bodySmall
menuRow.modelData.triggered() elide: Text.ElideRight
root.close() }
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 (menuRow.modelData.hasChildren) {
var point = menuRow.QsWindow.contentItem.mapFromItem(menuRow, menuRow.width, menuRow.height / 2)
menuRow.modelData.display(menuRow.QsWindow.window, point.x, point.y)
} else {
menuRow.modelData.triggered()
root.close()
}
} }
} }
} }