first commit
This commit is contained in:
179
bar/Bar.qml
Normal file
179
bar/Bar.qml
Normal file
@@ -0,0 +1,179 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
import "../config" as Cfg
|
||||
|
||||
PanelWindow {
|
||||
id: barWindow
|
||||
|
||||
required property var modelData
|
||||
property bool isTop
|
||||
|
||||
readonly property var cfg: Cfg.Config
|
||||
|
||||
color: "transparent"
|
||||
anchors {
|
||||
top: isTop
|
||||
bottom: !isTop
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
implicitHeight: cfg.barHeight
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-bar"
|
||||
WlrLayershell.exclusiveZone: cfg.barHeight
|
||||
|
||||
Item {
|
||||
id: animatedWrapper
|
||||
anchors.fill: parent
|
||||
|
||||
opacity: 0
|
||||
scale: 0.95
|
||||
y: barWindow.isTop ? -20 : 20
|
||||
|
||||
Component.onCompleted: barEntrance.start()
|
||||
|
||||
ParallelAnimation {
|
||||
id: barEntrance
|
||||
|
||||
NumberAnimation {
|
||||
target: animatedWrapper
|
||||
property: "opacity"
|
||||
from: 0; to: 1
|
||||
duration: 400
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: animatedWrapper
|
||||
property: "scale"
|
||||
from: 0.95; to: 1.0
|
||||
duration: 500
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: animatedWrapper
|
||||
property: "y"
|
||||
to: 0
|
||||
duration: 600
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: barContent
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: cfg.margin
|
||||
anchors.rightMargin: cfg.margin
|
||||
|
||||
function bindModule(item, side) {
|
||||
item.height = Qt.binding(() => barContent.height - 12)
|
||||
|
||||
const props = {
|
||||
"panelRadius": cfg.radius / 2,
|
||||
"borderWidth": cfg.panelBorderWidth,
|
||||
"isTop": barWindow.isTop,
|
||||
"targetScreen": barWindow.modelData,
|
||||
"barHeight": cfg.barHeight,
|
||||
"barMargin": cfg.margin,
|
||||
"barSide": side,
|
||||
"menuWindow": barWindow
|
||||
}
|
||||
for (let prop in props) {
|
||||
if (prop in item) item[prop] = props[prop]
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real sideMaxWidth: Math.max(0,
|
||||
(width - centerRow.implicitWidth - 2 * cfg.panelSpacing) / 2)
|
||||
|
||||
Item {
|
||||
id: leftContainer
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
width: Math.max(0, Math.min(leftRow.implicitWidth, barContent.sideMaxWidth))
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
id: leftRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: cfg.moduleSpacing
|
||||
transformOrigin: Item.Left
|
||||
scale: leftContainer.width / Math.max(leftContainer.width, implicitWidth)
|
||||
|
||||
Repeater {
|
||||
model: cfg.leftModules
|
||||
Loader {
|
||||
required property string modelData
|
||||
source: Qt.resolvedUrl("modules/" + modelData + ".qml")
|
||||
onLoaded: barContent.bindModule(item, "left")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rightContainer
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
width: Math.max(0, Math.min(rightRow.implicitWidth, barContent.sideMaxWidth))
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
id: rightRow
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: cfg.moduleSpacing
|
||||
transformOrigin: Item.Right
|
||||
scale: rightContainer.width / Math.max(rightContainer.width, implicitWidth)
|
||||
|
||||
Repeater {
|
||||
model: cfg.rightModules
|
||||
Loader {
|
||||
required property string modelData
|
||||
source: Qt.resolvedUrl("modules/" + modelData + ".qml")
|
||||
onLoaded: barContent.bindModule(item, "right")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: centerContainer
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
width: Math.max(0, Math.min(
|
||||
centerRow.implicitWidth,
|
||||
barContent.width
|
||||
- 2 * (Math.max(leftContainer.width, rightContainer.width)
|
||||
+ cfg.panelSpacing)
|
||||
))
|
||||
x: (barContent.width - width) / 2
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
id: centerRow
|
||||
anchors.centerIn: parent
|
||||
spacing: cfg.moduleSpacing
|
||||
transformOrigin: Item.Center
|
||||
scale: centerContainer.width / Math.max(centerContainer.width, implicitWidth)
|
||||
|
||||
Repeater {
|
||||
model: cfg.centerModules
|
||||
Loader {
|
||||
required property string modelData
|
||||
source: Qt.resolvedUrl("modules/" + modelData + ".qml")
|
||||
onLoaded: barContent.bindModule(item, "center")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
bar/modules/ArchLogo.qml
Normal file
91
bar/modules/ArchLogo.qml
Normal file
@@ -0,0 +1,91 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
property var launcher: null
|
||||
|
||||
readonly property bool launcherActive: !!launcher && launcher.isOpen
|
||||
readonly property bool isHovered: hoverHandler.hovered
|
||||
|
||||
property real spinTarget: 0
|
||||
|
||||
onTargetScreenChanged: Qt.callLater(() => {
|
||||
launcher = (targetScreen && targetScreen.launcher) ? targetScreen.launcher : null
|
||||
})
|
||||
|
||||
onLauncherActiveChanged: {
|
||||
spinTarget += launcherActive ? 360 : -360
|
||||
if (launcherActive && !clickAnim.running)
|
||||
clickAnim.restart()
|
||||
}
|
||||
|
||||
width: Cfg.Config.logoIconSize * 2 + 4
|
||||
height: parent.height
|
||||
|
||||
color: isHovered ? t.archLogoBgHover : t.archLogoBg
|
||||
radius: panelRadius
|
||||
|
||||
border.color: launcherActive ? t.archLogoBorderActive : t.archLogoBorder
|
||||
border.width: borderWidth
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 250 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 250 } }
|
||||
|
||||
HoverHandler { id: hoverHandler }
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
clickAnim.restart()
|
||||
if (root.targetScreen && root.targetScreen.launcher) {
|
||||
root.targetScreen.launcher.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: clickAnim
|
||||
NumberAnimation { target: logoText; property: "scale"; from: 1.0; to: 0.7; duration: 60; easing.type: Easing.OutQuad }
|
||||
NumberAnimation { target: logoText; property: "scale"; from: 0.7; to: 1.2; duration: 150; easing.type: Easing.OutBack }
|
||||
NumberAnimation { target: logoText; property: "scale"; to: 1.0; duration: 100 }
|
||||
}
|
||||
|
||||
Text {
|
||||
id: logoText
|
||||
anchors.centerIn: parent
|
||||
text: Cfg.Config.logoIcon
|
||||
|
||||
color: launcherActive ? t.archLogoIconActive : t.archLogoIcon
|
||||
font.pixelSize: Cfg.Config.logoIconSize
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 250 } }
|
||||
|
||||
scale: clickAnim.running ? logoText.scale : 1.0
|
||||
Behavior on scale {
|
||||
enabled: !clickAnim.running
|
||||
NumberAnimation { duration: 250; easing.type: Easing.OutBack }
|
||||
}
|
||||
|
||||
rotation: root.spinTarget
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: 600
|
||||
easing.type: Easing.InOutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
331
bar/modules/Clock.qml
Normal file
331
bar/modules/Clock.qml
Normal file
@@ -0,0 +1,331 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
readonly property int tzOffsetHours: -timeManager.now.getTimezoneOffset() / 60
|
||||
|
||||
width: clockLayout.implicitWidth + 24
|
||||
height: Cfg.Config.moduleHeight
|
||||
radius: panelRadius
|
||||
|
||||
color: hoverHandler.hovered ? t.clockBgHover : t.clockBg
|
||||
border.color: hoverHandler.hovered ? t.clockBorderHover : t.clockBorder
|
||||
border.width: borderWidth
|
||||
|
||||
anchors.right: parent.right
|
||||
|
||||
Behavior on width { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } }
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: calendarPopup.toggle()
|
||||
}
|
||||
|
||||
Item {
|
||||
id: timeManager
|
||||
property date now: new Date()
|
||||
Timer {
|
||||
interval: 1000; running: true; repeat: true
|
||||
onTriggered: timeManager.now = new Date()
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: clockLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
text: ""
|
||||
color: root.t.clockIcon
|
||||
font.pixelSize: 14
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: 8
|
||||
}
|
||||
|
||||
Text {
|
||||
text: timeManager.now.toLocaleTimeString(Qt.locale(), "HH:mm")
|
||||
color: root.t.clockTime
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: secondsText
|
||||
text: ":" + timeManager.now.toLocaleTimeString(Qt.locale(), "ss")
|
||||
color: root.t.clockSeconds
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
opacity: (hoverHandler.hovered || calendarPopup.popupOpen) ? 1.0 : 0.0
|
||||
visible: opacity > 0
|
||||
Layout.preferredWidth: (hoverHandler.hovered || calendarPopup.popupOpen) ? implicitWidth : 0
|
||||
clip: true
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: calendarPopup
|
||||
screen: root.targetScreen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function open() { hideTimer.stop(); visible = true; popupOpen = true }
|
||||
function close() { popupOpen = false; hideTimer.restart() }
|
||||
function toggle() { popupOpen ? close() : open() }
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: Cfg.Config.popupAnimDuration + 70
|
||||
onTriggered: calendarPopup.visible = false
|
||||
}
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-calendar"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
mask: popupOpen ? null : _noInput
|
||||
Region { id: _noInput }
|
||||
|
||||
TapHandler { onTapped: calendarPopup.close() }
|
||||
|
||||
Item {
|
||||
id: calClipContainer
|
||||
readonly property int cardW: 284
|
||||
readonly property int screenPad: root.barMargin + 10
|
||||
|
||||
x: {
|
||||
let centerX = root.mapToItem(null, 0, 0).x + root.width / 2
|
||||
let desiredX = centerX - cardW / 2
|
||||
return Math.max(screenPad, Math.min(desiredX, parent.width - cardW - screenPad))
|
||||
}
|
||||
width: cardW
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
|
||||
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
|
||||
height: parent.height - root.barHeight - 10
|
||||
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
width: parent.width
|
||||
height: cardCol.implicitHeight + 24
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
|
||||
transform: Translate {
|
||||
y: calendarPopup.popupOpen ? 0 : (root.isTop ? -card.height : card.height)
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Cfg.Config.popupAnimDuration;
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
enabled: calendarPopup.visible
|
||||
}
|
||||
}
|
||||
|
||||
opacity: calendarPopup.popupOpen ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
|
||||
color: root.t.calCardBg
|
||||
radius: Cfg.Config.popupRadius
|
||||
border.color: root.t.calCardBorder
|
||||
border.width: Cfg.Config.popupBorderWidth
|
||||
layer.enabled: true
|
||||
|
||||
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
|
||||
|
||||
ColumnLayout {
|
||||
id: cardCol
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
anchors.margins: 16
|
||||
anchors.topMargin: 18
|
||||
spacing: 14
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
text: timeManager.now.toLocaleDateString(Qt.locale(), "dddd")
|
||||
color: root.t.clockPopupDim
|
||||
font.pixelSize: 12
|
||||
font.capitalization: Font.AllUppercase
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
Text {
|
||||
text: timeManager.now.toLocaleDateString(Qt.locale(), "d MMMM yyyy")
|
||||
color: root.t.clockPopupHeader
|
||||
font.pixelSize: 22
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.calSeparator }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
text: "‹"; color: root.t.clockPopupDim; font.pixelSize: 18; font.bold: true
|
||||
MouseArea {
|
||||
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
cal.displayMonth -= 1
|
||||
if (cal.displayMonth < 0) { cal.displayMonth = 11; cal.displayYear -= 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: new Date(cal.displayYear, cal.displayMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy")
|
||||
color: root.t.clockPopupText
|
||||
font.pixelSize: 13; font.bold: true
|
||||
Layout.fillWidth: true; horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "›"; color: root.t.clockPopupDim; font.pixelSize: 18; font.bold: true
|
||||
MouseArea {
|
||||
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
cal.displayMonth += 1
|
||||
if (cal.displayMonth > 11) { cal.displayMonth = 0; cal.displayYear += 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: cal
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: gridCol.implicitHeight
|
||||
property int displayYear: timeManager.now.getFullYear()
|
||||
property int displayMonth: timeManager.now.getMonth()
|
||||
|
||||
Connections {
|
||||
target: calendarPopup
|
||||
function onPopupOpenChanged() {
|
||||
if (calendarPopup.popupOpen) {
|
||||
cal.displayYear = timeManager.now.getFullYear()
|
||||
cal.displayMonth = timeManager.now.getMonth()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: gridCol
|
||||
anchors.fill: parent
|
||||
spacing: 2
|
||||
|
||||
Row {
|
||||
spacing: 0
|
||||
Repeater {
|
||||
model: ["Mo","Tu","We","Th","Fr","Sa","Su"]
|
||||
Text {
|
||||
width: (card.width - 32) / 7
|
||||
text: modelData; color: root.t.clockPopupDim
|
||||
font.pixelSize: 11; horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: 6
|
||||
Row {
|
||||
id: weekRow
|
||||
property int weekIndex: index
|
||||
Repeater {
|
||||
model: 7
|
||||
delegate: Item {
|
||||
width: (card.width - 32) / 7; height: 26
|
||||
readonly property int dayNum: (weekRow.weekIndex * 7 + index) - ((new Date(cal.displayYear, cal.displayMonth, 1).getDay() + 6) % 7) + 1
|
||||
readonly property bool inMonth: dayNum >= 1 && dayNum <= new Date(cal.displayYear, cal.displayMonth + 1, 0).getDate()
|
||||
readonly property bool isToday: inMonth && dayNum === timeManager.now.getDate() && cal.displayYear === timeManager.now.getFullYear() && cal.displayMonth === timeManager.now.getMonth()
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent; width: 22; height: 22; radius: 11
|
||||
color: isToday ? root.t.accent : "transparent"
|
||||
visible: inMonth
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: inMonth ? dayNum : ""
|
||||
color: isToday ? root.t.todayText : (index >= 5 ? root.t.clockPopupDim : root.t.clockPopupText)
|
||||
font.pixelSize: 12; font.bold: isToday
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.calSeparator }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true; spacing: 12
|
||||
Text { text: ""; color: root.t.clockIcon; font.pixelSize: 16 }
|
||||
ColumnLayout {
|
||||
spacing: 0; Layout.fillWidth: true
|
||||
Text {
|
||||
text: timeManager.now.toLocaleTimeString(Qt.locale(), "t") || "UTC"
|
||||
color: root.t.clockPopupText; font.pixelSize: 12; font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "UTC " + (root.tzOffsetHours >= 0 ? "+" : "") + root.tzOffsetHours
|
||||
color: root.t.clockPopupDim; font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: timeManager.now.toLocaleTimeString(Qt.locale(), "HH:mm:ss")
|
||||
color: root.t.clockTime; font.pixelSize: 20; font.bold: true; font.family: "monospace"
|
||||
}
|
||||
}
|
||||
|
||||
Item { height: 4 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
357
bar/modules/Notifications.qml
Normal file
357
bar/modules/Notifications.qml
Normal file
@@ -0,0 +1,357 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
import "../../config" as Cfg
|
||||
import "../../notifications" as Notif
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
property int unreadCount: 0
|
||||
property bool historyOpen: historyPopup.popupOpen
|
||||
|
||||
width: 35
|
||||
height: 40
|
||||
radius: panelRadius
|
||||
|
||||
color: hoverHandler.hovered ? t.notifBgHover : t.notifBg
|
||||
border.color: (hoverHandler.hovered || historyOpen) ? t.notifBorderActive : t.notifBorder
|
||||
border.width: borderWidth
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
|
||||
HoverHandler { id: hoverHandler; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
if (historyPopup.popupOpen) {
|
||||
historyPopup.close()
|
||||
} else {
|
||||
root.unreadCount = 0
|
||||
historyPopup.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
width: Cfg.Config.notifIconSize + 10
|
||||
height: Cfg.Config.notifIconSize + 10
|
||||
|
||||
Text {
|
||||
id: bellIcon
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: root.unreadCount > 0 ? t.notifIconActive : t.notifIcon
|
||||
font.pixelSize: Cfg.Config.notifIconSize
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.unreadCount > 0
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
width: Math.max(14, badgeLabel.implicitWidth + 6)
|
||||
height: 14
|
||||
radius: 7
|
||||
color: t.notifBadgeBg
|
||||
|
||||
Behavior on width { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
|
||||
Text {
|
||||
id: badgeLabel
|
||||
anchors.centerIn: parent
|
||||
text: root.unreadCount > 99 ? "99+" : root.unreadCount.toString()
|
||||
color: t.notifBadgeText
|
||||
font.pixelSize: 8
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListModel { id: notifHistory }
|
||||
ListModel { id: activeToasts }
|
||||
|
||||
function removeToast(toastId) {
|
||||
for (var i = 0; i < activeToasts.count; i++) {
|
||||
if (activeToasts.get(i).toastId === toastId) {
|
||||
activeToasts.remove(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function urgencyColor(urgency) {
|
||||
if (urgency === NotificationUrgency.Critical) return t.notifUrgencyCritical
|
||||
if (urgency === NotificationUrgency.Normal) return t.notifUrgencyNormal
|
||||
return t.notifUrgencyLow
|
||||
}
|
||||
|
||||
function urgencyIcon(urgency) {
|
||||
if (urgency === NotificationUrgency.Critical) return ""
|
||||
if (urgency === NotificationUrgency.Normal) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
NotificationServer {
|
||||
id: notifServer
|
||||
keepOnReload: false
|
||||
actionIconsSupported: true
|
||||
bodyMarkupSupported: true
|
||||
bodySupported: true
|
||||
persistenceSupported: true
|
||||
imageSupported: true
|
||||
|
||||
onNotification: function(notif) {
|
||||
notif.tracked = true
|
||||
var timeStr = new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
|
||||
var toastId = notif.id + "_" + Date.now()
|
||||
|
||||
notifHistory.insert(0, {
|
||||
nid: notif.id,
|
||||
appName: notif.appName || "Unknown",
|
||||
summary: notif.summary || "",
|
||||
body: notif.body || "",
|
||||
urgency: notif.urgency,
|
||||
timeStr: timeStr
|
||||
})
|
||||
while (notifHistory.count > Cfg.Config.notificationHistorySize)
|
||||
notifHistory.remove(notifHistory.count - 1)
|
||||
|
||||
var cfgTimeout = notif.urgency === NotificationUrgency.Critical
|
||||
? Cfg.Config.notifTimeoutCritical
|
||||
: (notif.urgency === NotificationUrgency.Low
|
||||
? Cfg.Config.notifTimeoutLow
|
||||
: Cfg.Config.notifTimeoutNormal)
|
||||
|
||||
var finalTimeout = (notif.expireTimeout > 0) ? notif.expireTimeout : cfgTimeout
|
||||
|
||||
var toastEntry = {
|
||||
toastId: toastId,
|
||||
nid: notif.id,
|
||||
appName: notif.appName || "Unknown",
|
||||
summary: notif.summary || "",
|
||||
body: notif.body || "",
|
||||
urgency: notif.urgency,
|
||||
timeStr: timeStr,
|
||||
timeout: finalTimeout
|
||||
}
|
||||
if (toastLayer.atBottom) activeToasts.append(toastEntry)
|
||||
else activeToasts.insert(0, toastEntry)
|
||||
|
||||
if (!root.historyOpen) root.unreadCount++
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: toastLayer
|
||||
screen: root.targetScreen
|
||||
visible: activeToasts.count > 0
|
||||
color: "transparent"
|
||||
|
||||
readonly property string pos: Cfg.Config.notificationPosition
|
||||
readonly property bool atRight: pos === "topright" || pos === "bottomright"
|
||||
readonly property bool atBottom: pos === "bottomleft" || pos === "bottomright"
|
||||
|
||||
anchors { top: true; bottom: true; right: atRight; left: !atRight }
|
||||
implicitWidth: 350 + root.barMargin + 4
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.namespace: "main-shell-notifications-toasts"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
mask: Region { item: toastCol }
|
||||
|
||||
readonly property bool shareEdge: (root.isTop && !atBottom) || (!root.isTop && atBottom)
|
||||
readonly property int edgeGap: shareEdge ? root.barMargin + root.barHeight + 10 : root.barMargin + 10
|
||||
|
||||
Column {
|
||||
id: toastCol
|
||||
x: !toastLayer.atRight ? 4 : 4
|
||||
y: !toastLayer.atBottom ? toastLayer.edgeGap : parent.height - toastLayer.edgeGap - toastCol.height
|
||||
spacing: 8
|
||||
width: 350
|
||||
|
||||
Repeater {
|
||||
model: activeToasts
|
||||
Notif.NotificationCard {
|
||||
width: toastCol.width
|
||||
toastId: model.toastId
|
||||
appName: model.appName
|
||||
summary: model.summary
|
||||
body: model.body
|
||||
urgency: model.urgency
|
||||
timeStr: model.timeStr
|
||||
timeout: model.timeout
|
||||
t: root.t
|
||||
urgencyColor: root.urgencyColor
|
||||
urgencyIcon: root.urgencyIcon
|
||||
onDismissed: (id) => root.removeToast(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: historyPopup
|
||||
screen: root.targetScreen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function open() { hideTimer.stop(); visible = true; popupOpen = true }
|
||||
function close() { popupOpen = false; hideTimer.restart() }
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: Cfg.Config.popupAnimDuration + 70
|
||||
onTriggered: historyPopup.visible = false
|
||||
}
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-notifications-history"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
mask: popupOpen ? null : _noInput
|
||||
Region { id: _noInput }
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: historyPopup.close()
|
||||
}
|
||||
|
||||
Item {
|
||||
id: histClipContainer
|
||||
readonly property int cardW: 340
|
||||
readonly property int screenPad: root.barMargin + 10
|
||||
|
||||
x: {
|
||||
var cx = root.mapToItem(null, 0, 0).x + root.width / 2
|
||||
return Math.max(screenPad, Math.min(cx - cardW / 2, parent.width - cardW - screenPad))
|
||||
}
|
||||
width: cardW
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
|
||||
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
|
||||
height: parent.height - root.barHeight - 10
|
||||
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: historyCard
|
||||
width: parent.width
|
||||
height: Math.min(histHeader.implicitHeight + 56 + (notifHistory.count > 0 ? notifHistory.count * 76 : 90), 520)
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
|
||||
transform: Translate {
|
||||
y: historyPopup.popupOpen ? 0 : (root.isTop ? -historyCard.height : historyCard.height)
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Cfg.Config.popupAnimDuration;
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
enabled: historyPopup.visible
|
||||
}
|
||||
}
|
||||
|
||||
opacity: historyPopup.popupOpen ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
|
||||
color: t.notifHistoryBg
|
||||
radius: Cfg.Config.popupRadius
|
||||
border.color: t.notifHistoryBorder
|
||||
border.width: Cfg.Config.popupBorderWidth
|
||||
layer.enabled: true
|
||||
|
||||
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
|
||||
|
||||
RowLayout {
|
||||
id: histHeader
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
anchors.topMargin: 20; anchors.leftMargin: 20; anchors.rightMargin: 20
|
||||
spacing: 10
|
||||
|
||||
Text { text: ""; color: t.notifToastAppName; font.pixelSize: 18; Layout.alignment: Qt.AlignVCenter }
|
||||
Text {
|
||||
text: "Notifications"; color: t.notifToastAppName; font.pixelSize: 15; font.bold: true
|
||||
Layout.fillWidth: true; Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: notifHistory.count > 0
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
width: clearText.implicitWidth + 16
|
||||
height: 24
|
||||
radius: 12
|
||||
color: clearHover.hovered ? t.notifHistoryHover : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
|
||||
Text { id: clearText; anchors.centerIn: parent; text: "Clear all"; color: t.notifToastDim; font.pixelSize: 11 }
|
||||
HoverHandler { id: clearHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: { notifHistory.clear(); root.unreadCount = 0 } }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: histDivider
|
||||
anchors { left: parent.left; right: parent.right; top: histHeader.bottom }
|
||||
anchors.topMargin: 12; height: 1; color: t.notifSeparator
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: histFlick
|
||||
anchors { left: parent.left; right: parent.right; top: histDivider.bottom; bottom: parent.bottom }
|
||||
anchors.leftMargin: 8; anchors.rightMargin: 8
|
||||
anchors.topMargin: 6; anchors.bottomMargin: 8; clip: true
|
||||
contentHeight: histCol.implicitHeight
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
|
||||
Column {
|
||||
id: histCol
|
||||
width: histFlick.width; spacing: 2; topPadding: 4; bottomPadding: 4; leftPadding: 8; rightPadding: 8
|
||||
|
||||
Item {
|
||||
width: histCol.width - 16; height: 90; visible: notifHistory.count === 0
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent; spacing: 8
|
||||
Text { text: ""; color: t.notifHistoryEmpty; font.pixelSize: 30; Layout.alignment: Qt.AlignHCenter }
|
||||
Text { text: "No notifications"; color: t.notifHistoryEmpty; font.pixelSize: 12; Layout.alignment: Qt.AlignHCenter }
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: notifHistory
|
||||
Notif.HistoryCard {
|
||||
width: histCol.width - 16; x: 8; itemIndex: index
|
||||
appName: model.appName; summary: model.summary; body: model.body; urgency: model.urgency; timeStr: model.timeStr
|
||||
t: root.t; urgencyColor: root.urgencyColor; urgencyIcon: root.urgencyIcon
|
||||
onDismissed: (idx) => notifHistory.remove(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
181
bar/modules/Stats.qml
Normal file
181
bar/modules/Stats.qml
Normal file
@@ -0,0 +1,181 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
width: statsLayout.implicitWidth + 20
|
||||
height: Cfg.Config.moduleHeight
|
||||
color: t.statsBg
|
||||
radius: panelRadius
|
||||
border.color: t.statsBorder
|
||||
border.width: borderWidth
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 250; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
HoverHandler { cursorShape: Qt.PointingHandCursor }
|
||||
|
||||
property real cpuVal: 0
|
||||
property real memVal: 0
|
||||
property real diskVal: 0
|
||||
|
||||
Timer {
|
||||
interval: 2000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
cpuProc.running = true
|
||||
memProc.running = true
|
||||
diskProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cpuProc
|
||||
command: ["sh", "-c", "top -bn1 | awk '/^%Cpu/ {print 100-$8}'"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
let val = parseFloat(data.trim())
|
||||
root.cpuVal = isNaN(val) ? 0 : val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: memProc
|
||||
command: ["sh", "-c", "free | awk '/Mem:/ {print ($2-$7)/$2 * 100}'"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
let val = parseFloat(data.trim())
|
||||
root.memVal = isNaN(val) ? 0 : val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: diskProc
|
||||
command: ["sh", "-c", "df / --output=pcent | tail -1 | tr -dc '0-9'"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
let val = parseFloat(data.trim())
|
||||
root.diskVal = isNaN(val) ? 0 : val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component StatRing: Item {
|
||||
id: ring
|
||||
|
||||
property real value: 0.0
|
||||
property string label: ""
|
||||
property color ringColor: Cfg.Config.theme.accent
|
||||
property color trackColor: Cfg.Config.theme.statsTrackColor
|
||||
property color labelColor: Cfg.Config.theme.textMain
|
||||
|
||||
Behavior on value {
|
||||
NumberAnimation {
|
||||
duration: 800
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real fraction: Math.max(0, Math.min(value, 100)) / 100
|
||||
|
||||
width: Cfg.Config.statsRingSize
|
||||
height: Cfg.Config.statsRingSize
|
||||
|
||||
onFractionChanged: arc.requestPaint()
|
||||
onRingColorChanged: arc.requestPaint()
|
||||
|
||||
Canvas {
|
||||
id: arc
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
var cx = width / 2
|
||||
var cy = height / 2
|
||||
var lw = 2.8
|
||||
var r = Math.min(cx, cy) - lw / 2 - 0.5
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, r, 0, Math.PI * 2)
|
||||
ctx.strokeStyle = ring.trackColor
|
||||
ctx.lineWidth = lw
|
||||
ctx.stroke()
|
||||
|
||||
if (ring.fraction > 0) {
|
||||
var start = -Math.PI / 2
|
||||
var end = start + (ring.fraction * Math.PI * 2)
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, r, start, end)
|
||||
ctx.strokeStyle = ring.ringColor
|
||||
ctx.lineWidth = lw
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler { id: hov }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: Cfg.Config.statsShowPercentByDefault
|
||||
? (hov.hovered ? ring.label : Math.round(ring.value) + "%")
|
||||
: (hov.hovered ? Math.round(ring.value) + "%" : ring.label)
|
||||
color: ring.labelColor
|
||||
font.pixelSize: Cfg.Config.statsRingFontSize
|
||||
font.bold: true
|
||||
font.family: "monospace"
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: statsLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 10
|
||||
|
||||
StatRing {
|
||||
value: root.cpuVal
|
||||
label: "cpu"
|
||||
ringColor: root.t.statsCpuColor
|
||||
trackColor: root.t.statsTrackColor
|
||||
labelColor: root.t.statsText
|
||||
}
|
||||
|
||||
StatRing {
|
||||
value: root.memVal
|
||||
label: "ram"
|
||||
ringColor: root.t.statsMemColor
|
||||
trackColor: root.t.statsTrackColor
|
||||
labelColor: root.t.statsText
|
||||
}
|
||||
|
||||
StatRing {
|
||||
value: root.diskVal
|
||||
label: "disk"
|
||||
ringColor: root.t.statsDiskColor
|
||||
trackColor: root.t.statsTrackColor
|
||||
labelColor: root.t.statsText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
161
bar/modules/Tray.qml
Normal file
161
bar/modules/Tray.qml
Normal file
@@ -0,0 +1,161 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property string barSide: "right"
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
property bool expanded: false
|
||||
property bool manuallyExpanded: false
|
||||
property var menuWindow: null
|
||||
|
||||
implicitWidth: bgRect.width
|
||||
implicitHeight: bgRect.height
|
||||
|
||||
Rectangle {
|
||||
id: bgRect
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
height: Cfg.Config.trayHeight
|
||||
width: mainRow.width
|
||||
radius: root.panelRadius
|
||||
|
||||
color: bgHover.hovered ? root.t.trayBgHover : root.t.trayBg
|
||||
border.color: bgHover.hovered ? root.t.trayBorderHover : root.t.trayBorder
|
||||
border.width: root.borderWidth
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
HoverHandler {
|
||||
id: bgHover
|
||||
onHoveredChanged: {
|
||||
if (!root.manuallyExpanded) {
|
||||
root.expanded = hovered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: mainRow
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
leftPadding: root.expanded && root.barSide === "right" ? 8 : 0
|
||||
rightPadding: root.expanded && 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.expanded ? 4 : 0
|
||||
|
||||
Behavior on spacing { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } }
|
||||
|
||||
Item {
|
||||
id: toggleArea
|
||||
width: Cfg.Config.trayHeight
|
||||
height: Cfg.Config.trayHeight
|
||||
z: 10
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
root.manuallyExpanded = !root.manuallyExpanded;
|
||||
root.expanded = root.manuallyExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.expanded
|
||||
? (root.barSide === "left" ? "" : "")
|
||||
: (root.barSide === "left" ? "" : "")
|
||||
color: root.t.trayIcon
|
||||
font.pixelSize: Cfg.Config.trayHeight * 0.55
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: iconsRow
|
||||
spacing: 6
|
||||
clip: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
width: root.expanded ? implicitWidth : 0
|
||||
opacity: root.expanded ? 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) {
|
||||
menuAnchor.open()
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: function(wheel) {
|
||||
wheel.accepted = true
|
||||
item.scroll(wheel.angleDelta.y / 120, false)
|
||||
}
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: item.icon
|
||||
}
|
||||
|
||||
QsMenuAnchor {
|
||||
id: menuAnchor
|
||||
menu: item.menu
|
||||
anchor.window: root.menuWindow
|
||||
anchor.adjustment: PopupAdjustment.Flip
|
||||
anchor.onAnchoring: {
|
||||
if (!root.menuWindow) return
|
||||
const global = iconDelegate.mapToGlobal(0, iconDelegate.height)
|
||||
const local = root.menuWindow.contentItem.mapFromGlobal(
|
||||
global.x, global.y
|
||||
)
|
||||
menuAnchor.anchor.rect = Qt.rect(
|
||||
local.x, local.y,
|
||||
iconDelegate.width, 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
530
bar/modules/Volume.qml
Normal file
530
bar/modules/Volume.qml
Normal file
@@ -0,0 +1,530 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
property int outputMaxVolume: Cfg.Config.volumeOutputMax
|
||||
property int inputMaxVolume: Cfg.Config.volumeInputMax
|
||||
|
||||
readonly property var sink: Pipewire.defaultAudioSink
|
||||
readonly property var source: Pipewire.defaultAudioSource
|
||||
|
||||
PwObjectTracker {
|
||||
objects: root.sinkNodes.concat(root.sourceNodes)
|
||||
}
|
||||
|
||||
readonly property var sinkNodes: {
|
||||
if (!Pipewire.nodes) return []
|
||||
return Pipewire.nodes.values.filter(n => !n.isStream && n.isSink)
|
||||
}
|
||||
readonly property var sourceNodes: {
|
||||
if (!Pipewire.nodes) return []
|
||||
return Pipewire.nodes.values.filter(n => !n.isStream && !n.isSink)
|
||||
}
|
||||
|
||||
property int _outVol: 0
|
||||
property int _inVol: 0
|
||||
|
||||
readonly property int outputVolume: {
|
||||
if (sink && sink.ready) { _outVol = Math.round(sink.audio.volume * 100); return _outVol }
|
||||
return _outVol
|
||||
}
|
||||
readonly property int inputVolume: {
|
||||
if (source && source.ready) { _inVol = Math.round(source.audio.volume * 100); return _inVol }
|
||||
return _inVol
|
||||
}
|
||||
|
||||
readonly property bool outputMuted: sink && sink.ready ? sink.audio.muted : false
|
||||
readonly property bool inputMuted: source && source.ready ? source.audio.muted : false
|
||||
|
||||
readonly property string outputDeviceName: sink ? (sink.description || sink.nickName || sink.name) : "No device"
|
||||
readonly property string inputDeviceName: source ? (source.description || source.nickName || source.name) : "No device"
|
||||
|
||||
function setOutputVol(pct) { if (sink && sink.ready) sink.audio.volume = pct / 100 }
|
||||
function setInputVol(pct) { if (source && source.ready) source.audio.volume = pct / 100 }
|
||||
function toggleOutputMute() { if (sink && sink.ready) sink.audio.muted = !sink.audio.muted }
|
||||
function toggleInputMute() { if (source && source.ready) source.audio.muted = !source.audio.muted }
|
||||
|
||||
Process {
|
||||
id: defaultSetter
|
||||
running: false
|
||||
property string nodeId: "0"
|
||||
command: ["wpctl", "set-default", nodeId]
|
||||
}
|
||||
function setDefaultDevice(node) {
|
||||
defaultSetter.nodeId = node.id.toString()
|
||||
defaultSetter.running = true
|
||||
}
|
||||
|
||||
readonly property string iconSpeakerHigh: ""
|
||||
readonly property string iconSpeakerMed: ""
|
||||
readonly property string iconSpeakerLow: ""
|
||||
readonly property string iconSpeakerMuted: ""
|
||||
readonly property string iconMic: ""
|
||||
readonly property string iconMicMuted: ""
|
||||
|
||||
function speakerGlyph(muted, vol) {
|
||||
if (muted || vol === 0) return iconSpeakerMuted
|
||||
if (vol > 60) return iconSpeakerHigh
|
||||
if (vol > 25) return iconSpeakerMed
|
||||
return iconSpeakerLow
|
||||
}
|
||||
|
||||
width: chipLayout.implicitWidth + 15
|
||||
height: Cfg.Config.moduleHeight
|
||||
radius: panelRadius
|
||||
|
||||
color: hoverHandler.hovered ? t.volBgHover : t.volBg
|
||||
border.color: (hoverHandler.hovered || volumePopup.popupOpen) ? t.volBorderHover : t.volBorder
|
||||
border.width: borderWidth
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
HoverHandler { id: hoverHandler; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: volumePopup.toggle() }
|
||||
|
||||
RowLayout {
|
||||
id: chipLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: root.speakerGlyph(root.outputMuted, root.outputVolume)
|
||||
color: root.outputMuted ? root.t.volIconMuted : root.t.volIcon
|
||||
font.pixelSize: Cfg.Config.volumeIconSize
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: volumePopup
|
||||
screen: root.targetScreen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function open() { hideTimer.stop(); visible = true; popupOpen = true }
|
||||
function close() { popupOpen = false; hideTimer.restart() }
|
||||
function toggle() { popupOpen ? close() : open() }
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: Cfg.Config.popupAnimDuration + 70
|
||||
onTriggered: volumePopup.visible = false
|
||||
}
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-volume"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
mask: popupOpen ? null : _noInput
|
||||
Region { id: _noInput }
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: volumePopup.close()
|
||||
}
|
||||
|
||||
Item {
|
||||
id: volClipContainer
|
||||
readonly property int cardW: Cfg.Config.volumePopupWidth
|
||||
readonly property int screenPad: root.barMargin + 10
|
||||
|
||||
x: {
|
||||
let cx = root.mapToItem(null, 0, 0).x + root.width / 2
|
||||
return Math.max(screenPad, Math.min(cx - cardW / 2, parent.width - cardW - screenPad))
|
||||
}
|
||||
width: cardW
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
|
||||
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
|
||||
height: parent.height - root.barHeight - 10
|
||||
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: volCard
|
||||
width: parent.width
|
||||
height: popupCol.implicitHeight + 32
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
|
||||
transform: Translate {
|
||||
y: volumePopup.popupOpen ? 0 : (root.isTop ? -volCard.height : volCard.height)
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Cfg.Config.popupAnimDuration;
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
enabled: volumePopup.visible
|
||||
}
|
||||
}
|
||||
|
||||
opacity: volumePopup.popupOpen ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
|
||||
radius: Cfg.Config.popupRadius
|
||||
color: root.t.volPopupBg
|
||||
border.color: root.t.volPopupBorder
|
||||
border.width: Cfg.Config.popupBorderWidth
|
||||
layer.enabled: true
|
||||
|
||||
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
|
||||
|
||||
ColumnLayout {
|
||||
id: popupCol
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
anchors.margins: 16
|
||||
anchors.topMargin: 18
|
||||
spacing: 14
|
||||
|
||||
VolumeSection {
|
||||
id: outputSection
|
||||
Layout.fillWidth: true
|
||||
label: "Output"
|
||||
icon: root.speakerGlyph(root.outputMuted, root.outputVolume)
|
||||
iconColor: root.t.accent
|
||||
volume: root.outputVolume
|
||||
maxVolume: root.outputMaxVolume
|
||||
muted: root.outputMuted
|
||||
accentColor: root.t.volHandle
|
||||
dimColor: root.t.textDim
|
||||
textColor: root.t.textMain
|
||||
trackColor: root.t.volTrack
|
||||
deviceName: root.outputDeviceName
|
||||
nodes: root.sinkNodes
|
||||
currentNode: root.sink
|
||||
onSliderMoved: vol => root.setOutputVol(vol)
|
||||
onMuteToggled: root.toggleOutputMute()
|
||||
onDeviceSelected: node => root.setDefaultDevice(node)
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.separator }
|
||||
|
||||
VolumeSection {
|
||||
id: inputSection
|
||||
Layout.fillWidth: true
|
||||
label: "Input"
|
||||
icon: root.inputMuted ? root.iconMicMuted : root.iconMic
|
||||
iconColor: root.t.statusInfo
|
||||
volume: root.inputVolume
|
||||
maxVolume: root.inputMaxVolume
|
||||
muted: root.inputMuted
|
||||
accentColor: root.t.statusInfo
|
||||
dimColor: root.t.textDim
|
||||
textColor: root.t.textMain
|
||||
trackColor: root.t.volTrack
|
||||
deviceName: root.inputDeviceName
|
||||
nodes: root.sourceNodes
|
||||
currentNode: root.source
|
||||
onSliderMoved: vol => root.setInputVol(vol)
|
||||
onMuteToggled: root.toggleInputMute()
|
||||
onDeviceSelected: node => root.setDefaultDevice(node)
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.separator }
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
Text {
|
||||
text: "Volume Limits"
|
||||
color: root.t.textDim
|
||||
font.pixelSize: 10
|
||||
font.capitalization: Font.AllUppercase
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
LimitRow {
|
||||
Layout.fillWidth: true
|
||||
icon: root.iconSpeakerHigh
|
||||
iconColor: root.t.accent
|
||||
labelText: "Output max"
|
||||
value: root.outputMaxVolume
|
||||
minValue: 10
|
||||
maxValue: 300
|
||||
textColor: root.t.textMain
|
||||
dimColor: root.t.textDim
|
||||
borderColor: root.t.volPopupBorder
|
||||
onDecrement: root.outputMaxVolume = Math.max(10, root.outputMaxVolume - 10)
|
||||
onIncrement: root.outputMaxVolume = Math.min(300, root.outputMaxVolume + 10)
|
||||
}
|
||||
LimitRow {
|
||||
Layout.fillWidth: true
|
||||
icon: root.iconMic
|
||||
iconColor: root.t.statusInfo
|
||||
labelText: "Input max"
|
||||
value: root.inputMaxVolume
|
||||
minValue: 10
|
||||
maxValue: 300
|
||||
textColor: root.t.textMain
|
||||
dimColor: root.t.textDim
|
||||
borderColor: root.t.volPopupBorder
|
||||
onDecrement: root.inputMaxVolume = Math.max(10, root.inputMaxVolume - 10)
|
||||
onIncrement: root.inputMaxVolume = Math.min(300, root.inputMaxVolume + 10)
|
||||
}
|
||||
}
|
||||
Item { height: 2 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component VolumeSection: ColumnLayout {
|
||||
id: vs
|
||||
spacing: 8
|
||||
required property string label
|
||||
required property string icon
|
||||
required property color iconColor
|
||||
required property int volume
|
||||
required property int maxVolume
|
||||
required property bool muted
|
||||
required property color accentColor
|
||||
required property color dimColor
|
||||
required property color textColor
|
||||
required property color trackColor
|
||||
required property string deviceName
|
||||
required property var nodes
|
||||
required property var currentNode
|
||||
|
||||
signal sliderMoved(int vol)
|
||||
signal muteToggled()
|
||||
signal deviceSelected(var node)
|
||||
|
||||
property bool pickerOpen: false
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
Text {
|
||||
text: vs.icon
|
||||
color: vs.muted ? vs.dimColor : vs.iconColor
|
||||
font.pixelSize: Cfg.Config.volumeIconSize
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: Cfg.Config.volumeIconSize + 4
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
Text {
|
||||
text: vs.label
|
||||
color: vs.textColor
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: vs.muted ? "muted" : vs.volume + "%"
|
||||
color: vs.muted ? vs.dimColor : vs.textColor
|
||||
font.pixelSize: 12
|
||||
font.bold: !vs.muted
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.preferredWidth: 44
|
||||
}
|
||||
Rectangle {
|
||||
width: 22; height: 22; radius: 6
|
||||
color: muteHover.hovered ? (vs.muted ? "#33ffffff" : "#22ffffff") : (vs.muted ? root.t.volMuteBg : "transparent")
|
||||
border.color: vs.muted ? vs.accentColor : vs.dimColor
|
||||
border.width: 1
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: vs.muted ? "" : ""
|
||||
color: vs.muted ? vs.accentColor : vs.dimColor
|
||||
font.pixelSize: 11
|
||||
}
|
||||
HoverHandler { id: muteHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: vs.muteToggled() }
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: sliderItem
|
||||
Layout.fillWidth: true
|
||||
height: 22
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width; height: Cfg.Config.volumeSliderHeight; radius: 2
|
||||
color: vs.trackColor
|
||||
Rectangle {
|
||||
width: Math.min(parent.width, parent.width * (vs.volume / Math.max(1, vs.maxVolume)))
|
||||
height: parent.height; radius: parent.radius
|
||||
color: vs.muted ? vs.dimColor : vs.accentColor
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
readonly property real ratio: vs.volume / Math.max(1, vs.maxVolume)
|
||||
x: Math.min(sliderItem.width - width, Math.max(0, sliderItem.width * ratio - width / 2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Cfg.Config.volumeHandleSize; height: Cfg.Config.volumeHandleSize; radius: width / 2
|
||||
color: vs.muted ? vs.dimColor : vs.accentColor
|
||||
border.color: root.t.volHandleBorder
|
||||
border.width: 1
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
|
||||
onPressed: pos => sliderItem.seek(pos.x)
|
||||
onPositionChanged: pos => { if (pressed) sliderItem.seek(pos.x) }
|
||||
}
|
||||
function seek(x) {
|
||||
let ratio = Math.max(0.0, Math.min(1.0, x / sliderItem.width))
|
||||
vs.sliderMoved(Math.round(ratio * vs.maxVolume))
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
Text { text: ""; color: vs.dimColor; font.pixelSize: 11 }
|
||||
Text {
|
||||
text: vs.deviceName
|
||||
color: vs.textColor
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Rectangle {
|
||||
width: 20; height: 20; radius: 5
|
||||
color: chevHover.hovered ? "#22ffffff" : "transparent"
|
||||
border.color: vs.pickerOpen ? vs.accentColor : root.t.volPopupBorder
|
||||
border.width: 1
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: vs.pickerOpen ? "" : ""
|
||||
color: vs.pickerOpen ? vs.accentColor : vs.dimColor
|
||||
font.pixelSize: 10
|
||||
}
|
||||
HoverHandler { id: chevHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: vs.pickerOpen = !vs.pickerOpen }
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: vs.pickerOpen ? Math.min(deviceFlickable.contentHeight, 120) : 0
|
||||
opacity: vs.pickerOpen ? 1.0 : 0.0
|
||||
clip: true
|
||||
|
||||
Behavior on Layout.preferredHeight { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
Behavior on opacity { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
|
||||
Flickable {
|
||||
id: deviceFlickable
|
||||
anchors.fill: parent
|
||||
contentWidth: width
|
||||
contentHeight: deviceList.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceList
|
||||
width: deviceFlickable.width
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: vs.nodes
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
readonly property bool isActive: vs.currentNode !== null && modelData.id === vs.currentNode.id
|
||||
Layout.fillWidth: true; height: 28; radius: 7
|
||||
color: isActive ? Qt.alpha(vs.accentColor, 0.18) : (rowHover.hovered ? root.t.volPickerHover : "transparent")
|
||||
border.color: isActive ? Qt.alpha(vs.accentColor, 0.45) : "transparent"
|
||||
border.width: 1
|
||||
RowLayout {
|
||||
anchors { fill: parent; leftMargin: 8; rightMargin: 8 }
|
||||
spacing: 8
|
||||
Rectangle {
|
||||
width: 6; height: 6; radius: 3
|
||||
color: isActive ? vs.accentColor : "transparent"
|
||||
border.color: isActive ? "transparent" : vs.dimColor
|
||||
border.width: 1
|
||||
}
|
||||
Text {
|
||||
text: modelData.description || modelData.nickName || modelData.name
|
||||
color: isActive ? vs.accentColor : vs.textColor
|
||||
font.pixelSize: 11; font.bold: isActive
|
||||
elide: Text.ElideRight; Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
HoverHandler { id: rowHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: { vs.deviceSelected(modelData); vs.pickerOpen = false } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component LimitRow: RowLayout {
|
||||
id: lr
|
||||
spacing: 8
|
||||
required property string icon
|
||||
required property color iconColor
|
||||
required property string labelText
|
||||
required property int value
|
||||
required property int minValue
|
||||
required property int maxValue
|
||||
required property color textColor
|
||||
required property color dimColor
|
||||
required property color borderColor
|
||||
|
||||
signal decrement()
|
||||
signal increment()
|
||||
|
||||
Text { text: lr.icon; color: lr.iconColor; font.pixelSize: 13 }
|
||||
Text {
|
||||
text: lr.labelText
|
||||
color: lr.textColor
|
||||
font.pixelSize: 11
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Rectangle {
|
||||
width: 22; height: 22; radius: 5
|
||||
color: decHover.hovered ? "#22ffffff" : "transparent"
|
||||
border.color: lr.borderColor; border.width: 1
|
||||
Text {
|
||||
anchors.centerIn: parent; text: "−"
|
||||
color: lr.value <= lr.minValue ? lr.dimColor : lr.textColor
|
||||
font.pixelSize: 14
|
||||
}
|
||||
HoverHandler { id: decHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: { if (lr.value > lr.minValue) lr.decrement() } }
|
||||
}
|
||||
Text {
|
||||
text: lr.value + "%"
|
||||
color: lr.textColor
|
||||
font.pixelSize: 12; font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.preferredWidth: 44
|
||||
}
|
||||
Rectangle {
|
||||
width: 22; height: 22; radius: 5
|
||||
color: incHover.hovered ? "#22ffffff" : "transparent"
|
||||
border.color: lr.borderColor; border.width: 1
|
||||
Text {
|
||||
anchors.centerIn: parent; text: "+"
|
||||
color: lr.value >= lr.maxValue ? lr.dimColor : lr.textColor
|
||||
font.pixelSize: 14
|
||||
}
|
||||
HoverHandler { id: incHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: { if (lr.value < lr.maxValue) lr.increment() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
62
bar/modules/Workspace.qml
Normal file
62
bar/modules/Workspace.qml
Normal file
@@ -0,0 +1,62 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Hyprland
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
width: workspaceLayout.implicitWidth + 24
|
||||
height: Cfg.Config.moduleHeight
|
||||
color: t.wsPanelBg
|
||||
radius: panelRadius
|
||||
border.color: t.wsPanelBorder
|
||||
border.width: borderWidth
|
||||
|
||||
RowLayout {
|
||||
id: workspaceLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: Hyprland.workspaces
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: modelData.active ? 34 : 28
|
||||
Layout.preferredHeight: 22
|
||||
radius: 11
|
||||
|
||||
color: modelData.active
|
||||
? root.t.wsActiveBg
|
||||
: (wsHover.containsMouse ? root.t.wsHoverBg : root.t.wsInactiveBg)
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 200 } }
|
||||
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.name
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
color: modelData.active
|
||||
? root.t.wsActiveText
|
||||
: (wsHover.containsMouse ? root.t.wsHoverText : root.t.wsInactiveText)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: wsHover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: modelData.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user