Descovered a but on Slint where if the reported sizes differ preferences would not rport. HOpefully fixed. This could probably affect more than just Slint, just happened to catch it there.

This commit is contained in:
Storm Dragon committed 2026-11-01 14:33:50 -05:00
1 parent 7591f65b58
commit d0fdce7285
9 files changed
+90

No files matched your search

+28
View File
@@ -47,3 +47,31 @@ check pitch and volume with the quick speech keys, then save and reopen the
settings. Values such as 4.5 must remain 4.5 in announcements and TOML rather
than exposing floating-point tails. Check all voice types and custom key echo;
untouched inherited values must remain unset.
## Preferences focus with differing window bounds
On Slint/i3, GTK can report a frame at (0,0), sized 1916×994, while X11
reports its client at (2,18), sized 1912×974. Rejecting that frame by geometry
silences control announcements even though Tab still moves focus. The reader
registers the PID of its own preferences helper until the helper exits. That
helper can bypass the geometry comparison when X11 identifies the same
foreground process and GTK reports an active, showing, non-iconified window.
Other applications retain the normal foreground correlation checks.
The adapter dropdown regression also checks that preferences activation and
focused-control events are accepted:
```sh
go test ./internal/atspi ./internal/app -count=1
CTHULHU_BINARY=/tmp/cthulhu-speech-preferences CTHULHU_PREFS_ADAPTER_ONLY=1 python3 tests/native_preferences.py
```
On the affected Slint desktop, use the rebuilt reader to open preferences.
Tab and Shift+Tab through controls, switch notebook pages with Ctrl+PageUp and
Ctrl+PageDown, open a dropdown, change its selection, and close it with Escape.
Confirm each control's name and value is announced. Apply a change, close and
reopen preferences, and confirm it persists. Switch to another application
while preferences remains open: speech and Braille must follow that application,
then resume preferences announcements when returning. Repeat after closing and
reopening preferences and after changing window size. Private X11 tests do not
establish physical-key, audible-speech or Braille acceptance on Slint.
+1
View File
@@ -313,6 +313,7 @@ func Run(ctx context.Context, options Options) error {
preferences.speechWorker = speechMessages
preferences.braille = brailleOutput
preferences.documentReview = objectReview
preferences.preferencesProcess, _ = adapter.(interface{ SetPreferencesProcess(int) })
objectReview.SetDocumentBrailleEnabled(settings.EnableBraille)
if updater, ok := sounds.(soundSettingsUpdater); ok {
preferences.sound = updater
+1
View File
@@ -65,6 +65,7 @@ type preferencesWorker struct {
refreshHistory func()
sound soundSettingsUpdater
geometry soundGeometryCaptureConfigurer
preferencesProcess interface{ SetPreferencesProcess(int) }
textAttributes textAttributeCaptureConfigurer
bindings interface {
SetKeyBindings(context.Context, []input.BindingOverride) error
+4
View File
@@ -69,6 +69,10 @@ func (worker *preferencesWorker) window(ctx context.Context) (result error) {
worker.live.preferencesPID.Store(int64(command.Process.Pid))
defer worker.live.preferencesPID.Store(0)
}
if worker.preferencesProcess != nil {
worker.preferencesProcess.SetPreferencesProcess(command.Process.Pid)
defer worker.preferencesProcess.SetPreferencesProcess(0)
}
defer func() {
stop()
in.Close()
+1
View File
@@ -121,6 +121,7 @@ type Adapter struct {
foregroundReader *xorg.ForegroundReader // GLib thread only
foregroundProcesses map[int]bool // GLib thread; provider PID matches for current client
foreground atomic.Pointer[ForegroundState]
preferencesPID atomic.Int64 // Owned preferences helper; zero when closed.
echoFocus atomic.Pointer[echoFocus]
eventListener *C.AtspiEventListener // GLib owner; released with subscriptions.
foregroundChanges chan struct{}
@@ -129,6 +129,14 @@ func (adapter *Adapter) matchForeground(window *C.AtspiAccessible, id ObjectID,
}
return false
}
// Tiling window managers can report GTK's frame and X11's client at
// different origins and sizes. Our own active preferences child is known
// by its launched PID; require X11 to confirm that process is foreground.
if preferences_foreground_matches(state, pid, adapter.preferencesPID.Load()) {
adapter.foregroundWindow = id
adapter.foregroundRecovery = 0
return true
}
component := C.atspi_accessible_get_component_iface(window)
if component == nil {
if check != nil {
+11
View File
@@ -26,6 +26,17 @@ type ForegroundState struct {
Epoch uint64
}
// SetPreferencesProcess scopes geometry-independent foreground matching to the
// UI child launched by this reader. Clear it when that child exits.
func (adapter *Adapter) SetPreferencesProcess(pid int) {
adapter.preferencesPID.Store(int64(pid))
}
func preferences_foreground_matches(client xorg.Foreground, pid int, preferencesPID int64) bool {
return client.Known && client.Window != 0 && preferencesPID > 0 &&
int64(pid) == preferencesPID && client.PID == pid
}
func foregroundWindowMatches(client xorg.Foreground, pid int, bounds Rectangle) bool {
if client.PID > 0 && pid > 0 && client.PID != pid {
return false
+31
View File
@@ -44,6 +44,37 @@ func TestForegroundWindowCorrelationSteamBigPictureScale(t *testing.T) {
}
}
func TestPreferencesForegroundIdentity(t *testing.T) {
// Slint/i3 reports GTK's frame and the X11 client with different borders.
client := xorg.Foreground{Known: true, Window: 45, PID: 123,
X: 2, Y: 18, Width: 1912, Height: 974, OuterX: 2, OuterY: 18, OuterWidth: 1912, OuterHeight: 974}
if foregroundWindowMatches(client, 123, Rectangle{Width: 1916, Height: 994}) {
t.Fatal("fixture must reproduce the reported bounds mismatch")
}
for _, test := range []struct {
name string
client xorg.Foreground
pid int
owned int64
want bool
}{
{"owned foreground helper", client, 123, 123, true},
{"helper closed", client, 123, 0, false},
{"another reader's helper", client, 123, 124, false},
{"other provider", client, 124, 123, false},
{"helper in background", xorg.Foreground{Known: true, Window: 46, PID: 124}, 123, 123, false},
{"missing foreground PID", xorg.Foreground{Known: true, Window: 45}, 123, 123, false},
{"empty workspace", xorg.Foreground{Known: true, PID: 123}, 123, 123, false},
{"unknown foreground", xorg.Foreground{Window: 45, PID: 123}, 123, 123, false},
} {
t.Run(test.name, func(t *testing.T) {
if got := preferences_foreground_matches(test.client, test.pid, test.owned); got != test.want {
t.Fatalf("match = %v, want %v", got, test.want)
}
})
}
}
func TestForegroundMenuPopupCorrelation(t *testing.T) {
client := xorg.Foreground{Known: true, Window: 45, PID: 123}
main := NewObjectID(":1.40", "/main")
@@ -77,6 +77,8 @@ func TestNativePreferencesDropdownEscape(t *testing.T) {
if err := command.Start(); err != nil {
t.Fatal(err)
}
adapter.SetPreferencesProcess(command.Process.Pid)
defer adapter.SetPreferencesProcess(0)
helperDone := make(chan error, 1)
go func() { helperDone <- command.Wait() }()
helperJoined := false
@@ -118,6 +120,9 @@ func TestNativePreferencesDropdownEscape(t *testing.T) {
select {
case event := <-adapter.Events():
t.Logf("%s %s %q capture=%s", event.Type, event.Source.Role, event.Source.Name, event.SnapshotDuration)
if (event.Type == WindowActivatedEvent || event.Type == FocusedStateEvent && event.Detail1 == 1) && event.ForegroundMismatch {
t.Fatalf("owned preferences focus rejected: %+v", event)
}
if event.Type == FocusedStateEvent && event.Detail1 == 1 {
// Match the running reader's browser-mode classification.
// Without these fresh ancestry queries, stale cached focus