Read attached MATE menus outside the focused panel

Validate attached panel menus through fresh showing state and selected menu ancestors, and recognize explicit GTK menu focus without a FOCUSED state. Preserve X11 foreground ownership and reject stale menu focus and actions after dismissal.

Add native ownership and speech/braille regressions covering desktop, bottom panel, top panel, editor, reopening, submenus, and impostor rejection.

Validation: targeted native race tests, AT-SPI race suite, normal app suite, required speech regression check, build, vet, and parity ledger passed. The broader app race run exceeded its three-minute limit while compressing a Tentacle fixture. golangci-lint is unavailable; live MATE speech and physical braille acceptance remain pending.
This commit is contained in:
ak47 committed 2026-11-11 23:12:33 -05:00
1 parent 8592d3be11
commit f27dec8502
15 files changed
+206 -34

No files matched your search

+27 -7
View File
@@ -33,8 +33,22 @@ showing, non-iconified, non-defunct native menu window. The existing menu
structure check permits a window with one menu child, or an unnamed frame structure check permits a window with one menu child, or an unnamed frame
with one menu-bar child. A panel application name alone grants no exception. with one menu-bar child. A panel application name alone grants no exception.
This check is used both before routine background content capture and during Attached Applications menus need a second check. GTK parents these menus under
foreground validation. It also works when X11 has no foreground client, as the menu-bar item in the top panel, which can remain inactive when the menu
opens over the desktop, bottom panel, or another application. Require a freshly
showing menu source, selected menu ancestors through the menu bar, a showing
non-iconified/non-defunct host, and the verified panel process for both source
and host. Ordinary host controls cannot use this exception. Panel menu items
also cannot borrow another panel's foreground through the generic same-process
focused-item shortcut after the menu closes.
GTK menu containers can emit focus-gained without exposing FOCUSED in their
state set. Normalize that explicit signal just as for native menu items, so
the event loop can follow the accepted menu into its panel window without a
separate activation. Foreground validation still precedes presentation.
These checks are used both before routine background content capture and during
foreground validation. They also work when X11 has no foreground client, as
when a panel menu opens over an empty desktop. Ordinary panel windows and when a panel menu opens over an empty desktop. Ordinary panel windows and
hidden menu items retain normal filtering. Native actions and review use the hidden menu items retain normal filtering. Native actions and review use the
same window validation. The correlated X11 application is preserved; no popup same window validation. The correlated X11 application is preserved; no popup
@@ -69,14 +83,18 @@ python3 tests/native_test_session.py env CTHULHU_NATIVE_PANEL_APPLET=1 go test -
go test ./internal/app -run '^TestNotificationProvidersAndHistoryMatchReferenceBehavior$' -count=1 go test ./internal/app -run '^TestNotificationProvidersAndHistoryMatchReferenceBehavior$' -count=1
``` ```
The private GTK fixture uses separate panel and editor providers. Native The private GTK fixture uses separate panel and editor providers, plus two
checks cover menu activation over an editor and an empty desktop, forward and panel windows on the same provider. Native checks cover detached and attached
reverse navigation, a submenu, Escape, editor restoration, rejection of an menus over an editor, an empty desktop and the bottom panel, forward and
reverse navigation, a submenu, Escape, editor restoration, accessible action
validation while open and rejection after dismissal, rejection of an
ordinary panel window, a late focus signal from a hidden menu, and an unrelated ordinary panel window, a late focus signal from a hidden menu, and an unrelated
process claiming the panel's application name. Panel icon captures verify process claiming the panel's application name. Panel icon captures verify
inherited labels, own-name/description precedence and unrelated-container inherited labels, own-name/description precedence and unrelated-container
rejection. The event-loop check verifies queued speech and Braille frames for rejection. The event-loop check verifies queued speech and Braille frames for
navigation, dismissal, editor return and reopening. It emits no audible speech. navigation, dismissal, editor return and reopening. Attached-menu presentation
is checked with initial focus in the editor, desktop, bottom panel and top
panel. It emits no audible speech.
The socket/plug fixture uses separate host, applet and editor processes. It The socket/plug fixture uses separate host, applet and editor processes. It
checks the shared containing-window identity, preserved applet identity, icon checks the shared containing-window identity, preserved applet identity, icon
@@ -109,7 +127,9 @@ Review the diff before submission, as required by the contribution policy.
The fixtures model the native contracts; they do not run the reporter's Fedora The fixtures model the native contracts; they do not run the reporter's Fedora
MATE session. After starting the candidate through the normal launcher: MATE session. After starting the candidate through the normal launcher:
1. Open the Applications menu over a browser, a terminal and the desktop. 1. Open the Applications menu with Alt+F1 over a browser, a terminal and the
desktop. Repeat after focusing the bottom panel and then the top panel;
opening announcements and navigation must work from each starting point.
Navigate forward/backward, enter/leave submenus, and check spoken labels Navigate forward/backward, enter/leave submenus, and check spoken labels
and Braille if available. Repeat with Places/System menus and panel context and Braille if available. Repeat with Places/System menus and panel context
menus offered by the installed MATE configuration. menus offered by the installed MATE configuration.
+50 -2
View File
@@ -24,6 +24,24 @@ func TestNativeMatePanelMenuPresentation(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_PANEL_MENU") != "1" { if os.Getenv("CTHULHU_NATIVE_PANEL_MENU") != "1" {
t.Skip("requires private Xorg/AT-SPI session") t.Skip("requires private Xorg/AT-SPI session")
} }
for _, test := range []struct {
name, priorFocus string
attached bool
}{
{"detached", "editor", false},
{"attached over editor", "editor", true},
{"attached over desktop", "desktop", true},
{"attached over bottom panel", "bottom", true},
{"attached over top panel", "top", true},
} {
t.Run(test.name, func(t *testing.T) {
checkNativePanelMenuPresentation(t, test.attached, test.priorFocus)
})
}
}
func checkNativePanelMenuPresentation(t *testing.T, attached bool, priorFocus string) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() defer cancel()
adapter := atspi.New() adapter := atspi.New()
@@ -33,7 +51,11 @@ func TestNativeMatePanelMenuPresentation(t *testing.T) {
defer adapter.Stop(context.Background()) defer adapter.Stop(context.Background())
start := func(name string) (io.WriteCloser, string) { start := func(name string) (io.WriteCloser, string) {
t.Helper() t.Helper()
command := exec.CommandContext(ctx, "/usr/bin/python3", "../../tests/gtk_panel_menu.py", name) args := []string{"../../tests/gtk_panel_menu.py", name}
if attached && name != "editor" {
args = append(args, "--attached")
}
command := exec.CommandContext(ctx, "/usr/bin/python3", args...)
input, err := command.StdinPipe() input, err := command.StdinPipe()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -54,7 +76,7 @@ func TestNativeMatePanelMenuPresentation(t *testing.T) {
} }
return input, fields[1] return input, fields[1]
} }
panel, _ := start("mate-panel") panel, panelXID := start("mate-panel")
editor, xid := start("editor") editor, xid := start("editor")
key := func(args ...string) { key := func(args ...string) {
t.Helper() t.Helper()
@@ -92,6 +114,29 @@ func TestNativeMatePanelMenuPresentation(t *testing.T) {
fmt.Fprintln(editor, "edit Foreground ready") fmt.Fprintln(editor, "edit Foreground ready")
wait("Foreground ready") wait("Foreground ready")
for iteration := 0; iteration < 2; iteration++ { for iteration := 0; iteration < 2; iteration++ {
switch priorFocus {
case "desktop":
key("windowfocus", "0")
for adapter.Foreground().Client.Window != 0 {
select {
case <-adapter.ForegroundChanges():
case <-ctx.Done():
t.Fatal("empty desktop foreground missing")
}
}
case "bottom":
output, err := exec.CommandContext(ctx, "xdotool", "search", "--name", "^Bottom panel fixture mate-panel$").Output()
if err != nil {
t.Fatal(err)
}
key("windowfocus", "--sync", strings.TrimSpace(string(output)))
fmt.Fprintln(panel, "bottom")
wait("Bottom panel ready")
case "top":
key("windowfocus", "--sync", panelXID)
fmt.Fprintf(panel, "edit Top panel ready %d\n", iteration)
wait("Top panel ready")
}
fmt.Fprintln(panel, "popup") fmt.Fprintln(panel, "popup")
wait("menu") wait("menu")
// The initial bare menu may have no label. Navigation must acquire // The initial bare menu may have no label. Navigation must acquire
@@ -104,6 +149,9 @@ func TestNativeMatePanelMenuPresentation(t *testing.T) {
wait(step.name) wait(step.name)
} }
key("key", "Escape") key("key", "Escape")
if priorFocus != "editor" {
key("windowfocus", "--sync", xid)
}
text := fmt.Sprintf("Returned to editor %d", iteration) text := fmt.Sprintf("Returned to editor %d", iteration)
fmt.Fprintln(editor, "edit "+text) fmt.Fprintln(editor, "edit "+text)
wait(text) wait(text)
+1 -1
View File
@@ -70,7 +70,7 @@ func (adapter *Adapter) native_action_source(object ObjectID) (*C.AtspiAccessibl
defer C.g_object_unref(C.gpointer(window)) defer C.g_object_unref(C.gpointer(window))
adapter.refreshForeground() adapter.refreshForeground()
windowID := nativeObjectID(window) windowID := nativeObjectID(window)
if !adapter.matchesForeground(window, windowID) { if !adapter.matchesForeground(source, window, windowID) {
return nil, ActionTarget{}, ErrStaleObject return nil, ActionTarget{}, ErrStaleObject
} }
role := C.atspi_accessible_get_role_name(source, nil) role := C.atspi_accessible_get_role_name(source, nil)
+1 -1
View File
@@ -220,7 +220,7 @@ func (adapter *Adapter) activateNativeDocumentObject(ctx context.Context, positi
if window == nil { if window == nil {
return ErrStaleObject return ErrStaleObject
} }
foreground := adapter.matchesForeground(window, nativeObjectID(window)) foreground := adapter.matchesForeground(source, window, nativeObjectID(window))
C.g_object_unref(C.gpointer(window)) C.g_object_unref(C.gpointer(window))
if !foreground { if !foreground {
return ErrStaleObject return ErrStaleObject
@@ -287,9 +287,9 @@ func (adapter *Adapter) enrichNativeWindowEvent(event *C.AtspiEvent, target *C.A
focusedMenuItem := (eventType == FocusEvent || eventType == FocusedStateEvent) && focusedMenuItem := (eventType == FocusEvent || eventType == FocusedStateEvent) &&
(snapshot.Role == "menu item" || snapshot.Role == "check menu item" || snapshot.Role == "radio menu item") && snapshot.Focused (snapshot.Role == "menu item" || snapshot.Role == "check menu item" || snapshot.Role == "radio menu item") && snapshot.Focused
if eventType == WindowActivatedEvent { if eventType == WindowActivatedEvent {
focusEvent.ForegroundMismatch = !adapter.matchForeground(window, focusEvent.Window, &focusEvent.ForegroundCheck, false) focusEvent.ForegroundMismatch = !adapter.matchForeground(target, window, focusEvent.Window, &focusEvent.ForegroundCheck, false)
} else { } else {
focusEvent.ForegroundMismatch = !adapter.matchForeground(window, focusEvent.Window, nil, focusedMenuItem) focusEvent.ForegroundMismatch = !adapter.matchForeground(target, window, focusEvent.Window, nil, focusedMenuItem)
} }
if !focusEvent.ForegroundMismatch && window != nil && if !focusEvent.ForegroundMismatch && window != nil &&
(eventType == WindowActivatedEvent || snapshot.Role == "table" || snapshot.Role == "table cell") { (eventType == WindowActivatedEvent || snapshot.Role == "table" || snapshot.Role == "table cell") {
@@ -58,7 +58,7 @@ func (adapter *Adapter) filterNativeBackgroundEvent(event *C.AtspiEvent, eventTy
focusedMenuItem = true focusedMenuItem = true
} }
} }
if adapter.matchForeground(window, windowID, nil, focusedMenuItem) { if adapter.matchForeground(event.source, window, windowID, nil, focusedMenuItem) {
return false return false
} }
if origin.BusName == adapter.foregroundWindow.BusName { if origin.BusName == adapter.foregroundWindow.BusName {
@@ -117,11 +117,12 @@ func normalizeNativeMenuFocus(event *C.AtspiEvent, target *C.AtspiAccessible, ev
// Shared Qt actions can report focus through their original, hidden menu // Shared Qt actions can report focus through their original, hidden menu
// even while another menu emits focus-gained for that very accessible. // even while another menu emits focus-gained for that very accessible.
// The explicit event establishes focus; the queried state may disagree. // The explicit event establishes focus; the queried state may disagree.
// GTK menu containers also emit focus-gained without a FOCUSED state.
// Do not transfer this evidence to a redirected descendant or a dead item. // Do not transfer this evidence to a redirected descendant or a dead item.
if snapshot.ID() == originID && !snapshot.Defunct && if snapshot.ID() == originID && !snapshot.Defunct &&
(eventType == FocusEvent || eventType == FocusedStateEvent && C.cthulhu_event_detail1(event) == 1) { (eventType == FocusEvent || eventType == FocusedStateEvent && C.cthulhu_event_detail1(event) == 1) {
switch snapshot.Role { switch snapshot.Role {
case "menu item", "check menu item", "radio menu item": case "menu", "menu item", "check menu item", "radio menu item":
snapshot.Focused = true snapshot.Focused = true
} }
} }
+39
View File
@@ -605,6 +605,45 @@ gboolean cthulhu_active_menu_window(AtspiAccessible *window) {
return menu; return menu;
} }
// GTK attaches panel menus to their menu-bar item, so their accessible parent
// chain reaches the inactive panel rather than a separate popup window. Require
// a showing menu source and selected menu ancestors up to the menu bar. A panel
// window, ordinary control, closed menu, or hidden submenu grants no exception.
gboolean cthulhu_showing_attached_menu(AtspiAccessible *source, AtspiAccessible *window) {
if (source == NULL || window == NULL) return FALSE;
atspi_accessible_clear_cache_single(window);
AtspiStateSet *states = atspi_accessible_get_state_set(window);
gboolean available = states != NULL && atspi_state_set_contains(states, ATSPI_STATE_SHOWING) &&
!atspi_state_set_contains(states, ATSPI_STATE_ICONIFIED) &&
!atspi_state_set_contains(states, ATSPI_STATE_DEFUNCT);
if (states != NULL) g_object_unref(states);
if (!available) return FALSE;
AtspiAccessible *current = g_object_ref(source);
for (int depth = 0; current != NULL && depth < 32; depth++) {
atspi_accessible_clear_cache_single(current);
AtspiRole role = atspi_accessible_get_role(current, NULL);
if (role != ATSPI_ROLE_MENU && role != ATSPI_ROLE_MENU_ITEM &&
role != ATSPI_ROLE_CHECK_MENU_ITEM && role != ATSPI_ROLE_RADIO_MENU_ITEM) break;
states = atspi_accessible_get_state_set(current);
gboolean showing = states != NULL && atspi_state_set_contains(states, ATSPI_STATE_SHOWING) &&
!atspi_state_set_contains(states, ATSPI_STATE_DEFUNCT);
gboolean selected = states != NULL && atspi_state_set_contains(states, ATSPI_STATE_SELECTED);
if (states != NULL) g_object_unref(states);
if (!showing || (depth > 0 && !selected)) break;
AtspiAccessible *parent = atspi_accessible_get_parent(current, NULL);
if (parent == NULL) break;
if (atspi_accessible_get_role(parent, NULL) == ATSPI_ROLE_MENU_BAR) {
g_object_unref(parent);
g_object_unref(current);
return selected;
}
g_object_unref(current);
current = parent;
}
if (current != NULL) g_object_unref(current);
return FALSE;
}
static gboolean cthulhu_is_menu(AtspiAccessible *source) { static gboolean cthulhu_is_menu(AtspiAccessible *source) {
return atspi_accessible_get_role(source, NULL) == ATSPI_ROLE_MENU; return atspi_accessible_get_role(source, NULL) == ATSPI_ROLE_MENU;
} }
+16 -7
View File
@@ -62,11 +62,11 @@ func cthulhuAtspiForegroundCallback(data unsafe.Pointer) {
adapter.recoverForeground() adapter.recoverForeground()
} }
func (adapter *Adapter) matchesForeground(window *C.AtspiAccessible, id ObjectID) bool { func (adapter *Adapter) matchesForeground(source, window *C.AtspiAccessible, id ObjectID) bool {
return adapter.matchForeground(window, id, nil, false) return adapter.matchForeground(source, window, id, nil, false)
} }
func (adapter *Adapter) matchForeground(window *C.AtspiAccessible, id ObjectID, check *ForegroundCheck, focusedMenuItem bool) bool { func (adapter *Adapter) matchForeground(source, window *C.AtspiAccessible, id ObjectID, check *ForegroundCheck, focusedMenuItem bool) bool {
state := adapter.Foreground().Client state := adapter.Foreground().Client
if !state.Known { if !state.Known {
return true return true
@@ -85,7 +85,7 @@ func (adapter *Adapter) matchForeground(window *C.AtspiAccessible, id ObjectID,
// Desktop panel menus can grab the keyboard while X11 still names the // Desktop panel menus can grab the keyboard while X11 still names the
// previous application. Orca accepts their active, showing menu window. // previous application. Orca accepts their active, showing menu window.
// Keep the correlated X11 owner so dismissal can return to that client. // Keep the correlated X11 owner so dismissal can return to that client.
if adapter.is_panel_menu(window) { if adapter.is_panel_menu(source, window) {
return true return true
} }
if state.Window == 0 { if state.Window == 0 {
@@ -98,6 +98,12 @@ func (adapter *Adapter) matchForeground(window *C.AtspiAccessible, id ObjectID,
if adapter.foregroundWindow == id { if adapter.foregroundWindow == id {
return true return true
} }
// Panel menus must pass the fresh popup/attached-menu checks above.
// A delayed focused-item signal from a closed menu must not borrow
// another panel's same-process foreground ownership.
if adapter.applicationProcess(window) == "mate-panel" {
focusedMenuItem = false
}
matched := foregroundMenuPopupMatches(state, adapter.foregroundWindow, id, pid, matched := foregroundMenuPopupMatches(state, adapter.foregroundWindow, id, pid,
focusedMenuItem || C.cthulhu_active_menu_window(window) != 0) focusedMenuItem || C.cthulhu_active_menu_window(window) != 0)
if matched { if matched {
@@ -188,12 +194,15 @@ func (adapter *Adapter) matchForeground(window *C.AtspiAccessible, id ObjectID,
// The exception is for a native menu from the panel process, never an // The exception is for a native menu from the panel process, never an
// arbitrary panel control or a provider claiming the panel's accessible name. // arbitrary panel control or a provider claiming the panel's accessible name.
// Query fresh window state on the GLib owner; no popup state is retained. // Query fresh window state on the GLib owner; no popup state is retained.
func (adapter *Adapter) is_panel_menu(window *C.AtspiAccessible) bool { func (adapter *Adapter) is_panel_menu(source, window *C.AtspiAccessible) bool {
if adapter.applicationProcess(window) != "mate-panel" { if adapter.applicationProcess(window) != "mate-panel" {
return false return false
} }
C.atspi_accessible_clear_cache_single(window) C.atspi_accessible_clear_cache_single(window)
return C.cthulhu_active_menu_window(window) != 0 if C.cthulhu_active_menu_window(window) != 0 {
return true
}
return adapter.applicationProcess(source) == "mate-panel" && C.cthulhu_showing_attached_menu(source, window) != 0
} }
func (adapter *Adapter) replacesForegroundWindow(window *C.AtspiAccessible, id ObjectID, foregroundPID, pid int) bool { func (adapter *Adapter) replacesForegroundWindow(window *C.AtspiAccessible, id ObjectID, foregroundPID, pid int) bool {
@@ -310,7 +319,7 @@ func (adapter *Adapter) recoverForeground() {
if window == nil { if window == nil {
continue continue
} }
if state.Epoch == adapter.Foreground().Epoch && adapter.matchesForeground(window, nativeObjectID(window)) { if state.Epoch == adapter.Foreground().Epoch && adapter.matchesForeground(window, window, nativeObjectID(window)) {
target := adapter.libreOfficeFrameTarget(window) target := adapter.libreOfficeFrameTarget(window)
officeFrame := target != nil officeFrame := target != nil
if target == nil { if target == nil {
+1
View File
@@ -140,6 +140,7 @@ gboolean cthulhu_active_document(AtspiAccessible *source);
gboolean cthulhu_in_active_document(AtspiAccessible *source); gboolean cthulhu_in_active_document(AtspiAccessible *source);
gboolean cthulhu_in_inactive_window(AtspiAccessible *source); gboolean cthulhu_in_inactive_window(AtspiAccessible *source);
gboolean cthulhu_active_menu_window(AtspiAccessible *window); gboolean cthulhu_active_menu_window(AtspiAccessible *window);
gboolean cthulhu_showing_attached_menu(AtspiAccessible *source, AtspiAccessible *window);
AtspiAccessible *cthulhu_popup_menu_target(AtspiAccessible *window); AtspiAccessible *cthulhu_popup_menu_target(AtspiAccessible *window);
AtspiAccessible *cthulhu_inline_menu_container(AtspiAccessible *source); AtspiAccessible *cthulhu_inline_menu_container(AtspiAccessible *source);
const gchar *cthulhu_accessible_path(AtspiAccessible *source); const gchar *cthulhu_accessible_path(AtspiAccessible *source);
+1 -1
View File
@@ -108,7 +108,7 @@ func (adapter *Adapter) route_document_braille(ctx context.Context, route Braill
if window == nil { if window == nil {
return ErrStaleObject return ErrStaleObject
} }
foreground := adapter.matchesForeground(window, nativeObjectID(window)) foreground := adapter.matchesForeground(source, window, nativeObjectID(window))
C.g_object_unref(C.gpointer(window)) C.g_object_unref(C.gpointer(window))
if !foreground { if !foreground {
return ErrStaleObject return ErrStaleObject
+46 -9
View File
@@ -23,16 +23,24 @@ func TestNativeMatePanelMenuOutsideForeground(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
name, processName string name, processName string
empty bool empty bool
attached bool
bottom bool
}{ }{
{"over editor", "mate-panel", false}, {"over editor", "mate-panel", false, false, false},
{"empty desktop", "mate-panel", true}, {"empty desktop", "mate-panel", true, false, false},
{"impostor", "impostor", false}, {"impostor", "impostor", false, false, false},
{"attached over editor", "mate-panel", false, true, false},
{"attached empty desktop", "mate-panel", true, true, false},
{"attached over bottom panel", "mate-panel", false, true, true},
{"attached impostor", "impostor", false, true, false},
} { } {
t.Run(test.name, func(t *testing.T) { check_native_panel_menu(t, test.processName, test.empty) }) t.Run(test.name, func(t *testing.T) {
check_native_panel_menu(t, test.processName, test.empty, test.attached, test.bottom)
})
} }
} }
func check_native_panel_menu(t *testing.T, processName string, empty bool) { func check_native_panel_menu(t *testing.T, processName string, empty, attached, bottom bool) {
t.Helper() t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel() defer cancel()
@@ -43,7 +51,11 @@ func check_native_panel_menu(t *testing.T, processName string, empty bool) {
defer adapter.Stop(context.Background()) defer adapter.Stop(context.Background())
start := func(name string) (io.WriteCloser, string) { start := func(name string) (io.WriteCloser, string) {
t.Helper() t.Helper()
command := exec.CommandContext(ctx, "/usr/bin/python3", "../../tests/gtk_panel_menu.py", name) args := []string{"../../tests/gtk_panel_menu.py", name}
if attached && name != "editor" {
args = append(args, "--attached")
}
command := exec.CommandContext(ctx, "/usr/bin/python3", args...)
input, err := command.StdinPipe() input, err := command.StdinPipe()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -95,6 +107,16 @@ func check_native_panel_menu(t *testing.T, processName string, empty bool) {
} }
fmt.Fprintln(editor, "edit Foreground ready") fmt.Fprintln(editor, "edit Foreground ready")
owned := wait(func(event Event) bool { return event.Payload == "Foreground ready" && !event.ForegroundMismatch }) owned := wait(func(event Event) bool { return event.Payload == "Foreground ready" && !event.ForegroundMismatch })
editorOwned := owned
if bottom {
output, err := exec.CommandContext(ctx, "xdotool", "search", "--name", "^Bottom panel fixture mate-panel$").Output()
if err != nil {
t.Fatal(err)
}
key("windowfocus", "--sync", strings.TrimSpace(string(output)))
fmt.Fprintln(panel, "bottom")
owned = wait(func(event Event) bool { return event.Payload == "Bottom panel ready" && !event.ForegroundMismatch })
}
if empty { if empty {
key("windowfocus", "0") key("windowfocus", "0")
for adapter.Foreground().Client.Window != 0 { for adapter.Foreground().Client.Window != 0 {
@@ -108,6 +130,10 @@ func check_native_panel_menu(t *testing.T, processName string, empty bool) {
} }
fmt.Fprintln(panel, "popup") fmt.Fprintln(panel, "popup")
activation := wait(func(event Event) bool { activation := wait(func(event Event) bool {
if attached && processName == "mate-panel" {
return event.Source.Role == "menu" && event.Source.ApplicationProcess == "mate-panel" &&
(event.Type == FocusEvent || event.Type == FocusedStateEvent && event.Detail1 == 1)
}
return event.Type == WindowActivatedEvent && event.Source.ApplicationName == "mate-panel" return event.Type == WindowActivatedEvent && event.Source.ApplicationName == "mate-panel"
}) })
if processName != "mate-panel" { if processName != "mate-panel" {
@@ -120,6 +146,7 @@ func check_native_panel_menu(t *testing.T, processName string, empty bool) {
if activation.ForegroundMismatch || activation.Foreground != owned.Foreground { if activation.ForegroundMismatch || activation.Foreground != owned.Foreground {
t.Fatalf("panel popup rejected with unchanged X11 foreground: %+v", activation) t.Fatalf("panel popup rejected with unchanged X11 foreground: %+v", activation)
} }
var firstItem ObjectID
for _, step := range []struct{ key, name string }{ for _, step := range []struct{ key, name string }{
{"Down", "First application"}, {"Down", "Utilities"}, {"Right", "Example utility"}, {"Down", "First application"}, {"Down", "Utilities"}, {"Right", "Example utility"},
{"Left", "Utilities"}, {"Down", "Last application"}, {"Up", "Utilities"}, {"Left", "Utilities"}, {"Down", "Last application"}, {"Up", "Utilities"},
@@ -131,15 +158,25 @@ func check_native_panel_menu(t *testing.T, processName string, empty bool) {
if event.ForegroundMismatch || event.Foreground != owned.Foreground || event.Source.ApplicationProcess != "mate-panel" { if event.ForegroundMismatch || event.Foreground != owned.Foreground || event.Source.ApplicationProcess != "mate-panel" {
t.Fatalf("panel navigation lost ownership: %+v", event) t.Fatalf("panel navigation lost ownership: %+v", event)
} }
if step.name == "First application" {
firstItem = event.Source.ID()
actions, err := adapter.AccessibleActions(ctx, firstItem)
if err != nil || actions.Target.Window != event.Window || len(actions.Actions) == 0 {
t.Fatalf("open menu actions lost ownership: %+v, %v", actions, err)
}
}
} }
key("key", "Escape") key("key", "Escape")
fmt.Fprintln(panel, "ordinary") fmt.Fprintln(panel, "ordinary")
ordinary := wait(func(event Event) bool { ordinary := wait(func(event Event) bool {
return event.Type == WindowActivatedEvent && event.Source.ApplicationName == "mate-panel" return event.Type == WindowActivatedEvent && event.WindowTitle == "Panel menu fixture "+processName
}) })
if !ordinary.ForegroundMismatch { if !ordinary.ForegroundMismatch {
t.Fatalf("ordinary panel window borrowed menu exception: %+v", ordinary) t.Fatalf("ordinary panel window borrowed menu exception: %+v", ordinary)
} }
if _, err := adapter.AccessibleActions(ctx, firstItem); err == nil {
t.Fatal("closed menu retained accessible action ownership")
}
fmt.Fprintln(panel, "stale") fmt.Fprintln(panel, "stale")
wait(func(event Event) bool { wait(func(event Event) bool {
if event.Source.Name == "First application" && (event.Type == FocusEvent || event.Type == FocusedStateEvent) { if event.Source.Name == "First application" && (event.Type == FocusEvent || event.Type == FocusedStateEvent) {
@@ -147,12 +184,12 @@ func check_native_panel_menu(t *testing.T, processName string, empty bool) {
} }
return event.Payload == "Stale barrier" return event.Payload == "Stale barrier"
}) })
if empty { if empty || bottom {
key("windowfocus", "--sync", xid) key("windowfocus", "--sync", xid)
} }
fmt.Fprintln(editor, "edit Foreground restored") fmt.Fprintln(editor, "edit Foreground restored")
restored := wait(func(event Event) bool { return event.Payload == "Foreground restored" && !event.ForegroundMismatch }) restored := wait(func(event Event) bool { return event.Payload == "Foreground restored" && !event.ForegroundMismatch })
if restored.Window != owned.Window || !empty && restored.Foreground != owned.Foreground { if restored.Window != editorOwned.Window || !empty && !bottom && restored.Foreground != editorOwned.Foreground {
t.Fatalf("panel dismissal changed editor ownership: %+v", restored) t.Fatalf("panel dismissal changed editor ownership: %+v", restored)
} }
} }
+1 -1
View File
@@ -41,7 +41,7 @@ func TestNativeWaylandForegroundPolicy(t *testing.T) {
if adapter.Foreground().Client.Known { if adapter.Foreground().Client.Known {
t.Fatal("X11 foreground used as Wayland desktop authority") t.Fatal("X11 foreground used as Wayland desktop authority")
} }
if !adapter.matchesForeground(nil, ObjectID{}) { if !adapter.matchesForeground(nil, nil, ObjectID{}) {
t.Fatal("missing X11 foreground rejected Wayland accessibility") t.Fatal("missing X11 foreground rejected Wayland accessibility")
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
id,python_source,subsystem,behavior,owner,go_destination,status,automated_evidence,x11_evidence,wayland_evidence,speech_evidence,braille_evidence,notes id,python_source,subsystem,behavior,owner,go_destination,status,automated_evidence,x11_evidence,wayland_evidence,speech_evidence,braille_evidence,notes
embedded-panel-applet,Orca src/orca/script_utilities.py:frame_and_dialog;top_level_object;src/orca/scripts/default.py:_on_focused_changed,focus,Follow embedded GTK applet focus through the socket host window while preserving the source provider,,internal/atspi internal/app,in-progress,Private native regression reproduces rejection before the fix; socket and plug tests cover MATE and ordinary hosts; see docs/testing/mate-desktop.md,Separate host/applet/editor providers cover icon labels; Tab and Shift+Tab; editor return; background and unrelated providers; socket detachment.,,Queued speech verified; real MATE listening pending.,Native event-loop frames verified; physical Braille acceptance pending.,Refresh parent links through embedded frames before normal X11 host validation. No process-name bypass or retained embedding state. Human review and live panel traversal required. embedded-panel-applet,Orca src/orca/script_utilities.py:frame_and_dialog;top_level_object;src/orca/scripts/default.py:_on_focused_changed,focus,Follow embedded GTK applet focus through the socket host window while preserving the source provider,,internal/atspi internal/app,in-progress,Private native regression reproduces rejection before the fix; socket and plug tests cover MATE and ordinary hosts; see docs/testing/mate-desktop.md,Separate host/applet/editor providers cover icon labels; Tab and Shift+Tab; editor return; background and unrelated providers; socket detachment.,,Queued speech verified; real MATE listening pending.,Native event-loop frames verified; physical Braille acceptance pending.,Refresh parent links through embedded frames before normal X11 host validation. No process-name bypass or retained embedding state. Human review and live panel traversal required.
submission-clear-deletion,src/cthulhu/scripts/default.py:onTextDeleted,speech,Avoid announcing an entry clear after plain Enter before the submitted chat message,,internal/app/submitted_text.go;internal/input/intent.go,in-progress,Submission replay fails against the original presenter and passes with the fix; editing regression matrix and focused race checks pass.,Private GTK submission and selected-text replacement checks use native snapshots and the real X11 input listener.,,Speech requests verified; live Pidgin listening pending.,Braille update path retained; physical display acceptance pending.,Requires focused text or entry plus fresh plain Enter and known empty line at offset zero. Selection replacement and modified Enter retain existing feedback. No Thunderbird or word-echo changes. See docs/testing/messaging.md. submission-clear-deletion,src/cthulhu/scripts/default.py:onTextDeleted,speech,Avoid announcing an entry clear after plain Enter before the submitted chat message,,internal/app/submitted_text.go;internal/input/intent.go,in-progress,Submission replay fails against the original presenter and passes with the fix; editing regression matrix and focused race checks pass.,Private GTK submission and selected-text replacement checks use native snapshots and the real X11 input listener.,,Speech requests verified; live Pidgin listening pending.,Braille update path retained; physical display acceptance pending.,Requires focused text or entry plus fresh plain Enter and known empty line at offset zero. Selection replacement and modified Enter retain existing feedback. No Thunderbird or word-echo changes. See docs/testing/messaging.md.
mate-panel-menu,Orca src/orca/scripts/default.py;src/orca/ax_utilities.py;src/orca/generator.py,focus,Read active MATE panel menus across X11 application boundaries and label unnamed panel icons,,internal/atspi internal/app,in-progress,Private native and event-loop regressions; see docs/testing/mate-desktop.md,Separate GTK providers cover editor and empty desktop menus; submenus; reverse navigation; Escape; hidden/impostor rejection; icon label precedence.,,Queued presentation verified; actual Fedora MATE listening pending.,Native event-loop frames verified; physical display acceptance pending.,Fresh active/showing native menu structure and verified mate-panel process only. Preserves X11 correlation without retained popup state. Existing Marco and notification paths retained. Human review and live MATE acceptance required. mate-panel-menu,Orca src/orca/scripts/default.py;src/orca/ax_utilities.py;src/orca/generator.py,focus,Read active MATE panel menus across X11 application boundaries and label unnamed panel icons,,internal/atspi internal/app,in-progress,Private native and event-loop regressions; see docs/testing/mate-desktop.md,Separate GTK providers and two panel windows cover detached and attached menus over editor and desktop; bottom/top panel presentation; submenus; reverse navigation; Escape; stale focus/action and impostor rejection; icon label precedence.,,Queued presentation verified; actual Fedora MATE listening pending.,Native event-loop frames verified; physical display acceptance pending.,Fresh active/showing popup structure or showing attached menu with selected ancestors and verified mate-panel process only. Explicit menu focus normalized before event-loop ownership. Preserves X11 correlation without retained popup state. Existing Marco and notification paths retained. Human review and live MATE acceptance required.
mate-window-switcher,Orca src/orca/scripts/switcher/script.py;src/orca/scripts/switcher/script_utilities.py,focus,Present Marco switcher selections while the old application retains X11 foreground,,internal/atspi internal/app,in-progress,go test ./internal/app -run '^TestMateWindowSwitcher' -count=1; see docs/testing/window-switcher.md,Private GTK provider checks repeated insertions and showing events outside foreground without taking ownership.,,Actual Marco listening and physical key acceptance pending.,Transient flash requests tested; physical display acceptance pending.,Verified Marco process and application plus visible status-bar container only. Preserves application focus and review. No name-signal duplication or general background bypass. Original log lacks later cycling insertions; real cycling and Escape remain live gates. mate-window-switcher,Orca src/orca/scripts/switcher/script.py;src/orca/scripts/switcher/script_utilities.py,focus,Present Marco switcher selections while the old application retains X11 foreground,,internal/atspi internal/app,in-progress,go test ./internal/app -run '^TestMateWindowSwitcher' -count=1; see docs/testing/window-switcher.md,Private GTK provider checks repeated insertions and showing events outside foreground without taking ownership.,,Actual Marco listening and physical key acceptance pending.,Transient flash requests tested; physical display acceptance pending.,Verified Marco process and application plus visible status-bar container only. Preserves application focus and review. No name-signal duplication or general background bypass. Original log lacks later cycling insertions; real cycling and Escape remain live gates.
gtk-alert-message,src/cthulhu/speech_generator.py;src/cthulhu/script_utilities.py,speech,Read a GTK alert message before its first focused button,,internal/atspi internal/app,in-progress,go test ./internal/app ./internal/atspi -count=1; python3 tests/native_alert.py,Private GTK/Xvfb/AT-SPI fixture captured the complete question on 2026-09-26.,,Physical FastSMRW listening pending.,,Alert window creation captures its label text; activation does not interrupt the message; the first button appends and later buttons use ordinary focus speech. gtk-alert-message,src/cthulhu/speech_generator.py;src/cthulhu/script_utilities.py,speech,Read a GTK alert message before its first focused button,,internal/atspi internal/app,in-progress,go test ./internal/app ./internal/atspi -count=1; python3 tests/native_alert.py,Private GTK/Xvfb/AT-SPI fixture captured the complete question on 2026-09-26.,,Physical FastSMRW listening pending.,,Alert window creation captures its label text; activation does not interrupt the message; the first button appends and later buttons use ordinary focus speech.
yad-simple-prompt,src/cthulhu/formatting.py;src/cthulhu/scripts/default.py,speech,Read a simple YAD frame question before its first focused button,,internal/atspi internal/app,implemented,go test ./internal/app ./internal/atspi -count=1; python3 tests/native_alert.py,Private YAD/Xvfb/AT-SPI fixture captured the exact question on frame activation or first button focus. The live 2026-09-27 log showed a third path where activation targets the focused button; an event-loop regression covers its speech order.,,Maintainer confirmed the YAD question was spoken on the updated build on 2026-09-27.,,YAD exposes the prompt as a frame with layout nodes and a label plus buttons. The generic Python frame format appears to omit the label; this Go addition follows the user's explicit example. A strict simple prompt shape limits frame text capture and bounded first-button refresh handles late labels. yad-simple-prompt,src/cthulhu/formatting.py;src/cthulhu/scripts/default.py,speech,Read a simple YAD frame question before its first focused button,,internal/atspi internal/app,implemented,go test ./internal/app ./internal/atspi -count=1; python3 tests/native_alert.py,Private YAD/Xvfb/AT-SPI fixture captured the exact question on frame activation or first button focus. The live 2026-09-27 log showed a third path where activation targets the focused button; an event-loop regression covers its speech order.,,Maintainer confirmed the YAD question was spoken on the updated build on 2026-09-27.,,YAD exposes the prompt as a frame with layout nodes and a label plus buttons. The generic Python frame format appears to omit the label; this Go addition follows the user's explicit example. A strict simple prompt shape limits frame text capture and bounded first-button refresh handles late labels.
1 id python_source subsystem behavior owner go_destination status automated_evidence x11_evidence wayland_evidence speech_evidence braille_evidence notes
2 embedded-panel-applet Orca src/orca/script_utilities.py:frame_and_dialog;top_level_object;src/orca/scripts/default.py:_on_focused_changed focus Follow embedded GTK applet focus through the socket host window while preserving the source provider internal/atspi internal/app in-progress Private native regression reproduces rejection before the fix; socket and plug tests cover MATE and ordinary hosts; see docs/testing/mate-desktop.md Separate host/applet/editor providers cover icon labels; Tab and Shift+Tab; editor return; background and unrelated providers; socket detachment. Queued speech verified; real MATE listening pending. Native event-loop frames verified; physical Braille acceptance pending. Refresh parent links through embedded frames before normal X11 host validation. No process-name bypass or retained embedding state. Human review and live panel traversal required.
3 submission-clear-deletion src/cthulhu/scripts/default.py:onTextDeleted speech Avoid announcing an entry clear after plain Enter before the submitted chat message internal/app/submitted_text.go;internal/input/intent.go in-progress Submission replay fails against the original presenter and passes with the fix; editing regression matrix and focused race checks pass. Private GTK submission and selected-text replacement checks use native snapshots and the real X11 input listener. Speech requests verified; live Pidgin listening pending. Braille update path retained; physical display acceptance pending. Requires focused text or entry plus fresh plain Enter and known empty line at offset zero. Selection replacement and modified Enter retain existing feedback. No Thunderbird or word-echo changes. See docs/testing/messaging.md.
4 mate-panel-menu Orca src/orca/scripts/default.py;src/orca/ax_utilities.py;src/orca/generator.py focus Read active MATE panel menus across X11 application boundaries and label unnamed panel icons internal/atspi internal/app in-progress Private native and event-loop regressions; see docs/testing/mate-desktop.md Separate GTK providers cover editor and empty desktop menus; submenus; reverse navigation; Escape; hidden/impostor rejection; icon label precedence. Separate GTK providers and two panel windows cover detached and attached menus over editor and desktop; bottom/top panel presentation; submenus; reverse navigation; Escape; stale focus/action and impostor rejection; icon label precedence. Queued presentation verified; actual Fedora MATE listening pending. Native event-loop frames verified; physical display acceptance pending. Fresh active/showing native menu structure and verified mate-panel process only. Preserves X11 correlation without retained popup state. Existing Marco and notification paths retained. Human review and live MATE acceptance required. Fresh active/showing popup structure or showing attached menu with selected ancestors and verified mate-panel process only. Explicit menu focus normalized before event-loop ownership. Preserves X11 correlation without retained popup state. Existing Marco and notification paths retained. Human review and live MATE acceptance required.
5 mate-window-switcher Orca src/orca/scripts/switcher/script.py;src/orca/scripts/switcher/script_utilities.py focus Present Marco switcher selections while the old application retains X11 foreground internal/atspi internal/app in-progress go test ./internal/app -run '^TestMateWindowSwitcher' -count=1; see docs/testing/window-switcher.md Private GTK provider checks repeated insertions and showing events outside foreground without taking ownership. Actual Marco listening and physical key acceptance pending. Transient flash requests tested; physical display acceptance pending. Verified Marco process and application plus visible status-bar container only. Preserves application focus and review. No name-signal duplication or general background bypass. Original log lacks later cycling insertions; real cycling and Escape remain live gates.
6 gtk-alert-message src/cthulhu/speech_generator.py;src/cthulhu/script_utilities.py speech Read a GTK alert message before its first focused button internal/atspi internal/app in-progress go test ./internal/app ./internal/atspi -count=1; python3 tests/native_alert.py Private GTK/Xvfb/AT-SPI fixture captured the complete question on 2026-09-26. Physical FastSMRW listening pending. Alert window creation captures its label text; activation does not interrupt the message; the first button appends and later buttons use ordinary focus speech.
7 yad-simple-prompt src/cthulhu/formatting.py;src/cthulhu/scripts/default.py speech Read a simple YAD frame question before its first focused button internal/atspi internal/app implemented go test ./internal/app ./internal/atspi -count=1; python3 tests/native_alert.py Private YAD/Xvfb/AT-SPI fixture captured the exact question on frame activation or first button focus. The live 2026-09-27 log showed a third path where activation targets the focused button; an event-loop regression covers its speech order. Maintainer confirmed the YAD question was spoken on the updated build on 2026-09-27. YAD exposes the prompt as a frame with layout nodes and a label plus buttons. The generic Python frame format appears to omit the label; this Go addition follows the user's explicit example. A strict simple prompt shape limits frame text capture and bounded first-button refresh handles late labels.
+17
View File
@@ -62,6 +62,18 @@ submenuItem.set_submenu(submenu)
menu.append(submenuItem) menu.append(submenuItem)
menu.append(Gtk.MenuItem(label="Last application")) menu.append(Gtk.MenuItem(label="Last application"))
menu.show_all() menu.show_all()
if "--attached" in sys.argv:
menuBar = Gtk.MenuBar()
applications = Gtk.MenuItem(label="Applications")
applications.set_submenu(menu)
menuBar.append(applications)
box.pack_start(menuBar, False, False, 0)
menuBar.show_all()
bottomWindow = Gtk.Window(title="Bottom panel fixture " + processName)
bottomEntry = Gtk.Entry()
bottomEntry.get_accessible().set_name("Bottom panel entry")
bottomWindow.add(bottomEntry)
bottomWindow.show_all()
pendingInput = "" pendingInput = ""
@@ -75,11 +87,16 @@ def receive_command(_source, _condition):
while "\n" in pendingInput: while "\n" in pendingInput:
command, pendingInput = pendingInput.split("\n", 1) command, pendingInput = pendingInput.split("\n", 1)
if command == "popup": if command == "popup":
if "--attached" in sys.argv:
menuBar.select_item(applications)
menu.popup_at_widget(button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, None) menu.popup_at_widget(button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, None)
elif command == "close": elif command == "close":
menu.popdown() menu.popdown()
elif command == "ordinary": elif command == "ordinary":
window.get_accessible().emit("activate") window.get_accessible().emit("activate")
elif command == "bottom":
bottomEntry.set_text("Bottom panel ready")
bottomEntry.grab_focus()
elif command == "stale": elif command == "stale":
firstItem.get_accessible().notify_state_change(Atk.StateType.FOCUSED, True) firstItem.get_accessible().notify_state_change(Atk.StateType.FOCUSED, True)
barrier.set_text("Stale barrier") barrier.set_text("Stale barrier")