Add polished system feedback overlays
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
// Compact, click-through system feedback. Repeated hardware-key presses update
|
||||
// this one surface in place, avoiding a stack of notification cards.
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property var modelData: null
|
||||
readonly property var presentation: OsdState.state
|
||||
readonly property bool belongsHere: !OsdState.monitorName || !root.modelData
|
||||
|| root.modelData.name === OsdState.monitorName
|
||||
readonly property bool requestedVisible: OsdState.active && root.belongsHere
|
||||
readonly property int horizontalPadding: 18
|
||||
readonly property int contentGap: 14
|
||||
readonly property int progressWidth: 156
|
||||
readonly property int progressLabelWidth: 48
|
||||
readonly property int messageWidth: Math.min(300, Math.max(52, Math.ceil(labelMetrics.advanceWidth)))
|
||||
readonly property int desiredWidth: root.horizontalPadding * 2 + 24 + root.contentGap
|
||||
+ (root.presentation.progress
|
||||
? root.progressWidth + root.contentGap + root.progressLabelWidth
|
||||
: root.messageWidth)
|
||||
|
||||
screen: root.modelData
|
||||
anchors.bottom: true
|
||||
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
implicitWidth: root.desiredWidth
|
||||
implicitHeight: 64
|
||||
color: "transparent"
|
||||
mask: Region {}
|
||||
|
||||
WlrLayershell.namespace: "qs-popover-osd"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
property bool mapped: false
|
||||
visible: root.mapped
|
||||
|
||||
onRequestedVisibleChanged: {
|
||||
if (root.requestedVisible) {
|
||||
unmapTimer.stop();
|
||||
root.mapped = true;
|
||||
entrance.restart();
|
||||
} else if (root.mapped) {
|
||||
unmapTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.requestedVisible)
|
||||
root.mapped = true;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: unmapTimer
|
||||
interval: Theme.durNormal
|
||||
onTriggered: root.mapped = false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: surface
|
||||
anchors.fill: parent
|
||||
radius: Theme.popoverRadius
|
||||
color: Theme.alpha(Theme.bgPopover, 0.9)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.fg, 0.1)
|
||||
opacity: root.requestedVisible ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
id: slide
|
||||
y: root.requestedVisible ? 0 : 8
|
||||
Behavior on y {
|
||||
NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
PrismEdge {
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 1
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
inset: parent.radius
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: root.contentGap
|
||||
|
||||
ThemedIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: 24
|
||||
icon: root.presentation.icon
|
||||
iconFallback: "dialog-information-symbolic"
|
||||
tint: Theme.fg
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: root.presentation.progress
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: visible ? root.progressWidth : 0
|
||||
height: 8
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: height / 2
|
||||
color: Theme.alpha(Theme.fg, 0.14)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width * root.presentation.ratio
|
||||
radius: height / 2
|
||||
color: Theme.accent
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: root.presentation.progress ? root.progressLabelWidth : root.messageWidth
|
||||
text: root.presentation.label
|
||||
color: Theme.fg
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
horizontalAlignment: root.presentation.progress ? Text.AlignRight : Text.AlignLeft
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: labelMetrics
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.DemiBold
|
||||
text: root.presentation.label
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: entrance
|
||||
NumberAnimation {
|
||||
target: surface
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Theme.durNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: slide
|
||||
property: "y"
|
||||
from: 8
|
||||
to: 0
|
||||
duration: Theme.durNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
module qs.modules.osd
|
||||
Osd 1.0 Osd.qml
|
||||
@@ -0,0 +1,45 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import "../modules/osd/OsdModel.js" as OsdModel
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool active: false
|
||||
property string monitorName: ""
|
||||
property var state: OsdModel.messageState("dialog-information-symbolic", "", 1400)
|
||||
|
||||
function present(next: var): void {
|
||||
root.state = next;
|
||||
root.monitorName = Hyprland.focusedMonitor?.name ?? "";
|
||||
root.active = true;
|
||||
if (next.duration > 0) {
|
||||
hideTimer.interval = next.duration;
|
||||
hideTimer.restart();
|
||||
} else {
|
||||
hideTimer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
function progress(kind: string, value: int, maximum: int, label: string): void {
|
||||
root.present(OsdModel.progressState(kind, value, maximum, label, 1400));
|
||||
}
|
||||
|
||||
function message(kind: string, label: string): void {
|
||||
root.present(OsdModel.messageState(kind, label, 1400));
|
||||
}
|
||||
|
||||
function hide(): void {
|
||||
hideTimer.stop();
|
||||
root.active = false;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 1400
|
||||
onTriggered: root.active = false
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import qs.modules.datemenu
|
||||
import qs.modules.focus
|
||||
import qs.modules.signals
|
||||
import qs.modules.settings
|
||||
import qs.modules.osd
|
||||
|
||||
// Clipboard and datemenu ship explicit qmldir manifests because they were
|
||||
// added while this daily-driver shell was already running; that also keeps
|
||||
@@ -72,6 +73,11 @@ ShellRoot {
|
||||
SignalGlass {}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
Osd {}
|
||||
}
|
||||
|
||||
// ── Single-instance overlays ────────────────────────────────────────────
|
||||
// These are always constructed but only *visible* when ShellState says so.
|
||||
// They're cheap while hidden, and keeping them alive means opening the
|
||||
@@ -194,6 +200,25 @@ ShellRoot {
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "osd"
|
||||
|
||||
function progress(kind: string, value: int, maximum: int, label: string): void {
|
||||
OsdState.progress(kind, value, maximum, label);
|
||||
}
|
||||
function message(kind: string, label: string): void {
|
||||
OsdState.message(kind, label);
|
||||
}
|
||||
function hide(): void { OsdState.hide(); }
|
||||
function status(): string {
|
||||
return JSON.stringify({
|
||||
active: OsdState.active,
|
||||
monitorName: OsdState.monitorName,
|
||||
state: OsdState.state
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "activity"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user