1948 lines
94 KiB
Plaintext
1948 lines
94 KiB
Plaintext
import Quickshell
|
||
import Quickshell.Io
|
||
import Quickshell.Hyprland
|
||
import Quickshell.Wayland
|
||
import Quickshell.Widgets
|
||
import Quickshell.Services.SystemTray
|
||
import Quickshell.Services.Pipewire
|
||
import Quickshell.Services.UPower
|
||
import Quickshell.Bluetooth
|
||
import QtQuick
|
||
import QtQuick.Layouts
|
||
import QtQuick.Controls
|
||
|
||
ShellRoot {
|
||
id: root
|
||
|
||
// ───────────── SETTINGS & THEMING (from ~/.config/foot/foot.ini) ─────────────
|
||
readonly property string mono: "Maple Mono, JetBrainsMono Nerd Font, monospace"
|
||
readonly property int barWidth: 42
|
||
readonly property int wsCount: 5
|
||
|
||
// Foot colorscheme palette: no harsh pure white, cohesive dark aesthetic
|
||
readonly property color cBar: "#151515" // background
|
||
readonly property color cPanel: "#181818" // opaque Foot-black popup surface
|
||
readonly property color cSurface: "#202020" // card / elevated item
|
||
readonly property color cSurfaceHover: "#282828" // hover state
|
||
readonly property color cBorder: "#2f2f2f" // subtle border
|
||
readonly property color cBorderFocus: "#454545" // active border
|
||
readonly property color cText: "#D0D0D0" // foreground text
|
||
readonly property color cDim: "#505050" // bright0 (dim / muted)
|
||
readonly property color cSubtle: "#757575" // secondary text
|
||
readonly property color cBright: "#F5F5F5" // selection-foreground (soft crisp white)
|
||
readonly property color cAccent: "#6C99BB" // selection-background / regular4 (steel blue)
|
||
readonly property color cSelected: "#303030" // neutral selected state, aligned with Foot's dark palette
|
||
readonly property color cSuccess: "#7B9246" // regular2 (olive green)
|
||
readonly property color cWarn: "#D3A04D" // regular3 (warm amber)
|
||
readonly property color cDanger: "#A53C23" // regular1 (crimson rust)
|
||
readonly property color cTeal: "#5EA8A2" // regular6 (teal cyan)
|
||
|
||
// ───────────── SYSTEM STATE ─────────────
|
||
property bool launcherOpen: false
|
||
property bool powerMenuOpen: false
|
||
property string powerConfirmAction: ""
|
||
property string activeApplet: "" // "wifi", "bluetooth", "sound", "calendar", or ""
|
||
|
||
function closeMenus(): void {
|
||
root.launcherOpen = false
|
||
root.activeApplet = ""
|
||
root.powerMenuOpen = false
|
||
root.powerConfirmAction = ""
|
||
}
|
||
|
||
function runPowerAction(action: string): void {
|
||
root.closeMenus()
|
||
if (action === "logout") Quickshell.execDetached(["hyprctl", "dispatch", "exit"])
|
||
else if (action === "reboot") Quickshell.execDetached(["foot", "-e", "doas", "/sbin/reboot"])
|
||
else if (action === "poweroff") Quickshell.execDetached(["foot", "-e", "doas", "/sbin/poweroff"])
|
||
}
|
||
|
||
function runBluetoothAction(action: string, address: string): void {
|
||
root.btActionPending = true
|
||
root.btActionAddress = address
|
||
root.btActionMessage = ""
|
||
btActionProc.action = action
|
||
btActionProc.address = address
|
||
btActionProc.running = true
|
||
}
|
||
|
||
property bool netUp: false
|
||
property bool wifiEnabled: true
|
||
property string activeSsid: ""
|
||
property string ipAddress: ""
|
||
property var wifiNetworks: []
|
||
|
||
property bool btOn: false
|
||
property bool btConnected: false
|
||
property bool btScanning: false
|
||
property bool btActionPending: false
|
||
property string btActionAddress: ""
|
||
property string btActionMessage: ""
|
||
property var btDevices: []
|
||
|
||
// Writable volume/mute values are only available after each PipeWire node
|
||
// is bound. Track the complete live node model, including app streams.
|
||
PwObjectTracker { objects: Pipewire.nodes.values }
|
||
|
||
IpcHandler {
|
||
target: "launcher"
|
||
function toggle(): void { root.launcherOpen = !root.launcherOpen }
|
||
function open(): void { root.launcherOpen = true }
|
||
function close(): void { root.launcherOpen = false }
|
||
}
|
||
|
||
IpcHandler {
|
||
target: "power"
|
||
function toggle(): void {
|
||
root.launcherOpen = false
|
||
root.activeApplet = ""
|
||
root.powerConfirmAction = ""
|
||
root.powerMenuOpen = !root.powerMenuOpen
|
||
}
|
||
function open(): void {
|
||
root.launcherOpen = false
|
||
root.activeApplet = ""
|
||
root.powerConfirmAction = ""
|
||
root.powerMenuOpen = true
|
||
}
|
||
function close(): void { root.closeMenus() }
|
||
}
|
||
|
||
SystemClock { id: clock; precision: SystemClock.Seconds }
|
||
|
||
// Periodic system status collector (Network + Bluetooth)
|
||
Process {
|
||
id: statusProc
|
||
command: ["python3", "-c", `
|
||
import subprocess, json
|
||
|
||
data = {
|
||
'net_up': False,
|
||
'wifi_enabled': False,
|
||
'active_ssid': '',
|
||
'ip': '',
|
||
'bt_on': False,
|
||
'bt_connected': False,
|
||
'bt_devices': []
|
||
}
|
||
|
||
# Network check
|
||
try:
|
||
state = subprocess.run(['nmcli', '-t', '-f', 'STATE', 'general'], capture_output=True, text=True, timeout=2).stdout.strip()
|
||
data['net_up'] = state.startswith('connected')
|
||
|
||
wifi_rad = subprocess.run(['nmcli', '-t', '-f', 'WIFI', 'general'], capture_output=True, text=True, timeout=2).stdout.strip()
|
||
data['wifi_enabled'] = 'enabled' in wifi_rad
|
||
|
||
conns = subprocess.run(['nmcli', '-t', '-f', 'NAME,TYPE,DEVICE,STATE', 'con', 'show', '--active'], capture_output=True, text=True, timeout=2).stdout.strip()
|
||
for line in conns.split('\\n'):
|
||
if line:
|
||
parts = line.split(':')
|
||
if len(parts) >= 2 and ('wireless' in parts[1] or 'ethernet' in parts[1]):
|
||
data['active_ssid'] = parts[0]
|
||
break
|
||
|
||
ip_out = subprocess.run(['sh', '-c', "ip -4 route get 1.1.1.1 2>/dev/null | awk '{print $7}'"], capture_output=True, text=True, timeout=2).stdout.strip()
|
||
data['ip'] = ip_out
|
||
except Exception:
|
||
pass
|
||
|
||
# Bluetooth check
|
||
try:
|
||
show = subprocess.run(['bluetoothctl', 'show'], capture_output=True, text=True, timeout=2).stdout
|
||
if 'Powered: yes' in show:
|
||
data['bt_on'] = True
|
||
devs = subprocess.run(['bluetoothctl', 'devices'], capture_output=True, text=True, timeout=2).stdout
|
||
for line in devs.strip().split('\\n'):
|
||
if line.startswith('Device '):
|
||
parts = line.split(' ', 2)
|
||
if len(parts) >= 3:
|
||
mac, name = parts[1], parts[2]
|
||
info = subprocess.run(['bluetoothctl', 'info', mac], capture_output=True, text=True, timeout=1).stdout
|
||
conn = 'Connected: yes' in info
|
||
if conn:
|
||
data['bt_connected'] = True
|
||
paired = 'Paired: yes' in info
|
||
trusted = 'Trusted: yes' in info
|
||
data['bt_devices'].append({'mac': mac, 'name': name, 'connected': conn, 'paired': paired, 'trusted': trusted})
|
||
data['bt_devices'].sort(key=lambda d: (not d['connected'], not d['paired'], d['name'].lower()))
|
||
except Exception:
|
||
pass
|
||
|
||
print(json.dumps(data))
|
||
`]
|
||
stdout: StdioCollector {
|
||
onStreamFinished: {
|
||
try {
|
||
const res = JSON.parse(text)
|
||
root.netUp = res.net_up
|
||
root.wifiEnabled = res.wifi_enabled
|
||
root.activeSsid = res.active_ssid
|
||
root.ipAddress = res.ip
|
||
root.btOn = res.bt_on
|
||
root.btConnected = res.bt_connected
|
||
root.btDevices = res.bt_devices
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Discovery is started from the Bluetooth panel and is time limited.
|
||
Process {
|
||
id: btScanProc
|
||
command: ["sh", "-c", "bluetoothctl scan on && sleep 15 && bluetoothctl scan off"]
|
||
stdout: StdioCollector {
|
||
onStreamFinished: {
|
||
root.btScanning = false
|
||
statusProc.running = true
|
||
}
|
||
}
|
||
stderr: StdioCollector {
|
||
onStreamFinished: {
|
||
if (text.trim().length > 0)
|
||
console.warn("Bluetooth scan:", text.trim())
|
||
}
|
||
}
|
||
}
|
||
|
||
// Pair/connect operations run non-interactively from the menu; the user
|
||
// never has to open a bluetoothctl terminal.
|
||
Process {
|
||
id: btActionProc
|
||
property string action: ""
|
||
property string address: ""
|
||
command: action === "pair"
|
||
? ["bluetoothctl", "--agent", "NoInputNoOutput", "--timeout", "30", "pair", address]
|
||
: ["bluetoothctl", "--timeout", "20", action, address]
|
||
stdout: StdioCollector {
|
||
onStreamFinished: {
|
||
const lines = text.trim().split("\n").filter(line => line.length > 0)
|
||
if (lines.length > 0)
|
||
root.btActionMessage = lines[lines.length - 1]
|
||
statusProc.running = true
|
||
}
|
||
}
|
||
stderr: StdioCollector {
|
||
onStreamFinished: {
|
||
if (text.trim().length > 0) {
|
||
root.btActionMessage = text.trim().split("\n").pop()
|
||
console.warn("Bluetooth action:", text.trim())
|
||
}
|
||
statusProc.running = true
|
||
}
|
||
}
|
||
onExited: {
|
||
root.btActionPending = false
|
||
root.btActionAddress = ""
|
||
statusProc.running = true
|
||
}
|
||
}
|
||
|
||
Timer {
|
||
interval: 4000
|
||
running: true
|
||
repeat: true
|
||
triggeredOnStart: true
|
||
onTriggered: statusProc.running = true
|
||
}
|
||
|
||
// Wi-Fi network scanner
|
||
Process {
|
||
id: wifiScanProc
|
||
command: ["nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY,IN-USE", "dev", "wifi"]
|
||
stdout: StdioCollector {
|
||
onStreamFinished: {
|
||
const lines = text.trim().split("\n")
|
||
let results = []
|
||
for (let l of lines) {
|
||
if (!l) continue
|
||
const parts = l.split(":")
|
||
if (parts[0] && !results.some(r => r.ssid === parts[0])) {
|
||
results.push({
|
||
ssid: parts[0],
|
||
signal: parseInt(parts[1]) || 0,
|
||
security: parts[2] || "Open",
|
||
active: parts[3] === "*"
|
||
})
|
||
}
|
||
}
|
||
results.sort((a, b) => (b.active ? 1 : 0) - (a.active ? 1 : 0) || b.signal - a.signal)
|
||
root.wifiNetworks = results
|
||
}
|
||
}
|
||
}
|
||
|
||
// ───────────── VERTICAL BAR ─────────────
|
||
Variants {
|
||
model: Quickshell.screens
|
||
|
||
PanelWindow {
|
||
id: bar
|
||
required property var modelData
|
||
screen: modelData
|
||
|
||
anchors { top: true; bottom: true; left: true }
|
||
implicitWidth: root.barWidth
|
||
color: root.cBar
|
||
|
||
Rectangle {
|
||
anchors { top: parent.top; bottom: parent.bottom; right: parent.right }
|
||
width: 1
|
||
color: root.cBorder
|
||
}
|
||
|
||
// Top: Workspaces
|
||
ColumnLayout {
|
||
anchors { top: parent.top; topMargin: 12; horizontalCenter: parent.horizontalCenter }
|
||
spacing: 6
|
||
|
||
// Workspaces
|
||
Repeater {
|
||
model: root.wsCount
|
||
delegate: Rectangle {
|
||
required property int index
|
||
readonly property int wsId: index + 1
|
||
readonly property bool active: Hyprland.focusedWorkspace?.id === wsId
|
||
readonly property bool occupied: Hyprland.workspaces.values.some(w => w.id === wsId)
|
||
|
||
Layout.alignment: Qt.AlignHCenter
|
||
implicitWidth: 32; implicitHeight: 32
|
||
radius: 0
|
||
color: active ? root.cSelected : (wsHover.containsMouse ? root.cSurface : "transparent")
|
||
border.color: active ? root.cBorderFocus : "transparent"
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: parent.wsId
|
||
font.family: root.mono
|
||
font.pixelSize: 12
|
||
font.bold: parent.active
|
||
color: parent.active ? root.cBright : (parent.occupied ? root.cText : root.cDim)
|
||
}
|
||
|
||
MouseArea {
|
||
id: wsHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: Hyprland.dispatch('hl.dsp.focus({ workspace = "' + parent.wsId + '" })')
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Center: Clock (Clickable, opens Calendar)
|
||
Rectangle {
|
||
anchors.centerIn: parent
|
||
implicitWidth: 34
|
||
implicitHeight: 52
|
||
radius: 0
|
||
color: root.activeApplet === "calendar" ? root.cSelected : (clockHover.containsMouse ? root.cSurface : "transparent")
|
||
border.color: root.activeApplet === "calendar" ? root.cBorderFocus : "transparent"
|
||
|
||
ColumnLayout {
|
||
anchors.centerIn: parent
|
||
spacing: 1
|
||
|
||
Text {
|
||
Layout.alignment: Qt.AlignHCenter
|
||
text: Qt.formatDateTime(clock.date, "HH")
|
||
font.family: root.mono
|
||
font.pixelSize: 13
|
||
font.bold: true
|
||
color: root.activeApplet === "calendar" ? root.cBright : (clockHover.containsMouse ? root.cBright : root.cText)
|
||
}
|
||
|
||
Rectangle {
|
||
Layout.alignment: Qt.AlignHCenter
|
||
width: 4; height: 1
|
||
color: root.activeApplet === "calendar" ? root.cBorderFocus : root.cBorder
|
||
}
|
||
|
||
Text {
|
||
Layout.alignment: Qt.AlignHCenter
|
||
text: Qt.formatDateTime(clock.date, "mm")
|
||
font.family: root.mono
|
||
font.pixelSize: 13
|
||
font.bold: true
|
||
color: root.activeApplet === "calendar" ? root.cBright : (clockHover.containsMouse ? root.cBright : root.cDim)
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
id: clockHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: root.activeApplet = (root.activeApplet === "calendar" ? "" : "calendar")
|
||
}
|
||
}
|
||
|
||
// Bottom: Network, Bluetooth, Audio Triggers
|
||
ColumnLayout {
|
||
anchors { bottom: parent.bottom; bottomMargin: 12; horizontalCenter: parent.horizontalCenter }
|
||
spacing: 6
|
||
|
||
// Network Button
|
||
Rectangle {
|
||
Layout.alignment: Qt.AlignHCenter
|
||
implicitWidth: 32; implicitHeight: 32
|
||
radius: 0
|
||
color: root.activeApplet === "wifi" ? root.cSelected : (netHover.containsMouse ? root.cSurface : "transparent")
|
||
border.color: root.activeApplet === "wifi" ? root.cBorderFocus : "transparent"
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: root.netUp ? (root.activeSsid ? "" : "") : ""
|
||
font.family: root.mono; font.pixelSize: 14
|
||
color: root.activeApplet === "wifi" ? root.cBright : (root.netUp ? root.cText : root.cDim)
|
||
}
|
||
|
||
MouseArea {
|
||
id: netHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: {
|
||
if (root.activeApplet === "wifi") {
|
||
root.activeApplet = ""
|
||
} else {
|
||
root.activeApplet = "wifi"
|
||
wifiScanProc.running = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle { Layout.alignment: Qt.AlignHCenter; width: 16; height: 1; color: root.cBorder }
|
||
|
||
// Bluetooth Button
|
||
Rectangle {
|
||
Layout.alignment: Qt.AlignHCenter
|
||
implicitWidth: 32; implicitHeight: 32
|
||
radius: 0
|
||
color: root.activeApplet === "bluetooth" ? root.cSelected : (btHover.containsMouse ? root.cSurface : "transparent")
|
||
border.color: root.activeApplet === "bluetooth" ? root.cBorderFocus : "transparent"
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: root.btConnected ? "" : (root.btOn ? "" : "")
|
||
font.family: root.mono; font.pixelSize: 14
|
||
color: root.activeApplet === "bluetooth" ? root.cBright : (root.btOn ? root.cText : root.cDim)
|
||
}
|
||
|
||
MouseArea {
|
||
id: btHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: root.activeApplet = (root.activeApplet === "bluetooth" ? "" : "bluetooth")
|
||
}
|
||
}
|
||
|
||
Rectangle { Layout.alignment: Qt.AlignHCenter; width: 16; height: 1; color: root.cBorder }
|
||
|
||
// Audio Button
|
||
Rectangle {
|
||
Layout.alignment: Qt.AlignHCenter
|
||
implicitWidth: 32; implicitHeight: 32
|
||
radius: 0
|
||
color: root.activeApplet === "sound" ? root.cSelected : (soundHover.containsMouse ? root.cSurface : "transparent")
|
||
border.color: root.activeApplet === "sound" ? root.cBorderFocus : "transparent"
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: {
|
||
if (Pipewire.defaultAudioSink?.audio?.muted) return ""
|
||
const vol = Pipewire.defaultAudioSink?.audio?.volume ?? 0
|
||
if (vol > 0.5) return ""
|
||
if (vol > 0) return ""
|
||
return ""
|
||
}
|
||
font.family: root.mono; font.pixelSize: 14
|
||
color: {
|
||
if (Pipewire.defaultAudioSink?.audio?.muted) return root.cDanger
|
||
if (root.activeApplet === "sound") return root.cBright
|
||
return soundHover.containsMouse ? root.cBright : root.cText
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
id: soundHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: root.activeApplet = (root.activeApplet === "sound" ? "" : "sound")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ───────────── MENU BACKDROP BLUR ─────────────
|
||
// Blur the active monitor behind open shell surfaces while preserving the bar.
|
||
Variants {
|
||
model: Quickshell.screens
|
||
|
||
PanelWindow {
|
||
id: menuBackdrop
|
||
required property var modelData
|
||
screen: modelData
|
||
visible: (root.activeApplet !== "" || root.launcherOpen || root.powerMenuOpen)
|
||
&& modelData.name === (Hyprland.focusedMonitor?.name ?? Quickshell.screens[0]?.name)
|
||
|
||
anchors { top: true; bottom: true; left: true; right: true }
|
||
color: "transparent"
|
||
mask: Region {}
|
||
|
||
WlrLayershell.layer: WlrLayer.Top
|
||
WlrLayershell.namespace: "menu-backdrop"
|
||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||
exclusionMode: ExclusionMode.Ignore
|
||
|
||
BackgroundEffect.blurRegion: Region {
|
||
item: menuBackdrop.contentItem
|
||
|
||
// The vertical bar occupies the leftmost 42 logical pixels.
|
||
Region {
|
||
x: 0
|
||
y: 0
|
||
width: root.barWidth
|
||
height: menuBackdrop.height
|
||
intersection: Intersection.Subtract
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ───────────── APP LAUNCHER OVERLAY ─────────────
|
||
PanelWindow {
|
||
id: launcherWindow
|
||
visible: root.launcherOpen
|
||
|
||
screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? Quickshell.screens[0]
|
||
|
||
// Full-screen overlay surface; the launcher itself stays centered.
|
||
anchors { top: true; bottom: true; left: true; right: true }
|
||
color: "transparent"
|
||
|
||
WlrLayershell.layer: WlrLayer.Overlay
|
||
WlrLayershell.namespace: "launcher"
|
||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||
exclusionMode: ExclusionMode.Ignore
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onClicked: root.launcherOpen = false
|
||
}
|
||
|
||
Rectangle {
|
||
anchors.centerIn: parent
|
||
width: 340
|
||
height: 480
|
||
color: root.cPanel
|
||
border.color: root.cBorder
|
||
border.width: 1
|
||
radius: 0
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
}
|
||
|
||
FocusScope {
|
||
id: launcherScope
|
||
anchors.fill: parent
|
||
focus: launcherWindow.visible
|
||
|
||
Keys.onEscapePressed: root.closeMenus()
|
||
|
||
ColumnLayout {
|
||
anchors.fill: parent
|
||
anchors.margins: 12
|
||
spacing: 10
|
||
|
||
// Search Input Box
|
||
Rectangle {
|
||
Layout.fillWidth: true
|
||
implicitHeight: 36
|
||
color: root.cSurface
|
||
border.color: root.cBorderFocus
|
||
border.width: 1
|
||
radius: 0
|
||
|
||
RowLayout {
|
||
anchors.fill: parent
|
||
anchors.leftMargin: 10
|
||
anchors.rightMargin: 10
|
||
spacing: 8
|
||
|
||
Text {
|
||
text: ""
|
||
font.family: root.mono
|
||
font.pixelSize: 13
|
||
color: root.cSubtle
|
||
}
|
||
|
||
TextInput {
|
||
id: searchInput
|
||
Layout.fillWidth: true
|
||
font.family: root.mono
|
||
font.pixelSize: 12
|
||
color: root.cBright
|
||
focus: true
|
||
selectByMouse: true
|
||
|
||
Text {
|
||
text: "Search applications..."
|
||
font.family: root.mono
|
||
font.pixelSize: 12
|
||
color: root.cDim
|
||
visible: searchInput.text === ""
|
||
}
|
||
|
||
Keys.onDownPressed: appListView.incrementCurrentIndex()
|
||
Keys.onUpPressed: appListView.decrementCurrentIndex()
|
||
Keys.onReturnPressed: {
|
||
if (appListView.currentItem) {
|
||
appListView.currentItem.launch()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Application List
|
||
ListView {
|
||
id: appListView
|
||
Layout.fillWidth: true
|
||
Layout.fillHeight: true
|
||
clip: true
|
||
spacing: 3
|
||
currentIndex: 0
|
||
|
||
model: appListProc.apps
|
||
|
||
delegate: Rectangle {
|
||
id: appItem
|
||
required property var modelData
|
||
required property int index
|
||
|
||
width: appListView.width
|
||
height: 38
|
||
radius: 0
|
||
color: ListView.isCurrentItem ? root.cSelected : (itemArea.containsMouse ? root.cSurfaceHover : "transparent")
|
||
border.color: ListView.isCurrentItem ? root.cBorderFocus : "transparent"
|
||
|
||
function launch() {
|
||
Quickshell.execDetached(["sh", "-c", modelData.exec])
|
||
root.launcherOpen = false
|
||
searchInput.text = ""
|
||
}
|
||
|
||
RowLayout {
|
||
anchors.fill: parent
|
||
anchors.leftMargin: 10
|
||
anchors.rightMargin: 10
|
||
spacing: 10
|
||
|
||
Text {
|
||
text: ""
|
||
font.family: root.mono
|
||
font.pixelSize: 13
|
||
color: ListView.isCurrentItem ? root.cAccent : root.cSubtle
|
||
}
|
||
|
||
Text {
|
||
text: modelData.name
|
||
font.family: root.mono
|
||
font.pixelSize: 12
|
||
color: ListView.isCurrentItem ? root.cBright : root.cText
|
||
Layout.fillWidth: true
|
||
elide: Text.ElideRight
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
id: itemArea
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: appItem.launch()
|
||
}
|
||
}
|
||
|
||
// Empty search state
|
||
Item {
|
||
anchors.centerIn: parent
|
||
visible: appListView.count === 0
|
||
ColumnLayout {
|
||
anchors.centerIn: parent
|
||
spacing: 4
|
||
Text { Layout.alignment: Qt.AlignHCenter; text: "No applications found"; font.family: root.mono; font.pixelSize: 12; color: root.cDim }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Desktop applications scanner with flatpak and standard fallbacks
|
||
Process {
|
||
id: appListProc
|
||
property var rawApps: []
|
||
property var apps: {
|
||
const query = searchInput.text.toLowerCase().trim()
|
||
if (!query) return rawApps
|
||
return rawApps.filter(a => a.name.toLowerCase().includes(query))
|
||
}
|
||
|
||
command: ["python3", "-c", `
|
||
import os, glob
|
||
|
||
dirs = [
|
||
'/usr/share/applications',
|
||
'/usr/local/share/applications',
|
||
os.path.expanduser('~/.local/share/applications'),
|
||
os.path.expanduser('~/.local/share/flatpak/exports/share/applications'),
|
||
'/var/lib/flatpak/exports/share/applications'
|
||
]
|
||
xdg = os.environ.get('XDG_DATA_DIRS', '')
|
||
if xdg:
|
||
for p in xdg.split(':'):
|
||
app_dir = os.path.join(p, 'applications')
|
||
if app_dir not in dirs:
|
||
dirs.append(app_dir)
|
||
|
||
apps = []
|
||
seen = set()
|
||
for d in dirs:
|
||
if not os.path.isdir(d):
|
||
continue
|
||
for path in glob.glob(os.path.join(d, '*.desktop')):
|
||
try:
|
||
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
||
name, exec_cmd, nodisplay = '', '', False
|
||
for line in f:
|
||
line = line.strip()
|
||
if line.startswith('Name=') and not name:
|
||
name = line.split('=', 1)[1].strip()
|
||
elif line.startswith('Exec=') and not exec_cmd:
|
||
exec_cmd = line.split('=', 1)[1].strip().split('%')[0].strip()
|
||
elif line.startswith('NoDisplay=true'):
|
||
nodisplay = True
|
||
if name and exec_cmd and not nodisplay:
|
||
if name.lower() not in seen:
|
||
seen.add(name.lower())
|
||
apps.append((name, exec_cmd))
|
||
except:
|
||
pass
|
||
|
||
# Fallbacks for essential desktop tools
|
||
fallbacks = [
|
||
('Terminal (Foot)', 'foot'),
|
||
('Fish Shell', 'foot -e fish'),
|
||
]
|
||
for name, cmd in fallbacks:
|
||
if name.lower() not in seen:
|
||
apps.append((name, cmd))
|
||
|
||
apps.sort(key=lambda x: x[0].lower())
|
||
for name, cmd in apps:
|
||
print(f"{name}|||{cmd}")
|
||
`]
|
||
stdout: StdioCollector {
|
||
onStreamFinished: {
|
||
const lines = text.trim().split("\n")
|
||
let parsed = []
|
||
for (let line of lines) {
|
||
const parts = line.split("|||")
|
||
if (parts.length === 2 && parts[0]) {
|
||
parsed.push({ name: parts[0], exec: parts[1] })
|
||
}
|
||
}
|
||
appListProc.rawApps = parsed
|
||
}
|
||
}
|
||
}
|
||
|
||
Component.onCompleted: appListProc.running = true
|
||
onVisibleChanged: {
|
||
if (visible) {
|
||
searchInput.text = ""
|
||
searchInput.forceActiveFocus()
|
||
appListProc.running = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ───────────── CALENDAR POPUP WINDOW ─────────────
|
||
PanelWindow {
|
||
id: calendarWindow
|
||
visible: root.activeApplet === "calendar"
|
||
|
||
screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? Quickshell.screens[0]
|
||
|
||
// Vertically centered beside the bar; this overlay reserves no space from app windows.
|
||
anchors { left: true; top: true }
|
||
margins.left: root.barWidth + 14
|
||
margins.top: Math.max(8, (screen.height - implicitHeight) / 2)
|
||
|
||
implicitWidth: 320
|
||
implicitHeight: 390
|
||
color: "transparent"
|
||
|
||
WlrLayershell.layer: WlrLayer.Overlay
|
||
WlrLayershell.namespace: "calendar"
|
||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||
aboveWindows: true
|
||
exclusionMode: ExclusionMode.Ignore
|
||
|
||
Rectangle {
|
||
anchors.fill: parent
|
||
color: root.cPanel
|
||
border.color: root.cBorder
|
||
border.width: 1
|
||
radius: 0
|
||
|
||
FocusScope {
|
||
id: calendarScope
|
||
anchors.fill: parent
|
||
focus: calendarWindow.visible
|
||
Keys.onEscapePressed: root.closeMenus()
|
||
|
||
property date viewDate: new Date()
|
||
|
||
function getCellData(index) {
|
||
const y = calendarScope.viewDate.getFullYear()
|
||
const m = calendarScope.viewDate.getMonth()
|
||
const firstDay = new Date(y, m, 1)
|
||
// Monday as 0, Sunday as 6
|
||
const startDay = (firstDay.getDay() + 6) % 7
|
||
const date = new Date(y, m, 1 - startDay + index)
|
||
const now = clock.date
|
||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||
const offset = Math.round((date.getTime() - today.getTime()) / 86400000)
|
||
|
||
return {
|
||
day: date.getDate(),
|
||
isCurrentMonth: date.getMonth() === m,
|
||
isToday: offset === 0,
|
||
isWeekend: index % 7 >= 5,
|
||
dayOffset: offset
|
||
}
|
||
}
|
||
|
||
ColumnLayout {
|
||
anchors.fill: parent
|
||
anchors.margins: 16
|
||
spacing: 12
|
||
|
||
// Header: Full Date & Clock
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
|
||
ColumnLayout {
|
||
spacing: 2
|
||
Text {
|
||
text: Qt.formatDateTime(clock.date, "dddd, MMMM d, yyyy")
|
||
font.family: root.mono; font.pixelSize: 12; font.bold: true; color: root.cBright
|
||
}
|
||
Text {
|
||
text: Qt.formatDateTime(clock.date, "HH:mm:ss")
|
||
font.family: root.mono; font.pixelSize: 11; color: root.cAccent
|
||
}
|
||
}
|
||
|
||
Item { Layout.fillWidth: true }
|
||
}
|
||
|
||
Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder }
|
||
|
||
// Month Navigation Row
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
|
||
Text {
|
||
text: Qt.formatDate(calendarScope.viewDate, "MMMM yyyy")
|
||
font.family: root.mono; font.pixelSize: 13; font.bold: true; color: root.cBright
|
||
Layout.fillWidth: true
|
||
}
|
||
|
||
// Prev Month Button
|
||
Rectangle {
|
||
width: 26; height: 26; radius: 0
|
||
color: prevHover.containsMouse ? root.cSurfaceHover : root.cSurface
|
||
border.color: root.cBorder
|
||
|
||
Text { anchors.centerIn: parent; text: ""; font.family: root.mono; font.pixelSize: 12; color: root.cText }
|
||
MouseArea {
|
||
id: prevHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: {
|
||
const v = calendarScope.viewDate
|
||
calendarScope.viewDate = new Date(v.getFullYear(), v.getMonth() - 1, 1)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Today Button
|
||
Rectangle {
|
||
implicitWidth: 46; implicitHeight: 26; radius: 0
|
||
color: todayHover.containsMouse ? root.cSurfaceHover : root.cSurface
|
||
border.color: root.cBorder
|
||
|
||
Text { anchors.centerIn: parent; text: "Today"; font.family: root.mono; font.pixelSize: 10; color: root.cAccent }
|
||
MouseArea {
|
||
id: todayHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: calendarScope.viewDate = new Date()
|
||
}
|
||
}
|
||
|
||
// Next Month Button
|
||
Rectangle {
|
||
width: 26; height: 26; radius: 0
|
||
color: nextHover.containsMouse ? root.cSurfaceHover : root.cSurface
|
||
border.color: root.cBorder
|
||
|
||
Text { anchors.centerIn: parent; text: ""; font.family: root.mono; font.pixelSize: 12; color: root.cText }
|
||
MouseArea {
|
||
id: nextHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: {
|
||
const v = calendarScope.viewDate
|
||
calendarScope.viewDate = new Date(v.getFullYear(), v.getMonth() + 1, 1)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Weekdays Header (Mon..Sun)
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 2
|
||
|
||
Repeater {
|
||
model: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
|
||
delegate: Item {
|
||
Layout.fillWidth: true
|
||
height: 20
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: modelData
|
||
font.family: root.mono; font.pixelSize: 11; font.bold: true
|
||
color: (index >= 5) ? root.cDanger : root.cSubtle
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 42-day Calendar Grid
|
||
GridLayout {
|
||
id: calendarGrid
|
||
Layout.fillWidth: true
|
||
Layout.fillHeight: true
|
||
columns: 7
|
||
rows: 6
|
||
rowSpacing: 3
|
||
columnSpacing: 2
|
||
|
||
Repeater {
|
||
model: 42
|
||
delegate: Rectangle {
|
||
required property int index
|
||
readonly property var cell: calendarScope.getCellData(index)
|
||
readonly property string dayOffsetText: {
|
||
if (cell.dayOffset > 0)
|
||
return cell.dayOffset === 1 ? "In 1 day" : "In " + cell.dayOffset + " days"
|
||
const daysAgo = Math.abs(cell.dayOffset)
|
||
return daysAgo === 1 ? "1 day ago" : daysAgo + " days ago"
|
||
}
|
||
|
||
Layout.fillWidth: true
|
||
Layout.fillHeight: true
|
||
radius: 0
|
||
z: dayHover.containsMouse ? 2 : 0
|
||
color: cell.isToday ? root.cAccent : (dayHover.containsMouse ? root.cSurfaceHover : "transparent")
|
||
border.color: (cell.isToday) ? root.cAccent : "transparent"
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: parent.cell.day
|
||
font.family: root.mono
|
||
font.pixelSize: 11
|
||
font.bold: parent.cell.isToday
|
||
color: {
|
||
if (parent.cell.isToday) return root.cBar
|
||
if (parent.cell.isWeekend) return root.cDanger
|
||
if (parent.cell.isCurrentMonth) return root.cText
|
||
return root.cDim
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
visible: dayHover.containsMouse && !cell.isToday
|
||
z: 3
|
||
anchors.bottom: parent.top
|
||
anchors.bottomMargin: 4
|
||
anchors.horizontalCenter: parent.horizontalCenter
|
||
implicitWidth: dayOffsetLabel.implicitWidth + 14
|
||
implicitHeight: 24
|
||
radius: 0
|
||
color: root.cSurface
|
||
border.color: root.cBorder
|
||
|
||
Text {
|
||
id: dayOffsetLabel
|
||
anchors.centerIn: parent
|
||
text: parent.parent.dayOffsetText
|
||
font.family: root.mono
|
||
font.pixelSize: 10
|
||
color: root.cText
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
id: dayHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ───────────── APPLETS POPUP WINDOW (NETWORK / BLUETOOTH / AUDIO) ─────────────
|
||
PanelWindow {
|
||
id: appletWindow
|
||
visible: root.activeApplet !== "" && root.activeApplet !== "calendar"
|
||
|
||
screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? Quickshell.screens[0]
|
||
|
||
// Keep a clear 14px gutter from the vertical bar, anchored to bottom
|
||
anchors { left: true; bottom: true }
|
||
margins.left: root.barWidth + 14
|
||
margins.bottom: 8
|
||
|
||
implicitWidth: 360
|
||
implicitHeight: 460
|
||
color: "transparent"
|
||
|
||
WlrLayershell.layer: WlrLayer.Overlay
|
||
WlrLayershell.namespace: "applet"
|
||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||
aboveWindows: true
|
||
exclusionMode: ExclusionMode.Ignore
|
||
|
||
Rectangle {
|
||
anchors.fill: parent
|
||
color: root.cPanel
|
||
border.color: root.cBorder
|
||
border.width: 1
|
||
radius: 0
|
||
|
||
FocusScope {
|
||
id: appletScope
|
||
anchors.fill: parent
|
||
focus: appletWindow.visible
|
||
Keys.onEscapePressed: root.closeMenus()
|
||
|
||
ColumnLayout {
|
||
anchors.fill: parent
|
||
anchors.margins: 16
|
||
spacing: 12
|
||
|
||
// Each bar control opens its own view; keep this header informational.
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
Text {
|
||
text: root.activeApplet === "wifi" ? "NETWORK" : (root.activeApplet === "bluetooth" ? "BLUETOOTH" : "AUDIO")
|
||
font.family: root.mono
|
||
font.pixelSize: 12
|
||
font.bold: true
|
||
color: root.cBright
|
||
}
|
||
Item { Layout.fillWidth: true }
|
||
}
|
||
|
||
Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder }
|
||
|
||
// Container for the 3 Applet Views
|
||
Item {
|
||
Layout.fillWidth: true
|
||
Layout.fillHeight: true
|
||
|
||
// ══════════════════ NETWORK VIEW ══════════════════
|
||
ColumnLayout {
|
||
anchors.fill: parent
|
||
visible: root.activeApplet === "wifi"
|
||
spacing: 12
|
||
|
||
// Status & Toggle Card
|
||
Rectangle {
|
||
Layout.fillWidth: true
|
||
implicitHeight: 64
|
||
color: root.cSurface
|
||
border.color: root.cBorder
|
||
radius: 0
|
||
|
||
RowLayout {
|
||
anchors.fill: parent
|
||
anchors.margins: 12
|
||
spacing: 10
|
||
|
||
Text {
|
||
text: root.netUp ? (root.activeSsid ? "" : "") : ""
|
||
font.family: root.mono; font.pixelSize: 22
|
||
color: root.netUp ? root.cSuccess : root.cWarn
|
||
}
|
||
|
||
ColumnLayout {
|
||
spacing: 2
|
||
Text {
|
||
text: root.netUp ? (root.activeSsid ? root.activeSsid : "Wired Connection") : "Disconnected"
|
||
font.family: root.mono; font.pixelSize: 13; font.bold: true; color: root.cBright
|
||
elide: Text.ElideRight
|
||
}
|
||
Text {
|
||
text: root.ipAddress ? ("IP: " + root.ipAddress) : (root.wifiEnabled ? "Wi-Fi Enabled" : "Wi-Fi Disabled")
|
||
font.family: root.mono; font.pixelSize: 10; color: root.cSubtle
|
||
}
|
||
}
|
||
|
||
Item { Layout.fillWidth: true }
|
||
|
||
// Wi-Fi Toggle Pill
|
||
Rectangle {
|
||
implicitWidth: 36; implicitHeight: 20; radius: 0
|
||
color: root.wifiEnabled ? root.cAccent : root.cSurfaceHover
|
||
border.color: root.wifiEnabled ? root.cAccent : root.cBorder
|
||
border.width: 1
|
||
|
||
Rectangle {
|
||
x: root.wifiEnabled ? 18 : 2; y: 2; width: 16; height: 16; radius: 0
|
||
color: root.wifiEnabled ? root.cBar : root.cDim
|
||
Behavior on x { NumberAnimation { duration: 150 } }
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: {
|
||
const newState = !root.wifiEnabled
|
||
root.wifiEnabled = newState
|
||
Quickshell.execDetached(["nmcli", "radio", "wifi", newState ? "on" : "off"])
|
||
if (newState) wifiScanProc.running = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Keep the network action in the panel; no terminal launcher.
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 0
|
||
|
||
Rectangle {
|
||
Layout.fillWidth: true; implicitHeight: 28; radius: 0
|
||
color: rescanHover.containsMouse ? root.cSurfaceHover : root.cSurface
|
||
border.color: root.cBorder
|
||
|
||
RowLayout {
|
||
anchors.centerIn: parent; spacing: 6
|
||
Text { text: ""; font.family: root.mono; font.pixelSize: 11; color: root.cAccent }
|
||
Text { text: "Rescan"; font.family: root.mono; font.pixelSize: 11; color: root.cText }
|
||
}
|
||
MouseArea {
|
||
id: rescanHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: wifiScanProc.running = true
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// Available Networks Header
|
||
Text {
|
||
text: "AVAILABLE NETWORKS"
|
||
font.family: root.mono; font.pixelSize: 10; font.bold: true; color: root.cSubtle
|
||
Layout.topMargin: 2
|
||
}
|
||
|
||
// Wi-Fi Networks List
|
||
ListView {
|
||
Layout.fillWidth: true
|
||
Layout.fillHeight: true
|
||
clip: true
|
||
spacing: 4
|
||
model: root.wifiNetworks
|
||
|
||
delegate: Rectangle {
|
||
width: ListView.view.width
|
||
height: 36
|
||
radius: 0
|
||
color: modelData.active ? root.cSelected : (wifiItemHover.containsMouse ? root.cSurfaceHover : root.cSurface)
|
||
border.color: modelData.active ? root.cAccent : "transparent"
|
||
|
||
RowLayout {
|
||
anchors.fill: parent
|
||
anchors.leftMargin: 10
|
||
anchors.rightMargin: 10
|
||
spacing: 10
|
||
|
||
Text {
|
||
text: ""
|
||
font.family: root.mono; font.pixelSize: 12
|
||
color: modelData.active ? root.cAccent : (modelData.signal > 50 ? root.cText : root.cDim)
|
||
}
|
||
|
||
Text {
|
||
text: modelData.ssid
|
||
font.family: root.mono; font.pixelSize: 12
|
||
color: modelData.active ? root.cBright : root.cText
|
||
Layout.fillWidth: true
|
||
elide: Text.ElideRight
|
||
}
|
||
|
||
// Security lock
|
||
Text {
|
||
visible: modelData.security && !modelData.security.includes("Open")
|
||
text: ""
|
||
font.family: root.mono; font.pixelSize: 10; color: root.cDim
|
||
}
|
||
|
||
// Signal percentage
|
||
Text {
|
||
text: modelData.signal + "%"
|
||
font.family: root.mono; font.pixelSize: 10; color: root.cSubtle
|
||
}
|
||
|
||
// Active badge
|
||
Rectangle {
|
||
visible: modelData.active
|
||
width: 8; height: 8; radius: 0
|
||
color: root.cSuccess
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
id: wifiItemHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: {
|
||
if (modelData.active) {
|
||
Quickshell.execDetached(["nmcli", "connection", "down", "id", modelData.ssid])
|
||
} else {
|
||
Quickshell.execDetached(["foot", "-e", "nmtui-connect", modelData.ssid])
|
||
}
|
||
root.activeApplet = ""
|
||
}
|
||
}
|
||
}
|
||
|
||
// Empty state
|
||
Item {
|
||
anchors.centerIn: parent
|
||
visible: root.wifiNetworks.length === 0
|
||
ColumnLayout {
|
||
anchors.centerIn: parent
|
||
spacing: 4
|
||
Text { Layout.alignment: Qt.AlignHCenter; text: root.wifiEnabled ? "Scanning for networks..." : "Wi-Fi is currently disabled"; font.family: root.mono; font.pixelSize: 12; color: root.cDim }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ══════════════════ BLUETOOTH VIEW ══════════════════
|
||
ColumnLayout {
|
||
anchors.fill: parent
|
||
visible: root.activeApplet === "bluetooth"
|
||
spacing: 12
|
||
|
||
// Status & Toggle Card
|
||
Rectangle {
|
||
Layout.fillWidth: true
|
||
implicitHeight: 64
|
||
color: root.cSurface
|
||
border.color: root.cBorder
|
||
radius: 0
|
||
|
||
RowLayout {
|
||
anchors.fill: parent
|
||
anchors.margins: 12
|
||
spacing: 10
|
||
|
||
Text {
|
||
text: root.btConnected ? "" : (root.btOn ? "" : "")
|
||
font.family: root.mono; font.pixelSize: 22
|
||
color: root.btOn ? (root.btConnected ? root.cSuccess : root.cAccent) : root.cDim
|
||
}
|
||
|
||
ColumnLayout {
|
||
spacing: 2
|
||
Text {
|
||
text: root.btOn ? (root.btConnected ? "Device Connected" : "Bluetooth Enabled") : "Bluetooth Disabled"
|
||
font.family: root.mono; font.pixelSize: 13; font.bold: true; color: root.cBright
|
||
}
|
||
Text {
|
||
text: root.btActionMessage || (root.btDevices.length > 0 ? (root.btDevices.length + " known devices") : "No devices")
|
||
font.family: root.mono; font.pixelSize: 10; color: root.cSubtle
|
||
elide: Text.ElideRight
|
||
Layout.maximumWidth: 190
|
||
}
|
||
}
|
||
|
||
Item { Layout.fillWidth: true }
|
||
|
||
// Bluetooth Power Toggle Pill
|
||
Rectangle {
|
||
implicitWidth: 36; implicitHeight: 20; radius: 0
|
||
color: root.btOn ? root.cAccent : root.cSurfaceHover
|
||
border.color: root.btOn ? root.cAccent : root.cBorder
|
||
border.width: 1
|
||
|
||
Rectangle {
|
||
x: root.btOn ? 18 : 2; y: 2; width: 16; height: 16; radius: 0
|
||
color: root.btOn ? root.cBar : root.cDim
|
||
Behavior on x { NumberAnimation { duration: 150 } }
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: {
|
||
const newState = !root.btOn
|
||
root.btOn = newState
|
||
if (Bluetooth.defaultAdapter)
|
||
Bluetooth.defaultAdapter.enabled = newState
|
||
statusProc.running = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Start a bounded discovery from this panel. Pairing and
|
||
// connect actions remain here, without opening a terminal.
|
||
Rectangle {
|
||
Layout.fillWidth: true
|
||
implicitHeight: 30
|
||
radius: 0
|
||
color: btScanHover.containsMouse ? root.cSurfaceHover : root.cSurface
|
||
border.color: root.cBorder
|
||
|
||
RowLayout {
|
||
anchors.centerIn: parent
|
||
spacing: 7
|
||
Text {
|
||
text: root.btScanning ? "◌" : (root.btOn ? "" : "")
|
||
font.family: root.mono; font.pixelSize: 11
|
||
color: root.cAccent
|
||
}
|
||
Text {
|
||
text: root.btScanning ? "Scanning nearby devices…" : (root.btOn ? "Scan nearby devices" : "Enable Bluetooth")
|
||
font.family: root.mono; font.pixelSize: 11
|
||
color: root.cText
|
||
}
|
||
}
|
||
MouseArea {
|
||
id: btScanHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
enabled: !root.btScanning
|
||
onClicked: {
|
||
if (!root.btOn) {
|
||
if (Bluetooth.defaultAdapter)
|
||
Bluetooth.defaultAdapter.enabled = true
|
||
statusProc.running = true
|
||
} else {
|
||
root.btScanning = true
|
||
btScanProc.running = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Paired Devices Header
|
||
Text {
|
||
text: "NEARBY & PAIRED DEVICES"
|
||
font.family: root.mono; font.pixelSize: 10; font.bold: true; color: root.cSubtle
|
||
Layout.topMargin: 2
|
||
}
|
||
|
||
// Bluetooth Devices List
|
||
ListView {
|
||
Layout.fillWidth: true
|
||
Layout.fillHeight: true
|
||
clip: true
|
||
spacing: 4
|
||
model: root.btDevices
|
||
|
||
delegate: Rectangle {
|
||
width: ListView.view.width
|
||
height: 50
|
||
radius: 0
|
||
color: modelData.connected ? root.cSelected : root.cSurface
|
||
border.color: modelData.connected ? root.cBorderFocus : "transparent"
|
||
|
||
RowLayout {
|
||
anchors.fill: parent
|
||
anchors.leftMargin: 10
|
||
anchors.rightMargin: 10
|
||
spacing: 10
|
||
|
||
Text {
|
||
text: ""
|
||
font.family: root.mono; font.pixelSize: 14
|
||
color: modelData.connected ? root.cSuccess : root.cSubtle
|
||
}
|
||
|
||
ColumnLayout {
|
||
Layout.fillWidth: true; spacing: 1
|
||
Text {
|
||
text: modelData.name
|
||
font.family: root.mono; font.pixelSize: 12
|
||
color: modelData.connected ? root.cBright : root.cText
|
||
elide: Text.ElideRight
|
||
}
|
||
Text {
|
||
text: modelData.mac
|
||
font.family: root.mono; font.pixelSize: 9
|
||
color: root.cDim
|
||
}
|
||
}
|
||
|
||
// Connect / Disconnect Action Button
|
||
Rectangle {
|
||
implicitWidth: modelData.connected ? 78 : (modelData.paired ? 62 : 48)
|
||
implicitHeight: 22
|
||
radius: 0
|
||
color: modelData.connected ? root.cDanger : (btActionHover.containsMouse ? root.cSelected : root.cSurfaceHover)
|
||
border.color: root.cBorder
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: root.btActionPending && root.btActionAddress === modelData.mac
|
||
? "Working…"
|
||
: (modelData.connected ? "Disconnect" : (modelData.paired ? "Connect" : "Pair"))
|
||
font.family: root.mono; font.pixelSize: 9
|
||
color: modelData.connected ? root.cBright : root.cText
|
||
}
|
||
|
||
MouseArea {
|
||
id: btActionHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
enabled: !root.btActionPending
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: {
|
||
const action = modelData.connected ? "disconnect" : (modelData.paired ? "connect" : "pair")
|
||
root.runBluetoothAction(action, modelData.mac)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Empty state
|
||
Item {
|
||
anchors.centerIn: parent
|
||
visible: root.btDevices.length === 0
|
||
ColumnLayout {
|
||
anchors.centerIn: parent
|
||
spacing: 4
|
||
Text {
|
||
Layout.alignment: Qt.AlignHCenter
|
||
text: root.btOn ? (root.btScanning ? "Looking for nearby devices…" : "No devices found yet") : "Bluetooth is powered off"
|
||
font.family: root.mono; font.pixelSize: 12; color: root.cDim
|
||
}
|
||
Text {
|
||
Layout.alignment: Qt.AlignHCenter
|
||
text: "Scan nearby devices, then choose Pair or Connect"
|
||
font.family: root.mono; font.pixelSize: 10; color: root.cSubtle
|
||
visible: root.btOn && !root.btScanning
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ══════════════════ AUDIO VIEW ══════════════════
|
||
ScrollView {
|
||
anchors.fill: parent
|
||
contentWidth: availableWidth
|
||
clip: true
|
||
visible: root.activeApplet === "sound"
|
||
|
||
ColumnLayout {
|
||
width: parent.width
|
||
spacing: 10
|
||
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
Text {
|
||
text: "OUTPUT"
|
||
font.family: root.mono; font.pixelSize: 10; font.bold: true
|
||
color: root.cSubtle
|
||
}
|
||
Item { Layout.fillWidth: true }
|
||
Text {
|
||
text: Pipewire.defaultAudioSink?.description || Pipewire.defaultAudioSink?.name || "No output device"
|
||
font.family: root.mono; font.pixelSize: 10
|
||
color: root.cDim
|
||
elide: Text.ElideRight
|
||
Layout.maximumWidth: 190
|
||
}
|
||
}
|
||
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 10
|
||
Text {
|
||
text: Pipewire.defaultAudioSink?.audio?.muted ? "" : ""
|
||
font.family: root.mono; font.pixelSize: 15
|
||
color: Pipewire.defaultAudioSink?.audio?.muted ? root.cDanger : root.cText
|
||
}
|
||
Slider {
|
||
id: masterVolSlider
|
||
Layout.fillWidth: true
|
||
from: 0; to: 1.0; stepSize: 0.01
|
||
value: Pipewire.defaultAudioSink?.audio?.volume ?? 0
|
||
onMoved: {
|
||
if (Pipewire.defaultAudioSink?.audio)
|
||
Pipewire.defaultAudioSink.audio.volume = value
|
||
}
|
||
background: Rectangle {
|
||
x: masterVolSlider.leftPadding
|
||
y: masterVolSlider.topPadding + masterVolSlider.availableHeight / 2 - height / 2
|
||
width: masterVolSlider.availableWidth; height: 3
|
||
color: root.cBorder
|
||
Rectangle {
|
||
width: masterVolSlider.visualPosition * parent.width; height: parent.height
|
||
color: Pipewire.defaultAudioSink?.audio?.muted ? root.cDim : root.cAccent
|
||
}
|
||
}
|
||
handle: Rectangle {
|
||
x: masterVolSlider.leftPadding + masterVolSlider.visualPosition * (masterVolSlider.availableWidth - width)
|
||
y: masterVolSlider.topPadding + masterVolSlider.availableHeight / 2 - height / 2
|
||
width: 10; height: 10; color: root.cBright
|
||
border.color: root.cBorderFocus; border.width: 1
|
||
}
|
||
}
|
||
Text {
|
||
text: Pipewire.defaultAudioSink?.audio?.muted ? "Muted" : Math.round((Pipewire.defaultAudioSink?.audio?.volume ?? 0) * 100) + "%"
|
||
font.family: root.mono; font.pixelSize: 10; color: root.cText
|
||
horizontalAlignment: Text.AlignRight
|
||
Layout.preferredWidth: 38
|
||
}
|
||
Rectangle {
|
||
implicitWidth: 30; implicitHeight: 28
|
||
color: muteHover.containsMouse ? root.cSelected : "transparent"
|
||
border.color: muteHover.containsMouse ? root.cBorderFocus : root.cBorder
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: Pipewire.defaultAudioSink?.audio?.muted ? "" : ""
|
||
font.family: root.mono; font.pixelSize: 12; color: root.cText
|
||
}
|
||
MouseArea {
|
||
id: muteHover
|
||
anchors.fill: parent; hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: {
|
||
if (Pipewire.defaultAudioSink?.audio)
|
||
Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder }
|
||
|
||
Text {
|
||
text: "PLAYING NOW"
|
||
font.family: root.mono; font.pixelSize: 10; font.bold: true
|
||
color: root.cSubtle
|
||
}
|
||
Repeater {
|
||
model: Pipewire.nodes.values.filter(n => n.isStream && n.audio && n.type === "Stream/Output/Audio")
|
||
delegate: RowLayout {
|
||
required property var modelData
|
||
Layout.fillWidth: true
|
||
spacing: 8
|
||
|
||
ColumnLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 2
|
||
Text {
|
||
text: modelData.properties["application.name"] || modelData.description || modelData.name || "Audio stream"
|
||
font.family: root.mono; font.pixelSize: 10
|
||
color: root.cText
|
||
elide: Text.ElideRight
|
||
Layout.fillWidth: true
|
||
}
|
||
Text {
|
||
text: modelData.properties["media.title"]
|
||
? modelData.properties["media.title"]
|
||
: (modelData.properties["media.name"] || "Active playback")
|
||
font.family: root.mono; font.pixelSize: 9
|
||
color: root.cDim
|
||
elide: Text.ElideRight
|
||
Layout.fillWidth: true
|
||
}
|
||
}
|
||
|
||
Slider {
|
||
id: playbackSlider
|
||
Layout.preferredWidth: 82
|
||
from: 0; to: 1.0; stepSize: 0.01
|
||
value: modelData.audio?.volume ?? 0
|
||
onMoved: if (modelData.audio) modelData.audio.volume = value
|
||
background: Rectangle {
|
||
x: playbackSlider.leftPadding
|
||
y: playbackSlider.topPadding + playbackSlider.availableHeight / 2 - height / 2
|
||
width: playbackSlider.availableWidth; height: 2; color: root.cBorder
|
||
Rectangle {
|
||
width: playbackSlider.visualPosition * parent.width
|
||
height: parent.height
|
||
color: root.cAccent
|
||
}
|
||
}
|
||
handle: Rectangle {
|
||
x: playbackSlider.leftPadding + playbackSlider.visualPosition * (playbackSlider.availableWidth - width)
|
||
y: playbackSlider.topPadding + playbackSlider.availableHeight / 2 - height / 2
|
||
width: 8; height: 8; color: root.cText
|
||
}
|
||
}
|
||
|
||
Text {
|
||
text: Math.round((modelData.audio?.volume ?? 0) * 100) + "%"
|
||
font.family: root.mono; font.pixelSize: 9
|
||
color: root.cSubtle
|
||
Layout.preferredWidth: 30
|
||
horizontalAlignment: Text.AlignRight
|
||
}
|
||
|
||
Rectangle {
|
||
implicitWidth: 26; implicitHeight: 24
|
||
color: streamMuteHover.containsMouse ? root.cSelected : "transparent"
|
||
border.color: streamMuteHover.containsMouse ? root.cBorderFocus : "transparent"
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: modelData.audio?.muted ? "" : ""
|
||
font.family: root.mono; font.pixelSize: 11
|
||
color: modelData.audio?.muted ? root.cDanger : root.cText
|
||
}
|
||
MouseArea {
|
||
id: streamMuteHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: if (modelData.audio) modelData.audio.muted = !modelData.audio.muted
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Text {
|
||
text: "No active playback"
|
||
visible: Pipewire.nodes.values.filter(n => n.isStream && n.audio && n.type === "Stream/Output/Audio").length === 0
|
||
font.family: root.mono; font.pixelSize: 10
|
||
color: root.cDim
|
||
}
|
||
|
||
Text {
|
||
text: "AVAILABLE OUTPUTS"
|
||
font.family: root.mono; font.pixelSize: 10; font.bold: true; color: root.cSubtle
|
||
}
|
||
Repeater {
|
||
model: Pipewire.nodes.values.filter(n => n.type === "Audio/Sink")
|
||
delegate: ColumnLayout {
|
||
required property var modelData
|
||
readonly property bool isDefault: Pipewire.defaultAudioSink?.id === modelData.id
|
||
Layout.fillWidth: true
|
||
spacing: 3
|
||
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
Text {
|
||
text: isDefault ? "●" : "○"
|
||
font.family: root.mono; font.pixelSize: 10
|
||
color: isDefault ? root.cAccent : root.cDim
|
||
}
|
||
Text {
|
||
text: modelData.description || modelData.name || "Output device"
|
||
font.family: root.mono; font.pixelSize: 10
|
||
color: isDefault ? root.cBright : root.cText
|
||
Layout.fillWidth: true; elide: Text.ElideRight
|
||
}
|
||
Text {
|
||
text: Math.round((modelData.audio?.volume ?? 0) * 100) + "%"
|
||
font.family: root.mono; font.pixelSize: 9; color: root.cSubtle
|
||
}
|
||
MouseArea {
|
||
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
|
||
onClicked: Pipewire.preferredDefaultAudioSink = modelData
|
||
}
|
||
}
|
||
Slider {
|
||
id: sinkSlider
|
||
Layout.fillWidth: true
|
||
from: 0; to: 1.0; stepSize: 0.01
|
||
value: modelData.audio?.volume ?? 0
|
||
onMoved: if (modelData.audio) modelData.audio.volume = value
|
||
background: Rectangle {
|
||
x: sinkSlider.leftPadding
|
||
y: sinkSlider.topPadding + sinkSlider.availableHeight / 2 - height / 2
|
||
width: sinkSlider.availableWidth; height: 2; color: root.cBorder
|
||
Rectangle { width: sinkSlider.visualPosition * parent.width; height: parent.height; color: isDefault ? root.cAccent : root.cDim }
|
||
}
|
||
handle: Rectangle {
|
||
x: sinkSlider.leftPadding + sinkSlider.visualPosition * (sinkSlider.availableWidth - width)
|
||
y: sinkSlider.topPadding + sinkSlider.availableHeight / 2 - height / 2
|
||
width: 8; height: 8; color: root.cText
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder; Layout.topMargin: 2 }
|
||
|
||
Text {
|
||
text: "INPUTS"
|
||
font.family: root.mono; font.pixelSize: 10; font.bold: true; color: root.cSubtle
|
||
}
|
||
Repeater {
|
||
model: Pipewire.nodes.values.filter(n => n.type === "Audio/Source" && !n.description.includes("Monitor"))
|
||
delegate: ColumnLayout {
|
||
required property var modelData
|
||
Layout.fillWidth: true
|
||
spacing: 3
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
Text { text: ""; font.family: root.mono; font.pixelSize: 10; color: root.cSubtle }
|
||
Text {
|
||
text: modelData.description || modelData.name || "Microphone"
|
||
font.family: root.mono; font.pixelSize: 10; color: root.cText
|
||
Layout.fillWidth: true; elide: Text.ElideRight
|
||
}
|
||
Text {
|
||
text: Math.round((modelData.audio?.volume ?? 0) * 100) + "%"
|
||
font.family: root.mono; font.pixelSize: 9; color: root.cSubtle
|
||
}
|
||
}
|
||
Slider {
|
||
id: srcSlider
|
||
Layout.fillWidth: true
|
||
from: 0; to: 1.0; stepSize: 0.01
|
||
value: modelData.audio?.volume ?? 0
|
||
onMoved: if (modelData.audio) modelData.audio.volume = value
|
||
background: Rectangle {
|
||
x: srcSlider.leftPadding
|
||
y: srcSlider.topPadding + srcSlider.availableHeight / 2 - height / 2
|
||
width: srcSlider.availableWidth; height: 2; color: root.cBorder
|
||
Rectangle { width: srcSlider.visualPosition * parent.width; height: parent.height; color: root.cTeal }
|
||
}
|
||
handle: Rectangle {
|
||
x: srcSlider.leftPadding + srcSlider.visualPosition * (srcSlider.availableWidth - width)
|
||
y: srcSlider.topPadding + srcSlider.availableHeight / 2 - height / 2
|
||
width: 8; height: 8; color: root.cText
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ───────────── POWER MENU ─────────────
|
||
PanelWindow {
|
||
id: powerWindow
|
||
visible: root.powerMenuOpen
|
||
screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? Quickshell.screens[0]
|
||
|
||
anchors { top: true; bottom: true; left: true; right: true }
|
||
color: "transparent"
|
||
|
||
WlrLayershell.layer: WlrLayer.Overlay
|
||
WlrLayershell.namespace: "power-menu"
|
||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||
exclusionMode: ExclusionMode.Ignore
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onClicked: root.closeMenus()
|
||
}
|
||
|
||
Rectangle {
|
||
anchors.centerIn: parent
|
||
width: 330
|
||
height: root.powerConfirmAction === "" ? 310 : 190
|
||
color: root.cPanel
|
||
border.color: root.cBorder
|
||
border.width: 1
|
||
radius: 0
|
||
|
||
MouseArea { anchors.fill: parent }
|
||
|
||
FocusScope {
|
||
anchors.fill: parent
|
||
focus: powerWindow.visible
|
||
Keys.onEscapePressed: {
|
||
if (root.powerConfirmAction !== "") root.powerConfirmAction = ""
|
||
else root.closeMenus()
|
||
}
|
||
|
||
ColumnLayout {
|
||
anchors.fill: parent
|
||
anchors.margins: 18
|
||
spacing: 8
|
||
|
||
Text {
|
||
text: root.powerConfirmAction === "" ? "POWER" : "CONFIRM ACTION"
|
||
font.family: root.mono
|
||
font.pixelSize: 11
|
||
font.bold: true
|
||
color: root.cBright
|
||
}
|
||
|
||
Text {
|
||
Layout.fillWidth: true
|
||
text: root.powerConfirmAction === "" ? "Choose what to do with this session." :
|
||
(root.powerConfirmAction === "reboot" ? "Restart the computer now?" : "Shut down the computer now?")
|
||
font.family: root.mono
|
||
font.pixelSize: 10
|
||
color: root.cSubtle
|
||
wrapMode: Text.WordWrap
|
||
}
|
||
|
||
Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder }
|
||
|
||
ColumnLayout {
|
||
Layout.fillWidth: true
|
||
Layout.fillHeight: true
|
||
spacing: 4
|
||
visible: root.powerConfirmAction === ""
|
||
|
||
Repeater {
|
||
model: [
|
||
{ label: "Log out", icon: "", action: "logout", confirm: false },
|
||
{ label: "Restart…", icon: "", action: "reboot", confirm: true },
|
||
{ label: "Shut down…", icon: "", action: "poweroff", confirm: true },
|
||
]
|
||
|
||
delegate: Rectangle {
|
||
required property var modelData
|
||
Layout.fillWidth: true
|
||
implicitHeight: 42
|
||
color: powerActionHover.containsMouse ? root.cSurfaceHover : "transparent"
|
||
border.color: powerActionHover.containsMouse ? root.cBorderFocus : "transparent"
|
||
border.width: 1
|
||
|
||
RowLayout {
|
||
anchors.fill: parent
|
||
anchors.leftMargin: 10
|
||
anchors.rightMargin: 10
|
||
spacing: 12
|
||
|
||
Text {
|
||
text: modelData.icon
|
||
font.family: root.mono
|
||
font.pixelSize: 14
|
||
color: root.cSubtle
|
||
}
|
||
Text {
|
||
Layout.fillWidth: true
|
||
text: modelData.label
|
||
font.family: root.mono
|
||
font.pixelSize: 11
|
||
color: root.cText
|
||
}
|
||
Text {
|
||
text: modelData.confirm ? "›" : ""
|
||
font.family: root.mono
|
||
font.pixelSize: 14
|
||
color: root.cSubtle
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
id: powerActionHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: {
|
||
if (modelData.confirm) root.powerConfirmAction = modelData.action
|
||
else root.runPowerAction(modelData.action)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
visible: root.powerConfirmAction !== ""
|
||
Item { Layout.fillWidth: true }
|
||
|
||
Rectangle {
|
||
implicitWidth: 88
|
||
implicitHeight: 34
|
||
color: cancelPowerHover.containsMouse ? root.cSurfaceHover : "transparent"
|
||
border.color: root.cBorder
|
||
border.width: 1
|
||
Text { anchors.centerIn: parent; text: "Cancel"; font.family: root.mono; font.pixelSize: 10; color: root.cText }
|
||
MouseArea {
|
||
id: cancelPowerHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: root.powerConfirmAction = ""
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
implicitWidth: 104
|
||
implicitHeight: 34
|
||
color: confirmPowerHover.containsMouse ? root.cSurfaceHover : root.cSelected
|
||
border.color: root.cBorderFocus
|
||
border.width: 1
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: root.powerConfirmAction === "reboot" ? "Restart" : "Shut down"
|
||
font.family: root.mono
|
||
font.pixelSize: 10
|
||
color: root.cBright
|
||
}
|
||
MouseArea {
|
||
id: confirmPowerHover
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: root.runPowerAction(root.powerConfirmAction)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|