Possible fix for mate's window switcher.
This commit is contained in:
1 parent
eb5b2038d6
commit
e9a2dc1975
11 files changed
+472
No files matched your search
@@ -111,6 +111,9 @@ func (handler *accessibilityEventHandler) handle(event atspi.Event) {
|
||||
"selection_end", event.Source.SelectionEnd)
|
||||
}
|
||||
syncForeground()
|
||||
if handler.handle_window_switcher(event) {
|
||||
return
|
||||
}
|
||||
if handler.messaging.handle(event) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
)
|
||||
|
||||
// A window-manager switcher can leave the old client as X11 foreground while
|
||||
// Alt is held. Present its selection without replacing that client's focus,
|
||||
// review position, browser mode or native ownership. No session state is kept.
|
||||
func (handler *accessibilityEventHandler) handle_window_switcher(event atspi.Event) bool {
|
||||
if !atspi.IsWindowSwitcherSelection(event) {
|
||||
return false
|
||||
}
|
||||
if handler.focusGate.InternalUIActive() ||
|
||||
handler.foreground != nil && event.Foreground.Epoch != handler.state.foregroundState.Epoch {
|
||||
return true
|
||||
}
|
||||
if title := strings.TrimSpace(event.Source.Name); title != "" {
|
||||
// The system role also flashes the title on Braille through outputQueue.
|
||||
// Normal input interruption and FIFO speech policy still apply.
|
||||
handler.speech.Enqueue(speechRequest{Text: title, Role: "system", Cause: "window-switcher", BypassFocusGate: true})
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/braille"
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
||||
)
|
||||
|
||||
type switcherBrailleRecorder struct{ flashes chan string }
|
||||
|
||||
func (*switcherBrailleRecorder) EnqueueFrame(braille.Frame) {}
|
||||
func (display *switcherBrailleRecorder) Flash(text string, _ config.BrailleOptions) bool {
|
||||
display.flashes <- text
|
||||
return true
|
||||
}
|
||||
|
||||
func mate_switcher_event(eventType, title string, foreground atspi.ForegroundState) atspi.Event {
|
||||
event := ownerEvent(eventType, ":1.43", "/switcher", "/switcher/status")
|
||||
event.Source.ApplicationName, event.Source.ApplicationProcess = "marco", "marco"
|
||||
event.Source.Role, event.Source.Name = "status bar", title
|
||||
event.Source.StatesKnown, event.Source.Visible, event.Source.Showing = true, true, true
|
||||
event.Foreground, event.ForegroundMismatch = foreground, true
|
||||
return event
|
||||
}
|
||||
|
||||
func TestMateWindowSwitcherOutsideForeground(t *testing.T) {
|
||||
adapter := &foregroundAdapter{fakeAdapter: fakeAdapter{events: make(chan atspi.Event, 32)}, changes: make(chan struct{}, 1)}
|
||||
foreground := atspi.ForegroundState{Client: xorg.Foreground{Known: true, Window: 101}, Epoch: 1}
|
||||
adapter.state.Store(&foreground)
|
||||
queue := &fakeSpeechQueue{spoken: make(chan string, 32), requests: make(chan speechRequest, 32)}
|
||||
live := &liveSettings{}
|
||||
live.Store(config.DefaultSettings())
|
||||
display := &switcherBrailleRecorder{flashes: make(chan string, 16)}
|
||||
output := outputQueue{speech: queue, braille: display, live: live}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
_ = processEventsWithSettings(ctx, slog.New(slog.DiscardHandler), output, adapter, nil, nil, 0, live)
|
||||
}()
|
||||
defer func() { cancel(); <-done }()
|
||||
|
||||
focus := ownerEvent(atspi.FocusedStateEvent, ":1.42", "/editor", "/editor/button")
|
||||
focus.Source.Role, focus.Source.Name, focus.Source.Focused = "push button", "Save", true
|
||||
focus.Detail1, focus.Foreground = 1, foreground
|
||||
adapter.events <- focus
|
||||
expectActivationSpeech(t, queue.spoken, "Save, push button")
|
||||
<-queue.requests
|
||||
|
||||
// Replay the log's non-focused status insertion, then repeated forward
|
||||
// and reverse selection updates without an X11 foreground change.
|
||||
for _, title := range []string{"Editor", "Terminal", "Editor"} {
|
||||
adapter.events <- mate_switcher_event(atspi.TextInsertedEvent, title, foreground)
|
||||
expectActivationSpeech(t, queue.spoken, title)
|
||||
request := <-queue.requests
|
||||
if request.Cause != "window-switcher" || request.Role != "system" || !request.BypassFocusGate {
|
||||
t.Fatalf("switcher request = %+v", request)
|
||||
}
|
||||
select {
|
||||
case got := <-display.flashes:
|
||||
if got != title {
|
||||
t.Fatalf("Braille title = %q; want %q", got, title)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("missing switcher Braille message")
|
||||
}
|
||||
}
|
||||
showing := mate_switcher_event(atspi.ShowingStateEvent, "Browser", foreground)
|
||||
showing.Detail1 = 1
|
||||
adapter.events <- showing
|
||||
expectActivationSpeech(t, queue.spoken, "Browser")
|
||||
<-queue.requests
|
||||
adapter.events <- mate_switcher_event(atspi.SelectionChangedEvent, "Terminal", foreground)
|
||||
expectActivationSpeech(t, queue.spoken, "Terminal")
|
||||
<-queue.requests
|
||||
|
||||
// Duplicate name signals, hidden/destroyed switchers, stale epochs and
|
||||
// another application's status bar cannot borrow the exception.
|
||||
for _, change := range []func(*atspi.Event){
|
||||
func(e *atspi.Event) { e.Type = atspi.AccessibleNameEvent },
|
||||
func(e *atspi.Event) { e.Type, e.Detail1 = atspi.ShowingStateEvent, 0 },
|
||||
func(e *atspi.Event) { e.Source.Role = "label" },
|
||||
func(e *atspi.Event) { e.Source.StatesKnown = false },
|
||||
func(e *atspi.Event) { e.Source.Visible = false },
|
||||
func(e *atspi.Event) { e.Source.Showing = false },
|
||||
func(e *atspi.Event) { e.Source.Defunct = true },
|
||||
func(e *atspi.Event) { e.Foreground.Epoch-- },
|
||||
func(e *atspi.Event) { e.Source.ApplicationName, e.Source.ApplicationProcess = "editor", "editor" },
|
||||
func(e *atspi.Event) { e.Source.ApplicationProcess = "python3" },
|
||||
func(e *atspi.Event) { e.Window.BusName = ":1.44" },
|
||||
func(e *atspi.Event) { e.Source.Name = " " },
|
||||
} {
|
||||
event := mate_switcher_event(atspi.TextInsertedEvent, "Unwanted output", foreground)
|
||||
change(&event)
|
||||
adapter.events <- event
|
||||
}
|
||||
// A name change on the original control proves that switcher output did
|
||||
// not replace application ownership or the locus of focus. FIFO provides
|
||||
// a barrier for every rejected event above.
|
||||
focus.Type, focus.Source.Name = atspi.AccessibleNameEvent, "Still focused"
|
||||
adapter.events <- focus
|
||||
expectActivationSpeech(t, queue.spoken, "Still focused, push button")
|
||||
}
|
||||
|
||||
func TestMateWindowSwitcherTerminalHandoffAndLock(t *testing.T) {
|
||||
queue := &fakeSpeechQueue{spoken: make(chan string, 4)}
|
||||
gate := &focusGate{}
|
||||
gate.suspended.Store(true)
|
||||
handler := accessibilityEventHandler{focusGate: gate, speech: gatedSpeech{speech: queue, gate: gate}}
|
||||
event := mate_switcher_event(atspi.TextInsertedEvent, "Editor", atspi.ForegroundState{})
|
||||
if !handler.handle_window_switcher(event) {
|
||||
t.Fatal("switcher selection was not handled")
|
||||
}
|
||||
expectActivationSpeech(t, queue.spoken, "Editor")
|
||||
// The top-level accessibility handler must reject the event before the
|
||||
// special path, even though terminal-handoff output is allowed above.
|
||||
gate.secure.Store(true)
|
||||
handler.handle(event)
|
||||
if len(queue.spoken) != 0 {
|
||||
t.Fatal("switcher selection spoke during screen lock")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user