Fix Home shelf slider interactions
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import qs.modules.quicksettings
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property int confirmedValue: 30
|
||||||
|
property int commitCount: 0
|
||||||
|
property int lastCommit: -1
|
||||||
|
|
||||||
|
HomeBrightnessSlider {
|
||||||
|
id: slider
|
||||||
|
width: 200
|
||||||
|
value: root.confirmedValue
|
||||||
|
accessibleName: "Desk lamp brightness"
|
||||||
|
onCommitted: value => {
|
||||||
|
root.commitCount += 1;
|
||||||
|
root.lastCommit = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "home-brightness-slider-test"
|
||||||
|
|
||||||
|
function reset(value: int): void {
|
||||||
|
root.confirmedValue = value;
|
||||||
|
root.commitCount = 0;
|
||||||
|
root.lastCommit = -1;
|
||||||
|
slider.cancelPointerInteraction();
|
||||||
|
}
|
||||||
|
function external(value: int): void { root.confirmedValue = value; }
|
||||||
|
function press(position: int): void { slider.beginPointerInteraction(position); }
|
||||||
|
function move(position: int): void { slider.movePointerInteraction(position); }
|
||||||
|
function release(): void { slider.releasePointerInteraction(); }
|
||||||
|
function cancel(): void { slider.cancelPointerInteraction(); }
|
||||||
|
function wheel(delta: int): void { slider.commitWheel(delta); }
|
||||||
|
function status(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
confirmedValue: root.confirmedValue,
|
||||||
|
previewValue: slider.previewValue,
|
||||||
|
interactionActive: slider.interactionActive,
|
||||||
|
commitCount: root.commitCount,
|
||||||
|
lastCommit: root.lastCommit,
|
||||||
|
accessibleRoleIsSlider: slider.Accessible.role === Accessible.Slider,
|
||||||
|
accessibleName: slider.Accessible.name,
|
||||||
|
accessibleDescription: slider.Accessible.description,
|
||||||
|
accessibleFocusable: slider.Accessible.focusable
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,8 +9,10 @@ Item {
|
|||||||
|
|
||||||
property int value: 0
|
property int value: 0
|
||||||
property int previewValue: 0
|
property int previewValue: 0
|
||||||
|
property bool interactionActive: false
|
||||||
|
property string accessibleName: "Brightness"
|
||||||
|
|
||||||
readonly property bool pressed: drag.pressed
|
readonly property bool pressed: root.interactionActive
|
||||||
|
|
||||||
signal previewChanged(int value)
|
signal previewChanged(int value)
|
||||||
signal committed(int value)
|
signal committed(int value)
|
||||||
@@ -20,8 +22,18 @@ Item {
|
|||||||
activeFocusOnTab: root.enabled
|
activeFocusOnTab: root.enabled
|
||||||
opacity: root.enabled ? 1 : 0.42
|
opacity: root.enabled ? 1 : 0.42
|
||||||
|
|
||||||
|
Accessible.role: Accessible.Slider
|
||||||
|
Accessible.name: root.accessibleName
|
||||||
|
// Qt 6.11's installed Accessible attached type has no structured value or
|
||||||
|
// range properties, so expose both in the supported live description.
|
||||||
|
Accessible.description: root.previewValue + " percent, range 0 to 100"
|
||||||
|
Accessible.focusable: root.enabled
|
||||||
|
Accessible.focused: root.activeFocus
|
||||||
|
Accessible.onIncreaseAction: root.commitStep(5)
|
||||||
|
Accessible.onDecreaseAction: root.commitStep(-5)
|
||||||
|
|
||||||
onValueChanged: {
|
onValueChanged: {
|
||||||
if (!drag.pressed)
|
if (!root.interactionActive)
|
||||||
root.updatePreview(root.value, false);
|
root.updatePreview(root.value, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +65,7 @@ Item {
|
|||||||
color: Theme.warn
|
color: Theme.warn
|
||||||
|
|
||||||
Behavior on width {
|
Behavior on width {
|
||||||
enabled: !drag.pressed
|
enabled: !root.interactionActive
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.durFast
|
duration: Theme.durFast
|
||||||
easing.type: Easing.OutQuad
|
easing.type: Easing.OutQuad
|
||||||
@@ -82,26 +94,21 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
enabled: root.enabled
|
enabled: root.enabled
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
preventStealing: true
|
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
|
||||||
onPressed: event => {
|
onPressed: event => {
|
||||||
root.forceActiveFocus();
|
root.forceActiveFocus();
|
||||||
root.previewAt(event.x);
|
root.beginPointerInteraction(event.x);
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
onPositionChanged: event => {
|
onPositionChanged: event => root.movePointerInteraction(event.x)
|
||||||
if (pressed)
|
|
||||||
root.previewAt(event.x);
|
|
||||||
}
|
|
||||||
onReleased: event => {
|
onReleased: event => {
|
||||||
root.committed(root.previewValue);
|
root.releasePointerInteraction();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
onCanceled: root.updatePreview(root.value, false)
|
onCanceled: root.cancelPointerInteraction()
|
||||||
onWheel: event => {
|
onWheel: event => {
|
||||||
const direction = event.angleDelta.y >= 0 ? 5 : -5;
|
root.commitWheel(event.angleDelta.y);
|
||||||
root.commitStep(direction);
|
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,6 +130,36 @@ Item {
|
|||||||
root.updatePreview(pointerX / Math.max(1, drag.width) * 100, true);
|
root.updatePreview(pointerX / Math.max(1, drag.width) * 100, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function beginPointerInteraction(pointerX: real): void {
|
||||||
|
if (!root.enabled)
|
||||||
|
return;
|
||||||
|
root.interactionActive = true;
|
||||||
|
root.previewAt(pointerX);
|
||||||
|
}
|
||||||
|
|
||||||
|
function movePointerInteraction(pointerX: real): void {
|
||||||
|
if (root.interactionActive)
|
||||||
|
root.previewAt(pointerX);
|
||||||
|
}
|
||||||
|
|
||||||
|
function releasePointerInteraction(): void {
|
||||||
|
if (!root.interactionActive)
|
||||||
|
return;
|
||||||
|
root.interactionActive = false;
|
||||||
|
root.committed(root.previewValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelPointerInteraction(): void {
|
||||||
|
root.interactionActive = false;
|
||||||
|
root.updatePreview(root.value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitWheel(delta: int): void {
|
||||||
|
if (delta === 0)
|
||||||
|
return;
|
||||||
|
root.commitStep(delta > 0 ? 5 : -5);
|
||||||
|
}
|
||||||
|
|
||||||
function commitStep(delta: int): void {
|
function commitStep(delta: int): void {
|
||||||
if (!root.enabled)
|
if (!root.enabled)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ Rectangle {
|
|||||||
? root.pendingBrightness
|
? root.pendingBrightness
|
||||||
: root.confirmedBrightness
|
: root.confirmedBrightness
|
||||||
enabled: root.dimmerEnabled
|
enabled: root.dimmerEnabled
|
||||||
|
accessibleName: root.entity.name + " brightness"
|
||||||
onCommitted: value => root.brightnessRequested(value)
|
onCommitted: value => root.brightnessRequested(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,12 +113,21 @@ rg -Fq 'signal previewChanged(int value)' "$quicksettings_path/HomeBrightnessSli
|
|||||||
|| fail 'brightness slider has no preview contract'
|
|| fail 'brightness slider has no preview contract'
|
||||||
rg -Fq 'signal committed(int value)' "$quicksettings_path/HomeBrightnessSlider.qml" \
|
rg -Fq 'signal committed(int value)' "$quicksettings_path/HomeBrightnessSlider.qml" \
|
||||||
|| fail 'brightness slider has no release-commit contract'
|
|| fail 'brightness slider has no release-commit contract'
|
||||||
rg -Fq 'onReleased:' "$quicksettings_path/HomeBrightnessSlider.qml" \
|
rg -Fq 'onReleased: event =>' "$quicksettings_path/HomeBrightnessSlider.qml" \
|
||||||
|| fail 'brightness slider does not commit on pointer release'
|
|| fail 'brightness slider does not route pointer release through its interaction state'
|
||||||
|
rg -Fq 'root.releasePointerInteraction();' "$quicksettings_path/HomeBrightnessSlider.qml" \
|
||||||
|
|| fail 'brightness slider release is not wired to one-shot commit semantics'
|
||||||
|
rg -Fq 'onCanceled: root.cancelPointerInteraction()' "$quicksettings_path/HomeBrightnessSlider.qml" \
|
||||||
|
|| fail 'brightness slider does not restore its external value when a Flickable steals the pointer'
|
||||||
|
if rg -Fq 'preventStealing: true' "$quicksettings_path/HomeBrightnessSlider.qml"; then
|
||||||
|
fail 'brightness slider blocks the expanded shelf from stealing vertical drags'
|
||||||
|
fi
|
||||||
rg -Fq 'onWheel:' "$quicksettings_path/HomeBrightnessSlider.qml" \
|
rg -Fq 'onWheel:' "$quicksettings_path/HomeBrightnessSlider.qml" \
|
||||||
|| fail 'brightness slider has no wheel commit path'
|
|| fail 'brightness slider has no wheel commit path'
|
||||||
rg -Fq 'onCommitted: value => root.brightnessRequested(value)' "$quicksettings_path/HomeTile.qml" \
|
rg -Fq 'onCommitted: value => root.brightnessRequested(value)' "$quicksettings_path/HomeTile.qml" \
|
||||||
|| fail 'Home tile does not forward the slider release commit'
|
|| fail 'Home tile does not forward the slider release commit'
|
||||||
|
rg -Fq 'accessibleName: root.entity.name + " brightness"' "$quicksettings_path/HomeTile.qml" \
|
||||||
|
|| fail 'Home tile does not give its dimmer an accessory-specific accessible name'
|
||||||
|
|
||||||
cp -a "$source_config_path" "$config_path"
|
cp -a "$source_config_path" "$config_path"
|
||||||
: >"$helper_log"
|
: >"$helper_log"
|
||||||
|
|||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
harness="$repo_dir/config/dot/quickshell/home-brightness-slider-harness.qml"
|
||||||
|
state_home="$(mktemp -d /tmp/panama-home-brightness-slider.XXXXXX)"
|
||||||
|
shell_log="$state_home/quickshell.log"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'Home brightness slider contract: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
qs_for_harness() {
|
||||||
|
QS_DISABLE_CRASH_HANDLER=1 XDG_STATE_HOME="$state_home" \
|
||||||
|
qs -p "$harness" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
qs_for_harness kill >/dev/null 2>&1 || true
|
||||||
|
rm -rf "$state_home"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
start_harness() {
|
||||||
|
qs_for_harness --daemonize >"$shell_log" 2>&1
|
||||||
|
for _ in $(seq 1 50); do
|
||||||
|
if qs_for_harness ipc show 2>/dev/null \
|
||||||
|
| rg -q '^target home-brightness-slider-test$'; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
sed -n '1,160p' "$shell_log" >&2
|
||||||
|
fail 'isolated slider harness did not start'
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_status() {
|
||||||
|
local filter="$1"
|
||||||
|
local message="$2"
|
||||||
|
local status=""
|
||||||
|
|
||||||
|
status="$(qs_for_harness ipc call home-brightness-slider-test status)"
|
||||||
|
jq -e "$filter" <<<"$status" >/dev/null || {
|
||||||
|
printf 'Slider status: %s\n' "$status" >&2
|
||||||
|
fail "$message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start_harness
|
||||||
|
|
||||||
|
qs_for_harness ipc call home-brightness-slider-test reset 30 >/dev/null
|
||||||
|
assert_status \
|
||||||
|
'.accessibleRoleIsSlider == true and .accessibleName == "Desk lamp brightness" and .accessibleDescription == "30 percent, range 0 to 100" and .accessibleFocusable == true' \
|
||||||
|
'slider accessibility metadata is incomplete or incorrect'
|
||||||
|
qs_for_harness ipc call home-brightness-slider-test press 140 >/dev/null
|
||||||
|
assert_status \
|
||||||
|
'.confirmedValue == 30 and .previewValue == 70 and .interactionActive == true and .commitCount == 0 and .accessibleDescription == "70 percent, range 0 to 100"' \
|
||||||
|
'pointer press did not remain a local preview'
|
||||||
|
qs_for_harness ipc call home-brightness-slider-test release >/dev/null
|
||||||
|
assert_status \
|
||||||
|
'.previewValue == 70 and .interactionActive == false and .commitCount == 1 and .lastCommit == 70' \
|
||||||
|
'one pointer release did not emit exactly one commit'
|
||||||
|
qs_for_harness ipc call home-brightness-slider-test release >/dev/null
|
||||||
|
assert_status '.commitCount == 1' 'release without an interaction emitted another commit'
|
||||||
|
|
||||||
|
qs_for_harness ipc call home-brightness-slider-test reset 30 >/dev/null
|
||||||
|
qs_for_harness ipc call home-brightness-slider-test wheel 120 >/dev/null
|
||||||
|
assert_status \
|
||||||
|
'.previewValue == 35 and .interactionActive == false and .commitCount == 1 and .lastCommit == 35' \
|
||||||
|
'one wheel event did not emit exactly one five-point commit'
|
||||||
|
|
||||||
|
qs_for_harness ipc call home-brightness-slider-test reset 30 >/dev/null
|
||||||
|
qs_for_harness ipc call home-brightness-slider-test press 160 >/dev/null
|
||||||
|
assert_status \
|
||||||
|
'.previewValue == 80 and .interactionActive == true and .commitCount == 0' \
|
||||||
|
'cancellation setup did not enter local preview state'
|
||||||
|
qs_for_harness ipc call home-brightness-slider-test external 45 >/dev/null
|
||||||
|
assert_status \
|
||||||
|
'.confirmedValue == 45 and .previewValue == 80 and .interactionActive == true and .commitCount == 0' \
|
||||||
|
'external confirmed or pending state replaced an active local preview'
|
||||||
|
qs_for_harness ipc call home-brightness-slider-test cancel >/dev/null
|
||||||
|
assert_status \
|
||||||
|
'.confirmedValue == 45 and .previewValue == 45 and .interactionActive == false and .commitCount == 0 and .lastCommit == -1' \
|
||||||
|
'canceled interaction committed or failed to restore the external value'
|
||||||
|
|
||||||
|
trap - EXIT
|
||||||
|
cleanup
|
||||||
|
printf 'Home brightness slider contract: PASS\n'
|
||||||
Reference in New Issue
Block a user