import QtQuick Item { id: root property bool visibleState: false property bool expired: true property string currentEvent: "" property string currentValue: "" property string currentRole: "info" property int restartCount: 0 readonly property int lifetimeMs: 1200 readonly property var supportedEvents: ["volume", "microphone", "brightness", "media", "workspace", "screenshot", "color", "error"] function roleFor(event, value) { const name = String(event); const text = String(value); if (name === "error") return "error"; if (name === "microphone" && (text === "muted" || text === "active")) return "warning"; if (name === "volume" || name === "brightness") return "ui.accent"; return "info"; } function isNumeric(value) { const text = String(value).replace("%", "").trim(); return text.length > 0 && isFinite(Number(text)); } // 0..1 for the meter-style events (volume/brightness/microphone) when the // value is a real number; -1 otherwise (mute toggles, media, color, error) // so the bar is hidden rather than drawn empty. function normalizedValue(event, value) { const name = String(event); if (name !== "volume" && name !== "brightness" && name !== "microphone") return -1; if (!isNumeric(value)) return -1; return Math.max(0, Math.min(1, Number(String(value).replace("%", "")) / 100)); } // freedesktop icon name for the event; resolved to a path at render time // via Quickshell.iconPath(). All names verified present in the Adwaita theme. function iconName(event, value) { const name = String(event); const text = String(value); const muted = text === "muted" || (isNumeric(text) && Number(text.replace("%", "")) <= 0); if (name === "volume") return muted ? "audio-volume-muted" : (Number(text.replace("%", "")) < 34 ? "audio-volume-low" : (Number(text.replace("%", "")) < 67 ? "audio-volume-medium" : "audio-volume-high")); if (name === "microphone") return muted ? "microphone-sensitivity-muted" : "microphone-sensitivity-high"; if (name === "brightness") return "display-brightness-symbolic"; if (name === "media") return text.indexOf("pause") >= 0 ? "media-playback-pause" : "media-playback-start"; if (name === "screenshot") return "applets-screenshooter"; if (name === "color") return "color-picker"; if (name === "error") return "dialog-error"; return "dialog-information"; } function displayLabel(event) { const name = String(event); const labels = { "volume": "Volume", "microphone": "Microphone", "brightness": "Brightness", "media": "Media", "screenshot": "Screenshot", "color": "Color", "error": "Error" }; return labels[name] || (name.length > 0 ? name : "OSD"); } // right-aligned glance value: "72%" for meters, a word for toggles, the // raw payload (media event / colour hex) otherwise. Errors render their // message in the body line instead, so the corner stays empty. function displayValue(event, value) { const name = String(event); const text = String(value); const normalized = normalizedValue(name, text); if (normalized >= 0) return `${Math.round(normalized * 100)}%`; if (text === "muted") return "muted"; if (text === "unmuted") return "on"; if (name === "error") return ""; return text; } function iconRole(event, value) { const name = String(event); const text = String(value); if (name === "volume") return Number(text) <= 0 ? "audio-muted" : "audio-volume"; if (name === "microphone") return text === "muted" ? "microphone-muted" : "microphone-active"; if (name === "brightness") return "brightness"; if (name === "screenshot") return "screenshot"; if (name === "color") return "color-picker"; if (name === "error") return "danger"; return name.length > 0 ? name : "info"; } function numericText(event, value) { const normalized = normalizedValue(event, value); return normalized >= 0 ? `${Math.round(normalized * 100)}` : ""; } function valueIndicator(event, value) { const name = String(event); const text = String(value); const normalized = normalizedValue(name, text); const hasProgress = normalized >= 0; return { "source": "Nuguland OsdValueIndicator.qml plus Nuguland OSD sliders", "layout": "icon-left-value-right", "iconRole": iconRole(name, text), "iconName": iconName(name, text), "displayLabel": displayLabel(name), "displayValue": displayValue(name, text), "value": text, "normalizedValue": hasProgress ? normalized : null, "progressBar": { "visible": hasProgress, "from": 0, "to": 1, "value": hasProgress ? normalized : 0, "role": currentRole }, "numericText": { "text": numericText(name, text), "odometerMotion": hasProgress, "hardSeparator": hasProgress ? "%" : "" }, "slideFromEdge": { "edge": "right", "hiddenOffset": "width+sidebar-gap", "visibleOffset": 0 }, "wheelActions": name === "volume" ? ["volume-up", "volume-down"] : name === "brightness" ? ["brightness-up", "brightness-down"] : [], "protectionMessage": { "visible": name === "error", "role": "error", "text": name === "error" ? text : "" }, "hardSeparator": hasProgress ? "%" : "" }; } function stateObject() { return { "visible": visibleState, "expired": expired, "event": currentEvent, "value": currentValue, "role": currentRole, "iconName": iconName(currentEvent, currentValue), "displayLabel": displayLabel(currentEvent), "displayValue": displayValue(currentEvent, currentValue), "lifetimeMs": lifetimeMs, "restartCount": restartCount, "supportedEvents": supportedEvents, "surface": "masked-overlay", "blocksInput": false, "reservesSpace": false, "valueIndicator": valueIndicator(currentEvent, currentValue) }; } function stateJson() { return JSON.stringify(stateObject()); } function trigger(event, value) { const name = supportedEvents.indexOf(String(event)) >= 0 ? String(event) : "error"; currentEvent = name; currentValue = String(value); currentRole = roleFor(name, currentValue); visibleState = true; expired = false; restartCount += 1; expiryTimer.restart(); return stateJson(); } function expire() { expiryTimer.stop(); visibleState = false; expired = true; return stateJson(); } Timer { id: expiryTimer interval: root.lifetimeMs repeat: false onTriggered: root.expire() } }