added tray menu + fixed antialising
This commit is contained in:
@@ -128,16 +128,11 @@ PanelWindow {
|
|||||||
implicitWidth: Theme.padM * 2
|
implicitWidth: Theme.padM * 2
|
||||||
implicitHeight: Theme.barHeight
|
implicitHeight: Theme.barHeight
|
||||||
|
|
||||||
Rectangle {
|
Skew {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: 1
|
width: 1
|
||||||
height: 20
|
height: 20
|
||||||
color: Theme.alpha(Theme.outline, 0.9)
|
color: Theme.alpha(Theme.outline, 0.9)
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 20 * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ Item {
|
|||||||
readonly property real ruleInset: 5
|
readonly property real ruleInset: 5
|
||||||
readonly property real ruleBand: root.standalone ? 0 : root.ruleHeight + root.ruleInset
|
readonly property real ruleBand: root.standalone ? 0 : root.ruleHeight + root.ruleInset
|
||||||
|
|
||||||
|
// Modules with a notion of progress (the player's track position) draw it in
|
||||||
|
// the same band, so it can never be struck through the content. A module
|
||||||
|
// cannot do this for itself: anything it declares becomes a child of the
|
||||||
|
// content holder, whose bottom edge is above the band, and the rule lands
|
||||||
|
// across the text instead of under it. Negative means "no progress".
|
||||||
|
property real progress: -1
|
||||||
|
|
||||||
// Total horizontal inset a module must add on top of its content width.
|
// Total horizontal inset a module must add on top of its content width.
|
||||||
readonly property real contentPad: standalone
|
readonly property real contentPad: standalone
|
||||||
? (panelLoader.item?.contentPad ?? root.padding)
|
? (panelLoader.item?.contentPad ?? root.padding)
|
||||||
@@ -100,16 +107,24 @@ Item {
|
|||||||
height: root.ruleHeight
|
height: root.ruleHeight
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
Rectangle {
|
// Track progress, shown until the pointer arrives and the hover rule
|
||||||
|
// takes the band over.
|
||||||
|
Skew {
|
||||||
|
height: parent.height
|
||||||
|
width: parent.width * Math.max(0, Math.min(1, root.progress))
|
||||||
|
color: Theme.accent
|
||||||
|
visible: root.progress >= 0 && !root.hovered && !root.active
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation { duration: 480; easing.type: Easing.OutQuad }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Skew {
|
||||||
height: parent.height
|
height: parent.height
|
||||||
width: root.hovered || root.active ? parent.width : 0
|
width: root.hovered || root.active ? parent.width : 0
|
||||||
color: root.accent
|
color: root.accent
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 2 * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on width {
|
Behavior on width {
|
||||||
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
|
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,15 +50,11 @@ BarPill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Skew {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: 2
|
width: 2
|
||||||
height: 22
|
height: 22
|
||||||
color: Theme.alpha(Theme.accent, 0.6)
|
color: Theme.alpha(Theme.accent, 0.6)
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SplitText {
|
SplitText {
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ BarPill {
|
|||||||
padding: Theme.padS
|
padding: Theme.padS
|
||||||
visible: Media.hasPlayer || width > 1
|
visible: Media.hasPlayer || width > 1
|
||||||
|
|
||||||
|
// Track position, drawn by the pill in its own rule band.
|
||||||
|
progress: Media.canSeek ? Media.progress : -1
|
||||||
|
|
||||||
implicitWidth: Media.hasPlayer
|
implicitWidth: Media.hasPlayer
|
||||||
? layout.implicitWidth + contentPad * 2
|
? layout.implicitWidth + contentPad * 2
|
||||||
: 0
|
: 0
|
||||||
@@ -83,12 +86,21 @@ BarPill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Title and artist size to the text, up to a cap.
|
||||||
|
//
|
||||||
|
// These used to be a flat 130px wide each, which meant a short title
|
||||||
|
// still reserved 130px and left a hand-sized hole between the artist and
|
||||||
|
// the equaliser — the pill looked broken rather than compact. They take
|
||||||
|
// what they need now and only start scrolling past the cap.
|
||||||
Column {
|
Column {
|
||||||
|
id: meta
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
|
readonly property real cap: 150
|
||||||
|
|
||||||
Marquee {
|
Marquee {
|
||||||
width: 130
|
width: Math.min(meta.cap, implicitWidth)
|
||||||
height: 14
|
height: 14
|
||||||
text: Media.title || Media.identity
|
text: Media.title || Media.identity
|
||||||
color: Theme.text
|
color: Theme.text
|
||||||
@@ -99,7 +111,7 @@ BarPill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Marquee {
|
Marquee {
|
||||||
width: 130
|
width: Math.min(meta.cap, implicitWidth)
|
||||||
height: 12
|
height: 12
|
||||||
text: Media.artist || Media.album || Media.identity
|
text: Media.artist || Media.album || Media.identity
|
||||||
color: Theme.muted
|
color: Theme.muted
|
||||||
@@ -146,22 +158,6 @@ BarPill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track progress, hairline along the bottom of the pill.
|
|
||||||
Rectangle {
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.bottomMargin: 3
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: root.leanInset
|
|
||||||
width: Math.max(0, (root.width - root.leanInset * 2) * Media.progress)
|
|
||||||
height: 2
|
|
||||||
color: Theme.accent
|
|
||||||
visible: Media.canSeek
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation { duration: 480; easing.type: Easing.OutQuad }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: if (visible) titleGlitch.restart()
|
onVisibleChanged: if (visible) titleGlitch.restart()
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ BarPill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unread badge, chopped like everything else.
|
// Unread badge, chopped like everything else.
|
||||||
Rectangle {
|
Skew {
|
||||||
visible: Notifs.unread > 0 && !Notifs.dnd
|
visible: Notifs.unread > 0 && !Notifs.dnd
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: 2
|
anchors.rightMargin: 2
|
||||||
@@ -45,6 +45,8 @@ BarPill {
|
|||||||
height: 12
|
height: 12
|
||||||
color: Theme.accent
|
color: Theme.accent
|
||||||
|
|
||||||
|
// The badge count stays upright. It used to inherit the shear from the
|
||||||
|
// transform on the block below, which at 8px left the digits smeared.
|
||||||
Text {
|
Text {
|
||||||
id: badge
|
id: badge
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -55,11 +57,6 @@ BarPill {
|
|||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
}
|
}
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 12 * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|||||||
@@ -177,17 +177,12 @@ PanelWindow {
|
|||||||
implicitWidth: Theme.railWidth
|
implicitWidth: Theme.railWidth
|
||||||
implicitHeight: Theme.padM + 4
|
implicitHeight: Theme.padM + 4
|
||||||
|
|
||||||
Rectangle {
|
Skew {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: 22
|
width: 22
|
||||||
height: 1
|
height: 1
|
||||||
|
vertical: true
|
||||||
color: Theme.alpha(Theme.outline, 0.9)
|
color: Theme.alpha(Theme.outline, 0.9)
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, 0, 0, 0,
|
|
||||||
-Theme.skew, 1, 0, 22 * Theme.skew / 2,
|
|
||||||
0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,15 +49,11 @@ BarPill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
component Divider: Rectangle {
|
component Divider: Skew {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: 1
|
width: 1
|
||||||
height: 18
|
height: 18
|
||||||
color: Theme.alpha(Theme.outline, 0.8)
|
color: Theme.alpha(Theme.outline, 0.8)
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 18 * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The CPU/MEM/GPU labels are gone: three characters of chrome per metric,
|
// The CPU/MEM/GPU labels are gone: three characters of chrome per metric,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Quickshell.Services.SystemTray
|
|||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import "root:/config"
|
import "root:/config"
|
||||||
import "root:/components"
|
import "root:/components"
|
||||||
|
import "root:/popouts"
|
||||||
|
|
||||||
// StatusNotifier tray, as a vertical column for the right-hand rail. Left click
|
// StatusNotifier tray, as a vertical column for the right-hand rail. Left click
|
||||||
// activates, right click opens the app's own menu, middle click is the
|
// activates, right click opens the app's own menu, middle click is the
|
||||||
@@ -72,7 +73,7 @@ Column {
|
|||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
onTapped: {
|
onTapped: {
|
||||||
if (entry.modelData.onlyMenu) {
|
if (entry.modelData.onlyMenu) {
|
||||||
menuAnchor.open();
|
menu.toggle();
|
||||||
} else {
|
} else {
|
||||||
entry.modelData.activate();
|
entry.modelData.activate();
|
||||||
}
|
}
|
||||||
@@ -81,7 +82,9 @@ Column {
|
|||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onTapped: menuAnchor.open()
|
onTapped: {
|
||||||
|
if (entry.modelData.hasMenu) menu.toggle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
@@ -90,13 +93,10 @@ Column {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Menus open to the left, into the screen rather than off its edge.
|
// Menus open to the left, into the screen rather than off its edge.
|
||||||
QsMenuAnchor {
|
TrayMenu {
|
||||||
id: menuAnchor
|
id: menu
|
||||||
menu: entry.modelData.menu
|
anchorItem: entry
|
||||||
anchor.item: entry
|
menuHandle: entry.modelData.menu
|
||||||
anchor.edges: Edges.Left
|
|
||||||
anchor.gravity: Edges.Left
|
|
||||||
anchor.margins.right: Theme.popoutGap
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Skew {
|
||||||
id: tick
|
id: tick
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
@@ -89,18 +89,10 @@ Item {
|
|||||||
if (pip.occupied) return Theme.alpha(Theme.text, 0.5);
|
if (pip.occupied) return Theme.alpha(Theme.text, 0.5);
|
||||||
return "transparent";
|
return "transparent";
|
||||||
}
|
}
|
||||||
border.color: pip.occupied || pip.focused
|
borderColor: pip.occupied || pip.focused
|
||||||
? "transparent"
|
? "transparent"
|
||||||
: Theme.alpha(Theme.text, 0.35)
|
: Theme.alpha(Theme.text, 0.35)
|
||||||
border.width: 1
|
borderWidth: 1
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(
|
|
||||||
1, -Theme.skew, 0, pip.height * Theme.skew,
|
|
||||||
0, 1, 0, 0,
|
|
||||||
0, 0, 1, 0,
|
|
||||||
0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
ColorAnimation { duration: Theme.durBase }
|
ColorAnimation { duration: Theme.durBase }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import "root:/config"
|
import "root:/config"
|
||||||
|
import "root:/components"
|
||||||
|
|
||||||
// A segmented load meter — slanted ticks that light up left to right. Reads as
|
// A segmented load meter — slanted ticks that light up left to right. Reads as
|
||||||
// a gauge at a glance and keeps the panel language consistent.
|
// a gauge at a glance and keeps the panel language consistent.
|
||||||
@@ -31,9 +32,10 @@ Item {
|
|||||||
width: root.segmentWidth
|
width: root.segmentWidth
|
||||||
height: root.height
|
height: root.height
|
||||||
|
|
||||||
Rectangle {
|
Skew {
|
||||||
width: root.segmentWidth
|
width: root.segmentWidth
|
||||||
height: root.height
|
height: root.height
|
||||||
|
lean: root.lean
|
||||||
color: parent.index < root.litCount ? root.activeColor : root.idleColor
|
color: parent.index < root.litCount ? root.activeColor : root.idleColor
|
||||||
|
|
||||||
// Peak segments glow, so a pegged CPU is visible peripherally.
|
// Peak segments glow, so a pegged CPU is visible peripherally.
|
||||||
@@ -41,14 +43,6 @@ Item {
|
|||||||
? (parent.index >= root.segments - 2 ? 1.0 : 0.9)
|
? (parent.index >= root.segments - 2 ? 1.0 : 0.9)
|
||||||
: 1.0
|
: 1.0
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(
|
|
||||||
1, -root.lean, 0, root.height * root.lean,
|
|
||||||
0, 1, 0, 0,
|
|
||||||
0, 0, 1, 0,
|
|
||||||
0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
ColorAnimation { duration: Theme.durBase }
|
ColorAnimation { duration: Theme.durBase }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Services.Notifications
|
import Quickshell.Services.Notifications
|
||||||
import "root:/config"
|
import "root:/config"
|
||||||
|
import "root:/components"
|
||||||
import "root:/services"
|
import "root:/services"
|
||||||
|
|
||||||
// One notification, used both in the history list and as a floating toast.
|
// One notification, used both in the history list and as a floating toast.
|
||||||
@@ -17,14 +18,20 @@ Item {
|
|||||||
|
|
||||||
signal dismissed()
|
signal dismissed()
|
||||||
|
|
||||||
|
// The card's own geometry, published so anything drawn along its edges —
|
||||||
|
// the toast countdown rule — can stay inside the chopped corners instead of
|
||||||
|
// running out past them.
|
||||||
|
readonly property real lean: 0.05
|
||||||
|
readonly property real chop: 12
|
||||||
|
|
||||||
implicitHeight: panel.height
|
implicitHeight: panel.height
|
||||||
|
|
||||||
P5Panel {
|
P5Panel {
|
||||||
id: panel
|
id: panel
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: layout.implicitHeight + Theme.padM * 2
|
height: layout.implicitHeight + Theme.padM * 2
|
||||||
lean: 0.05
|
lean: root.lean
|
||||||
chop: 12
|
chop: root.chop
|
||||||
fill: Theme.surface
|
fill: Theme.surface
|
||||||
fillOpacity: 0.97
|
fillOpacity: 0.97
|
||||||
border: Theme.alpha(root.tone, hover.hovered ? 0.95 : 0.5)
|
border: Theme.alpha(root.tone, hover.hovered ? 0.95 : 0.5)
|
||||||
@@ -117,18 +124,13 @@ Item {
|
|||||||
implicitWidth: actionLabel.implicitWidth + Theme.padM
|
implicitWidth: actionLabel.implicitWidth + Theme.padM
|
||||||
implicitHeight: 22
|
implicitHeight: 22
|
||||||
|
|
||||||
Rectangle {
|
Skew {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: actionHover.hovered
|
color: actionHover.hovered
|
||||||
? Theme.alpha(root.tone, 0.3)
|
? Theme.alpha(root.tone, 0.3)
|
||||||
: Theme.alpha(root.tone, 0.1)
|
: Theme.alpha(root.tone, 0.1)
|
||||||
border.width: 1
|
borderWidth: 1
|
||||||
border.color: Theme.alpha(root.tone, 0.6)
|
borderColor: Theme.alpha(root.tone, 0.6)
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import "root:/config"
|
import "root:/config"
|
||||||
|
import "root:/components"
|
||||||
|
|
||||||
// Leaning slider with a chopped handle. Drag or click anywhere on the rail.
|
// Leaning slider with a chopped handle. Drag or click anywhere on the rail.
|
||||||
Item {
|
Item {
|
||||||
@@ -17,33 +18,23 @@ Item {
|
|||||||
readonly property real _lean: Theme.skew * height
|
readonly property real _lean: Theme.skew * height
|
||||||
|
|
||||||
// Rail
|
// Rail
|
||||||
Rectangle {
|
Skew {
|
||||||
id: rail
|
id: rail
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
height: 6
|
height: 6
|
||||||
color: Theme.alpha(Theme.outline, 0.7)
|
color: Theme.alpha(Theme.outline, 0.7)
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 6 * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill
|
// Fill
|
||||||
Rectangle {
|
Skew {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
width: Math.max(0, Math.min(1, root.value)) * root.width
|
width: Math.max(0, Math.min(1, root.value)) * root.width
|
||||||
height: 6
|
height: 6
|
||||||
color: root.enabledControl ? root.accent : Theme.muted
|
color: root.enabledControl ? root.accent : Theme.muted
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 6 * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on width {
|
Behavior on width {
|
||||||
enabled: !drag.active
|
enabled: !drag.active
|
||||||
NumberAnimation { duration: Theme.durFast }
|
NumberAnimation { duration: Theme.durFast }
|
||||||
@@ -55,7 +46,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle
|
// Handle
|
||||||
Rectangle {
|
Skew {
|
||||||
id: handle
|
id: handle
|
||||||
width: 6
|
width: 6
|
||||||
height: hover.hovered || drag.active ? 18 : 14
|
height: hover.hovered || drag.active ? 18 : 14
|
||||||
@@ -63,11 +54,6 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
x: Math.max(0, Math.min(1, root.value)) * root.width - width / 2
|
x: Math.max(0, Math.min(1, root.value)) * root.width - width / 2
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, height * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on height {
|
Behavior on height {
|
||||||
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutBack }
|
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutBack }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Shapes
|
||||||
|
import "root:/config"
|
||||||
|
import "root:/components"
|
||||||
|
|
||||||
|
// A leaning block — the small-scale counterpart to P5Panel.
|
||||||
|
//
|
||||||
|
// Every tick, rule, meter segment and slider rail in this shell leans at
|
||||||
|
// Theme.skew. Shearing a plain Rectangle with a Matrix4x4 does that, but Qt
|
||||||
|
// rasterises a Rectangle's geometry with no antialiasing at all, so the slanted
|
||||||
|
// edges come out as a hard pixel staircase — at 18px tall that staircase is
|
||||||
|
// most of what you see, and it reads as broken rendering rather than as a
|
||||||
|
// deliberate angle. Drawing the parallelogram as a Shape with the curve
|
||||||
|
// renderer gets analytic coverage AA on the diagonals instead.
|
||||||
|
//
|
||||||
|
// Drop-in for a sheared Rectangle: set width/height as before and use `color`,
|
||||||
|
// `borderColor` and `borderWidth` the same way.
|
||||||
|
Shape {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property color color: "transparent"
|
||||||
|
property color borderColor: "transparent"
|
||||||
|
property real borderWidth: 0
|
||||||
|
|
||||||
|
// Lean of the vertical edges as a fraction of height, matching P5Panel.
|
||||||
|
property real lean: Theme.skew
|
||||||
|
property bool leanLeft: false
|
||||||
|
|
||||||
|
// Wide, short pieces — the rail's hairline rules — slant their horizontal
|
||||||
|
// edges by a fraction of width instead, exactly as P5Panel does for the
|
||||||
|
// rail slab.
|
||||||
|
property bool vertical: false
|
||||||
|
|
||||||
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
asynchronous: false
|
||||||
|
|
||||||
|
ShapePath {
|
||||||
|
fillColor: root.color
|
||||||
|
strokeColor: root.borderWidth > 0 ? root.borderColor : "transparent"
|
||||||
|
strokeWidth: root.borderWidth
|
||||||
|
joinStyle: ShapePath.MiterJoin
|
||||||
|
capStyle: ShapePath.FlatCap
|
||||||
|
|
||||||
|
PathPolyline {
|
||||||
|
path: {
|
||||||
|
const w = root.width, h = root.height, l = root.lean;
|
||||||
|
if (root.vertical)
|
||||||
|
return Geom.closed(root.leanLeft ? Geom.parallelogramVL(w, h, l)
|
||||||
|
: Geom.parallelogramV(w, h, l));
|
||||||
|
return Geom.closed(root.leanLeft ? Geom.parallelogramL(w, h, l)
|
||||||
|
: Geom.parallelogram(w, h, l));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -231,17 +231,12 @@ Item {
|
|||||||
Repeater {
|
Repeater {
|
||||||
model: Math.min(surface.entry.length, 22)
|
model: Math.min(surface.entry.length, 22)
|
||||||
|
|
||||||
Rectangle {
|
Skew {
|
||||||
required property int index
|
required property int index
|
||||||
width: 10
|
width: 10
|
||||||
height: 20
|
height: 20
|
||||||
color: surface.failed ? Theme.accent : Theme.primary
|
color: surface.failed ? Theme.accent : Theme.primary
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 20 * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Each new block snaps in.
|
// Each new block snaps in.
|
||||||
Component.onCompleted: pop.start()
|
Component.onCompleted: pop.start()
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
|
|||||||
@@ -82,12 +82,24 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Countdown rule that drains along the bottom edge.
|
// Countdown rule that drains along the bottom edge.
|
||||||
Rectangle {
|
//
|
||||||
|
// It used to be a full-bleed rectangle across the whole toast,
|
||||||
|
// which meant it ran straight through the card's chopped
|
||||||
|
// bottom-left corner and out the other side — a stray bar
|
||||||
|
// hanging off the card at the one place the silhouette is
|
||||||
|
// supposed to be cut. It lives inside the cut now, and ends
|
||||||
|
// where the leaning right edge does.
|
||||||
|
readonly property real fuseSpan: Math.max(0,
|
||||||
|
width - card.chop - card.implicitHeight * card.lean)
|
||||||
|
|
||||||
|
Skew {
|
||||||
id: fuse
|
id: fuse
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.left: parent.left
|
anchors.bottomMargin: 1
|
||||||
|
x: card.chop
|
||||||
height: 2
|
height: 2
|
||||||
width: parent.width
|
width: toast.fuseSpan
|
||||||
|
lean: card.lean
|
||||||
color: Theme.alpha(card.tone, 0.7)
|
color: Theme.alpha(card.tone, 0.7)
|
||||||
visible: life.running
|
visible: life.running
|
||||||
}
|
}
|
||||||
@@ -96,7 +108,7 @@ PanelWindow {
|
|||||||
id: life
|
id: life
|
||||||
target: fuse
|
target: fuse
|
||||||
property: "width"
|
property: "width"
|
||||||
from: toast.width
|
from: toast.fuseSpan
|
||||||
to: 0
|
to: 0
|
||||||
running: false
|
running: false
|
||||||
duration: Notifs.timeoutFor(toast.modelData)
|
duration: Notifs.timeoutFor(toast.modelData)
|
||||||
|
|||||||
@@ -127,17 +127,12 @@ Popout {
|
|||||||
height: 30
|
height: 30
|
||||||
|
|
||||||
// Today gets a leaning crimson slab.
|
// Today gets a leaning crimson slab.
|
||||||
Rectangle {
|
Skew {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: 26
|
width: 26
|
||||||
height: 24
|
height: 24
|
||||||
visible: cell.modelData.today
|
visible: cell.modelData.today
|
||||||
color: Theme.accent
|
color: Theme.accent
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 24 * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
@@ -229,16 +224,11 @@ Popout {
|
|||||||
implicitWidth: 22
|
implicitWidth: 22
|
||||||
implicitHeight: 22
|
implicitHeight: 22
|
||||||
|
|
||||||
Rectangle {
|
Skew {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: stepHover.hovered ? Theme.alpha(Theme.accent, 0.25) : "transparent"
|
color: stepHover.hovered ? Theme.alpha(Theme.accent, 0.25) : "transparent"
|
||||||
border.width: 1
|
borderWidth: 1
|
||||||
border.color: Theme.alpha(Theme.outline, 0.7)
|
borderColor: Theme.alpha(Theme.outline, 0.7)
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Icon {
|
Icon {
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ Popout {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
contentWidth: 400
|
contentWidth: 400
|
||||||
contentHeight: 200 + (Media.players.length > 1 ? 34 : 0)
|
|
||||||
|
// The seek row used to sit flush against the bottom edge, where the slab's
|
||||||
|
// chamfer cuts the corner away — the elapsed time was half outside the
|
||||||
|
// panel. The extra height is the room that row needs to clear the cut.
|
||||||
|
contentHeight: 224 + (Media.players.length > 1 ? 34 : 0)
|
||||||
|
|
||||||
PopoutSurface {
|
PopoutSurface {
|
||||||
id: surface
|
id: surface
|
||||||
@@ -176,8 +180,10 @@ Popout {
|
|||||||
id: seekArea
|
id: seekArea
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: Theme.padS
|
||||||
|
anchors.rightMargin: Theme.padS
|
||||||
anchors.bottom: picker.visible ? picker.top : parent.bottom
|
anchors.bottom: picker.visible ? picker.top : parent.bottom
|
||||||
anchors.bottomMargin: Theme.padS
|
anchors.bottomMargin: picker.visible ? Theme.padS : Theme.padM
|
||||||
height: 26
|
height: 26
|
||||||
visible: Media.hasPlayer
|
visible: Media.hasPlayer
|
||||||
|
|
||||||
@@ -236,18 +242,13 @@ Popout {
|
|||||||
implicitWidth: chipLabel.implicitWidth + Theme.padM
|
implicitWidth: chipLabel.implicitWidth + Theme.padM
|
||||||
implicitHeight: 22
|
implicitHeight: 22
|
||||||
|
|
||||||
Rectangle {
|
Skew {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: chip.current
|
color: chip.current
|
||||||
? Theme.alpha(Theme.primary, 0.22)
|
? Theme.alpha(Theme.primary, 0.22)
|
||||||
: (chipHover.hovered ? Theme.alpha(Theme.primary, 0.1) : "transparent")
|
: (chipHover.hovered ? Theme.alpha(Theme.primary, 0.1) : "transparent")
|
||||||
border.width: 1
|
borderWidth: 1
|
||||||
border.color: chip.current ? Theme.primary : Theme.alpha(Theme.outline, 0.7)
|
borderColor: chip.current ? Theme.primary : Theme.alpha(Theme.outline, 0.7)
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
@@ -285,23 +286,18 @@ Popout {
|
|||||||
implicitWidth: primary ? 38 : 30
|
implicitWidth: primary ? 38 : 30
|
||||||
implicitHeight: primary ? 38 : 30
|
implicitHeight: primary ? 38 : 30
|
||||||
|
|
||||||
Rectangle {
|
Skew {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: {
|
color: {
|
||||||
if (!tb.enabledControl) return "transparent";
|
if (!tb.enabledControl) return "transparent";
|
||||||
if (tbHover.hovered) return tb.primary ? Theme.accent : Theme.alpha(Theme.primary, 0.22);
|
if (tbHover.hovered) return tb.primary ? Theme.accent : Theme.alpha(Theme.primary, 0.22);
|
||||||
return tb.primary ? Theme.alpha(Theme.primary, 0.18) : "transparent";
|
return tb.primary ? Theme.alpha(Theme.primary, 0.18) : "transparent";
|
||||||
}
|
}
|
||||||
border.width: tb.primary ? 2 : 1
|
borderWidth: tb.primary ? 2 : 1
|
||||||
border.color: tb.enabledControl
|
borderColor: tb.enabledControl
|
||||||
? (tbHover.hovered ? Theme.text : Theme.alpha(Theme.primary, 0.6))
|
? (tbHover.hovered ? Theme.text : Theme.alpha(Theme.primary, 0.6))
|
||||||
: Theme.alpha(Theme.outline, 0.4)
|
: Theme.alpha(Theme.outline, 0.4)
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, tb.height * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
ColorAnimation { duration: Theme.durFast }
|
ColorAnimation { duration: Theme.durFast }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,16 +202,11 @@ Popout {
|
|||||||
|
|
||||||
implicitHeight: 40
|
implicitHeight: 40
|
||||||
|
|
||||||
Rectangle {
|
Skew {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Theme.alpha(rateBlock.tone, 0.08)
|
color: Theme.alpha(rateBlock.tone, 0.08)
|
||||||
border.width: 1
|
borderWidth: 1
|
||||||
border.color: Theme.alpha(rateBlock.tone, 0.35)
|
borderColor: Theme.alpha(rateBlock.tone, 0.35)
|
||||||
|
|
||||||
transform: Matrix4x4 {
|
|
||||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, rateBlock.height * Theme.skew,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
|||||||
@@ -59,15 +59,12 @@ Item {
|
|||||||
color: Theme.alpha(root.accent, 0.8)
|
color: Theme.alpha(root.accent, 0.8)
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Skew {
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
width: 16
|
width: 16
|
||||||
height: 2
|
height: 2
|
||||||
color: Theme.accent
|
color: Theme.accent
|
||||||
transform: Matrix4x4 {
|
lean: 1.4
|
||||||
matrix: Qt.matrix4x4(1, -1.4, 0, 2.8,
|
|
||||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import "root:/config"
|
||||||
|
import "root:/components"
|
||||||
|
|
||||||
|
// The tray's context menu, on the shell's own slab.
|
||||||
|
//
|
||||||
|
// Quickshell can hand a StatusNotifier menu straight to Qt's platform menu
|
||||||
|
// implementation, but that draws a stock widget popup — square corners, system
|
||||||
|
// palette, no lean — hanging off the corner of a shell that has none of those
|
||||||
|
// things. This walks the menu with QsMenuOpener instead and draws it as a
|
||||||
|
// P5Panel, so a right click on a tray icon produces the same object language as
|
||||||
|
// everything else on the rail.
|
||||||
|
//
|
||||||
|
// Dismissal is a Hyprland focus grab: click anywhere outside and the compositor
|
||||||
|
// tells us to close.
|
||||||
|
PopupWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property Item anchorItem
|
||||||
|
property var menuHandle: null
|
||||||
|
|
||||||
|
// Room around the slab for the entry overshoot and the offset black copy.
|
||||||
|
readonly property int bleed: 20
|
||||||
|
readonly property int minWidth: 160
|
||||||
|
readonly property int maxWidth: 320
|
||||||
|
|
||||||
|
// The slab's own lean and padding, which the entries sit inside. Sizing the
|
||||||
|
// window to the entry widths alone left every label short by that inset and
|
||||||
|
// elided perfectly ordinary items.
|
||||||
|
readonly property real slabLean: 0.06
|
||||||
|
readonly property real slabPad: Theme.padS
|
||||||
|
|
||||||
|
readonly property real contentHeight: list.implicitHeight + slabPad * 2
|
||||||
|
readonly property real contentWidth:
|
||||||
|
Math.max(root.minWidth, Math.min(root.maxWidth, list.menuWidth))
|
||||||
|
+ (slabPad + contentHeight * slabLean) * 2
|
||||||
|
|
||||||
|
anchor.item: anchorItem
|
||||||
|
anchor.edges: Edges.Left
|
||||||
|
anchor.gravity: Edges.Left
|
||||||
|
anchor.adjustment: PopupAdjustment.SlideY
|
||||||
|
anchor.margins.right: Theme.popoutGap
|
||||||
|
|
||||||
|
implicitWidth: contentWidth + bleed * 2
|
||||||
|
implicitHeight: contentHeight + bleed * 2
|
||||||
|
color: "transparent"
|
||||||
|
visible: shown
|
||||||
|
|
||||||
|
property bool shown: false
|
||||||
|
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
if (!menuHandle) return;
|
||||||
|
closeAnim.stop();
|
||||||
|
shown = true;
|
||||||
|
openAnim.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if (!shown) return;
|
||||||
|
openAnim.stop();
|
||||||
|
closeAnim.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (shown) close(); else open();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The grab can only take hold once the popup is actually mapped. Asked for
|
||||||
|
// in the same tick as `shown`, Hyprland has no surface to hand it and
|
||||||
|
// clears it immediately — which closed the menu the instant it opened.
|
||||||
|
Timer {
|
||||||
|
id: grabDelay
|
||||||
|
interval: 60
|
||||||
|
running: root.shown
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
active: root.shown && !grabDelay.running
|
||||||
|
windows: [root]
|
||||||
|
onCleared: root.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: holder
|
||||||
|
x: root.bleed
|
||||||
|
y: root.bleed
|
||||||
|
width: root.contentWidth
|
||||||
|
height: root.contentHeight
|
||||||
|
|
||||||
|
opacity: 0
|
||||||
|
scale: 0.94
|
||||||
|
transformOrigin: Item.Right
|
||||||
|
|
||||||
|
PopoutSurface {
|
||||||
|
anchors.fill: parent
|
||||||
|
tone: Theme.accent
|
||||||
|
padding: root.slabPad
|
||||||
|
chop: 12
|
||||||
|
lean: root.slabLean
|
||||||
|
|
||||||
|
TrayMenuList {
|
||||||
|
id: list
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
// Only walk the menu while it is on screen. Bound unconditionally,
|
||||||
|
// every tray app would get a GetLayout the moment the shell
|
||||||
|
// starts, for a menu nobody has asked for yet.
|
||||||
|
menuHandle: root.shown ? root.menuHandle : null
|
||||||
|
onActivated: root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: openAnim
|
||||||
|
NumberAnimation {
|
||||||
|
target: holder; property: "opacity"
|
||||||
|
to: 1.0; duration: Theme.durBase; easing.type: Easing.OutExpo
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: holder; property: "scale"
|
||||||
|
to: 1.0; duration: Theme.durSlow; easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: holder; property: "x"
|
||||||
|
from: root.bleed + 14; to: root.bleed
|
||||||
|
duration: Theme.durSlow; easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: closeAnim
|
||||||
|
NumberAnimation {
|
||||||
|
target: holder; property: "opacity"
|
||||||
|
to: 0.0; duration: Theme.durFast; easing.type: Easing.InQuad
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: holder; property: "scale"
|
||||||
|
to: 0.96; duration: Theme.durFast; easing.type: Easing.InQuad
|
||||||
|
}
|
||||||
|
onFinished: {
|
||||||
|
root.shown = false;
|
||||||
|
holder.x = root.bleed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
//@ pragma UseQApplication
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import "root:/config"
|
import "root:/config"
|
||||||
|
|||||||
Reference in New Issue
Block a user