Fix Phone controls runtime accessibility
This commit is contained in:
@@ -138,8 +138,20 @@ Item {
|
|||||||
color: actionMouse.containsMouse && actionMouse.enabled
|
color: actionMouse.containsMouse && actionMouse.enabled
|
||||||
? Theme.alpha(Theme.accent, 0.15)
|
? Theme.alpha(Theme.accent, 0.15)
|
||||||
: Theme.alpha(Theme.accent, 0.075)
|
: Theme.alpha(Theme.accent, 0.075)
|
||||||
|
border.width: actionMouse.activeFocus ? 2 : 0
|
||||||
|
border.color: Theme.accent
|
||||||
opacity: actionMouse.enabled ? 1 : 0.48
|
opacity: actionMouse.enabled ? 1 : 0.48
|
||||||
|
|
||||||
|
Accessible.role: Accessible.Button
|
||||||
|
Accessible.name: actionButton.modelData.label
|
||||||
|
Accessible.description: root.actionAccessibleDescription(actionButton.modelData.id)
|
||||||
|
Accessible.focusable: actionMouse.enabled
|
||||||
|
Accessible.focused: actionMouse.activeFocus
|
||||||
|
Accessible.onPressAction: {
|
||||||
|
if (actionMouse.enabled)
|
||||||
|
root.invoke(actionButton.modelData.id);
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
@@ -162,11 +174,7 @@ Item {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
id: actionMouse
|
id: actionMouse
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
enabled: actionButton.modelData.id === "messages"
|
enabled: root.actionEnabled(actionButton.modelData.id)
|
||||||
? SystemSettings.bluebubblesAvailable
|
|
||||||
: KdeConnect.phoneReachable
|
|
||||||
&& !KdeConnect.transferActive
|
|
||||||
&& KdeConnect.supports(actionButton.modelData.id)
|
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
activeFocusOnTab: enabled
|
activeFocusOnTab: enabled
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
@@ -181,7 +189,7 @@ Item {
|
|||||||
Text {
|
Text {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
visible: root.phone && !KdeConnect.phoneReachable && root.actionModels.length > 0
|
visible: root.phone && !KdeConnect.phoneReachable && root.actionModels.length > 0
|
||||||
text: "Actions become available when the iPhone reconnects"
|
text: "KDE Connect actions are unavailable until the iPhone reconnects"
|
||||||
color: Theme.fgMuted
|
color: Theme.fgMuted
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
@@ -279,6 +287,20 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function actionEnabled(action: string): bool {
|
||||||
|
if (action === "messages")
|
||||||
|
return SystemSettings.bluebubblesAvailable;
|
||||||
|
return KdeConnect.phoneReachable
|
||||||
|
&& !KdeConnect.transferActive
|
||||||
|
&& KdeConnect.supports(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionAccessibleDescription(action: string): string {
|
||||||
|
if (action === "messages")
|
||||||
|
return SystemSettings.bluebubblesAvailable ? "Opens BlueBubbles" : "BlueBubbles is not installed";
|
||||||
|
return root.actionEnabled(action) ? "Available through KDE Connect" : "Unavailable through KDE Connect";
|
||||||
|
}
|
||||||
|
|
||||||
function localPath(selectedUrl: url): string {
|
function localPath(selectedUrl: url): string {
|
||||||
const value = String(selectedUrl);
|
const value = String(selectedUrl);
|
||||||
if (!value.startsWith("file://"))
|
if (!value.startsWith("file://"))
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import qs.modules.quicksettings
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
// Component-only diagnostic surface. The contract replaces the two services in
|
||||||
|
// its copied configuration with fixture singletons before this file is run.
|
||||||
|
ShellRoot {
|
||||||
|
PhoneControls {
|
||||||
|
id: phoneControls
|
||||||
|
width: 408
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "phone-controls-test"
|
||||||
|
|
||||||
|
function fixture(name: string): void {
|
||||||
|
KdeConnect.available = false;
|
||||||
|
KdeConnect.transferActive = false;
|
||||||
|
KdeConnect.lastError = "";
|
||||||
|
|
||||||
|
if (name === "offline") {
|
||||||
|
KdeConnect.devices = [{
|
||||||
|
id: "fixture-phone",
|
||||||
|
name: "Fixture iPhone",
|
||||||
|
type: "phone",
|
||||||
|
paired: true,
|
||||||
|
reachable: false,
|
||||||
|
actions: []
|
||||||
|
}];
|
||||||
|
} else if (name === "transfer") {
|
||||||
|
KdeConnect.available = true;
|
||||||
|
KdeConnect.transferActive = true;
|
||||||
|
KdeConnect.devices = [{
|
||||||
|
id: "fixture-phone",
|
||||||
|
name: "Fixture iPhone",
|
||||||
|
type: "phone",
|
||||||
|
paired: true,
|
||||||
|
reachable: true,
|
||||||
|
actions: ["share", "clipboard", "ring"]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function status(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
actions: phoneControls.actionModels.map(action => ({
|
||||||
|
id: action.id,
|
||||||
|
enabled: phoneControls.actionEnabled(action.id)
|
||||||
|
})),
|
||||||
|
bluebubblesAvailable: SystemSettings.bluebubblesAvailable,
|
||||||
|
appLaunches: SystemSettings.launchCount,
|
||||||
|
phoneActions: KdeConnect.actionCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool available: false
|
||||||
|
property var devices: []
|
||||||
|
property bool transferActive: false
|
||||||
|
property string lastError: ""
|
||||||
|
property var recentExchange: null
|
||||||
|
property int actionCount: 0
|
||||||
|
|
||||||
|
readonly property var preferredPhone: {
|
||||||
|
const phones = root.devices.filter(device => device.type === "phone" && device.paired);
|
||||||
|
return phones.find(device => device.reachable) ?? phones[0] ?? null;
|
||||||
|
}
|
||||||
|
readonly property bool phoneReachable: root.preferredPhone?.reachable === true
|
||||||
|
readonly property var phoneActions: root.preferredPhone?.actions ?? []
|
||||||
|
|
||||||
|
function supports(action: string): bool {
|
||||||
|
return root.phoneActions.indexOf(action) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh(): void {}
|
||||||
|
function sendFile(path: string): void { root.actionCount += 1; }
|
||||||
|
function sendClipboard(): void { root.actionCount += 1; }
|
||||||
|
function ring(): void { root.actionCount += 1; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
property bool bluebubblesAvailable: true
|
||||||
|
property int launchCount: 0
|
||||||
|
|
||||||
|
function openApplication(id: string): bool {
|
||||||
|
launchCount += 1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,11 @@ set -euo pipefail
|
|||||||
project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
phone_controls="$project_root/config/dot/quickshell/modules/quicksettings/PhoneControls.qml"
|
phone_controls="$project_root/config/dot/quickshell/modules/quicksettings/PhoneControls.qml"
|
||||||
system_settings="$project_root/config/dot/quickshell/services/SystemSettings.qml"
|
system_settings="$project_root/config/dot/quickshell/services/SystemSettings.qml"
|
||||||
|
harness="$project_root/config/dot/quickshell/phone-controls-harness.qml"
|
||||||
|
kde_fixture="$project_root/tests/quickshell/fixtures/PhoneControlsKdeConnect.qml"
|
||||||
|
settings_fixture="$project_root/tests/quickshell/fixtures/PhoneControlsSystemSettings.qml"
|
||||||
|
state_home="$(mktemp -d /tmp/panama-phone-controls.XXXXXX)"
|
||||||
|
config_path="$state_home/quickshell"
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
printf 'Phone Messages contract: %s\n' "$1" >&2
|
printf 'Phone Messages contract: %s\n' "$1" >&2
|
||||||
@@ -13,9 +18,55 @@ fail() {
|
|||||||
|
|
||||||
[[ -f "$phone_controls" ]] || fail 'Phone controls component is missing'
|
[[ -f "$phone_controls" ]] || fail 'Phone controls component is missing'
|
||||||
[[ -f "$system_settings" ]] || fail 'System settings service is missing'
|
[[ -f "$system_settings" ]] || fail 'System settings service is missing'
|
||||||
|
[[ -f "$harness" ]] || fail 'Phone controls runtime harness is missing'
|
||||||
|
[[ -f "$kde_fixture" ]] || fail 'Phone controls KDE fixture is missing'
|
||||||
|
[[ -f "$settings_fixture" ]] || fail 'Phone controls System Settings fixture is missing'
|
||||||
|
|
||||||
# This is a static contract only: it starts no shell, sends no IPC, and never
|
cleanup() {
|
||||||
# evaluates the launch branch. Its only Flatpak use is source-text inspection.
|
XDG_STATE_HOME="$state_home" QS_DISABLE_CRASH_HANDLER=1 \
|
||||||
|
qs -p "$config_path/phone-controls-harness.qml" kill >/dev/null 2>&1 || true
|
||||||
|
rm -rf "$state_home"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
qs_for_harness() {
|
||||||
|
XDG_STATE_HOME="$state_home" QS_DISABLE_CRASH_HANDLER=1 \
|
||||||
|
qs -p "$config_path/phone-controls-harness.qml" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_harness() {
|
||||||
|
cp -a "$project_root/config/dot/quickshell" "$config_path"
|
||||||
|
cp "$kde_fixture" "$config_path/services/KdeConnect.qml"
|
||||||
|
cp "$settings_fixture" "$config_path/services/SystemSettings.qml"
|
||||||
|
qs_for_harness --daemonize >/dev/null
|
||||||
|
for _ in $(seq 1 40); do
|
||||||
|
if qs_for_harness ipc show 2>/dev/null | rg -q '^target phone-controls-test$'; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
fail 'isolated Phone controls harness did not start'
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_fixture_state() {
|
||||||
|
local fixture="$1"
|
||||||
|
local expected="$2"
|
||||||
|
|
||||||
|
qs_for_harness ipc call phone-controls-test fixture "$fixture" >/dev/null
|
||||||
|
local actual
|
||||||
|
actual="$(qs_for_harness ipc call phone-controls-test status)"
|
||||||
|
[[ -n "$actual" ]] || fail 'Phone controls do not expose runtime action enablement'
|
||||||
|
jq -e --argjson expected "$expected" '
|
||||||
|
.bluebubblesAvailable == true and .appLaunches == 0 and .phoneActions == 0 and
|
||||||
|
(.actions | length == 4) and
|
||||||
|
([.actions[] | select(.id == "messages") | .enabled] == [true]) and
|
||||||
|
([.actions[] | select(.id != "messages") | .enabled] == $expected)
|
||||||
|
' <<<"$actual" >/dev/null \
|
||||||
|
|| fail "unexpected $fixture action state: $actual"
|
||||||
|
}
|
||||||
|
|
||||||
|
# These source checks do not evaluate the launch branch. The isolated runtime
|
||||||
|
# harness below uses fixture singletons and only reads action-model status.
|
||||||
|
|
||||||
rg -Fq '"bluebubbles": ["flatpak", "run", "app.bluebubbles.BlueBubbles"]' "$system_settings" \
|
rg -Fq '"bluebubbles": ["flatpak", "run", "app.bluebubbles.BlueBubbles"]' "$system_settings" \
|
||||||
|| fail 'BlueBubbles does not use the fixed Flatpak argument vector'
|
|| fail 'BlueBubbles does not use the fixed Flatpak argument vector'
|
||||||
@@ -40,7 +91,9 @@ rg -Fq 'SystemSettings.bluebubblesAvailable' "$phone_controls" \
|
|||||||
|| fail 'Messages enablement does not read BlueBubbles availability'
|
|| fail 'Messages enablement does not read BlueBubbles availability'
|
||||||
rg -Fq 'KdeConnect.phoneReachable' "$phone_controls" \
|
rg -Fq 'KdeConnect.phoneReachable' "$phone_controls" \
|
||||||
|| fail 'KDE action reachability semantics are missing'
|
|| fail 'KDE action reachability semantics are missing'
|
||||||
rg -Fq 'KdeConnect.supports(actionButton.modelData.id)' "$phone_controls" \
|
rg -Fq 'function actionEnabled(action: string): bool' "$phone_controls" \
|
||||||
|
|| fail 'Phone controls do not expose action enablement'
|
||||||
|
rg -Fq 'KdeConnect.supports(action)' "$phone_controls" \
|
||||||
|| fail 'KDE action capability semantics are missing'
|
|| fail 'KDE action capability semantics are missing'
|
||||||
rg -Fq 'else if (action === "messages")' "$phone_controls" \
|
rg -Fq 'else if (action === "messages")' "$phone_controls" \
|
||||||
|| fail 'Messages has no independent invocation branch'
|
|| fail 'Messages has no independent invocation branch'
|
||||||
@@ -48,6 +101,20 @@ rg -Fq 'SystemSettings.openApplication("bluebubbles")' "$phone_controls" \
|
|||||||
|| fail 'Messages does not use the allow-listed BlueBubbles launcher'
|
|| fail 'Messages does not use the allow-listed BlueBubbles launcher'
|
||||||
rg -Fq 'BlueBubbles is not installed' "$phone_controls" \
|
rg -Fq 'BlueBubbles is not installed' "$phone_controls" \
|
||||||
|| fail 'Missing BlueBubbles has no quiet explanatory row'
|
|| fail 'Missing BlueBubbles has no quiet explanatory row'
|
||||||
|
rg -Fq 'KDE Connect actions are unavailable until the iPhone reconnects' "$phone_controls" \
|
||||||
|
|| fail 'Offline copy does not scope unavailability to KDE Connect actions'
|
||||||
|
rg -Fq 'border.width: actionMouse.activeFocus ? 2 : 0' "$phone_controls" \
|
||||||
|
|| fail 'Phone actions do not visibly indicate keyboard focus'
|
||||||
|
rg -Fq 'Accessible.role: Accessible.Button' "$phone_controls" \
|
||||||
|
|| fail 'Phone actions do not expose an Accessible button role'
|
||||||
|
rg -Fq 'Accessible.name: actionButton.modelData.label' "$phone_controls" \
|
||||||
|
|| fail 'Phone actions do not expose their accessible name'
|
||||||
|
rg -Fq 'Accessible.description: root.actionAccessibleDescription(actionButton.modelData.id)' "$phone_controls" \
|
||||||
|
|| fail 'Phone actions do not expose their availability state'
|
||||||
|
rg -Fq 'Accessible.focusable: actionMouse.enabled' "$phone_controls" \
|
||||||
|
|| fail 'Phone action accessibility does not respect disabled state'
|
||||||
|
rg -Fq 'Accessible.onPressAction:' "$phone_controls" \
|
||||||
|
|| fail 'Phone actions do not provide an accessible press handler'
|
||||||
|
|
||||||
# A static negative guard keeps the Messages branch from accidentally inheriting
|
# A static negative guard keeps the Messages branch from accidentally inheriting
|
||||||
# KDE Connect reachability, transfer, or plugin conditions.
|
# KDE Connect reachability, transfer, or plugin conditions.
|
||||||
@@ -55,4 +122,8 @@ messages_branch="$(sed -n '/else if (action === "messages")/,/^ }/p' "$phone_
|
|||||||
printf '%s\n' "$messages_branch" | rg -Fq 'KdeConnect.' \
|
printf '%s\n' "$messages_branch" | rg -Fq 'KdeConnect.' \
|
||||||
&& fail 'Messages is coupled to KDE Connect'
|
&& fail 'Messages is coupled to KDE Connect'
|
||||||
|
|
||||||
|
start_harness
|
||||||
|
assert_fixture_state offline '[false, false, false]'
|
||||||
|
assert_fixture_state transfer '[false, false, false]'
|
||||||
|
|
||||||
printf 'Phone Messages contract: PASS\n'
|
printf 'Phone Messages contract: PASS\n'
|
||||||
|
|||||||
Reference in New Issue
Block a user