488 lines
16 KiB
QML
488 lines
16 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: 10
|
|
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 + 8) : 0
|
|
anchors.bottomMargin: root.isTop ? 0 : (Cfg.Config.barHeight + 8)
|
|
height: parent.height - Cfg.Config.barHeight - 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|