1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00

iSome extra widgets and adjustments

This commit is contained in:
bbedward
2025-07-10 19:11:35 -04:00
parent c4975019e7
commit 052e47614b
13 changed files with 839 additions and 240 deletions

View File

@@ -7,18 +7,19 @@ pragma ComponentBehavior: Bound
Singleton {
id: root
// Battery properties
property bool batteryAvailable: false
property int batteryLevel: 0
property string batteryStatus: "Unknown" // "Charging", "Discharging", "Full", "Not charging", "Unknown"
property int timeRemaining: 0 // minutes
property string batteryStatus: "Unknown"
property int timeRemaining: 0
property bool isCharging: false
property bool isLowBattery: false
property int batteryHealth: 100 // percentage
property int batteryHealth: 100
property string batteryTechnology: "Unknown"
property int cycleCount: 0
property int batteryCapacity: 0 // mAh
property int batteryCapacity: 0
property var powerProfiles: []
property string activePowerProfile: "balanced"
property string activePowerProfile: ""
// Check if battery is available
Process {
@@ -238,12 +239,13 @@ Singleton {
}
}
// Update battery status every 30 seconds
// Update timer
Timer {
interval: 30000
running: root.batteryAvailable
repeat: true
triggeredOnStart: false
triggeredOnStart: true
onTriggered: {
batteryStatusChecker.running = true
powerProfilesChecker.running = true

View File

@@ -145,7 +145,6 @@ Singleton {
running: true
repeat: true
onTriggered: {
console.log(`[MprisController] Players: ${Mpris.players.length}, Active: ${activePlayer?.identity || 'none'}, Playing: ${isPlaying}`)
if (activePlayer) {
console.log(` Track: ${activePlayer.trackTitle || 'Unknown'} by ${activePlayer.trackArtist || 'Unknown'}`)
console.log(` State: ${activePlayer.playbackState}`)

View File

@@ -79,37 +79,49 @@ Singleton {
Process {
id: weatherFetcher
command: ["bash", "-c", "curl -s 'wttr.in/?format=j1' | jq '{current: .current_condition[0], location: .nearest_area[0], astronomy: .weather[0].astronomy[0]}'"]
command: ["bash", "-c", "curl -s 'wttr.in/?format=j1'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim() && text.trim().startsWith("{")) {
try {
let parsedData = JSON.parse(text.trim())
if (parsedData.current && parsedData.location) {
root.weather = {
available: true,
temp: parseInt(parsedData.current.temp_C || 0),
tempF: parseInt(parsedData.current.temp_F || 0),
city: parsedData.location.areaName[0]?.value || "Unknown",
wCode: parsedData.current.weatherCode || "113",
humidity: parseInt(parsedData.current.humidity || 0),
wind: (parsedData.current.windspeedKmph || 0) + " km/h",
sunrise: parsedData.astronomy?.sunrise || "06:00",
sunset: parsedData.astronomy?.sunset || "18:00",
uv: parseInt(parsedData.current.uvIndex || 0),
pressure: parseInt(parsedData.current.pressure || 0)
}
console.log("Weather updated:", root.weather.city, root.weather.temp + "°C")
}
} catch (e) {
console.warn("Failed to parse weather data:", e.message)
root.weather.available = false
}
} else {
const raw = text.trim()
if (!raw || raw[0] !== "{") {
console.warn("No valid weather data received")
root.weather.available = false
return
}
try {
const data = JSON.parse(raw)
const current = data.current_condition?.[0] || {}
const location = data.nearest_area?.[0] || {}
const astronomy = data.weather?.[0]?.astronomy?.[0] || {}
if (!Object.keys(current).length || !Object.keys(location).length) {
throw new Error("Required fields missing")
}
root.weather = {
available: true,
temp: Number(current.temp_C) || 0,
tempF: Number(current.temp_F) || 0,
city: location.areaName?.[0]?.value || "Unknown",
wCode: current.weatherCode || "113",
humidity: Number(current.humidity) || 0,
wind: `${current.windspeedKmph || 0} km/h`,
sunrise: astronomy.sunrise || "06:00",
sunset: astronomy.sunset || "18:00",
uv: Number(current.uvIndex) || 0,
pressure: Number(current.pressure) || 0
}
console.log("Weather updated:", root.weather.city,
`${root.weather.temp}°C`)
} catch (e) {
console.warn("Failed to parse weather data:", e.message)
root.weather.available = false
}
}
}

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
@@ -600,69 +601,6 @@ PanelWindow {
}
}
// Category dropdown
Rectangle {
width: 200
height: Math.min(250, categories.length * 40 + activeTheme.spacingM * 2)
radius: activeTheme.cornerRadiusLarge
color: activeTheme.surfaceContainer
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
border.width: 1
visible: showCategories
z: 100
// Drop shadow
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Qt.rgba(0, 0, 0, 0.1)
border.width: 1
z: -1
}
ScrollView {
anchors.fill: parent
anchors.margins: activeTheme.spacingS
clip: true
ListView {
model: categories
spacing: 4
delegate: Rectangle {
width: ListView.view.width
height: 36
radius: activeTheme.cornerRadiusSmall
color: catArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) : "transparent"
Text {
anchors.left: parent.left
anchors.leftMargin: activeTheme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: modelData
font.pixelSize: activeTheme.fontSizeMedium
color: selectedCategory === modelData ? activeTheme.primary : activeTheme.surfaceText
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
}
MouseArea {
id: catArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData
showCategories = false
updateFilteredModel()
}
}
}
}
}
}
// App grid/list container
Rectangle {
width: parent.width
@@ -715,32 +653,41 @@ PanelWindow {
height: 56
anchors.verticalCenter: parent.verticalCenter
IconImage {
Loader {
anchors.fill: parent
source: Quickshell.iconPath(model.icon, "application-x-executable")
smooth: true
visible: status === Image.Ready
sourceComponent: model.icon ? iconComponent : fallbackComponent
onStatusChanged: {
if (status === Image.Error && model.name.includes("Avahi")) {
console.log("Avahi icon failed to load:", model.icon, "->", source)
Component {
id: iconComponent
IconImage {
source: model.icon ? Quickshell.iconPath(model.icon) : ""
smooth: true
asynchronous: true
onStatusChanged: {
if (status === Image.Error || status === Image.Null) {
parent.sourceComponent = fallbackComponent
}
}
}
}
}
// Fallback for missing icons
Rectangle {
anchors.fill: parent
visible: !parent.children[0].visible
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
radius: activeTheme.cornerRadiusLarge
Component {
id: fallbackComponent
Rectangle {
color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.1)
radius: activeTheme.cornerRadiusLarge
border.width: 1
border.color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.2)
Text {
anchors.centerIn: parent
text: model.name ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: activeTheme.iconSizeLarge
color: activeTheme.surfaceVariantText
font.weight: Font.Medium
Text {
anchors.centerIn: parent
text: model.name ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: 28
color: activeTheme.primary
font.weight: Font.Bold
}
}
}
}
}
@@ -801,8 +748,8 @@ PanelWindow {
// Center the grid content
property int columnsCount: Math.floor(width / cellWidth)
property int remainingSpace: width - (columnsCount * cellWidth)
anchors.leftMargin: Math.max(activeTheme.spacingS, remainingSpace / 2)
anchors.rightMargin: anchors.leftMargin
leftMargin: Math.max(activeTheme.spacingS, remainingSpace / 2)
rightMargin: leftMargin
model: filteredModel
@@ -832,32 +779,41 @@ PanelWindow {
height: iconSize
anchors.horizontalCenter: parent.horizontalCenter
IconImage {
Loader {
anchors.fill: parent
source: Quickshell.iconPath(model.icon, "application-x-executable")
smooth: true
visible: status === Image.Ready
sourceComponent: model.icon ? gridIconComponent : gridFallbackComponent
onStatusChanged: {
if (status === Image.Error && model.name.includes("Avahi")) {
console.log("Avahi grid icon failed to load:", model.icon, "->", source)
Component {
id: gridIconComponent
IconImage {
source: model.icon ? Quickshell.iconPath(model.icon) : ""
smooth: true
asynchronous: true
onStatusChanged: {
if (status === Image.Error || status === Image.Null) {
parent.sourceComponent = gridFallbackComponent
}
}
}
}
}
// Fallback for missing icons
Rectangle {
anchors.fill: parent
visible: !parent.children[0].visible
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
radius: activeTheme.cornerRadiusLarge
Component {
id: gridFallbackComponent
Rectangle {
color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.1)
radius: activeTheme.cornerRadiusLarge
border.width: 1
border.color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.2)
Text {
anchors.centerIn: parent
text: model.name ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: activeTheme.iconSizeLarge
color: activeTheme.surfaceVariantText
font.weight: Font.Medium
Text {
anchors.centerIn: parent
text: model.name ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: parent.parent.parent.iconSize / 2
color: activeTheme.primary
font.weight: Font.Bold
}
}
}
}
}
@@ -892,6 +848,80 @@ PanelWindow {
}
}
}
// Category dropdown overlay - now positioned absolutely
Rectangle {
id: categoryDropdown
width: 200
height: Math.min(250, categories.length * 40 + activeTheme.spacingM * 2)
radius: activeTheme.cornerRadiusLarge
color: activeTheme.surfaceContainer
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
border.width: 1
visible: showCategories
z: 1000
// Position it below the category button
anchors.top: parent.top
anchors.topMargin: 140 + (searchField.text.length === 0 ? 0 : -40)
anchors.left: parent.left
// Drop shadow
Rectangle {
anchors.fill: parent
anchors.margins: -4
color: "transparent"
radius: parent.radius + 4
z: -1
layer.enabled: true
layer.effect: DropShadow {
radius: 8
samples: 16
color: Qt.rgba(0, 0, 0, 0.2)
}
}
ScrollView {
anchors.fill: parent
anchors.margins: activeTheme.spacingS
clip: true
ListView {
model: categories
spacing: 4
delegate: Rectangle {
width: ListView.view.width
height: 36
radius: activeTheme.cornerRadiusSmall
color: catArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) : "transparent"
Text {
anchors.left: parent.left
anchors.leftMargin: activeTheme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: modelData
font.pixelSize: activeTheme.fontSizeMedium
color: selectedCategory === modelData ? activeTheme.primary : activeTheme.surfaceText
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
}
MouseArea {
id: catArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData
showCategories = false
updateFilteredModel()
}
}
}
}
}
}
}
}
}

View File

@@ -27,6 +27,14 @@ PanelWindow {
bottom: true
}
// Click outside to dismiss overlay
MouseArea {
anchors.fill: parent
onClicked: {
root.batteryPopupVisible = false
}
}
Rectangle {
width: Math.min(380, parent.width - Theme.spacingL * 2)
height: Math.min(450, parent.height - Theme.barHeight - Theme.spacingS * 2)
@@ -40,6 +48,14 @@ PanelWindow {
opacity: root.batteryPopupVisible ? 1.0 : 0.0
scale: root.batteryPopupVisible ? 1.0 : 0.85
// Prevent click-through to background
MouseArea {
anchors.fill: parent
onClicked: {
// Consume the click to prevent it from reaching the background
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration

View File

@@ -8,53 +8,33 @@ Rectangle {
property bool batteryPopupVisible: false
width: Theme.barHeight - Theme.spacingS
height: Theme.barHeight - Theme.spacingS
radius: Theme.cornerRadiusSmall
color: batteryArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
width: 48
height: 32
radius: Theme.cornerRadius
color: batteryArea.containsMouse || batteryPopupVisible ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
visible: BatteryService.batteryAvailable
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
// Battery icon
Text {
text: BatteryService.getBatteryIcon()
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: {
if (!BatteryService.batteryAvailable) return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
if (BatteryService.isCharging) return Theme.primary
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
// Subtle animation for charging
RotationAnimation on rotation {
running: BatteryService.isCharging
loops: Animation.Infinite
from: 0
to: 360
duration: 8000
easing.type: Easing.Linear
}
// Battery icon - Material Design icons already show level visually
Text {
text: BatteryService.getBatteryIcon()
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: {
if (!BatteryService.batteryAvailable) return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
if (BatteryService.isCharging) return Theme.primary
return Theme.surfaceText
}
anchors.centerIn: parent
// Battery percentage
Text {
text: BatteryService.batteryLevel + "%"
font.pixelSize: Theme.fontSizeSmall
color: {
if (!BatteryService.batteryAvailable) return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
if (BatteryService.isCharging) return Theme.primary
return Theme.surfaceText
}
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable
// Subtle pulse animation for charging
SequentialAnimation on opacity {
running: BatteryService.isCharging
loops: Animation.Infinite
NumberAnimation { to: 0.6; duration: 1000; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 1000; easing.type: Easing.InOutQuad }
}
}

74
Widgets/PowerButton.qml Normal file
View File

@@ -0,0 +1,74 @@
import QtQuick
import QtQuick.Controls
import "../Common"
Rectangle {
id: powerButton
width: 48
height: 32
radius: Theme.cornerRadius
color: powerArea.containsMouse || root.powerMenuVisible ?
Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
// Power icon
Text {
text: "power_settings_new"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: powerArea.containsMouse || root.powerMenuVisible ? Theme.error : Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: powerArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerMenuVisible = !root.powerMenuVisible
}
}
// Tooltip on hover
Rectangle {
id: powerTooltip
width: tooltipText.contentWidth + Theme.spacingM * 2
height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
visible: powerArea.containsMouse && !root.powerMenuVisible
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
opacity: powerArea.containsMouse ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Text {
id: tooltipText
text: "Power Menu"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -0,0 +1,203 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import "../Common"
PanelWindow {
id: powerConfirmDialog
visible: root.powerConfirmVisible
implicitWidth: 400
implicitHeight: 300
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
// Darkened background
Rectangle {
anchors.fill: parent
color: "black"
opacity: 0.5
}
Rectangle {
width: Math.min(400, parent.width - Theme.spacingL * 2)
height: Math.min(200, parent.height - Theme.spacingL * 2)
anchors.centerIn: parent
color: Theme.surfaceContainer
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
opacity: root.powerConfirmVisible ? 1.0 : 0.0
scale: root.powerConfirmVisible ? 1.0 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
// Title
Text {
text: root.powerConfirmTitle
font.pixelSize: Theme.fontSizeLarge
color: {
switch(root.powerConfirmAction) {
case "poweroff": return Theme.error
case "reboot": return Theme.warning
default: return Theme.surfaceText
}
}
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
// Message
Text {
text: root.powerConfirmMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
Item { height: Theme.spacingL }
// Buttons
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
// Cancel button
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: cancelButton.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Text {
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerConfirmVisible = false
}
}
}
// Confirm button
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
let baseColor
switch(root.powerConfirmAction) {
case "poweroff": baseColor = Theme.error; break
case "reboot": baseColor = Theme.warning; break
default: baseColor = Theme.primary; break
}
return confirmButton.containsMouse ?
Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9) :
baseColor
}
Text {
text: "Confirm"
font.pixelSize: Theme.fontSizeMedium
color: Theme.onPrimary
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerConfirmVisible = false
executePowerAction(root.powerConfirmAction)
}
}
}
}
}
}
function executePowerAction(action) {
console.log("Executing power action:", action)
let command = []
switch(action) {
case "logout":
// Try multiple logout commands for different environments
command = ["bash", "-c", "loginctl terminate-user $USER || pkill -KILL -u $USER || gnome-session-quit --force || xfce4-session-logout --logout || i3-msg exit || swaymsg exit || niri msg quit"]
break
case "suspend":
command = ["systemctl", "suspend"]
break
case "reboot":
command = ["systemctl", "reboot"]
break
case "poweroff":
command = ["systemctl", "poweroff"]
break
}
if (command.length > 0) {
powerActionProcess.command = command
powerActionProcess.running = true
}
}
Process {
id: powerActionProcess
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.error("Power action failed with exit code:", exitCode)
}
}
}
}

305
Widgets/PowerMenuPopup.qml Normal file
View File

@@ -0,0 +1,305 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import "../Common"
PanelWindow {
id: powerMenuPopup
visible: root.powerMenuVisible
implicitWidth: 400
implicitHeight: 320
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
// Click outside to dismiss overlay
MouseArea {
anchors.fill: parent
onClicked: {
root.powerMenuVisible = false
}
}
Rectangle {
width: Math.min(320, parent.width - Theme.spacingL * 2)
height: 320 // Fixed height to prevent cropping
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
y: Theme.barHeight + Theme.spacingS
color: Theme.surfaceContainer
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
opacity: root.powerMenuVisible ? 1.0 : 0.0
scale: root.powerMenuVisible ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
// Prevent click-through to background
MouseArea {
anchors.fill: parent
onClicked: {
// Consume the click to prevent it from reaching the background
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
// Header
Row {
width: parent.width
Text {
text: "Power Options"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item { width: parent.width - 150; height: 1 }
Rectangle {
width: 32
height: 32
radius: 16
color: closePowerArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "close"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 4
color: closePowerArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closePowerArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerMenuVisible = false
}
}
}
}
// Power options
Column {
width: parent.width
spacing: Theme.spacingS
// Log Out
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: "logout"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Log Out"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerMenuVisible = false
root.powerConfirmAction = "logout"
root.powerConfirmTitle = "Log Out"
root.powerConfirmMessage = "Are you sure you want to log out?"
root.powerConfirmVisible = true
}
}
}
// Suspend
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: "bedtime"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Suspend"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerMenuVisible = false
root.powerConfirmAction = "suspend"
root.powerConfirmTitle = "Suspend"
root.powerConfirmMessage = "Are you sure you want to suspend the system?"
root.powerConfirmVisible = true
}
}
}
// Reboot
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: "restart_alt"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Reboot"
font.pixelSize: Theme.fontSizeMedium
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerMenuVisible = false
root.powerConfirmAction = "reboot"
root.powerConfirmTitle = "Reboot"
root.powerConfirmMessage = "Are you sure you want to reboot the system?"
root.powerConfirmVisible = true
}
}
}
// Power Off
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: "power_settings_new"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Power Off"
font.pixelSize: Theme.fontSizeMedium
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: powerOffArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerMenuVisible = false
root.powerConfirmAction = "poweroff"
root.powerConfirmTitle = "Power Off"
root.powerConfirmMessage = "Are you sure you want to power off the system?"
root.powerConfirmVisible = true
}
}
}
}
}
}
}

