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

View File

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

View File

@@ -79,37 +79,49 @@ Singleton {
Process { Process {
id: weatherFetcher 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 running: false
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (text.trim() && text.trim().startsWith("{")) { const raw = text.trim()
try { if (!raw || raw[0] !== "{") {
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 {
console.warn("No valid weather data received") console.warn("No valid weather data received")
root.weather.available = false 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
import QtQuick.Controls import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import Quickshell.Wayland 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 // App grid/list container
Rectangle { Rectangle {
width: parent.width width: parent.width
@@ -715,32 +653,41 @@ PanelWindow {
height: 56 height: 56
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
IconImage { Loader {
anchors.fill: parent anchors.fill: parent
source: Quickshell.iconPath(model.icon, "application-x-executable") sourceComponent: model.icon ? iconComponent : fallbackComponent
smooth: true
visible: status === Image.Ready
onStatusChanged: { Component {
if (status === Image.Error && model.name.includes("Avahi")) { id: iconComponent
console.log("Avahi icon failed to load:", model.icon, "->", source) 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
Text { Component {
anchors.centerIn: parent id: fallbackComponent
text: model.name ? model.name.charAt(0).toUpperCase() : "A" Rectangle {
font.pixelSize: activeTheme.iconSizeLarge color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.1)
color: activeTheme.surfaceVariantText radius: activeTheme.cornerRadiusLarge
font.weight: Font.Medium 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: 28
color: activeTheme.primary
font.weight: Font.Bold
}
}
} }
} }
} }
@@ -801,8 +748,8 @@ PanelWindow {
// Center the grid content // Center the grid content
property int columnsCount: Math.floor(width / cellWidth) property int columnsCount: Math.floor(width / cellWidth)
property int remainingSpace: width - (columnsCount * cellWidth) property int remainingSpace: width - (columnsCount * cellWidth)
anchors.leftMargin: Math.max(activeTheme.spacingS, remainingSpace / 2) leftMargin: Math.max(activeTheme.spacingS, remainingSpace / 2)
anchors.rightMargin: anchors.leftMargin rightMargin: leftMargin
model: filteredModel model: filteredModel
@@ -832,32 +779,41 @@ PanelWindow {
height: iconSize height: iconSize
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
IconImage { Loader {
anchors.fill: parent anchors.fill: parent
source: Quickshell.iconPath(model.icon, "application-x-executable") sourceComponent: model.icon ? gridIconComponent : gridFallbackComponent
smooth: true
visible: status === Image.Ready
onStatusChanged: { Component {
if (status === Image.Error && model.name.includes("Avahi")) { id: gridIconComponent
console.log("Avahi grid icon failed to load:", model.icon, "->", source) 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
Text { Component {
anchors.centerIn: parent id: gridFallbackComponent
text: model.name ? model.name.charAt(0).toUpperCase() : "A" Rectangle {
font.pixelSize: activeTheme.iconSizeLarge color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.1)
color: activeTheme.surfaceVariantText radius: activeTheme.cornerRadiusLarge
font.weight: Font.Medium 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: 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

@@ -26,6 +26,14 @@ PanelWindow {
right: true right: true
bottom: true bottom: true
} }
// Click outside to dismiss overlay
MouseArea {
anchors.fill: parent
onClicked: {
root.batteryPopupVisible = false
}
}
Rectangle { Rectangle {
width: Math.min(380, parent.width - Theme.spacingL * 2) width: Math.min(380, parent.width - Theme.spacingL * 2)
@@ -40,6 +48,14 @@ PanelWindow {
opacity: root.batteryPopupVisible ? 1.0 : 0.0 opacity: root.batteryPopupVisible ? 1.0 : 0.0
scale: root.batteryPopupVisible ? 1.0 : 0.85 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 { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: Theme.mediumDuration duration: Theme.mediumDuration

View File

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

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 QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import Quickshell.Widgets
Rectangle { Item {
property var theme property var theme
property var root
width: Math.max(40, systemTrayRow.implicitWidth + theme.spacingS * 2)
height: 32 height: 32
radius: theme.cornerRadius implicitWidth: trayRow.implicitWidth
color: Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08) visible: trayRow.children.length > 0
anchors.verticalCenter: parent.verticalCenter
visible: systemTrayRow.children.length > 0
Row { Row {
id: systemTrayRow id: trayRow
anchors.centerIn: parent anchors.centerIn: parent
spacing: theme.spacingXS spacing: theme.spacingXS
Repeater { Repeater {
model: SystemTray.items model: SystemTray.items
delegate: Rectangle { delegate: Rectangle {
required property SystemTrayItem modelData
width: 24 width: 24
height: 24 height: 24
radius: theme.cornerRadiusSmall radius: theme.cornerRadiusSmall
color: trayItemArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent" color: trayItemArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent"
property var trayItem: modelData IconImage {
Image {
anchors.centerIn: parent anchors.centerIn: parent
width: 18 width: 18
height: 18 height: 18
source: { source: parent.modelData.icon
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
smooth: true smooth: true
fillMode: Image.PreserveAspectFit
} }
MouseArea { MouseArea {
@@ -57,45 +43,23 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => { onClicked: (mouse) => {
if (!trayItem) return;
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
if (!trayItem.onlyMenu) { parent.modelData.activate()
trayItem.activate()
}
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
if (trayItem.hasMenu) { menuHandler.showMenu()
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")
}
} }
} }
} }
// Simple menu handling for now
QtObject { QtObject {
id: customTrayMenu id: menuHandler
property bool menuVisible: false function showMenu() {
if (parent.modelData.hasMenu) {
function showMenu(x, y) { console.log("Right-click menu for:", parent.modelData.title || "Unknown")
root.currentTrayMenu = customTrayMenu // TODO: Implement proper menu positioning
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
} }
} }

View File

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

View File

@@ -18,4 +18,7 @@ ClipboardHistory 1.0 ClipboardHistory.qml
CustomSlider 1.0 CustomSlider.qml CustomSlider 1.0 CustomSlider.qml
InputDialog 1.0 InputDialog.qml InputDialog 1.0 InputDialog.qml
BatteryWidget 1.0 BatteryWidget.qml BatteryWidget 1.0 BatteryWidget.qml
BatteryControlPopup 1.0 BatteryControlPopup.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 hasActiveMedia: activePlayer && (activePlayer.trackTitle || activePlayer.trackArtist)
property bool controlCenterVisible: false property bool controlCenterVisible: false
property bool batteryPopupVisible: 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 // Network properties from NetworkService
property string networkStatus: NetworkService.networkStatus property string networkStatus: NetworkService.networkStatus
@@ -259,6 +264,8 @@ ShellRoot {
id: globalInputDialog id: globalInputDialog
} }
BatteryControlPopup {} BatteryControlPopup {}
PowerMenuPopup {}
PowerConfirmDialog {}
// Application and clipboard components // Application and clipboard components
AppLauncher { AppLauncher {