diff --git a/config/dot/quickshell/display-arrangement-harness.qml b/config/dot/quickshell/display-arrangement-harness.qml index 72832ae..332c6fd 100644 --- a/config/dot/quickshell/display-arrangement-harness.qml +++ b/config/dot/quickshell/display-arrangement-harness.qml @@ -3,8 +3,11 @@ import Quickshell.Io import QtQuick import qs.modules.settings +import qs.services ShellRoot { + DisplayIdentify {} + QtObject { id: fixtureService @@ -71,5 +74,8 @@ ShellRoot { name: record.name, x: record.x, y: record.y, primary: record.primary }))); } + + function identify(): void { Displays.identify(); } + function identifying(): bool { return Displays.identifying; } } } diff --git a/config/dot/quickshell/modules/settings/DisplayArrangement.qml b/config/dot/quickshell/modules/settings/DisplayArrangement.qml index 9e43963..e5cbf8d 100644 --- a/config/dot/quickshell/modules/settings/DisplayArrangement.qml +++ b/config/dot/quickshell/modules/settings/DisplayArrangement.qml @@ -279,7 +279,8 @@ Item { spacing: 8 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 text: root.width >= 600 ? "Drag to arrange · arrows move 10 px · Shift moves 100 px" @@ -290,6 +291,13 @@ Item { elide: Text.ElideRight } + SettingsButton { + id: identifyButton + text: "Identify" + enabled: root.interactionEnabled + onClicked: root.displayService.identify() + } + SettingsButton { id: primaryButton text: "Make primary" diff --git a/config/dot/quickshell/modules/settings/DisplayIdentify.qml b/config/dot/quickshell/modules/settings/DisplayIdentify.qml new file mode 100644 index 0000000..91047d7 --- /dev/null +++ b/config/dot/quickshell/modules/settings/DisplayIdentify.qml @@ -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 + } + } + } + } +} diff --git a/config/dot/quickshell/modules/settings/qmldir b/config/dot/quickshell/modules/settings/qmldir index 75f779a..79c1719 100644 --- a/config/dot/quickshell/modules/settings/qmldir +++ b/config/dot/quickshell/modules/settings/qmldir @@ -43,6 +43,7 @@ ShortcutCapture 1.0 ShortcutCapture.qml ChoiceGrid 1.0 ChoiceGrid.qml DisplayModePicker 1.0 DisplayModePicker.qml DisplayArrangement 1.0 DisplayArrangement.qml +DisplayIdentify 1.0 DisplayIdentify.qml WifiPanel 1.0 WifiPanel.qml BluetoothPanel 1.0 BluetoothPanel.qml PasswordField 1.0 PasswordField.qml diff --git a/config/dot/quickshell/services/Displays.qml b/config/dot/quickshell/services/Displays.qml index d5f2f51..0a95b7a 100644 --- a/config/dot/quickshell/services/Displays.qml +++ b/config/dot/quickshell/services/Displays.qml @@ -43,6 +43,7 @@ Singleton { property int revertGeneration: -1 property bool externalChangeBlocked: false property int secondsLeft: 0 + property bool identifying: false readonly property bool awaitingConfirmation: root.pendingRequestedLayout !== null readonly property bool canConfirm: root.awaitingConfirmation @@ -123,6 +124,11 @@ Singleton { return false; } + function identify(): void { + root.identifying = true; + identifyTimer.restart(); + } + function parse(text: string, generation: int): void { try { 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."; } + Timer { + id: identifyTimer + interval: 3000 + repeat: false + onTriggered: root.identifying = false + } + Timer { id: verifyTimer property int attempts: 0 diff --git a/config/dot/quickshell/shell.qml b/config/dot/quickshell/shell.qml index 7b13a0b..d79cdd4 100644 --- a/config/dot/quickshell/shell.qml +++ b/config/dot/quickshell/shell.qml @@ -86,6 +86,8 @@ ShellRoot { Osd {} } + DisplayIdentify {} + // ── 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 diff --git a/tests/quickshell/display-arrangement-contract.sh b/tests/quickshell/display-arrangement-contract.sh index 89a2a31..0e4380a 100755 --- a/tests/quickshell/display-arrangement-contract.sh +++ b/tests/quickshell/display-arrangement-contract.sh @@ -6,6 +6,9 @@ repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" component="$repo_dir/config/dot/quickshell/modules/settings/DisplayArrangement.qml" page="$repo_dir/config/dot/quickshell/modules/settings/DisplaysPage.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() { 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' rg -Fq 'visible: Displays.monitors.length > 1' "$page" \ || 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)" harness_pid="" @@ -82,4 +101,11 @@ jq -e '. == [ ]' <<<"$primary" >/dev/null \ || 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'