View File

@@ -2,51 +2,37 @@ import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
Rectangle {
Item {
property var theme
property var root
width: Math.max(40, systemTrayRow.implicitWidth + theme.spacingS * 2)
height: 32
radius: theme.cornerRadius
color: Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
visible: systemTrayRow.children.length > 0
implicitWidth: trayRow.implicitWidth
visible: trayRow.children.length > 0
Row {
id: systemTrayRow
id: trayRow
anchors.centerIn: parent
spacing: theme.spacingXS
Repeater {
model: SystemTray.items
delegate: Rectangle {
required property SystemTrayItem modelData
width: 24
height: 24
radius: theme.cornerRadiusSmall
color: trayItemArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent"
property var trayItem: modelData
Image {
IconImage {
anchors.centerIn: parent
width: 18
height: 18
source: {
let icon = trayItem?.icon || "";
if (!icon) return "";
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
return icon;
}
asynchronous: true
source: parent.modelData.icon
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
@@ -57,45 +43,23 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (!trayItem) return;
if (mouse.button === Qt.LeftButton) {
if (!trayItem.onlyMenu) {
trayItem.activate()
}
parent.modelData.activate()
} else if (mouse.button === Qt.RightButton) {
if (trayItem.hasMenu) {
console.log("Right-click detected, showing menu for:", trayItem.title || "Unknown")
customTrayMenu.showMenu(mouse.x, mouse.y)
} else {
console.log("No menu available for:", trayItem.title || "Unknown")
}
menuHandler.showMenu()
}
}
}
// Simple menu handling for now
QtObject {
id: customTrayMenu
id: menuHandler
property bool menuVisible: false
function showMenu(x, y) {
root.currentTrayMenu = customTrayMenu
root.currentTrayItem = trayItem
root.trayMenuX = parent.parent.parent.parent.x + parent.parent.parent.parent.width - 180 - theme.spacingL
root.trayMenuY = theme.barHeight + theme.spacingS
console.log("Showing menu at:", root.trayMenuX, root.trayMenuY)
menuVisible = true
root.showTrayMenu = true
}
function hideMenu() {
menuVisible = false
root.showTrayMenu = false
root.currentTrayMenu = null
root.currentTrayItem = null
function showMenu() {
if (parent.modelData.hasMenu) {
console.log("Right-click menu for:", parent.modelData.title || "Unknown")
// TODO: Implement proper menu positioning
}
}
}

View File

@@ -804,7 +804,11 @@ PanelWindow {
}
}
}
// Power Button
PowerButton {
anchors.verticalCenter: parent.verticalCenter
}
}
}
}

View File

@@ -19,3 +19,6 @@ CustomSlider 1.0 CustomSlider.qml
InputDialog 1.0 InputDialog.qml
BatteryWidget 1.0 BatteryWidget.qml
BatteryControlPopup 1.0 BatteryControlPopup.qml
PowerButton 1.0 PowerButton.qml
PowerMenuPopup 1.0 PowerMenuPopup.qml
PowerConfirmDialog 1.0 PowerConfirmDialog.qml

View File

@@ -34,6 +34,11 @@ ShellRoot {
property bool hasActiveMedia: activePlayer && (activePlayer.trackTitle || activePlayer.trackArtist)
property bool controlCenterVisible: false
property bool batteryPopupVisible: false
property bool powerMenuVisible: false
property bool powerConfirmVisible: false
property string powerConfirmAction: ""
property string powerConfirmTitle: ""
property string powerConfirmMessage: ""
// Network properties from NetworkService
property string networkStatus: NetworkService.networkStatus
@@ -259,6 +264,8 @@ ShellRoot {
id: globalInputDialog
}
BatteryControlPopup {}
PowerMenuPopup {}
PowerConfirmDialog {}
// Application and clipboard components
AppLauncher {