added tray menu + fixed antialising
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// One level of a StatusNotifier menu, drawn in the shell's own language.
|
||||
//
|
||||
// Submenus expand inline rather than opening a second window: a tray menu is
|
||||
// two or three items deep at most, and a stack of leaning slabs chasing each
|
||||
// other across the screen would read as clutter. The type nests itself for
|
||||
// child levels, so this file is the whole menu.
|
||||
Column {
|
||||
id: root
|
||||
|
||||
// A QsMenuHandle — either the item's root menu or a submenu entry.
|
||||
required property var menuHandle
|
||||
|
||||
// Raised by any level when an entry is actually invoked, so the window can
|
||||
// close itself.
|
||||
signal activated()
|
||||
|
||||
// What the window should size itself to. Deliberately NOT `implicitWidth`.
|
||||
//
|
||||
// A Column takes its implicit width from how wide its children ended up, and
|
||||
// the rows are stretched to the Column's own width so a hover block can span
|
||||
// the entry. Sizing the window off that closes the loop — window width feeds
|
||||
// row width feeds Column implicit width feeds window width — and it does not
|
||||
// settle: each pass adds a float epsilon, so the shell spins at 100% CPU
|
||||
// forever. Slack's menu was the first to hit it and it took the whole bar
|
||||
// down with it, past the reach of anything but SIGKILL. Measuring the
|
||||
// strings against the font instead keeps sizing entirely out of the layout.
|
||||
readonly property real menuWidth: {
|
||||
let w = 0;
|
||||
for (const e of (opener.children?.values ?? [])) {
|
||||
if (e.isSeparator) continue;
|
||||
let ew = fm.advanceWidth(root.clean(e.text)) + Theme.padM * 2 + Theme.padS * 2;
|
||||
if (e.buttonType !== QsMenuButtonType.None || e.icon !== "")
|
||||
ew += 16 + Theme.padS;
|
||||
if (e.hasChildren) ew += 18;
|
||||
w = Math.max(w, ew);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
// DBus menus carry Qt's mnemonic underscores; nothing here handles
|
||||
// accelerators, so they would just be stray marks.
|
||||
function clean(s: string): string {
|
||||
return String(s ?? "").replace(/_([^_])/g, "$1");
|
||||
}
|
||||
|
||||
spacing: 0
|
||||
|
||||
FontMetrics {
|
||||
id: fm
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Theme.weightBody
|
||||
}
|
||||
|
||||
QsMenuOpener {
|
||||
id: opener
|
||||
menu: root.menuHandle
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: opener.children
|
||||
|
||||
Item {
|
||||
id: row
|
||||
|
||||
required property var modelData
|
||||
readonly property var entry: modelData
|
||||
readonly property bool interactive: entry.enabled && !entry.isSeparator
|
||||
readonly property bool hovered: rowHover.hovered && interactive
|
||||
|
||||
// Submenus stay collapsed until asked for.
|
||||
property bool expanded: false
|
||||
|
||||
// Height only. Width is the Column's business — see `menuWidth`.
|
||||
implicitHeight: entry.isSeparator ? 9 : 26 + sub.height
|
||||
|
||||
width: root.width
|
||||
|
||||
// ------------------------------------------------------ separator
|
||||
//
|
||||
// A horizontal rule slants by a fraction of its own width, so at
|
||||
// menu width the house lean turns a hairline into a 30px diagonal
|
||||
// gash straight across the entries above and below it. The rise is
|
||||
// pinned in pixels instead, matching the rail's dividers.
|
||||
Skew {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: row.entry.isSeparator
|
||||
width: parent.width - Theme.padM * 2
|
||||
x: Theme.padM
|
||||
height: 1
|
||||
vertical: true
|
||||
lean: width > 0 ? 5 / width : 0
|
||||
color: Theme.alpha(Theme.outline, 0.9)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- item
|
||||
Item {
|
||||
id: line
|
||||
width: parent.width
|
||||
height: row.entry.isSeparator ? 0 : 26
|
||||
visible: !row.entry.isSeparator
|
||||
|
||||
// Hover is a solid crimson block, the same statement the
|
||||
// workspace ticks and the rail icons make.
|
||||
Skew {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.padS
|
||||
anchors.rightMargin: Theme.padS
|
||||
color: row.hovered ? Theme.accent : "transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durFast }
|
||||
}
|
||||
}
|
||||
|
||||
// Check mark / radio dot, or the entry's own icon. Both occupy
|
||||
// the same slot so the labels line up either way.
|
||||
Item {
|
||||
id: leading
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.padM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: row.entry.buttonType !== QsMenuButtonType.None
|
||||
|| row.entry.icon !== "" ? 16 : 0
|
||||
height: 12
|
||||
|
||||
Skew {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: row.entry.buttonType !== QsMenuButtonType.None
|
||||
width: 10
|
||||
height: 10
|
||||
borderWidth: 1
|
||||
borderColor: row.hovered ? Theme.ink : Theme.alpha(Theme.text, 0.6)
|
||||
color: row.entry.checkState === Qt.Checked
|
||||
? (row.hovered ? Theme.ink : Theme.accent)
|
||||
: "transparent"
|
||||
}
|
||||
|
||||
IconImage {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: row.entry.buttonType === QsMenuButtonType.None
|
||||
&& row.entry.icon !== ""
|
||||
width: 14
|
||||
height: 14
|
||||
source: row.entry.icon
|
||||
asynchronous: true
|
||||
|
||||
// Same flattening the tray icons get — brand colour in
|
||||
// a two-hue palette is the one thing that breaks it.
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
colorization: 1.0
|
||||
colorizationColor: row.hovered ? Theme.ink : Theme.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.left: leading.right
|
||||
anchors.leftMargin: leading.width > 0 ? Theme.padS : Theme.padM
|
||||
anchors.right: arrow.left
|
||||
anchors.rightMargin: Theme.padS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
text: root.clean(row.entry.text)
|
||||
elide: Text.ElideRight
|
||||
color: !row.entry.enabled
|
||||
? Theme.muted
|
||||
: (row.hovered ? Theme.ink : Theme.text)
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Theme.weightBody
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
id: arrow
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.padM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: row.entry.hasChildren ? 18 : 0
|
||||
visible: row.entry.hasChildren
|
||||
text: row.expanded ? "" : ""
|
||||
color: row.hovered ? Theme.ink : Theme.subtext
|
||||
font.family: Theme.fontIcon
|
||||
font.pixelSize: Theme.fsMicro
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: rowHover
|
||||
enabled: row.interactive
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: row.interactive
|
||||
onTapped: {
|
||||
if (row.entry.hasChildren) {
|
||||
row.expanded = !row.expanded;
|
||||
} else {
|
||||
row.entry.triggered();
|
||||
root.activated();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- submenu
|
||||
Item {
|
||||
id: sub
|
||||
anchors.top: line.bottom
|
||||
width: parent.width
|
||||
height: subLoader.item ? subLoader.item.implicitHeight : 0
|
||||
clip: true
|
||||
visible: height > 0
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutExpo }
|
||||
}
|
||||
|
||||
// Loaded by URL rather than as an inline component: QML rejects
|
||||
// a type that instantiates itself, even lazily, and a menu tree
|
||||
// is the one place recursion is the natural shape.
|
||||
Loader {
|
||||
id: subLoader
|
||||
x: Theme.padM
|
||||
width: parent.width - Theme.padM
|
||||
|
||||
onLoaded: item.activated.connect(root.activated)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: row
|
||||
|
||||
function onExpandedChanged() {
|
||||
if (row.expanded)
|
||||
subLoader.setSource(Qt.resolvedUrl("TrayMenuList.qml"),
|
||||
{ menuHandle: row.entry });
|
||||
else
|
||||
subLoader.source = "";
|
||||
}
|
||||
}
|
||||
|
||||
Skew {
|
||||
x: Theme.padS
|
||||
width: 1
|
||||
height: parent.height
|
||||
visible: row.expanded
|
||||
color: Theme.alpha(Theme.accent, 0.8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user