Files
quickdots/bar/modules/Tray.qml

488 lines
17 KiB
QML

import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import "../../config" as Cfg
import "../../components"
ModuleChip {
id: root
property string barSide: "right"
property bool expanded: false
property bool manuallyExpanded: false
readonly property bool effectiveExpanded: expanded || trayMenuPopup.popupOpen
implicitWidth: mainRow.width
implicitHeight: Cfg.Config.barHeight
readonly property color tmBg: t.trayMenuBg
readonly property color tmBorder: t.trayMenuBorder
readonly property color tmText: t.trayMenuText
readonly property color tmDim: t.trayMenuDim
readonly property color tmHover: t.trayMenuHover
readonly property color tmCheck: t.trayMenuCheck
readonly property color tmSeparator: t.trayMenuSeparator
chipColor: t.trayBg
chipColorHovered: t.trayBgHover
chipBorderColor: t.trayBorder
chipBorderColorHovered: t.trayBorderHover
popupOpen: trayMenuPopup.popupOpen
HoverHandler {
onHoveredChanged: {
if (!root.manuallyExpanded) {
root.expanded = hovered;
}
}
}
Row {
id: mainRow
anchors.verticalCenter: parent.verticalCenter
leftPadding: root.effectiveExpanded && root.barSide === "right" ? 8 : 0
rightPadding: root.effectiveExpanded && root.barSide === "left" ? 8 : 0
Behavior on leftPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
Behavior on rightPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
layoutDirection: root.barSide === "left" ? Qt.LeftToRight : Qt.RightToLeft
spacing: root.effectiveExpanded ? 4 : 0
Behavior on spacing { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } }
Item {
id: toggleArea
width: Cfg.Config.trayToggleSize
height: Cfg.Config.trayToggleSize
z: 10
TapHandler {
onTapped: {
root.manuallyExpanded = !root.manuallyExpanded;
root.expanded = root.manuallyExpanded;
}
}
Text {
anchors.centerIn: parent
text: root.effectiveExpanded
? (root.barSide === "left" ? "󰅁" : "󰅂")
: (root.barSide === "left" ? "󰅂" : "󰅁")
color: root.t.trayIcon
font.pixelSize: Cfg.Config.trayToggleSize * 0.55
}
}
Row {
id: iconsRow
spacing: 6
clip: true
anchors.verticalCenter: parent.verticalCenter
width: root.effectiveExpanded ? implicitWidth : 0
opacity: root.effectiveExpanded ? 1 : 0
Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
Behavior on opacity { NumberAnimation { duration: 150 } }
Repeater {
model: SystemTray.items
delegate: MouseArea {
id: iconDelegate
required property SystemTrayItem modelData
property alias item: iconDelegate.modelData
implicitWidth: Cfg.Config.trayIconSize
implicitHeight: Cfg.Config.trayIconSize
width: Cfg.Config.trayIconSize
height: Cfg.Config.trayIconSize
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
onClicked: function(mouse) {
if (mouse.button === Qt.LeftButton) {
item.activate()
} else if (mouse.button === Qt.MiddleButton) {
item.secondaryActivate()
} else if (mouse.button === Qt.RightButton) {
if (item.hasMenu) {
trayMenuPopup.open(item, iconDelegate)
}
}
}
onWheel: function(wheel) {
wheel.accepted = true
item.scroll(wheel.angleDelta.y / 120, false)
}
IconImage {
anchors.fill: parent
source: item.icon
}
}
}
}
}
PanelWindow {
id: trayMenuPopup
screen: root.targetScreen
visible: false
color: "transparent"
property bool popupOpen: false
property bool _pendingOpen: false
property var activeTrayItem: null
property var activeDelegate: null
function open(item, delegate) {
if (popupOpen) close()
hideTimer.stop()
_pendingOpen = false
activeTrayItem = item
activeDelegate = delegate
rootMenuOpener.menu = item.menu
visible = true
if (menuCol.height > 0) {
popupOpen = true
} else {
_pendingOpen = true
openFallbackTimer.restart()
}
}
function close() {
_pendingOpen = false
openFallbackTimer.stop()
popupOpen = false
hideTimer.restart()
}
Connections {
target: menuCol
function onHeightChanged() {
if (trayMenuPopup._pendingOpen && menuCol.height > 0) {
trayMenuPopup._pendingOpen = false
openFallbackTimer.stop()
trayMenuPopup.popupOpen = true
}
}
}
Timer {
id: openFallbackTimer
interval: 400
onTriggered: {
if (trayMenuPopup._pendingOpen) {
trayMenuPopup._pendingOpen = false
trayMenuPopup.popupOpen = true
}
}
}
PopupHideTimer {
id: hideTimer
onTriggered: {
trayMenuPopup.visible = false
rootMenuOpener.menu = null
trayMenuPopup.activeTrayItem = null
trayMenuPopup.activeDelegate = null
}
}
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "main-shell-tray-menu"
WlrLayershell.exclusiveZone: -1
mask: popupOpen ? null : _trayMenuNoInput
Region { id: _trayMenuNoInput }
TapHandler { onTapped: trayMenuPopup.close() }
QsMenuOpener { id: rootMenuOpener }
Component {
id: trayMenuItemComponent
Item {
id: menuItem
required property var modelData
readonly property bool isSep: modelData.isSeparator
width: parent.width
height: isSep ? 5 : 32
Rectangle {
visible: isSep
anchors {
left: parent.left; right: parent.right
verticalCenter: parent.verticalCenter
}
anchors.leftMargin: 8; anchors.rightMargin: 8
height: 1
color: root.tmSeparator
}
Rectangle {
visible: !isSep
anchors.fill: parent
anchors.margins: 2
radius: 8
readonly property bool active: menuMouse.containsMouse
|| (submenuCard.visible
&& trayMenuPopup._submenuParentRef === menuItem)
color: active ? root.tmHover : "transparent"
Behavior on color { ColorAnimation { duration: 60 } }
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8; anchors.rightMargin: 8
spacing: 8
Item {
Layout.preferredWidth: 16
Layout.preferredHeight: 16
visible: !isSep
Text {
anchors.centerIn: parent
visible: modelData.buttonType === QsMenuButtonType.CheckBox
&& modelData.checkState === Qt.Checked
text: "✓"; color: root.tmCheck
font.pixelSize: 14
}
Text {
anchors.centerIn: parent
visible: modelData.buttonType === QsMenuButtonType.RadioButton
&& modelData.checkState === Qt.Checked
text: "●"; color: root.tmCheck
font.pixelSize: 9
}
Text {
anchors.centerIn: parent
visible: modelData.buttonType === QsMenuButtonType.RadioButton
&& modelData.checkState !== Qt.Checked
text: "○"; color: root.tmDim
font.pixelSize: 11
}
}
Image {
visible: !isSep && modelData.icon !== ""
source: modelData.icon
width: 16; height: 16
sourceSize: Qt.size(16, 16)
fillMode: Image.PreserveAspectFit
}
Text {
text: modelData.text
color: modelData.enabled ? root.tmText : root.tmDim
font.pixelSize: 13
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
visible: !isSep && modelData.hasChildren
text: "▶"
color: root.tmDim
font.pixelSize: 9
}
}
MouseArea {
id: menuMouse
anchors.fill: parent
hoverEnabled: true
enabled: !isSep && modelData.enabled
onClicked: {
if (modelData.hasChildren) {
closeSubmenuTimer.stop()
submenuDelayTimer.itemRef = menuItem
submenuDelayTimer.restart()
} else {
modelData.triggered()
trayMenuPopup.close()
}
}
onEntered: {
if (modelData.hasChildren) {
closeSubmenuTimer.stop()
submenuDelayTimer.itemRef = menuItem
submenuDelayTimer.restart()
} else {
closeSubmenuTimer.stop()
submenuCard.visible = false
}
}
onExited: {
if (modelData.hasChildren)
closeSubmenuTimer.restart()
}
}
}
}
}
Item {
id: menuCardWrapper
readonly property int cardW: 220
readonly property int pad: 10
x: {
if (!trayMenuPopup.activeDelegate)
return (parent.width - cardW) / 2
var gp = trayMenuPopup.activeDelegate.mapToGlobal(0, 0)
var cx = gp.x + trayMenuPopup.activeDelegate.width / 2
return Math.max(pad,
Math.min(cx - cardW / 2, parent.width - cardW - pad))
}
width: cardW
clip: true
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
anchors.topMargin: root.isTop ? (Cfg.Config.barHeight + root.barEdgePad + 8) : 0
anchors.bottomMargin: root.isTop ? 0 : (Cfg.Config.barHeight + root.barEdgePad + 8)
height: parent.height - Cfg.Config.barHeight - root.barEdgePad - 8
PopupCard {
id: menuCard
popupOpen: trayMenuPopup.popupOpen
isTop: root.isTop
animEnabled: true
height: menuCol.height + 12
color: root.tmBg
radius: Cfg.Config.popupRadius
border.color: root.tmBorder
border.width: Cfg.Config.popupBorderWidth
Column {
id: menuCol
anchors { left: parent.left; right: parent.right; top: parent.top }
anchors.margins: 6
spacing: 0
Repeater {
model: rootMenuOpener.children
delegate: trayMenuItemComponent
}
}
}
}
property var _submenuParentRef: null
QsMenuOpener { id: subMenuOpener }
Timer {
id: submenuDelayTimer
interval: 250
property var itemRef: null
onTriggered: {
if (!itemRef) return
var entry = itemRef.modelData
if (!entry || !entry.hasChildren) return
subMenuOpener.menu = entry
trayMenuPopup._submenuParentRef = itemRef
submenuCard.visible = true
}
}
Timer {
id: closeSubmenuTimer
interval: 300
onTriggered: {
if (!submenuCard.containsMouse) {
submenuCard.visible = false
subMenuOpener.menu = null
trayMenuPopup._submenuParentRef = null
}
}
}
Rectangle {
id: submenuCard
visible: false
property bool containsMouse: false
property int subW: Math.max(submenuCol.width, 180)
x: {
if (!trayMenuPopup._submenuParentRef) return 0
var pItem = trayMenuPopup._submenuParentRef
var gp = pItem.mapToGlobal(0, 0)
var lp = trayMenuPopup.contentItem.mapFromGlobal(gp.x, gp.y)
var fromLeft = lp.x + pItem.width + 4
if (fromLeft + subW + 10 > trayMenuPopup.contentItem.width)
return Math.max(2, lp.x - subW - 4)
return fromLeft
}
y: {
if (!trayMenuPopup._submenuParentRef) return 0
var pItem = trayMenuPopup._submenuParentRef
var gp = pItem.mapToGlobal(0, 0)
var lp = trayMenuPopup.contentItem.mapFromGlobal(gp.x, gp.y)
return Math.max(2, lp.y - 2)
}
width: subW
height: submenuCol.height + 12
color: root.tmBg
radius: Cfg.Config.popupRadius
border.color: root.tmBorder
border.width: Cfg.Config.popupBorderWidth
HoverHandler {
onHoveredChanged: {
submenuCard.containsMouse = hovered
if (!hovered)
closeSubmenuTimer.restart()
}
}
Column {
id: submenuCol
anchors { left: parent.left; right: parent.right; top: parent.top }
anchors.margins: 6
spacing: 0
Repeater {
model: subMenuOpener.children
delegate: trayMenuItemComponent
}
}
}
}
}