Files
omarchy/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_escape_routing.qml
T
asepharyana 1cdb82a76f Sync config from arch
- hypr/apps.lua
- hypr/autostart.lua
- hypr/envs.lua
- hypr/hyprland.lua
- hypr/hyprsunset.conf
- hypr/input.lua
- hypr/looknfeel.lua
- hypr/omasettings.lua
- hypr/xdph.conf
- omarchy/branding/about.txt
- omarchy/branding/screensaver.txt
- omarchy/extensions/omarchy-menu.jsonc
- omarchy/hooks/battery-low.d/play-warning-sound.sample
- omarchy/hooks/font-set.d/show-font-notification.sample
- omarchy/hooks/post-boot.d/weather.sample
- omarchy/hooks/post-update.d/install-voxtype.hook
- omarchy/hooks/post-update.d/setup-agent.hook
- omarchy/hooks/post-update.d/setup-fingerprint.hook
- omarchy/hooks/post-update.d/show-update-notification.sample
- omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
- omarchy/hooks/theme-set.d/show-theme-notification.sample
- omarchy/shell.json
- omarchy/shell.toml
- omarchy/theme.name
- omarchy/themes/azure-glow/README.md
- omarchy/themes/azure-glow/alacritty.toml
- omarchy/themes/azure-glow/btop.theme
- omarchy/themes/azure-glow/hyprland.conf
- omarchy/themes/azure-glow/hyprlock.conf
- omarchy/themes/azure-glow/icons.theme
- … 269 more
2026-09-23 15:19:12 +07:00

164 lines
5.8 KiB
QML

// Escape must cancel out of a form, and the panel's key wiring makes that
// non-obvious enough to be worth pinning down. Two separate traps live here.
//
// 1. PanelKeyCatcher goes `blocked` on every screen built around a text field
// -- the item form, PIN, fingerprint, the Send composer -- and a blocked
// catcher drops ALL keys, Escape included. So Escape is dispatched from the
// shortcut interceptor, which the catcher reaches through Keys.forwardTo
// before its own handler and regardless of `blocked`.
//
// 2. Qt does NOT clear active focus when an item is hidden. The search field
// keeps focus behind the item form, and its own Keys.onEscapePressed used
// to fire from back there and close the whole panel. Two things stop that:
// the handler ignores Escape unless the search box is the current screen,
// and focus is re-homed whenever the screen changes.
//
// Needs Qt, which any machine running the plugin already has:
//
// QT_QPA_PLATFORM=offscreen qmltestrunner -input tests/qml
//
import QtQuick
import QtQuick.Controls
import QtTest
// Namespaced so the kit's own TextField (which needs the shell's import path)
// does not shadow the plain QtQuick.Controls one used below.
import "file:/usr/share/omarchy/shell/Ui" as OmarchyUi
TestCase {
id: tc
name: "EscapeRouting"
when: windowShown
width: 300; height: 200
visible: true
property string screenName: "main"
property int interceptorEscapes: 0
property int catcherCloses: 0
property int panelCloses: 0
property string trail: ""
// Stands in for Panel.qml's handleEscape().
function handleEscape() {
tc.interceptorEscapes++
tc.trail += "dispatch(" + tc.screenName + ") "
if (tc.screenName === "edit") tc.screenName = "main"
else tc.panelCloses++
}
onScreenNameChanged: restoreScreenFocus()
function restoreScreenFocus() {
if (tc.screenName === "main") searchField.forceActiveFocus()
else if (tc.screenName === "edit") formField.forceActiveFocus()
}
// Stands in for Panel.qml's shortcutInterceptor.
Item {
id: interceptor
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape && !(event.modifiers & ~Qt.KeypadModifier)) {
tc.handleEscape()
event.accepted = true
}
}
}
OmarchyUi.PanelKeyCatcher {
id: catcher
anchors.fill: parent
Keys.forwardTo: [interceptor]
blocked: searchField.activeFocus || tc.screenName === "edit"
onCloseRequested: tc.catcherCloses++
Column {
anchors.fill: parent
Column {
visible: tc.screenName === "main"
TextField {
id: searchField
Keys.onEscapePressed: function(event) {
tc.trail += "searchField "
if (tc.screenName !== "main") { event.accepted = false; return }
if (text) text = ""
else tc.panelCloses++
}
}
}
Column {
visible: tc.screenName === "edit"
TextField { id: formField }
}
}
}
function init() {
tc.interceptorEscapes = 0
tc.catcherCloses = 0
tc.panelCloses = 0
tc.trail = ""
}
// The bug behind "Escape does nothing on the item form": on that screen the
// catcher is blocked and a field holds focus.
function test_1_escape_survives_a_blocked_catcher() {
tc.screenName = "edit"
formField.forceActiveFocus()
verify(formField.activeFocus, "the form field should hold focus")
keyClick(Qt.Key_Escape)
compare(tc.interceptorEscapes, 1, "Escape must reach the dispatch through a blocked catcher")
compare(tc.catcherCloses, 0, "a blocked catcher never fires closeRequested -- that was the bug")
compare(tc.panelCloses, 0, "and it must not close the panel")
}
// The bug behind "Escape closes the whole panel": hiding the main screen
// does not take focus off the search box, so it kept answering Escape.
function test_2_hidden_search_field_does_not_answer_escape() {
tc.screenName = "edit"
wait(0)
// Force the stale-owner state directly: focus the hidden search field
// while the form is showing. Re-homing normally prevents this, so this
// isolates the handler's own guard rather than leaning on that.
searchField.forceActiveFocus()
verify(searchField.activeFocus, "the hidden search field holds focus")
verify(!searchField.visible, "and it is hidden behind the form")
keyClick(Qt.Key_Escape)
compare(tc.panelCloses, 0, "a hidden search field must not close the panel: " + tc.trail)
compare(tc.screenName, "main", "Escape should cancel the edit instead")
}
// Re-homing focus on a screen change is what stops the stale owner
// accumulating in the first place.
function test_3_focus_follows_the_screen() {
tc.screenName = "main"
searchField.forceActiveFocus()
tc.screenName = "edit"
wait(0)
verify(formField.activeFocus, "the form field should take focus when the form opens")
verify(!searchField.activeFocus, "the hidden search field should not still hold it")
}
// Blocking exists so letters are typed rather than read as shortcuts;
// intercepting Escape must not cost that.
function test_4_typing_still_reaches_the_field() {
tc.screenName = "edit"
formField.text = ""
formField.forceActiveFocus()
keyClick(Qt.Key_J)
compare(formField.text, "j", "letters must still land in the field")
compare(tc.interceptorEscapes, 0, "no stray Escape")
}
// On unblocked screens both handlers are live; the interceptor accepting the
// event is what keeps the dispatch from running twice.
function test_5_escape_is_dispatched_once_when_unblocked() {
tc.screenName = "settings"
catcher.forceActiveFocus()
keyClick(Qt.Key_Escape)
compare(tc.interceptorEscapes, 1, "interceptor handles Escape")
compare(tc.catcherCloses, 0, "catcher must not fire once the interceptor accepted")
}
}