Add display identification overlays
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user