better structure
This commit is contained in:
193
config/quickshell/Modules/Bar/Misc/MonitorItem.qml
Normal file
193
config/quickshell/Modules/Bar/Misc/MonitorItem.qml
Normal file
@@ -0,0 +1,193 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string symbol
|
||||
property real maxValue: 100
|
||||
property real value: 100
|
||||
property string textValue: "" // override value in textDisplay if set
|
||||
property color fillColor: Colors.primary
|
||||
property string textSuffix: ""
|
||||
property bool pointerCursor: true
|
||||
property bool expandOnValueChange: false
|
||||
property real hideTimeOut: 2000 // ms
|
||||
property bool forceExpand: false
|
||||
property bool _expand: forceExpand || mouseArea.containsMouse
|
||||
property bool _isFirst: true
|
||||
property bool disableHover: false
|
||||
property bool critical: false
|
||||
property color criticalColor: Colors.red
|
||||
readonly property real ratio: value / maxValue
|
||||
property color realColor: critical ? criticalColor : fillColor
|
||||
|
||||
signal wheelUp()
|
||||
signal wheelDown()
|
||||
signal clicked()
|
||||
signal rightClicked()
|
||||
|
||||
implicitHeight: parent.height - 5
|
||||
implicitWidth: parent.height + (_expand ? textDisplay.width : 0)
|
||||
|
||||
Loader {
|
||||
id: connectionLoader
|
||||
|
||||
active: expandOnValueChange
|
||||
|
||||
sourceComponent: Connections {
|
||||
function onValueChanged() {
|
||||
// No need to expand (again) if already hovering
|
||||
if (mouseArea.containsMouse)
|
||||
return ;
|
||||
|
||||
// Skip first change (which is most likely initialization)
|
||||
if (root._isFirst) {
|
||||
root._isFirst = false;
|
||||
return ;
|
||||
}
|
||||
root.forceExpand = true;
|
||||
hideTimer.restart();
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
|
||||
interval: parent.hideTimeOut
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.forceExpand = false;
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: !disableHover
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: pointerCursor ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.LeftButton)
|
||||
root.clicked();
|
||||
else if (mouse.button === Qt.RightButton)
|
||||
root.rightClicked();
|
||||
}
|
||||
onWheel: (wheel) => {
|
||||
if (wheel.angleDelta.y > 0)
|
||||
root.wheelUp();
|
||||
else if (wheel.angleDelta.y < 0)
|
||||
root.wheelDown();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
id: progressDisplay
|
||||
|
||||
Layout.preferredHeight: parent.height
|
||||
Layout.preferredWidth: parent.height
|
||||
|
||||
Canvas {
|
||||
id: progressCircle
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.centerIn: parent
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
var centerX = width / 2;
|
||||
var centerY = height / 2;
|
||||
var radius = width / 2 - 3;
|
||||
var startAngle = -Math.PI / 2;
|
||||
var endAngle = startAngle - (2 * Math.PI * root.ratio);
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, endAngle, startAngle, false);
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeStyle = root.realColor;
|
||||
ctx.lineCap = "round";
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onRatioChanged() {
|
||||
progressCircle.requestPaint();
|
||||
}
|
||||
|
||||
function onRealColorChanged() {
|
||||
progressCircle.requestPaint();
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
id: symbolText
|
||||
|
||||
anchors.fill: parent
|
||||
text: symbol
|
||||
font.family: Fonts.nerd
|
||||
font.pointSize: Fonts.icon
|
||||
color: root.realColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: textDisplay
|
||||
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: root._expand ? textLabel.implicitWidth + 10 : 0
|
||||
clip: true
|
||||
|
||||
Text {
|
||||
id: textLabel
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 5
|
||||
text: (textValue || Math.round(root.value)) + root.textSuffix
|
||||
font.pointSize: Fonts.small
|
||||
font.family: Fonts.primary
|
||||
color: root.realColor
|
||||
opacity: root._expand ? 1 : 0
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on realColor {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
64
config/quickshell/Modules/Bar/Misc/SymbolButton.qml
Normal file
64
config/quickshell/Modules/Bar/Misc/SymbolButton.qml
Normal file
@@ -0,0 +1,64 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Constants
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string symbol
|
||||
property color buttonColor: Colors.distroColor
|
||||
readonly property alias hovered: mouseArea.containsMouse
|
||||
property real iconSize: Fonts.icon
|
||||
property real radius: Style.radiusS
|
||||
property bool disabledHover: false
|
||||
|
||||
signal clicked()
|
||||
signal rightClicked()
|
||||
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: parent.height
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: !disabledHover
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton)
|
||||
root.rightClicked();
|
||||
else if (mouse.button === Qt.LeftButton)
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
text: symbol
|
||||
font.family: Fonts.nerd
|
||||
font.pointSize: iconSize
|
||||
font.bold: false
|
||||
color: buttonColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: parent.hovered ? buttonColor : Colors.transparent
|
||||
opacity: 0.3
|
||||
radius: root.radius
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
160
config/quickshell/Modules/Bar/Misc/SystemTray.qml
Normal file
160
config/quickshell/Modules/Bar/Misc/SystemTray.qml
Normal file
@@ -0,0 +1,160 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
import qs.Utils
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
implicitWidth: trayFlow.implicitWidth + 20
|
||||
implicitHeight: parent.height
|
||||
radius: 0
|
||||
color: Colors.transparent
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Flow {
|
||||
id: trayFlow
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
flow: Flow.LeftToRight
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: Item {
|
||||
width: 18
|
||||
height: 18
|
||||
visible: modelData
|
||||
|
||||
IconImage {
|
||||
id: trayIcon
|
||||
|
||||
property ShellScreen screen: root.screen
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: 14
|
||||
height: 14
|
||||
smooth: false
|
||||
asynchronous: true
|
||||
backer.fillMode: Image.PreserveAspectFit
|
||||
source: {
|
||||
let icon = modelData?.icon || ""
|
||||
if (!icon) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Process icon path
|
||||
if (icon.includes("?path=")) {
|
||||
// Seems qmlfmt does not support the following ES6 syntax: const[name, path] = icon.split
|
||||
const chunks = icon.split("?path=")
|
||||
const name = chunks[0]
|
||||
const path = chunks[1]
|
||||
const fileName = name.substring(name.lastIndexOf("/") + 1)
|
||||
return `file://${path}/${fileName}`
|
||||
}
|
||||
return icon
|
||||
}
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: mouse => {
|
||||
if (!modelData) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
// Close any open menu first
|
||||
trayPanel.close()
|
||||
|
||||
if (!modelData.onlyMenu) {
|
||||
modelData.activate()
|
||||
}
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
// Close any open menu first
|
||||
trayPanel.close()
|
||||
|
||||
modelData.secondaryActivate && modelData.secondaryActivate()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
// Close the menu if it was visible
|
||||
if (trayPanel && trayPanel.visible) {
|
||||
trayPanel.close()
|
||||
return
|
||||
}
|
||||
|
||||
if (modelData.hasMenu && modelData.menu && trayMenu.item) {
|
||||
trayPanel.open()
|
||||
|
||||
// Position menu based on bar position
|
||||
let menuX, menuY
|
||||
// For horizontal bars: center horizontally and position below
|
||||
menuX = (width / 2) - (trayMenu.item.width / 2)
|
||||
menuY = root.height
|
||||
trayMenu.item.menu = modelData.menu
|
||||
trayMenu.item.showAt(parent, menuX, menuY)
|
||||
} else {
|
||||
Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
|
||||
}
|
||||
}
|
||||
}
|
||||
onEntered: {
|
||||
trayPanel.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: trayPanel
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
anchors.bottom: true
|
||||
visible: false
|
||||
color: Colors.transparent
|
||||
screen: root.screen
|
||||
|
||||
function open() {
|
||||
visible = true
|
||||
PanelService.willOpenPanel(trayPanel)
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false
|
||||
if (trayMenu.item) {
|
||||
trayMenu.item.hideMenu()
|
||||
}
|
||||
}
|
||||
|
||||
// Clicking outside of the rectangle to close
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: trayPanel.close()
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: trayMenu
|
||||
Component.onCompleted: {
|
||||
setSource("../Misc/TrayMenu.qml", {
|
||||
"screen": root.screen
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
253
config/quickshell/Modules/Bar/Misc/TrayMenu.qml
Normal file
253
config/quickshell/Modules/Bar/Misc/TrayMenu.qml
Normal file
@@ -0,0 +1,253 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
import qs.Utils
|
||||
import qs.Noctalia
|
||||
|
||||
PopupWindow {
|
||||
id: root
|
||||
property QsMenuHandle menu
|
||||
property var anchorItem: null
|
||||
property real anchorX
|
||||
property real anchorY
|
||||
property bool isSubMenu: false
|
||||
property bool isHovered: rootMouseArea.containsMouse
|
||||
property ShellScreen screen
|
||||
|
||||
readonly property int menuWidth: 180
|
||||
|
||||
implicitWidth: menuWidth
|
||||
|
||||
// Use the content height of the Flickable for implicit height
|
||||
implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9, flickable.contentHeight + (Style.marginS * 2))
|
||||
visible: false
|
||||
color: Colors.transparent
|
||||
anchor.item: anchorItem
|
||||
anchor.rect.x: anchorX
|
||||
anchor.rect.y: anchorY - (isSubMenu ? 0 : 4)
|
||||
|
||||
function showAt(item, x, y) {
|
||||
if (!item) {
|
||||
Logger.warn("TrayMenu", "AnchorItem is undefined, won't show menu.");
|
||||
return
|
||||
}
|
||||
|
||||
if (!opener.children || opener.children.values.length === 0) {
|
||||
Qt.callLater(() => showAt(item, x, y))
|
||||
return
|
||||
}
|
||||
|
||||
anchorItem = item
|
||||
anchorX = x
|
||||
anchorY = y
|
||||
|
||||
visible = true
|
||||
forceActiveFocus()
|
||||
|
||||
// Force update after showing.
|
||||
Qt.callLater(() => {
|
||||
root.anchor.updateAnchor()
|
||||
})
|
||||
}
|
||||
|
||||
function hideMenu() {
|
||||
visible = false
|
||||
|
||||
// Clean up all submenus recursively
|
||||
for (var i = 0; i < columnLayout.children.length; i++) {
|
||||
const child = columnLayout.children[i]
|
||||
if (child?.subMenu) {
|
||||
child.subMenu.hideMenu()
|
||||
child.subMenu.destroy()
|
||||
child.subMenu = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full-sized, transparent MouseArea to track the mouse.
|
||||
MouseArea {
|
||||
id: rootMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
Keys.onEscapePressed: root.hideMenu()
|
||||
}
|
||||
|
||||
QsMenuOpener {
|
||||
id: opener
|
||||
menu: root.menu
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Colors.base
|
||||
border.color: Colors.primary
|
||||
border.width: 2
|
||||
radius: Style.radiusM
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
contentHeight: columnLayout.implicitHeight
|
||||
interactive: true
|
||||
|
||||
// Use a ColumnLayout to handle menu item arrangement
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
width: flickable.width
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: opener.children ? [...opener.children.values] : []
|
||||
|
||||
delegate: Rectangle {
|
||||
id: entry
|
||||
required property var modelData
|
||||
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: {
|
||||
if (modelData?.isSeparator) {
|
||||
return 8
|
||||
} else {
|
||||
// Calculate based on text content
|
||||
const textHeight = text.contentHeight || (Style.fontSizeS * 1.2)
|
||||
return Math.max(28, textHeight + (Style.marginS * 2))
|
||||
}
|
||||
}
|
||||
|
||||
color: Colors.transparent
|
||||
property var subMenu: null
|
||||
|
||||
NDivider {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - (Style.marginM * 2)
|
||||
visible: modelData?.isSeparator ?? false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: mouseArea.containsMouse ? Colors.primary : Colors.transparent
|
||||
radius: Style.radiusS
|
||||
visible: !(modelData?.isSeparator ?? false)
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginM
|
||||
anchors.rightMargin: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
id: text
|
||||
Layout.fillWidth: true
|
||||
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
|
||||
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
|
||||
pointSize: Style.fontSizeS
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
family: Fonts.sans
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: Style.marginL
|
||||
Layout.preferredHeight: Style.marginL
|
||||
source: modelData?.icon ?? ""
|
||||
visible: (modelData?.icon ?? "") !== ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: modelData?.hasChildren ? "menu" : ""
|
||||
pointSize: Style.fontSizeS
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: modelData?.hasChildren ?? false
|
||||
color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && root.visible
|
||||
|
||||
onClicked: {
|
||||
if (modelData && !modelData.isSeparator && !modelData.hasChildren) {
|
||||
modelData.triggered()
|
||||
root.hideMenu()
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
if (!root.visible)
|
||||
return
|
||||
|
||||
// Close all sibling submenus
|
||||
for (var i = 0; i < columnLayout.children.length; i++) {
|
||||
const sibling = columnLayout.children[i]
|
||||
if (sibling !== entry && sibling?.subMenu) {
|
||||
sibling.subMenu.hideMenu()
|
||||
sibling.subMenu.destroy()
|
||||
sibling.subMenu = null
|
||||
}
|
||||
}
|
||||
|
||||
// Create submenu if needed
|
||||
if (modelData?.hasChildren) {
|
||||
if (entry.subMenu) {
|
||||
entry.subMenu.hideMenu()
|
||||
entry.subMenu.destroy()
|
||||
}
|
||||
|
||||
// Need a slight overlap so that menu don't close when moving the mouse to a submenu
|
||||
const submenuWidth = menuWidth // Assuming a similar width as the parent
|
||||
const overlap = 4 // A small overlap to bridge the mouse path
|
||||
|
||||
// Position with overlap
|
||||
const anchorX = -submenuWidth + overlap
|
||||
|
||||
// Create submenu
|
||||
entry.subMenu = Qt.createComponent("TrayMenu.qml").createObject(root, {
|
||||
"menu": modelData,
|
||||
"anchorItem": entry,
|
||||
"anchorX": anchorX,
|
||||
"anchorY": 0,
|
||||
"isSubMenu": true,
|
||||
"screen": root.screen
|
||||
})
|
||||
|
||||
if (entry.subMenu) {
|
||||
entry.subMenu.showAt(entry, anchorX, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
Qt.callLater(() => {
|
||||
if (entry.subMenu && !entry.subMenu.isHovered) {
|
||||
entry.subMenu.hideMenu()
|
||||
entry.subMenu.destroy()
|
||||
entry.subMenu = null
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (subMenu) {
|
||||
subMenu.destroy()
|
||||
subMenu = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user