Add transient settings OSD for Sway
This commit is contained in:
parent
195b777b84
commit
2bafbcd2be
14 changed files with 412 additions and 3 deletions
BIN
docs/assets/screenshots/quickshell-osd-synthetic-brightness.png
Normal file
BIN
docs/assets/screenshots/quickshell-osd-synthetic-brightness.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
|
|
@ -27,6 +27,7 @@ PanelWindow {
|
||||||
required property var applicationCoordinator
|
required property var applicationCoordinator
|
||||||
required property bool authenticationActive
|
required property bool authenticationActive
|
||||||
required property var keepAwakeController
|
required property var keepAwakeController
|
||||||
|
required property var osdService
|
||||||
screen: modelData
|
screen: modelData
|
||||||
|
|
||||||
// INVARIANT: une seule surface de barre porte l'inhibiteur, même avec
|
// INVARIANT: une seule surface de barre porte l'inhibiteur, même avec
|
||||||
|
|
@ -454,6 +455,7 @@ PanelWindow {
|
||||||
adapter: controlCenter.adapter
|
adapter: controlCenter.adapter
|
||||||
controlCenterOpen: controlCenter.visible
|
controlCenterOpen: controlCenter.visible
|
||||||
audioBrightnessController: controlCenter
|
audioBrightnessController: controlCenter
|
||||||
|
onAdjustedFromBar: kind => bar.osdService.barAction(kind, bar.screen)
|
||||||
keepAwakeController: bar.keepAwakeController
|
keepAwakeController: bar.keepAwakeController
|
||||||
clipboardOpen: clipboardLoader.active && !!clipboardLoader.item && !clipboardLoader.item.closing
|
clipboardOpen: clipboardLoader.active && !!clipboardLoader.item && !clipboardLoader.item.closing
|
||||||
onOpenPageRequested: page => {
|
onOpenPageRequested: page => {
|
||||||
|
|
@ -496,6 +498,27 @@ PanelWindow {
|
||||||
barWindow: bar
|
barWindow: bar
|
||||||
keepAwakeController: bar.keepAwakeController
|
keepAwakeController: bar.keepAwakeController
|
||||||
onThresholdApplied: rightStatusArea.refreshBatteryThreshold()
|
onThresholdApplied: rightStatusArea.refreshBatteryThreshold()
|
||||||
|
onVisibleChanged: bar.osdService.setPanel(bar.screen, visible, currentPage)
|
||||||
|
onCurrentPageChanged: bar.osdService.setPanel(bar.screen, visible, currentPage)
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: controlCenter
|
||||||
|
function snapshot() {
|
||||||
|
if (!bar.startupHost) return;
|
||||||
|
bar.osdService.sampleSink(controlCenter.volumeIdentity,
|
||||||
|
controlCenter.volumeAvailable, controlCenter.volumeRaw,
|
||||||
|
controlCenter.volumePercent, controlCenter.volumeMuted);
|
||||||
|
bar.osdService.sampleBrightness(controlCenter.brightnessIdentity,
|
||||||
|
controlCenter.brightnessAvailable, controlCenter.brightnessRaw,
|
||||||
|
controlCenter.brightnessPercent);
|
||||||
|
}
|
||||||
|
function onVolumeIdentityChanged() { snapshot(); }
|
||||||
|
function onVolumeAvailableChanged() { snapshot(); }
|
||||||
|
function onVolumeRawChanged() { snapshot(); }
|
||||||
|
function onVolumeMutedChanged() { snapshot(); }
|
||||||
|
function onBrightnessAvailableChanged() { snapshot(); }
|
||||||
|
function onBrightnessRawChanged() { snapshot(); }
|
||||||
|
Component.onCompleted: snapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
DateCenter {
|
DateCenter {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import "osd"
|
||||||
|
import "theme"
|
||||||
|
|
||||||
|
// Données de démonstration uniquement : aucun backend audio ni rétroéclairage.
|
||||||
|
ShellRoot {
|
||||||
|
// Fond de démonstration neutre sous la carte translucide : la capture ne
|
||||||
|
// révèle aucun pixel du bureau ni d'une application réelle.
|
||||||
|
PanelWindow {
|
||||||
|
screen: Quickshell.screens[0]
|
||||||
|
anchors { left: true; bottom: true }
|
||||||
|
margins.left: Math.max(0, Math.round((screen.width - implicitWidth) / 2))
|
||||||
|
margins.bottom: Math.min(40, Math.max(8, Math.round(screen.height / 20)))
|
||||||
|
implicitWidth: Math.min(320, Math.max(1, screen.width - 16))
|
||||||
|
implicitHeight: 80
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
WlrLayershell.layer: WlrLayer.Top
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
WlrLayershell.namespace: "labfy-osd-visual-backdrop"
|
||||||
|
mask: Region { width: 0; height: 0 }
|
||||||
|
color: Theme.surface0
|
||||||
|
}
|
||||||
|
OsdWindow {
|
||||||
|
hostScreen: Quickshell.screens[0]
|
||||||
|
kind: "brightness"
|
||||||
|
percent: 42
|
||||||
|
muted: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -211,6 +211,10 @@ Control Center, avec un minimum sûr de 10 % et un maximum de 100 %. Ses
|
||||||
écriture, y compris si le pilote arrondit. L'absence de sortie audio ou de
|
écriture, y compris si le pilote arrondit. L'absence de sortie audio ou de
|
||||||
rétroéclairage désactive l'icône concernée.
|
rétroéclairage désactive l'icône concernée.
|
||||||
|
|
||||||
|
L'OSD des réglages est décrit dans [osd/README.md](osd/README.md). Il observe
|
||||||
|
ces mêmes valeurs confirmées, ainsi que la sourdine de la source audio par
|
||||||
|
défaut, sans modifier les commandes des touches matérielles.
|
||||||
|
|
||||||
Le libellé `RESIZE` en Catppuccin Peach apparaît à gauche des workspaces
|
Le libellé `RESIZE` en Catppuccin Peach apparaît à gauche des workspaces
|
||||||
quand l'événement IPC Sway `mode` annonce `resize`. Un seul abonnement
|
quand l'événement IPC Sway `mode` annonce `resize`. Un seul abonnement
|
||||||
`I3IpcListener` partage cet état entre toutes les sorties. Une requête
|
`I3IpcListener` partage cet état entre toutes les sorties. Une requête
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,12 @@ PopupWindow {
|
||||||
readonly property bool volumeAvailable: volumeSlider.sinkAudio !== null
|
readonly property bool volumeAvailable: volumeSlider.sinkAudio !== null
|
||||||
readonly property bool volumeMuted: volumeSlider.muted
|
readonly property bool volumeMuted: volumeSlider.muted
|
||||||
readonly property int volumePercent: volumeSlider.percent
|
readonly property int volumePercent: volumeSlider.percent
|
||||||
|
readonly property var volumeIdentity: volumeSlider.sink
|
||||||
|
readonly property real volumeRaw: volumeSlider.sinkAudio ? volumeSlider.sinkAudio.volume : NaN
|
||||||
readonly property bool brightnessAvailable: brightnessSlider.available
|
readonly property bool brightnessAvailable: brightnessSlider.available
|
||||||
readonly property int brightnessPercent: brightnessSlider.percent
|
readonly property int brightnessPercent: brightnessSlider.percent
|
||||||
|
readonly property string brightnessIdentity: brightnessSlider.backlightPath
|
||||||
|
readonly property real brightnessRaw: brightnessSlider.brightness
|
||||||
function adjustVolume(steps) { volumeSlider.adjustBy(steps); }
|
function adjustVolume(steps) { volumeSlider.adjustBy(steps); }
|
||||||
function adjustBrightness(steps) { brightnessSlider.adjustBy(steps); }
|
function adjustBrightness(steps) { brightnessSlider.adjustBy(steps); }
|
||||||
readonly property var pendingAction: sessionExitGate.pendingAction
|
readonly property var pendingAction: sessionExitGate.pendingAction
|
||||||
|
|
|
||||||
36
quickshell/.config/quickshell/labfy-sway/osd/Observation.js
Normal file
36
quickshell/.config/quickshell/labfy-sway/osd/Observation.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
.pragma library
|
||||||
|
|
||||||
|
// CONTRACT: chaque identité de périphérique a une première valeur de référence,
|
||||||
|
// qui ne représente jamais une action. Les valeurs brutes détectent aussi les
|
||||||
|
// changements arrondis vers le même pourcentage affiché.
|
||||||
|
function observe(previous, identity, available, raw, muted) {
|
||||||
|
if (!identity || !available || !Number.isFinite(raw))
|
||||||
|
return { state: null, changed: false, volumeChanged: false, muteChanged: false };
|
||||||
|
const state = { identity: identity, raw: raw, muted: !!muted };
|
||||||
|
if (!previous || previous.identity !== identity)
|
||||||
|
return { state: state, changed: false, volumeChanged: false, muteChanged: false };
|
||||||
|
const volumeChanged = previous.raw !== raw;
|
||||||
|
const muteChanged = previous.muted !== !!muted;
|
||||||
|
return { state: state, changed: volumeChanged || muteChanged,
|
||||||
|
volumeChanged: volumeChanged, muteChanged: muteChanged };
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextCard(card, kind, value, muted, screen, now) {
|
||||||
|
return { kind: kind, value: value, muted: muted, screen: screen, deadline: now + 1500,
|
||||||
|
generation: card ? card.generation + 1 : 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function expire(card, generation, now) {
|
||||||
|
return !!card && card.generation === generation && now >= card.deadline;
|
||||||
|
}
|
||||||
|
|
||||||
|
function screenForEvent(screens, focusedName, origin, kind, now) {
|
||||||
|
if (origin && origin.kind === kind && origin.until >= now
|
||||||
|
&& screens.indexOf(origin.screen) >= 0) return origin.screen;
|
||||||
|
return screens.find(screen => screen.name === focusedName) || screens[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageShows(kind, page) {
|
||||||
|
return kind === "brightness" ? page === 0
|
||||||
|
: kind === "microphone" ? page === 15 : page === 0 || page === 15;
|
||||||
|
}
|
||||||
108
quickshell/.config/quickshell/labfy-sway/osd/OsdService.qml
Normal file
108
quickshell/.config/quickshell/labfy-sway/osd/OsdService.qml
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.I3
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import "Observation.js" as Observation
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: service
|
||||||
|
required property bool authenticationActive
|
||||||
|
property var sinkState: null
|
||||||
|
property var brightnessState: null
|
||||||
|
property var sourceState: null
|
||||||
|
property var card: null
|
||||||
|
property var panels: ({})
|
||||||
|
property var origin: null
|
||||||
|
readonly property var source: Pipewire.defaultAudioSource
|
||||||
|
readonly property var sourceAudio: source && source.ready && source.audio ? source.audio : null
|
||||||
|
|
||||||
|
// WHY: AudioMixerPage ne suit les nœuds que pendant son ouverture. Ce
|
||||||
|
// tracker léger permet de lire la sourdine du défaut sans ouvrir de flux.
|
||||||
|
PwObjectTracker { objects: [service.source] }
|
||||||
|
onSourceAudioChanged: sampleSource()
|
||||||
|
Connections {
|
||||||
|
target: service.sourceAudio
|
||||||
|
function onMutedChanged() { service.sampleSource(); }
|
||||||
|
}
|
||||||
|
function sampleSource() {
|
||||||
|
const audio = sourceAudio;
|
||||||
|
const result = Observation.observe(sourceState, source, !!audio,
|
||||||
|
audio ? audio.volume : NaN, audio ? audio.muted : false);
|
||||||
|
sourceState = result.state;
|
||||||
|
if (result.muteChanged) publish("microphone", 0, audio.muted);
|
||||||
|
}
|
||||||
|
function sampleSink(identity, available, raw, percent, muted) {
|
||||||
|
const result = Observation.observe(sinkState, identity, available, raw, muted);
|
||||||
|
sinkState = result.state;
|
||||||
|
if (result.muteChanged || result.volumeChanged)
|
||||||
|
publish("volume", percent, muted);
|
||||||
|
}
|
||||||
|
function sampleBrightness(identity, available, raw, percent) {
|
||||||
|
const result = Observation.observe(brightnessState, identity, available, raw, false);
|
||||||
|
brightnessState = result.state;
|
||||||
|
if (result.changed) publish("brightness", percent, false);
|
||||||
|
}
|
||||||
|
function barAction(kind, screen) {
|
||||||
|
// L'indice de provenance n'est consommé que par une valeur confirmée ;
|
||||||
|
// il expire afin qu'une modification externe ultérieure suive le focus.
|
||||||
|
origin = { kind: kind, screen: screen, until: Date.now() + 700 };
|
||||||
|
}
|
||||||
|
function setPanel(screen, visible, page) {
|
||||||
|
const next = Object.assign({}, panels);
|
||||||
|
if (screen) next[screen.name] = visible ? page : -1;
|
||||||
|
panels = next;
|
||||||
|
}
|
||||||
|
function panelShows(kind) {
|
||||||
|
// Tous les Control Centers montrent les mêmes réglages globaux ; une
|
||||||
|
// page visible sur une autre sortie suffit donc à rendre l'OSD redondant.
|
||||||
|
return Object.keys(panels).some(name => Observation.pageShows(kind, panels[name]));
|
||||||
|
}
|
||||||
|
function focusedScreen() {
|
||||||
|
const monitor = I3.focusedMonitor;
|
||||||
|
const screens = Quickshell.screens;
|
||||||
|
return Observation.screenForEvent(screens, monitor ? monitor.name : "",
|
||||||
|
null, "", Date.now());
|
||||||
|
}
|
||||||
|
function publish(kind, percent, muted) {
|
||||||
|
if (authenticationActive) return;
|
||||||
|
const now = Date.now();
|
||||||
|
const monitor = I3.focusedMonitor;
|
||||||
|
const screen = Observation.screenForEvent(Quickshell.screens,
|
||||||
|
monitor ? monitor.name : "", origin, kind, now);
|
||||||
|
if (origin && (origin.kind === kind || origin.until < now)) origin = null;
|
||||||
|
if (!screen || panelShows(kind)) return;
|
||||||
|
// INVARIANT: une seule carte et une seule échéance courante ; un ancien
|
||||||
|
// callback ne peut fermer une génération plus récente.
|
||||||
|
card = Observation.nextCard(card, kind, percent, muted, screen, now);
|
||||||
|
expiry.restart();
|
||||||
|
}
|
||||||
|
onAuthenticationActiveChanged: if (authenticationActive) {
|
||||||
|
card = null;
|
||||||
|
origin = null;
|
||||||
|
expiry.stop();
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
id: expiry
|
||||||
|
interval: 1500
|
||||||
|
repeat: false
|
||||||
|
property int generation: 0
|
||||||
|
onTriggered: {
|
||||||
|
if (Observation.expire(service.card, generation, Date.now())) service.card = null;
|
||||||
|
}
|
||||||
|
function restart() {
|
||||||
|
generation = service.card.generation;
|
||||||
|
stop();
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LazyLoader {
|
||||||
|
active: !!service.card && !service.authenticationActive
|
||||||
|
OsdWindow {
|
||||||
|
// LazyLoader peut garder l'objet pendant une frame de destruction.
|
||||||
|
hostScreen: service.card ? service.card.screen : service.focusedScreen()
|
||||||
|
kind: service.card ? service.card.kind : "volume"
|
||||||
|
percent: service.card ? service.card.value : 0
|
||||||
|
muted: service.card ? service.card.muted : false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
quickshell/.config/quickshell/labfy-sway/osd/OsdWindow.qml
Normal file
93
quickshell/.config/quickshell/labfy-sway/osd/OsdWindow.qml
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import "../components"
|
||||||
|
import "../theme"
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: window
|
||||||
|
required property var hostScreen
|
||||||
|
required property string kind
|
||||||
|
required property int percent
|
||||||
|
required property bool muted
|
||||||
|
screen: hostScreen
|
||||||
|
anchors { left: true; bottom: true }
|
||||||
|
margins.left: Math.max(0, Math.round((hostScreen.width - implicitWidth) / 2))
|
||||||
|
margins.bottom: Math.min(40, Math.max(8, Math.round(hostScreen.height / 20)))
|
||||||
|
implicitWidth: Math.min(320, Math.max(1, hostScreen.width - 16))
|
||||||
|
implicitHeight: 80
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
exclusiveZone: 0
|
||||||
|
aboveWindows: true
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
WlrLayershell.namespace: "labfy-setting-osd"
|
||||||
|
color: "transparent"
|
||||||
|
// CONTRACT: Region vide au niveau de la fenêtre Wayland : ni clic, ni
|
||||||
|
// défilement, ni glisser-déposer ne sont dirigés vers la carte.
|
||||||
|
mask: Region { width: 0; height: 0 }
|
||||||
|
|
||||||
|
readonly property color emphasis: kind === "brightness" ? Theme.yellow
|
||||||
|
: muted ? Theme.danger : Theme.mauve
|
||||||
|
readonly property string label: kind === "brightness" ? "Luminosité"
|
||||||
|
: kind === "microphone" ? "Microphone" : "Volume"
|
||||||
|
readonly property string glyph: kind === "brightness" ? ""
|
||||||
|
: kind === "microphone" ? (muted ? "" : "")
|
||||||
|
: (muted ? "" : "")
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 4
|
||||||
|
// WHY: un léger aperçu du contenu reste visible sans changer de palette.
|
||||||
|
color: Qt.rgba(Theme.popupBackground.r, Theme.popupBackground.g,
|
||||||
|
Theme.popupBackground.b, 0.92)
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
NerdIcon {
|
||||||
|
id: icon
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 13
|
||||||
|
text: window.glyph
|
||||||
|
color: window.emphasis
|
||||||
|
font.pixelSize: 26
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
anchors.left: icon.right
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.verticalCenter: icon.verticalCenter
|
||||||
|
text: window.label
|
||||||
|
color: Theme.foreground
|
||||||
|
font.pixelSize: 15
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
anchors.verticalCenter: icon.verticalCenter
|
||||||
|
text: window.kind === "microphone" ? (window.muted ? "Muet" : "Non muet")
|
||||||
|
: window.muted ? "Muet · " + window.percent + "%" : window.percent + "%"
|
||||||
|
color: window.muted ? Theme.danger : Theme.foreground
|
||||||
|
font.pixelSize: 14
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
visible: window.kind !== "microphone"
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 15
|
||||||
|
height: 7
|
||||||
|
radius: 3
|
||||||
|
color: Theme.buttonBackground
|
||||||
|
Rectangle {
|
||||||
|
width: Math.max(0, Math.min(1, window.percent / 100)) * parent.width
|
||||||
|
height: parent.height
|
||||||
|
radius: parent.radius
|
||||||
|
color: window.emphasis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
quickshell/.config/quickshell/labfy-sway/osd/README.md
Normal file
43
quickshell/.config/quickshell/labfy-sway/osd/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# OSD volume, luminosité et microphone
|
||||||
|
|
||||||
|
`OsdService` observe le sink PipeWire par défaut via le `VolumeSlider` déjà
|
||||||
|
partagé entre la barre et le Control Center. Il observe le rétroéclairage via
|
||||||
|
le `BrightnessSlider` existant, et suit en plus le nœud source PipeWire par
|
||||||
|
défaut avec `PwObjectTracker`, même si la page Audio n'a jamais été ouverte.
|
||||||
|
Ce dernier suivi ne crée aucun flux de capture. Les touches Sway gardent leurs
|
||||||
|
commandes `pactl` et `brightnessctl`, leurs pas et leurs bornes. Le défilement
|
||||||
|
de la barre utilise toujours les mêmes curseurs. Un changement externe des
|
||||||
|
mêmes valeurs est également détecté. Les flux d'applications du mixeur ne
|
||||||
|
déclenchent rien.
|
||||||
|
|
||||||
|
Chaque nouvelle identité de périphérique et chaque reconnexion établissent
|
||||||
|
une référence sans affichage. Une valeur brute identique est ignorée ; la
|
||||||
|
carte présente la valeur observée après confirmation, y compris un volume
|
||||||
|
externe supérieur à 100 %. Seule la jauge est plafonnée. Une valeur
|
||||||
|
indisponible ne devient pas `0 %`. Le microphone indique `Muet` ou `Non muet`,
|
||||||
|
sans prétendre détecter une capture active. Lorsqu'un contrôle correspondant
|
||||||
|
est visible dans un Control Center, l'OSD est supprimé. Toute carte active est
|
||||||
|
fermée dès le début d'une authentification Polkit, sans rejeu ultérieur.
|
||||||
|
|
||||||
|
La surface `labfy-setting-osd` est une seule `PanelWindow` de 320 × 80 pixels
|
||||||
|
logiques au maximum, centrée en bas à environ 40 pixels de la sortie choisie.
|
||||||
|
Son fond reprend `Theme.popupBackground` à 92 % d'opacité.
|
||||||
|
Une action de barre privilégie sa sortie ; les autres changements suivent le
|
||||||
|
moniteur focalisé de `I3`, avec repli sur la première sortie disponible.
|
||||||
|
Elle est sur la couche `Overlay`, au-dessus d'un contenu plein écran, avec
|
||||||
|
`WlrKeyboardFocus.None`, zone d'exclusion ignorée et région d'entrée Wayland
|
||||||
|
vide. Elle ne prend donc ni focus ni entrée pointeur. Sa création n'ouvre ni
|
||||||
|
panneau, ni notification, ni `IdleInhibitor` ; la tasse et le pont Firefox ne
|
||||||
|
sont pas liés à cette surface.
|
||||||
|
|
||||||
|
Chaque événement pertinent remplace le contenu de la carte et relance son
|
||||||
|
délai de 1500 ms. Une génération de minuteur empêche une ancienne échéance
|
||||||
|
de fermer une carte plus récente. La surface est détruite à l'expiration.
|
||||||
|
L'Overview évite de capturer cette surface sans supprimer ses miniatures
|
||||||
|
existantes.
|
||||||
|
|
||||||
|
`node tests/quickshell/test_osd_observation.js` vérifie les références, les
|
||||||
|
changements, les rafales, les échéances, la sourdine synthétique et le choix de
|
||||||
|
sortie. `OsdVisualFixture.qml` montre une luminosité fictive de 42 % ; sa
|
||||||
|
[capture recadrée](../../../../../docs/assets/screenshots/quickshell-osd-synthetic-brightness.png)
|
||||||
|
ne contient aucune donnée personnelle.
|
||||||
|
|
@ -9,6 +9,9 @@ utiles sur `eDP-1`, sans défilement. Sur les petits écrans, le défilement res
|
||||||
disponible si la grille dépasse réellement la hauteur utile. La surface
|
disponible si la grille dépasse réellement la hauteur utile. La surface
|
||||||
layer-shell `Overlay` prend le focus
|
layer-shell `Overlay` prend le focus
|
||||||
clavier exclusivement pendant l'ouverture et libère ce focus en se fermant.
|
clavier exclusivement pendant l'ouverture et libère ce focus en se fermant.
|
||||||
|
La surface `labfy-setting-osd` suspend seulement une nouvelle capture
|
||||||
|
automatique tant qu'elle est visible ; les miniatures déjà validées restent
|
||||||
|
disponibles.
|
||||||
|
|
||||||
## Provenance des miniatures
|
## Provenance des miniatures
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,8 @@ def state():
|
||||||
output_info = {item["name"]: item for item in outputs if item.get("active")}
|
output_info = {item["name"]: item for item in outputs if item.get("active")}
|
||||||
overlay_outputs = [item["name"] for item in outputs if item.get("active")
|
overlay_outputs = [item["name"] for item in outputs if item.get("active")
|
||||||
and any(surface.get("namespace") in {
|
and any(surface.get("namespace") in {
|
||||||
"labfy-workspace-overview", "labfy-applications-menu"}
|
"labfy-workspace-overview", "labfy-applications-menu",
|
||||||
|
"labfy-setting-osd"}
|
||||||
for surface in item.get("layer_shell_surfaces") or [])]
|
for surface in item.get("layer_shell_surfaces") or [])]
|
||||||
workspace_nodes = {}
|
workspace_nodes = {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,14 @@ import "notifications"
|
||||||
import "idlebridge"
|
import "idlebridge"
|
||||||
import "theme"
|
import "theme"
|
||||||
import "polkit"
|
import "polkit"
|
||||||
|
import "osd"
|
||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
id: shell
|
id: shell
|
||||||
// CONTRACT: one PolkitAgent owns the native request queue for the entire
|
// CONTRACT: one PolkitAgent owns the native request queue for the entire
|
||||||
// user session. A dialog is constructed only for its current flow.
|
// user session. A dialog is constructed only for its current flow.
|
||||||
PolkitAgent { id: polkitAgent }
|
PolkitAgent { id: polkitAgent }
|
||||||
|
OsdService { id: shellOsdService; authenticationActive: shell.authenticationActive }
|
||||||
property var polkitScreen: null
|
property var polkitScreen: null
|
||||||
readonly property bool authenticationActive: polkitAgent.flow !== null
|
readonly property bool authenticationActive: polkitAgent.flow !== null
|
||||||
signal authenticationOpening()
|
signal authenticationOpening()
|
||||||
|
|
@ -200,6 +202,7 @@ ShellRoot {
|
||||||
model: Quickshell.screens
|
model: Quickshell.screens
|
||||||
|
|
||||||
Bar {
|
Bar {
|
||||||
|
osdService: shellOsdService
|
||||||
applicationCoordinator: shell
|
applicationCoordinator: shell
|
||||||
authenticationActive: shell.authenticationActive
|
authenticationActive: shell.authenticationActive
|
||||||
keepAwakeController: shell
|
keepAwakeController: shell
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ Row {
|
||||||
signal toggleControlCenterRequested()
|
signal toggleControlCenterRequested()
|
||||||
signal toggleRemovableMediaRequested()
|
signal toggleRemovableMediaRequested()
|
||||||
signal toggleClipboardRequested()
|
signal toggleClipboardRequested()
|
||||||
|
signal adjustedFromBar(string kind)
|
||||||
function refreshBatteryThreshold() { batteryIndicator.refreshThreshold() }
|
function refreshBatteryThreshold() { batteryIndicator.refreshThreshold() }
|
||||||
function closeRemovableMedia() { removableMediaIndicator.popupOpen = false }
|
function closeRemovableMedia() { removableMediaIndicator.popupOpen = false }
|
||||||
function setRemovableMediaOpen(open) { removableMediaIndicator.popupOpen = open }
|
function setRemovableMediaOpen(open) { removableMediaIndicator.popupOpen = open }
|
||||||
|
|
@ -42,7 +43,10 @@ Row {
|
||||||
available: area.audioBrightnessController.volumeAvailable
|
available: area.audioBrightnessController.volumeAvailable
|
||||||
clickable: true
|
clickable: true
|
||||||
onActivated: area.openPageRequested(15)
|
onActivated: area.openPageRequested(15)
|
||||||
onAdjusted: steps => area.audioBrightnessController.adjustVolume(steps)
|
onAdjusted: steps => {
|
||||||
|
area.adjustedFromBar("volume");
|
||||||
|
area.audioBrightnessController.adjustVolume(steps);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ScrollIcon {
|
ScrollIcon {
|
||||||
glyph: ""
|
glyph: ""
|
||||||
|
|
@ -51,7 +55,10 @@ Row {
|
||||||
label: "Luminosité"
|
label: "Luminosité"
|
||||||
percent: area.audioBrightnessController.brightnessPercent
|
percent: area.audioBrightnessController.brightnessPercent
|
||||||
available: area.audioBrightnessController.brightnessAvailable
|
available: area.audioBrightnessController.brightnessAvailable
|
||||||
onAdjusted: steps => area.audioBrightnessController.adjustBrightness(steps)
|
onAdjusted: steps => {
|
||||||
|
area.adjustedFromBar("brightness");
|
||||||
|
area.audioBrightnessController.adjustBrightness(steps);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|
|
||||||
54
tests/quickshell/test_osd_observation.js
Normal file
54
tests/quickshell/test_osd_observation.js
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const vm = require("node:vm");
|
||||||
|
|
||||||
|
const source = fs.readFileSync(path.resolve(__dirname,
|
||||||
|
"../../quickshell/.config/quickshell/labfy-sway/osd/Observation.js"), "utf8")
|
||||||
|
.replace(/^\.pragma library\s*/, "");
|
||||||
|
const model = {};
|
||||||
|
vm.createContext(model);
|
||||||
|
vm.runInContext(source, model);
|
||||||
|
|
||||||
|
const sink = {};
|
||||||
|
let result = model.observe(null, sink, true, 0.57, false);
|
||||||
|
assert.equal(result.changed, false); // Première lecture : référence seulement.
|
||||||
|
result = model.observe(result.state, sink, true, 0.57, false);
|
||||||
|
assert.equal(result.changed, false);
|
||||||
|
result = model.observe(result.state, sink, true, 0.571, false);
|
||||||
|
assert.equal(result.volumeChanged, true); // Même pourcentage arrondi, autre valeur brute.
|
||||||
|
result = model.observe(result.state, sink, true, 0.571, true);
|
||||||
|
assert.equal(result.muteChanged, true);
|
||||||
|
let microphone = model.observe(null, "source A", true, 0.36, false);
|
||||||
|
assert.equal(microphone.changed, false); // Page Audio jamais ouverte.
|
||||||
|
microphone = model.observe(microphone.state, "source A", true, 0.36, true);
|
||||||
|
assert.equal(microphone.muteChanged, true);
|
||||||
|
microphone = model.observe(microphone.state, "source B", true, 0.2, false);
|
||||||
|
assert.equal(microphone.changed, false);
|
||||||
|
result = model.observe(result.state, {}, true, 0.25, false);
|
||||||
|
assert.equal(result.changed, false); // Nouveau défaut : état initial ignoré.
|
||||||
|
result = model.observe(result.state, null, false, NaN, false);
|
||||||
|
assert.equal(result.state, null);
|
||||||
|
result = model.observe(result.state, sink, true, 0.57, false);
|
||||||
|
assert.equal(result.changed, false); // Reconnexion : nouvelle référence.
|
||||||
|
|
||||||
|
const screen = { name: "sortie-test" };
|
||||||
|
let card = model.nextCard(null, "volume", 57, false, screen, 0);
|
||||||
|
card = model.nextCard(card, "volume", 58, false, screen, 500);
|
||||||
|
assert.equal(card.deadline, 2000);
|
||||||
|
assert.equal(model.expire(card, 1, 1500), false);
|
||||||
|
card = model.nextCard(card, "brightness", 42, false, screen, 800);
|
||||||
|
assert.equal(card.kind, "brightness");
|
||||||
|
assert.equal(model.expire(card, 2, 2000), false); // Ancien timer.
|
||||||
|
assert.equal(model.expire(card, 3, 2299), false);
|
||||||
|
assert.equal(model.expire(card, 3, 2300), true);
|
||||||
|
const other = { name: "autre-sortie" };
|
||||||
|
assert.equal(model.screenForEvent([screen, other], other.name, null, "volume", 0), other);
|
||||||
|
assert.equal(model.screenForEvent([screen, other], other.name,
|
||||||
|
{ kind: "volume", screen, until: 100 }, "volume", 50), screen);
|
||||||
|
assert.equal(model.screenForEvent([screen, other], other.name,
|
||||||
|
{ kind: "volume", screen, until: 100 }, "volume", 101), other);
|
||||||
|
assert.equal(model.pageShows("brightness", 0), true);
|
||||||
|
assert.equal(model.pageShows("microphone", 0), false);
|
||||||
|
assert.equal(model.pageShows("microphone", 15), true);
|
||||||
|
console.log("OSD observation et minuteur: PASS");
|
||||||
Loading…
Reference in a new issue