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