464 lines
14 KiB
QML
464 lines
14 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import Quickshell.Io
|
|
import Quickshell.Services.Mpris
|
|
import Quickshell.Services.Pipewire
|
|
import "." as D
|
|
import "../services" as S
|
|
import "../applets" as C
|
|
import "../modules" as M
|
|
|
|
PanelWindow {
|
|
id: root
|
|
|
|
required property var screen
|
|
|
|
visible: _winVisible
|
|
color: "transparent"
|
|
|
|
property bool _winVisible: false
|
|
|
|
WlrLayershell.layer: S.DockState.mode === "pinned" ? WlrLayer.Top : WlrLayer.Overlay
|
|
WlrLayershell.exclusiveZone: S.DockState.mode === "pinned" ? _dockWidth : 0
|
|
WlrLayershell.namespace: "nova-dock"
|
|
|
|
anchors.top: true
|
|
anchors.right: true
|
|
anchors.bottom: true
|
|
|
|
// Counteract the bar's exclusive zone so the dock extends to the top of the screen
|
|
margins.top: -S.Theme.barHeight
|
|
|
|
readonly property int _dockWidth: S.Modules.dock.width ?? 300
|
|
readonly property var _applets: S.Modules.dock.applets ?? {}
|
|
readonly property color _accent: S.Theme.base0C
|
|
|
|
implicitWidth: _dockWidth
|
|
|
|
// Set reserved width for bar/corners when pinned
|
|
onVisibleChanged: {
|
|
if (S.DockState.mode === "pinned")
|
|
S.DockState.reservedWidth = _dockWidth;
|
|
}
|
|
Connections {
|
|
target: S.DockState
|
|
function onModeChanged() {
|
|
if (S.DockState.mode === "pinned")
|
|
S.DockState.reservedWidth = root._dockWidth;
|
|
}
|
|
}
|
|
|
|
// Slide animation - managed manually so window stays visible during hide
|
|
property real _slideX: _dockWidth
|
|
|
|
Connections {
|
|
target: S.DockState
|
|
function onOpenChanged() {
|
|
if (S.DockState.open) {
|
|
root._winVisible = true;
|
|
_hideAnim.stop();
|
|
if (S.Theme.reducedMotion) {
|
|
root._slideX = 0;
|
|
} else {
|
|
_showAnim.start();
|
|
}
|
|
} else {
|
|
_showAnim.stop();
|
|
if (S.Theme.reducedMotion) {
|
|
root._slideX = root._dockWidth;
|
|
root._winVisible = false;
|
|
} else {
|
|
_hideAnim.start();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NumberAnimation {
|
|
id: _showAnim
|
|
target: root
|
|
property: "_slideX"
|
|
to: 0
|
|
duration: 200
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
|
|
NumberAnimation {
|
|
id: _hideAnim
|
|
target: root
|
|
property: "_slideX"
|
|
to: root._dockWidth
|
|
duration: 200
|
|
easing.type: Easing.OutCubic
|
|
onFinished: root._winVisible = false
|
|
}
|
|
|
|
// Overlay mode: close when cursor leaves
|
|
HoverHandler {
|
|
id: _dockHover
|
|
onHoveredChanged: {
|
|
if (!hovered && S.DockState.mode === "overlay")
|
|
_overlayCloseTimer.restart();
|
|
else
|
|
_overlayCloseTimer.stop();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: _overlayCloseTimer
|
|
interval: 200
|
|
onTriggered: if (S.DockState.mode === "overlay")
|
|
S.DockState.close()
|
|
}
|
|
|
|
// Background - fully opaque when pinned, semi-transparent in overlay
|
|
Rectangle {
|
|
id: _bg
|
|
anchors.fill: parent
|
|
color: S.Theme.base00
|
|
opacity: S.DockState.mode === "pinned" ? 1.0 : Math.max(S.Theme.barOpacity, 0.85)
|
|
|
|
transform: Translate {
|
|
x: root._slideX
|
|
}
|
|
}
|
|
|
|
// Left edge line - marks the virtual screen edge, matches bar border width.
|
|
// Uses base09 (rightmost bar gradient color).
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
width: 1
|
|
color: S.Theme.base09
|
|
opacity: _bg.opacity
|
|
|
|
transform: Translate {
|
|
x: root._slideX
|
|
}
|
|
}
|
|
|
|
// Content - inset past the edge line + gap
|
|
Flickable {
|
|
id: _flickable
|
|
anchors.fill: parent
|
|
anchors.leftMargin: S.Theme.groupSpacing + 1
|
|
contentHeight: _column.height
|
|
clip: true
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
transform: Translate {
|
|
x: root._slideX
|
|
}
|
|
|
|
Column {
|
|
id: _column
|
|
width: root._dockWidth
|
|
spacing: 8
|
|
topPadding: 8
|
|
bottomPadding: 8
|
|
leftPadding: 8
|
|
rightPadding: 8
|
|
|
|
// Clock
|
|
D.DockCard {
|
|
visible: root._applets.clock ?? true
|
|
icon: "\uF017"
|
|
title: "Clock"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.ClockApplet {
|
|
width: parent.width
|
|
accentColor: root._accent
|
|
currentDate: _clock.date
|
|
}
|
|
}
|
|
|
|
// CPU
|
|
D.DockCard {
|
|
id: _cpuCard
|
|
visible: root._applets.cpu ?? true
|
|
icon: "\uF2DB"
|
|
title: "CPU"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.CpuApplet {
|
|
width: parent.width
|
|
cores: S.SystemStats.cpuCores
|
|
coreMaxFreq: S.SystemStats.cpuCoreMaxFreq
|
|
coreTypes: S.SystemStats.cpuCoreTypes
|
|
processes: _cpuProcs.processes
|
|
accentColor: root._accent
|
|
active: _cpuCard.expanded
|
|
}
|
|
}
|
|
|
|
// GPU
|
|
D.DockCard {
|
|
visible: (root._applets.gpu ?? true) && S.SystemStats.gpuAvailable
|
|
icon: "\uEB4C"
|
|
title: "GPU"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.GpuApplet {
|
|
width: parent.width
|
|
active: parent.expanded
|
|
accentColor: root._accent
|
|
}
|
|
}
|
|
|
|
// Memory
|
|
D.DockCard {
|
|
id: _memCard
|
|
visible: root._applets.memory ?? true
|
|
icon: "\uEFC5"
|
|
title: "Memory"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.MemoryApplet {
|
|
width: parent.width
|
|
percent: S.SystemStats.memPercent
|
|
usedGb: S.SystemStats.memUsedGb
|
|
totalGb: S.SystemStats.memTotalGb
|
|
availGb: S.SystemStats.memAvailGb
|
|
cachedGb: S.SystemStats.memCachedGb
|
|
buffersGb: S.SystemStats.memBuffersGb
|
|
processes: _memProcs.processes
|
|
accentColor: root._accent
|
|
active: _memCard.expanded
|
|
}
|
|
}
|
|
|
|
// Temperature
|
|
D.DockCard {
|
|
visible: root._applets.temperature ?? true
|
|
icon: "\uF2C9"
|
|
title: "Temperature"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.TemperatureApplet {
|
|
width: parent.width
|
|
temp: S.SystemStats.tempCelsius
|
|
warm: S.Modules.temperature.warm || 80
|
|
hot: S.Modules.temperature.hot || 90
|
|
history: S.SystemStats.tempHistory
|
|
devices: S.SystemStats.tempDevices
|
|
accentColor: root._accent
|
|
deviceFilter: S.Modules.temperature.device || ""
|
|
active: parent.parent.expanded
|
|
}
|
|
}
|
|
|
|
// Disk
|
|
D.DockCard {
|
|
visible: root._applets.disk ?? true
|
|
icon: "\uF0C9"
|
|
title: "Disk"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.DiskApplet {
|
|
width: parent.width
|
|
mounts: S.SystemStats.diskMounts
|
|
accentColor: root._accent
|
|
}
|
|
}
|
|
|
|
// Battery
|
|
D.DockCard {
|
|
visible: (root._applets.battery ?? true) && S.BatteryService.available
|
|
icon: "\uDB80\uDC84"
|
|
title: "Battery"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.BatteryApplet {
|
|
width: parent.width
|
|
active: parent.expanded
|
|
accentColor: root._accent
|
|
}
|
|
}
|
|
|
|
// Network
|
|
D.DockCard {
|
|
visible: root._applets.network ?? true
|
|
icon: "\uF1EB"
|
|
title: "Network"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.NetworkApplet {
|
|
width: parent.width
|
|
accentColor: root._accent
|
|
}
|
|
}
|
|
|
|
// Bluetooth
|
|
D.DockCard {
|
|
visible: (root._applets.bluetooth ?? true) && S.BluetoothService.state !== "unavailable"
|
|
icon: "\uF294"
|
|
title: "Bluetooth"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.BluetoothApplet {
|
|
width: parent.width
|
|
accentColor: root._accent
|
|
}
|
|
}
|
|
|
|
// Volume
|
|
D.DockCard {
|
|
visible: root._applets.volume ?? true
|
|
icon: "\uF028"
|
|
title: "Sound"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.VolumeApplet {
|
|
width: parent.width
|
|
sink: Pipewire.defaultAudioSink
|
|
sinkList: root._sinkList
|
|
streamList: root._streamList
|
|
accentColor: root._accent
|
|
}
|
|
}
|
|
|
|
// Backlight
|
|
D.DockCard {
|
|
visible: (root._applets.backlight ?? true) && S.BacklightService.available
|
|
icon: "\uF185"
|
|
title: "Brightness"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.BacklightApplet {
|
|
width: parent.width
|
|
percent: S.BacklightService.percent
|
|
accentColor: root._accent
|
|
onSetPercent: pct => S.BacklightService.setPercent(pct)
|
|
}
|
|
}
|
|
|
|
// Weather
|
|
D.DockCard {
|
|
visible: (root._applets.weather ?? true) && S.WeatherService.available
|
|
icon: S.WeatherService.icon
|
|
title: "Weather"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.WeatherApplet {
|
|
width: parent.width
|
|
accentColor: root._accent
|
|
}
|
|
}
|
|
|
|
// Now Playing
|
|
D.DockCard {
|
|
visible: (root._applets.mpris ?? true) && S.MprisService.player !== null
|
|
icon: "\uF04B"
|
|
title: "Now Playing"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.MprisApplet {
|
|
width: parent.width
|
|
player: S.MprisService.player
|
|
players: S.MprisService.players
|
|
playing: S.MprisService.playing
|
|
accentColor: root._accent
|
|
cachedArt: S.MprisService.cachedArt
|
|
playerIdx: S.MprisService.playerIdx
|
|
onPlayerSwitched: idx => S.MprisService.switchPlayer(idx)
|
|
}
|
|
}
|
|
|
|
// Notifications
|
|
D.DockCard {
|
|
visible: root._applets.notifications ?? true
|
|
icon: "\uDB80\uDC9C"
|
|
title: "Notifications"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.NotifApplet {
|
|
width: parent.width
|
|
contentWidth: root._dockWidth - 16
|
|
accentColor: root._accent
|
|
}
|
|
}
|
|
|
|
// Power
|
|
D.DockCard {
|
|
visible: root._applets.power ?? true
|
|
icon: "\uF011"
|
|
title: "Power"
|
|
accentColor: root._accent
|
|
width: parent.width - 16
|
|
|
|
C.PowerApplet {
|
|
width: parent.width
|
|
accentColor: root._accent
|
|
onRunCommand: cmd => {
|
|
_runner.command = cmd;
|
|
_runner.running = true;
|
|
}
|
|
onDismiss: S.DockState.close()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shared resources
|
|
SystemClock {
|
|
id: _clock
|
|
precision: SystemClock.Seconds
|
|
}
|
|
|
|
M.ProcessList {
|
|
id: _cpuProcs
|
|
sortBy: "cpu"
|
|
active: _cpuCard.expanded && S.DockState.open
|
|
}
|
|
|
|
M.ProcessList {
|
|
id: _memProcs
|
|
sortBy: "mem"
|
|
active: _memCard.expanded && S.DockState.open
|
|
}
|
|
|
|
PwObjectTracker {
|
|
objects: [Pipewire.defaultAudioSink, ...root._streamList]
|
|
}
|
|
|
|
readonly property var _sinkList: {
|
|
const sinks = [];
|
|
if (Pipewire.nodes) {
|
|
for (const node of Pipewire.nodes.values)
|
|
if (!node.isStream && node.isSink)
|
|
sinks.push(node);
|
|
}
|
|
return sinks;
|
|
}
|
|
|
|
readonly property var _streamList: {
|
|
const streams = [];
|
|
if (Pipewire.nodes) {
|
|
for (const node of Pipewire.nodes.values)
|
|
if (node.isStream && node.audio)
|
|
streams.push(node);
|
|
}
|
|
return streams;
|
|
}
|
|
|
|
Process {
|
|
id: _runner
|
|
}
|
|
}
|