Add display identification overlays

This commit is contained in:
Gabriel Brown
2026-08-18 15:00:18 -04:00
parent 70a9d27c98
commit 0c3935f818
7 changed files with 142 additions and 1 deletions
@@ -3,8 +3,11 @@ import Quickshell.Io
import QtQuick import QtQuick
import qs.modules.settings import qs.modules.settings
import qs.services
ShellRoot { ShellRoot {
DisplayIdentify {}
QtObject { QtObject {
id: fixtureService id: fixtureService
@@ -71,5 +74,8 @@ ShellRoot {
name: record.name, x: record.x, y: record.y, primary: record.primary name: record.name, x: record.x, y: record.y, primary: record.primary
}))); })));
} }
function identify(): void { Displays.identify(); }
function identifying(): bool { return Displays.identifying; }
} }
} }
@@ -279,7 +279,8 @@ Item {
spacing: 8 spacing: 8
Text { Text {
width: Math.max(0, parent.width - primaryButton.width - applyButton.width - 16) width: Math.max(0, parent.width - identifyButton.width
- primaryButton.width - applyButton.width - 24)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: root.width >= 600 text: root.width >= 600
? "Drag to arrange · arrows move 10 px · Shift moves 100 px" ? "Drag to arrange · arrows move 10 px · Shift moves 100 px"
@@ -290,6 +291,13 @@ Item {
elide: Text.ElideRight elide: Text.ElideRight
} }
SettingsButton {
id: identifyButton
text: "Identify"
enabled: root.interactionEnabled
onClicked: root.displayService.identify()
}
SettingsButton { SettingsButton {
id: primaryButton id: primaryButton
text: "Make primary" text: "Make primary"
@@ -0,0 +1,85 @@
import Quickshell
import Quickshell.Wayland
import QtQuick
import qs.config
import qs.services
import qs.widgets
Variants {
model: Quickshell.screens
PanelWindow {
id: win
property var modelData: null
readonly property string connector: win.modelData?.name ?? "Display"
readonly property int number: Math.max(1,
Displays.monitors.findIndex(monitor => monitor.name === win.connector) + 1)
readonly property string description: Displays.monitorNamed(win.connector)?.description ?? "Connected display"
screen: win.modelData
visible: Displays.identifying
implicitWidth: 260
implicitHeight: 172
color: "transparent"
exclusiveZone: 0
exclusionMode: ExclusionMode.Ignore
mask: Region {}
WlrLayershell.namespace: "qs-display-identify"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
Rectangle {
anchors.fill: parent
radius: Theme.popoverRadius
color: Theme.alpha(Theme.bgPopover, 0.96)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.14)
PrismEdge {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
inset: parent.radius
}
Column {
anchors.centerIn: parent
width: parent.width - 32
spacing: 4
Text {
width: parent.width
horizontalAlignment: Text.AlignHCenter
text: String(win.number)
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: 68
font.weight: Font.DemiBold
}
Text {
width: parent.width
horizontalAlignment: Text.AlignHCenter
text: win.connector
color: Theme.accentAlt
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.DemiBold
elide: Text.ElideRight
}
Text {
width: parent.width
horizontalAlignment: Text.AlignHCenter
text: win.description
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
}
}
}
}
@@ -43,6 +43,7 @@ ShortcutCapture 1.0 ShortcutCapture.qml
ChoiceGrid 1.0 ChoiceGrid.qml ChoiceGrid 1.0 ChoiceGrid.qml
DisplayModePicker 1.0 DisplayModePicker.qml DisplayModePicker 1.0 DisplayModePicker.qml
DisplayArrangement 1.0 DisplayArrangement.qml DisplayArrangement 1.0 DisplayArrangement.qml
DisplayIdentify 1.0 DisplayIdentify.qml
WifiPanel 1.0 WifiPanel.qml WifiPanel 1.0 WifiPanel.qml
BluetoothPanel 1.0 BluetoothPanel.qml BluetoothPanel 1.0 BluetoothPanel.qml
PasswordField 1.0 PasswordField.qml PasswordField 1.0 PasswordField.qml
@@ -43,6 +43,7 @@ Singleton {
property int revertGeneration: -1 property int revertGeneration: -1
property bool externalChangeBlocked: false property bool externalChangeBlocked: false
property int secondsLeft: 0 property int secondsLeft: 0
property bool identifying: false
readonly property bool awaitingConfirmation: root.pendingRequestedLayout !== null readonly property bool awaitingConfirmation: root.pendingRequestedLayout !== null
readonly property bool canConfirm: root.awaitingConfirmation readonly property bool canConfirm: root.awaitingConfirmation
@@ -123,6 +124,11 @@ Singleton {
return false; return false;
} }
function identify(): void {
root.identifying = true;
identifyTimer.restart();
}
function parse(text: string, generation: int): void { function parse(text: string, generation: int): void {
try { try {
const raw = JSON.parse(text); const raw = JSON.parse(text);
@@ -520,6 +526,13 @@ Singleton {
root.lastError = "The previous display setting could not be verified. Open Displays and restore it manually."; root.lastError = "The previous display setting could not be verified. Open Displays and restore it manually.";
} }
Timer {
id: identifyTimer
interval: 3000
repeat: false
onTriggered: root.identifying = false
}
Timer { Timer {
id: verifyTimer id: verifyTimer
property int attempts: 0 property int attempts: 0
+2
View File
@@ -86,6 +86,8 @@ ShellRoot {
Osd {} Osd {}
} }
DisplayIdentify {}
// ── Single-instance overlays ──────────────────────────────────────────── // ── Single-instance overlays ────────────────────────────────────────────
// These are always constructed but only *visible* when ShellState says so. // These are always constructed but only *visible* when ShellState says so.
// They're cheap while hidden, and keeping them alive means opening the // They're cheap while hidden, and keeping them alive means opening the
@@ -6,6 +6,9 @@ repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
component="$repo_dir/config/dot/quickshell/modules/settings/DisplayArrangement.qml" component="$repo_dir/config/dot/quickshell/modules/settings/DisplayArrangement.qml"
page="$repo_dir/config/dot/quickshell/modules/settings/DisplaysPage.qml" page="$repo_dir/config/dot/quickshell/modules/settings/DisplaysPage.qml"
harness="$repo_dir/config/dot/quickshell/display-arrangement-harness.qml" harness="$repo_dir/config/dot/quickshell/display-arrangement-harness.qml"
identify="$repo_dir/config/dot/quickshell/modules/settings/DisplayIdentify.qml"
service="$repo_dir/config/dot/quickshell/services/Displays.qml"
shell="$repo_dir/config/dot/quickshell/shell.qml"
fail() { fail() {
printf 'display arrangement contract: %s\n' "$1" >&2 printf 'display arrangement contract: %s\n' "$1" >&2
@@ -30,6 +33,22 @@ rg -Fq 'DisplayArrangement {' "$page" \
|| fail 'Displays page does not expose the arrangement canvas' || fail 'Displays page does not expose the arrangement canvas'
rg -Fq 'visible: Displays.monitors.length > 1' "$page" \ rg -Fq 'visible: Displays.monitors.length > 1' "$page" \
|| fail 'arrangement is shown for a single display' || fail 'arrangement is shown for a single display'
[[ -f "$identify" ]] || fail 'DisplayIdentify.qml is missing'
for contract in \
'model: Quickshell.screens' \
'WlrLayershell.keyboardFocus: WlrKeyboardFocus.None' \
'mask: Region {}' \
'visible: Displays.identifying' \
'text: String(win.number)' \
'text: win.connector'; do
rg -Fq "$contract" "$identify" \
|| fail "display identification overlay is incomplete: $contract"
done
rg -Fq 'DisplayIdentify {}' "$shell" \
|| fail 'display identification overlays are not shell-owned'
rg -Fq 'function identify()' "$service" \
&& rg -Fq 'interval: 3000' "$service" \
|| fail 'display identification does not use one three-second service timer'
state_home="$(mktemp -d /tmp/panama-display-arrangement-state.XXXXXX)" state_home="$(mktemp -d /tmp/panama-display-arrangement-state.XXXXXX)"
harness_pid="" harness_pid=""
@@ -82,4 +101,11 @@ jq -e '. == [
]' <<<"$primary" >/dev/null \ ]' <<<"$primary" >/dev/null \
|| fail "Make primary did not normalize the selected output to 0,0: $primary" || fail "Make primary did not normalize the selected output to 0,0: $primary"
ipc identify >/dev/null
[[ "$(ipc identifying)" == "true" ]] \
|| fail 'identify did not reveal the overlays'
sleep 3.2
[[ "$(ipc identifying)" == "false" ]] \
|| fail 'identify overlays did not disappear after one three-second timer'
printf 'display arrangement contract: PASS\n' printf 'display arrangement contract: PASS\n'