rewrite bar with quickshell
This commit is contained in:
260
quickshell/Modules/Bar/Bar.qml
Normal file
260
quickshell/Modules/Bar/Bar.qml
Normal file
@@ -0,0 +1,260 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.UPower
|
||||
import Quickshell.Wayland
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Components
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Modules.Misc
|
||||
import qs.Services
|
||||
|
||||
Scope {
|
||||
id: rootScope
|
||||
|
||||
property var shell
|
||||
|
||||
Item {
|
||||
id: barRootItem
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
Item {
|
||||
property var modelData
|
||||
|
||||
PanelWindow {
|
||||
id: panel
|
||||
|
||||
screen: modelData
|
||||
WlrLayershell.namespace: "quickshell-bar"
|
||||
color: Colors.transparent
|
||||
implicitHeight: 45
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
right: true
|
||||
top: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: barBackground
|
||||
|
||||
anchors.fill: parent
|
||||
color: Niri.noFocus ? null : Colors.base
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: Qt.rgba(Colors.base.r, Colors.base.g, Colors.base.b, Niri.noFocus ? 0.8 : 1)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: Qt.rgba(Colors.base.r, Colors.base.g, Colors.base.b, Niri.noFocus ? 0 : 1)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: leftLayout
|
||||
|
||||
height: parent.height - 10
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: 5
|
||||
}
|
||||
|
||||
SymbolButton {
|
||||
symbol: Icons.distro
|
||||
buttonColor: Colors.distroColor
|
||||
onRightClicked: {
|
||||
if (action.running) {
|
||||
action.signal(15);
|
||||
return ;
|
||||
}
|
||||
action.exec(["rofi", "-show", "drun"]);
|
||||
}
|
||||
}
|
||||
|
||||
Separator {
|
||||
}
|
||||
|
||||
Workspace {
|
||||
screen: modelData
|
||||
}
|
||||
|
||||
Separator {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 10
|
||||
}
|
||||
|
||||
CavaBar {
|
||||
count: 6
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 10
|
||||
}
|
||||
|
||||
Separator {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 10
|
||||
}
|
||||
|
||||
FocusedWindow {
|
||||
maxWidth: 400
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: middleLayout
|
||||
|
||||
height: parent.height - 10
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Time {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rightLayout
|
||||
|
||||
height: parent.height - 10
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
rightMargin: 5
|
||||
}
|
||||
|
||||
NetworkSpeed {
|
||||
}
|
||||
|
||||
Separator {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 10
|
||||
}
|
||||
|
||||
Ip {
|
||||
showCountryCode: true
|
||||
}
|
||||
|
||||
CpuTemp {
|
||||
}
|
||||
|
||||
MemUsage {
|
||||
}
|
||||
|
||||
CpuUsage {
|
||||
}
|
||||
|
||||
Battery {
|
||||
}
|
||||
|
||||
Brightness {
|
||||
screen: modelData
|
||||
}
|
||||
|
||||
Volume {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 5
|
||||
}
|
||||
|
||||
Separator {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 5
|
||||
}
|
||||
|
||||
TrayExpander {
|
||||
screen: modelData
|
||||
}
|
||||
|
||||
SymbolButton {
|
||||
symbol: Caffeine.isInhibited ? Icons.idleInhibitorActivated : Icons.idleInhibitorDeactivated
|
||||
buttonColor: Caffeine.isInhibited ? Colors.peach : Colors.yellow
|
||||
onClicked: {
|
||||
Caffeine.manualToggle();
|
||||
}
|
||||
|
||||
Behavior on buttonColor {
|
||||
ColorAnimation {
|
||||
duration: 500
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SymbolButton {
|
||||
symbol: Icons.powerMenu
|
||||
buttonColor: Colors.red
|
||||
onClicked: {
|
||||
if (action.running) {
|
||||
action.signal(15);
|
||||
return ;
|
||||
}
|
||||
action.exec(["wlogout"]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
22
quickshell/Modules/Bar/Components/Battery.qml
Normal file
22
quickshell/Modules/Bar/Components/Battery.qml
Normal file
@@ -0,0 +1,22 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Services
|
||||
|
||||
MonitorItem {
|
||||
readonly property var battery: UPower.displayDevice
|
||||
readonly property bool isReady: (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
||||
readonly property real percent: (isReady ? (battery.percentage * 100) : 0)
|
||||
readonly property bool charging: (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
||||
property int lowBatteryThreshold: 15
|
||||
|
||||
symbol: {
|
||||
return charging ? Icons.charging : percent >= 80 ? Icons.battery100 : percent >= 60 ? Icons.battery75 : percent >= 40 ? Icons.battery50 : percent >= 20 ? Icons.battery25 : Icons.battery00;
|
||||
}
|
||||
fillColor: !isReady || charging || percent > lowBatteryThreshold ? Colors.sapphire : Colors.red
|
||||
value: percent
|
||||
maxValue: 100
|
||||
textSuffix: "%"
|
||||
pointerCursor: false
|
||||
}
|
||||
34
quickshell/Modules/Bar/Components/Brightness.qml
Normal file
34
quickshell/Modules/Bar/Components/Brightness.qml
Normal file
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Services
|
||||
|
||||
MonitorItem {
|
||||
property ShellScreen screen: null
|
||||
|
||||
function getMonitor() {
|
||||
return BrightnessService.getMonitorForScreen(screen) || null;
|
||||
}
|
||||
|
||||
symbol: Icons.brightness
|
||||
fillColor: Colors.blue
|
||||
value: {
|
||||
const monitor = getMonitor();
|
||||
return monitor ? Math.round(monitor.brightness * 100) : "N/A";
|
||||
}
|
||||
maxValue: 100
|
||||
textSuffix: "%"
|
||||
onWheelUp: {
|
||||
const monitor = getMonitor();
|
||||
if (monitor)
|
||||
monitor.increaseBrightness();
|
||||
|
||||
}
|
||||
onWheelDown: {
|
||||
const monitor = getMonitor();
|
||||
if (monitor)
|
||||
monitor.decreaseBrightness();
|
||||
|
||||
}
|
||||
}
|
||||
69
quickshell/Modules/Bar/Components/CavaBar.qml
Normal file
69
quickshell/Modules/Bar/Components/CavaBar.qml
Normal file
@@ -0,0 +1,69 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Constants
|
||||
import qs.Modules.Misc
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int count: 6
|
||||
property int barWidth: 5
|
||||
property int barSpacing: 3
|
||||
|
||||
implicitWidth: root.barWidth * root.count + root.barSpacing * (root.count - 1)
|
||||
implicitHeight: parent.height - 10
|
||||
|
||||
Cava {
|
||||
id: cavaProcess
|
||||
|
||||
count: root.count
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: root.barSpacing
|
||||
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: cavaProcess.values
|
||||
|
||||
Rectangle {
|
||||
width: root.barWidth
|
||||
implicitHeight: Math.max(1, modelData * (parent.height - 10))
|
||||
color: Colors.cavaList[Math.min(Math.floor(modelData * (Colors.cavaList.length - 1)), Colors.cavaList.length - 1)]
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
MusicManager.playPause();
|
||||
}
|
||||
onWheel: function(wheel) {
|
||||
if (wheel.angleDelta.y > 0)
|
||||
MusicManager.previous();
|
||||
else if (wheel.angleDelta.y < 0)
|
||||
MusicManager.next();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
27
quickshell/Modules/Bar/Components/CpuTemp.qml
Normal file
27
quickshell/Modules/Bar/Components/CpuTemp.qml
Normal file
@@ -0,0 +1,27 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Services
|
||||
|
||||
MonitorItem {
|
||||
symbol: Icons.cpuTemp > 80 ? Icons.tempHigh : Icons.cpuTemp > 50 ? Icons.tempMedium : Icons.tempLow
|
||||
fillColor: Icons.cpuTemp > 80 ? Colors.red : Colors.yellow
|
||||
value: Math.round(SystemStatService.cpuTemp)
|
||||
maxValue: 120
|
||||
textSuffix: "°C"
|
||||
onClicked: {
|
||||
if (action.running) {
|
||||
action.signal(15);
|
||||
return ;
|
||||
}
|
||||
action.exec(["ghostty", "-e", "btop"]);
|
||||
}
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
}
|
||||
27
quickshell/Modules/Bar/Components/CpuUsage.qml
Normal file
27
quickshell/Modules/Bar/Components/CpuUsage.qml
Normal file
@@ -0,0 +1,27 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Services
|
||||
|
||||
MonitorItem {
|
||||
symbol: Icons.cpu
|
||||
fillColor: Colors.teal
|
||||
value: Math.round(SystemStatService.cpuUsage)
|
||||
maxValue: 100
|
||||
textSuffix: "%"
|
||||
onClicked: {
|
||||
if (action.running) {
|
||||
action.signal(15);
|
||||
return ;
|
||||
}
|
||||
action.exec(["ghostty", "-e", "btop"]);
|
||||
}
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
}
|
||||
144
quickshell/Modules/Bar/Components/FocusedWindow.qml
Normal file
144
quickshell/Modules/Bar/Components/FocusedWindow.qml
Normal file
@@ -0,0 +1,144 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real maxWidth: 250
|
||||
property string fallbackIcon: "application-x-executable"
|
||||
|
||||
function getAppIcon() {
|
||||
try {
|
||||
const focusedWindow = Niri.getFocusedWindow();
|
||||
if (focusedWindow && focusedWindow.appId) {
|
||||
try {
|
||||
const idValue = focusedWindow.appId;
|
||||
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue);
|
||||
const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase());
|
||||
if (iconResult && iconResult !== "")
|
||||
return iconResult;
|
||||
|
||||
} catch (iconError) {
|
||||
console.warn("Error getting icon from CompositorService:", iconError);
|
||||
}
|
||||
}
|
||||
return ThemeIcons.iconFromName(root.fallbackIcon);
|
||||
} catch (e) {
|
||||
console.warn("Error in getAppIcon:", e);
|
||||
return ThemeIcons.iconFromName(root.fallbackIcon);
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: parent.height
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
visible: Niri.focusedWindowId !== -1
|
||||
|
||||
Item {
|
||||
// Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
id: iconContainer
|
||||
|
||||
implicitWidth: 18
|
||||
implicitHeight: 18
|
||||
|
||||
IconImage {
|
||||
id: windowIcon
|
||||
|
||||
anchors.fill: parent
|
||||
source: getAppIcon()
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
visible: source !== ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: titleContainer
|
||||
|
||||
implicitWidth: root.maxWidth
|
||||
implicitHeight: parent.height
|
||||
// Layout.alignment: Qt.AlignVCenter
|
||||
clip: true
|
||||
|
||||
Text {
|
||||
id: windowTitle
|
||||
|
||||
text: Niri.focusedWindowTitle
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.pointSize: Fonts.medium
|
||||
font.family: Fonts.primary
|
||||
color: Colors.accent
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
onEntered: {
|
||||
if (windowTitle.implicitWidth > titleContainer.width)
|
||||
windowTitle.x = titleContainer.width - windowTitle.implicitWidth;
|
||||
|
||||
}
|
||||
onExited: {
|
||||
windowTitle.x = 0;
|
||||
}
|
||||
onClicked: function(mouse) {
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
action.command = ["niri", "msg", "action", "close-window"];
|
||||
action.startDetached();
|
||||
} else if (mouse.button === Qt.LeftButton) {
|
||||
action.command = ["niri", "msg", "action", "center-window"];
|
||||
action.startDetached();
|
||||
}
|
||||
}
|
||||
onWheel: function(wheel) {
|
||||
if (wheel.angleDelta.y > 0) {
|
||||
action.command = ["niri", "msg", "action", "set-column-width", "+10%"];
|
||||
action.startDetached();
|
||||
} else if (wheel.angleDelta.y < 0) {
|
||||
action.command = ["niri", "msg", "action", "set-column-width", "-10%"];
|
||||
action.startDetached();
|
||||
} else if (wheel.angleDelta.x > 0) {
|
||||
action.command = ["niri", "msg", "action", "focus-column-left"];
|
||||
action.startDetached();
|
||||
} else if (wheel.angleDelta.x < 0) {
|
||||
action.command = ["niri", "msg", "action", "focus-column-right"];
|
||||
action.startDetached();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
85
quickshell/Modules/Bar/Components/Ip.qml
Normal file
85
quickshell/Modules/Bar/Components/Ip.qml
Normal file
@@ -0,0 +1,85 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
// Text {
|
||||
// id: ipText
|
||||
// anchors.verticalCenter: parent.verticalCenter
|
||||
// text: Icons.global + " " + (showCountryCode ? IpService.countryCode : IpService.ip)
|
||||
// font.pixelSize: Fonts.medium
|
||||
// color: Colors.peach
|
||||
// }
|
||||
|
||||
id: root
|
||||
|
||||
property bool showCountryCode: true
|
||||
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: layout.width + 10
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
text: Icons.global
|
||||
font.pointSize: Fonts.icon + 5
|
||||
color: Colors.peach
|
||||
}
|
||||
|
||||
Item {
|
||||
id: expander
|
||||
|
||||
implicitWidth: mouseArea.containsMouse ? ipText.implicitWidth + 10 : 0
|
||||
implicitHeight: parent.height
|
||||
clip: true
|
||||
|
||||
Text {
|
||||
id: ipText
|
||||
|
||||
text: showCountryCode ? IpService.countryCode : IpService.ip
|
||||
font.pointSize: showCountryCode ? Fonts.medium : Fonts.small
|
||||
font.family: Fonts.primary
|
||||
color: Colors.peach
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 5
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
WriteClipboard.write(showCountryCode ? IpService.countryCode : IpService.ip);
|
||||
SendNotification.show("Copied to clipboard", showCountryCode ? IpService.countryCode : IpService.ip);
|
||||
} else if (mouse.button === Qt.RightButton)
|
||||
showCountryCode = !showCountryCode;
|
||||
else if (mouse.button === Qt.MiddleButton)
|
||||
IpService.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
28
quickshell/Modules/Bar/Components/MemUsage.qml
Normal file
28
quickshell/Modules/Bar/Components/MemUsage.qml
Normal file
@@ -0,0 +1,28 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Services
|
||||
|
||||
MonitorItem {
|
||||
symbol: Icons.memory
|
||||
fillColor: Colors.green
|
||||
value: Math.round(SystemStatService.memPercent)
|
||||
maxValue: 100
|
||||
textValue: SystemStatService.memGb
|
||||
textSuffix: "G"
|
||||
onClicked: {
|
||||
if (action.running) {
|
||||
action.signal(15);
|
||||
return ;
|
||||
}
|
||||
action.exec(["ghostty", "-e", "btop"]);
|
||||
}
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
}
|
||||
52
quickshell/Modules/Bar/Components/NetworkSpeed.qml
Normal file
52
quickshell/Modules/Bar/Components/NetworkSpeed.qml
Normal file
@@ -0,0 +1,52 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: layout.width + 10
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: Icons.download
|
||||
font.pointSize: Fonts.icon - 3
|
||||
color: Colors.accent
|
||||
Layout.leftMargin: 10
|
||||
}
|
||||
|
||||
Text {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||
font.pointSize: Fonts.medium
|
||||
font.family: Fonts.primary
|
||||
color: Colors.accent
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 5
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Icons.upload
|
||||
font.pointSize: Fonts.icon - 3
|
||||
color: Colors.accent
|
||||
}
|
||||
|
||||
Text {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||
font.pointSize: Fonts.medium
|
||||
font.family: Fonts.primary
|
||||
color: Colors.accent
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
18
quickshell/Modules/Bar/Components/Separator.qml
Normal file
18
quickshell/Modules/Bar/Components/Separator.qml
Normal file
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Constants
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitHeight: parent.height
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 1.5
|
||||
height: parent.height * 0.32
|
||||
color: Colors.text
|
||||
}
|
||||
|
||||
}
|
||||
10
quickshell/Modules/Bar/Components/Time.qml
Normal file
10
quickshell/Modules/Bar/Components/Time.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
import QtQuick
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
|
||||
Text {
|
||||
text: TimeService.time + " | " + TimeService.dateString
|
||||
font.pointSize: Fonts.medium
|
||||
font.family: Fonts.primary
|
||||
color: Colors.accent
|
||||
}
|
||||
60
quickshell/Modules/Bar/Components/TrayExpander.qml
Normal file
60
quickshell/Modules/Bar/Components/TrayExpander.qml
Normal file
@@ -0,0 +1,60 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: layout.implicitWidth
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
SymbolButton {
|
||||
symbol: Icons.tray
|
||||
buttonColor: Colors.green
|
||||
}
|
||||
|
||||
Item {
|
||||
id: trayContainer
|
||||
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: mouseArea.containsMouse ? expandedTray.implicitWidth : 0
|
||||
clip: true
|
||||
|
||||
SystemTray {
|
||||
id: expandedTray
|
||||
|
||||
screen: root.screen
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
}
|
||||
21
quickshell/Modules/Bar/Components/Volume.qml
Normal file
21
quickshell/Modules/Bar/Components/Volume.qml
Normal file
@@ -0,0 +1,21 @@
|
||||
import QtQuick
|
||||
import qs.Constants
|
||||
import qs.Modules.Bar.Misc
|
||||
import qs.Services
|
||||
|
||||
MonitorItem {
|
||||
symbol: AudioService.muted ? Icons.volumeMuted : (AudioService.volume >= 0.66 ? Icons.volumeHigh : (AudioService.volume >= 0.33 ? Icons.volumeMedium : Icons.volumeLow))
|
||||
fillColor: Colors.lavender
|
||||
value: Math.round(AudioService.volume * 100)
|
||||
maxValue: 100
|
||||
textSuffix: "%"
|
||||
onWheelUp: {
|
||||
AudioService.increaseVolume();
|
||||
}
|
||||
onWheelDown: {
|
||||
AudioService.decreaseVolume();
|
||||
}
|
||||
onClicked: {
|
||||
AudioService.toggleMute();
|
||||
}
|
||||
}
|
||||
299
quickshell/Modules/Bar/Components/Workspace.qml
Normal file
299
quickshell/Modules/Bar/Components/Workspace.qml
Normal file
@@ -0,0 +1,299 @@
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Constants
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
property bool hovered: false
|
||||
property ListModel localWorkspaces
|
||||
property real masterProgress: 0
|
||||
property bool effectsActive: false
|
||||
property color effectColor: Colors.accent
|
||||
property int horizontalPadding: 16
|
||||
property int spacingBetweenPills: 8
|
||||
property bool isDestroying: false
|
||||
|
||||
signal workspaceChanged(int workspaceId, color accentColor)
|
||||
|
||||
function triggerUnifiedWave() {
|
||||
effectColor = Colors.accent;
|
||||
masterAnimation.restart();
|
||||
}
|
||||
|
||||
function updateWorkspaceFocus() {
|
||||
for (let i = 0; i < localWorkspaces.count; i++) {
|
||||
const ws = localWorkspaces.get(i);
|
||||
if (ws.isFocused === true) {
|
||||
root.triggerUnifiedWave();
|
||||
root.workspaceChanged(ws.id, Colors.accent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitWidth: {
|
||||
let total = 0;
|
||||
for (let i = 0; i < localWorkspaces.count; i++) {
|
||||
const ws = localWorkspaces.get(i);
|
||||
if (ws.isFocused)
|
||||
total += 44;
|
||||
else if (ws.isActive)
|
||||
total += 28;
|
||||
else
|
||||
total += 16;
|
||||
}
|
||||
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills;
|
||||
total += horizontalPadding * 2;
|
||||
return total;
|
||||
}
|
||||
height: parent.height
|
||||
Component.onCompleted: {
|
||||
localWorkspaces.clear();
|
||||
for (let i = 0; i < WorkspaceManager.workspaces.count; i++) {
|
||||
const ws = WorkspaceManager.workspaces.get(i);
|
||||
if (ws.output.toLowerCase() === screen.name.toLowerCase())
|
||||
localWorkspaces.append(ws);
|
||||
|
||||
}
|
||||
workspaceRepeater.model = localWorkspaces;
|
||||
updateWorkspaceFocus();
|
||||
}
|
||||
Component.onDestruction: {
|
||||
root.isDestroying = true;
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onWorkspacesChanged() {
|
||||
localWorkspaces.clear();
|
||||
for (let i = 0; i < WorkspaceManager.workspaces.count; i++) {
|
||||
const ws = WorkspaceManager.workspaces.get(i);
|
||||
if (ws.output.toLowerCase() === screen.name.toLowerCase())
|
||||
localWorkspaces.append(ws);
|
||||
|
||||
}
|
||||
workspaceRepeater.model = localWorkspaces;
|
||||
updateWorkspaceFocus();
|
||||
}
|
||||
|
||||
target: WorkspaceManager
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: masterAnimation
|
||||
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "effectsActive"
|
||||
value: true
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "masterProgress"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: 1000
|
||||
easing.type: Easing.OutQuint
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "effectsActive"
|
||||
value: false
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "masterProgress"
|
||||
value: 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: workspaceBackground
|
||||
|
||||
width: parent.width - 15
|
||||
height: 26
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: 12
|
||||
color: Colors.transparent
|
||||
layer.enabled: true
|
||||
|
||||
layer.effect: DropShadow {
|
||||
color: "black"
|
||||
radius: 12
|
||||
samples: 24
|
||||
verticalOffset: 0
|
||||
horizontalOffset: 0
|
||||
opacity: 0.1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
id: pillRow
|
||||
|
||||
spacing: spacingBetweenPills
|
||||
anchors.verticalCenter: workspaceBackground.verticalCenter
|
||||
width: root.width - horizontalPadding * 2
|
||||
x: horizontalPadding
|
||||
|
||||
Repeater {
|
||||
id: workspaceRepeater
|
||||
|
||||
model: localWorkspaces
|
||||
|
||||
Item {
|
||||
id: workspacePillContainer
|
||||
|
||||
height: 12
|
||||
width: {
|
||||
if (model.isFocused)
|
||||
return 44;
|
||||
else if (model.isActive)
|
||||
return 28;
|
||||
else
|
||||
return 16;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
// half of focused height (if you want to animate this too)
|
||||
|
||||
id: workspacePill
|
||||
|
||||
anchors.fill: parent
|
||||
radius: {
|
||||
if (model.isFocused)
|
||||
return 12;
|
||||
else
|
||||
return 6;
|
||||
}
|
||||
color: {
|
||||
if (model.isFocused)
|
||||
return Colors.accent;
|
||||
|
||||
if (model.isActive)
|
||||
return Colors.accent.lighter(130);
|
||||
|
||||
if (model.isUrgent)
|
||||
return Theme.error;
|
||||
|
||||
return Colors.surface2;
|
||||
}
|
||||
scale: model.isFocused ? 1 : 0.9
|
||||
z: 0
|
||||
|
||||
MouseArea {
|
||||
id: pillMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
WorkspaceManager.switchToWorkspace(model.idx);
|
||||
}
|
||||
z: 20
|
||||
hoverEnabled: true
|
||||
}
|
||||
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: pillBurst
|
||||
|
||||
anchors.centerIn: workspacePillContainer
|
||||
width: workspacePillContainer.width + 18 * root.masterProgress
|
||||
height: workspacePillContainer.height + 18 * root.masterProgress
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: root.effectColor
|
||||
border.width: 2 + 6 * (1 - root.masterProgress)
|
||||
opacity: root.effectsActive && model.isFocused ? (1 - root.masterProgress) * 0.7 : 0
|
||||
visible: root.effectsActive && model.isFocused
|
||||
z: 1
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
localWorkspaces: ListModel {
|
||||
}
|
||||
|
||||
}
|
||||
140
quickshell/Modules/Bar/Misc/MonitorItem.qml
Normal file
140
quickshell/Modules/Bar/Misc/MonitorItem.qml
Normal file
@@ -0,0 +1,140 @@
|
||||
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.accent
|
||||
property string textSuffix: ""
|
||||
property bool pointerCursor: true
|
||||
property alias hovered: mouseArea.containsMouse
|
||||
readonly property real ratio: value / maxValue
|
||||
|
||||
signal wheelUp()
|
||||
signal wheelDown()
|
||||
signal clicked()
|
||||
|
||||
implicitHeight: parent.height - 5
|
||||
implicitWidth: parent.height + (hovered ? textDisplay.width : 0)
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
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.fillColor;
|
||||
ctx.lineCap = "round";
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onRatioChanged() {
|
||||
progressCircle.requestPaint();
|
||||
}
|
||||
|
||||
function onFillColorChanged() {
|
||||
progressCircle.requestPaint();
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
id: symbolText
|
||||
|
||||
anchors.fill: parent
|
||||
text: symbol
|
||||
font.family: Fonts.nerd
|
||||
font.pointSize: Fonts.icon
|
||||
color: fillColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: textDisplay
|
||||
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: root.hovered ? 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.fillColor
|
||||
opacity: root.hovered ? 1 : 0
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
60
quickshell/Modules/Bar/Misc/SymbolButton.qml
Normal file
60
quickshell/Modules/Bar/Misc/SymbolButton.qml
Normal file
@@ -0,0 +1,60 @@
|
||||
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
|
||||
|
||||
signal clicked()
|
||||
signal rightClicked()
|
||||
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: parent.height
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
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: Fonts.icon
|
||||
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: 14
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 120
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
160
quickshell/Modules/Bar/Misc/SystemTray.qml
Normal file
160
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
|
||||
|
||||
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")
|
||||
console.log("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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
282
quickshell/Modules/Bar/Misc/TrayMenu.qml
Normal file
282
quickshell/Modules/Bar/Misc/TrayMenu.qml
Normal file
@@ -0,0 +1,282 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
|
||||
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 + 20)
|
||||
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) {
|
||||
console.warn("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.accent
|
||||
border.width: 2
|
||||
radius: 14
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
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 || (Fonts.small)
|
||||
return textHeight + 16
|
||||
}
|
||||
}
|
||||
|
||||
color: Colors.transparent
|
||||
property var subMenu: null
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 16
|
||||
height: 1
|
||||
anchors.centerIn: parent
|
||||
visible: modelData?.isSeparator ?? false
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: Colors.transparent
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.1
|
||||
color: Colors.accent
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.9
|
||||
color: Colors.accent
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Colors.transparent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: mouseArea.containsMouse ? Colors.accent : Colors.transparent
|
||||
radius: 10
|
||||
visible: !(modelData?.isSeparator ?? false)
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
id: text
|
||||
Layout.fillWidth: true
|
||||
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Colors.base : Colors.text) : Colors.text
|
||||
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
|
||||
font.pointSize: Fonts.small
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: 14
|
||||
Layout.preferredHeight: 14
|
||||
source: modelData?.icon ?? ""
|
||||
visible: (modelData?.icon ?? "") !== ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
Text {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: modelData?.hasChildren ?? false
|
||||
color: (mouseArea.containsMouse ? Colors.base : Colors.text)
|
||||
|
||||
text: {
|
||||
const icon = modelData?.hasChildren ? "menu" : ""
|
||||
if ((icon === undefined) || (icon === "")) {
|
||||
return ""
|
||||
}
|
||||
if (Icons.get(icon) === undefined) {
|
||||
console.warn("Icon", `"${icon}"`, "doesn't exist in the icons font")
|
||||
return Icons.get(Icons.defaultIcon)
|
||||
}
|
||||
return Icons.get(icon)
|
||||
}
|
||||
font.family: Icons.fontFamily
|
||||
font.pointSize: Fonts.small
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
81
quickshell/Modules/Misc/Cava.qml
Normal file
81
quickshell/Modules/Misc/Cava.qml
Normal file
@@ -0,0 +1,81 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
property int count: 32
|
||||
property int noiseReduction: 60
|
||||
property string channels: "mono"
|
||||
property string monoOption: "average"
|
||||
property var config: ({
|
||||
"general": {
|
||||
"bars": count,
|
||||
"framerate": 30,
|
||||
"autosens": 1
|
||||
},
|
||||
"smoothing": {
|
||||
"monstercat": 1,
|
||||
"gravity": 1e+06,
|
||||
"noise_reduction": noiseReduction
|
||||
},
|
||||
"output": {
|
||||
"method": "raw",
|
||||
"bit_format": 8,
|
||||
"channels": channels,
|
||||
"mono_option": monoOption
|
||||
}
|
||||
})
|
||||
property var values: Array(count).fill(0)
|
||||
|
||||
Process {
|
||||
id: process
|
||||
|
||||
property int index: 0
|
||||
|
||||
stdinEnabled: true
|
||||
running: !MusicManager.isAllPaused()
|
||||
command: ["cava", "-p", "/dev/stdin"]
|
||||
onExited: {
|
||||
stdinEnabled = true;
|
||||
index = 0;
|
||||
values = Array(count).fill(0);
|
||||
}
|
||||
onStarted: {
|
||||
for (const k in config) {
|
||||
if (typeof config[k] !== "object") {
|
||||
write(k + "=" + config[k] + "\n");
|
||||
continue;
|
||||
}
|
||||
write("[" + k + "]\n");
|
||||
const obj = config[k];
|
||||
for (const k2 in obj) {
|
||||
write(k2 + "=" + obj[k2] + "\n");
|
||||
}
|
||||
}
|
||||
stdinEnabled = false;
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: (data) => {
|
||||
const newValues = Array(count).fill(0);
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
newValues[i] = values[i];
|
||||
}
|
||||
if (process.index + data.length > count)
|
||||
process.index = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
newValues[i + process.index] = Math.min(data.charCodeAt(i), 128) / 128;
|
||||
}
|
||||
process.index += data.length;
|
||||
values = newValues;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
10
quickshell/Modules/Misc/CavaColorList.qml
Normal file
10
quickshell/Modules/Misc/CavaColorList.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Constants
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var colorList: [Colors.lavender, Colors.blue, Colors.sapphire, Colors.sky, Colors.teal, Colors.green, Colors.yellow, Colors.peach]
|
||||
}
|
||||
87
quickshell/Modules/Misc/Corner.qml
Normal file
87
quickshell/Modules/Misc/Corner.qml
Normal file
@@ -0,0 +1,87 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Constants
|
||||
|
||||
Shape {
|
||||
id: root
|
||||
|
||||
property string position: "topleft" // Corner position: topleft/topright/bottomleft/bottomright
|
||||
property real size: 1 // Scale multiplier for entire corner
|
||||
property int concaveWidth: 100 * size
|
||||
property int concaveHeight: 60 * size
|
||||
property int offsetX: -20
|
||||
property int offsetY: -20
|
||||
property color fillColor: Colors.base
|
||||
property int arcRadius: 20 * size
|
||||
property var modelData: null
|
||||
// Position flags derived from position string
|
||||
property bool _isTop: position.includes("top")
|
||||
property bool _isLeft: position.includes("left")
|
||||
property bool _isRight: position.includes("right")
|
||||
property bool _isBottom: position.includes("bottom")
|
||||
// Shift the path vertically if offsetY is negative to pull shape up
|
||||
property real pathOffsetY: Math.min(offsetY, 0)
|
||||
// Base coordinates for left corner shape, shifted by pathOffsetY vertically
|
||||
property real _baseStartX: 30 * size
|
||||
property real _baseStartY: (_isTop ? 20 * size : 0) + pathOffsetY
|
||||
property real _baseLineX: 30 * size
|
||||
property real _baseLineY: (_isTop ? 0 : 20 * size) + pathOffsetY
|
||||
property real _baseArcX: 50 * size
|
||||
property real _baseArcY: (_isTop ? 20 * size : 0) + pathOffsetY
|
||||
// Mirror coordinates for right corners
|
||||
property real _startX: _isRight ? (concaveWidth - _baseStartX) : _baseStartX
|
||||
property real _startY: _baseStartY
|
||||
property real _lineX: _isRight ? (concaveWidth - _baseLineX) : _baseLineX
|
||||
property real _lineY: _baseLineY
|
||||
property real _arcX: _isRight ? (concaveWidth - _baseArcX) : _baseArcX
|
||||
property real _arcY: _baseArcY
|
||||
// Arc direction varies by corner to maintain proper concave shape
|
||||
property int _arcDirection: {
|
||||
if (_isTop && _isLeft)
|
||||
return PathArc.Counterclockwise;
|
||||
|
||||
if (_isTop && _isRight)
|
||||
return PathArc.Clockwise;
|
||||
|
||||
if (_isBottom && _isLeft)
|
||||
return PathArc.Clockwise;
|
||||
|
||||
if (_isBottom && _isRight)
|
||||
return PathArc.Counterclockwise;
|
||||
|
||||
return PathArc.Counterclockwise;
|
||||
}
|
||||
|
||||
width: concaveWidth
|
||||
height: concaveHeight
|
||||
// Position relative to parent based on corner type
|
||||
x: _isLeft ? offsetX : (parent ? parent.width - width + offsetX : 0)
|
||||
y: _isTop ? offsetY : (parent ? parent.height - height + offsetY : 0)
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
layer.enabled: true
|
||||
layer.samples: 4
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
fillColor: root.fillColor
|
||||
strokeColor: root.fillColor
|
||||
startX: root._startX
|
||||
startY: root._startY
|
||||
|
||||
PathLine {
|
||||
x: root._lineX
|
||||
y: root._lineY
|
||||
}
|
||||
|
||||
PathArc {
|
||||
x: root._arcX
|
||||
y: root._arcY
|
||||
radiusX: root.arcRadius
|
||||
radiusY: root.arcRadius
|
||||
useLargeArc: false
|
||||
direction: root._arcDirection
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
178
quickshell/Modules/Misc/Corners.qml
Normal file
178
quickshell/Modules/Misc/Corners.qml
Normal file
@@ -0,0 +1,178 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Constants
|
||||
import qs.Modules.Misc
|
||||
import qs.Services
|
||||
|
||||
Scope {
|
||||
id: rootScope
|
||||
|
||||
property var shell
|
||||
property string namespace: "quickshell-corners"
|
||||
property int topMargin: 45
|
||||
property int cornerHeight: 20
|
||||
property real cornerSize: 1
|
||||
property real opacity: Niri.noFocus ? 0 : 1
|
||||
|
||||
Item {
|
||||
id: cornersRootItem
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
Item {
|
||||
property var modelData
|
||||
|
||||
PanelWindow {
|
||||
id: fakeBar
|
||||
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
color: "transparent"
|
||||
screen: modelData
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
visible: true
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
aboveWindows: false
|
||||
WlrLayershell.namespace: namespace
|
||||
implicitHeight: topMargin
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Colors.base
|
||||
opacity: rootScope.opacity
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: topLeftPanel
|
||||
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
color: "transparent"
|
||||
screen: modelData
|
||||
margins.top: topMargin
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
visible: true
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
aboveWindows: false
|
||||
WlrLayershell.namespace: namespace
|
||||
implicitHeight: cornerHeight
|
||||
|
||||
Corner {
|
||||
id: topLeftCorner
|
||||
|
||||
position: "bottomleft"
|
||||
size: rootScope.cornerSize
|
||||
offsetX: -32
|
||||
offsetY: 0
|
||||
anchors.top: parent.top
|
||||
opacity: rootScope.opacity
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: topRightPanel
|
||||
|
||||
anchors.top: true
|
||||
anchors.right: true
|
||||
color: "transparent"
|
||||
screen: modelData
|
||||
margins.top: topMargin
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
visible: true
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
aboveWindows: false
|
||||
WlrLayershell.namespace: namespace
|
||||
implicitHeight: cornerHeight
|
||||
|
||||
Corner {
|
||||
id: topRightCorner
|
||||
|
||||
position: "bottomright"
|
||||
size: rootScope.cornerSize
|
||||
offsetX: 32
|
||||
offsetY: 0
|
||||
anchors.top: parent.top
|
||||
opacity: rootScope.opacity
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: bottomLeftPanel
|
||||
|
||||
anchors.bottom: true
|
||||
anchors.left: true
|
||||
color: "transparent"
|
||||
screen: modelData
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
visible: true
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
aboveWindows: false
|
||||
WlrLayershell.namespace: namespace
|
||||
implicitHeight: cornerHeight
|
||||
|
||||
Corner {
|
||||
id: bottomLeftCorner
|
||||
|
||||
position: "topleft"
|
||||
size: rootScope.cornerSize
|
||||
offsetX: -32
|
||||
offsetY: 0
|
||||
anchors.top: parent.top
|
||||
opacity: rootScope.opacity
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: bottomRightPanel
|
||||
|
||||
anchors.bottom: true
|
||||
anchors.right: true
|
||||
color: "transparent"
|
||||
screen: modelData
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
visible: true
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
aboveWindows: false
|
||||
WlrLayershell.namespace: namespace
|
||||
implicitHeight: cornerHeight
|
||||
|
||||
Corner {
|
||||
id: bottomRightCorner
|
||||
|
||||
position: "topright"
|
||||
size: rootScope.cornerSize
|
||||
offsetX: 32
|
||||
offsetY: 0
|
||||
anchors.top: parent.top
|
||||
opacity: rootScope.opacity
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user