better structure

This commit is contained in:
2025-10-19 00:14:19 +02:00
parent 057afc086e
commit 8733656ed9
630 changed files with 81 additions and 137 deletions

View 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
}
}
}

View 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
}
}
}
}

View 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
})
}
}
}
}

View 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
}
}
}
}
}
}
}