Fix for repeating in some instances, e.g. pidgin.
This commit is contained in:
1 parent
4d29f2bb23
commit
b232c1eebb
20 files changed
+921
-7
No files matched your search
@@ -462,7 +462,24 @@ cthulhu_snapshot cthulhu_capture_snapshot(AtspiAccessible *source, gboolean capt
|
||||
}
|
||||
snapshot.description = atspi_accessible_get_description(source, &error);
|
||||
if (error != NULL) {
|
||||
g_error_free(error);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
// Orca uses the containing panel's label for otherwise unlabeled icons.
|
||||
// Keep an icon's own name/description and never borrow from other roles.
|
||||
if (!snapshot.defunct && g_strcmp0(snapshot.role, "icon") == 0 &&
|
||||
(snapshot.name == NULL || *snapshot.name == '\0') &&
|
||||
(snapshot.description == NULL || *snapshot.description == '\0')) {
|
||||
AtspiAccessible *parent = atspi_accessible_get_parent(source, &error);
|
||||
if (error == NULL && parent != NULL && atspi_accessible_get_role(parent, NULL) == ATSPI_ROLE_PANEL) {
|
||||
g_free(snapshot.name);
|
||||
snapshot.name = atspi_accessible_get_name(parent, NULL);
|
||||
if (snapshot.name == NULL || *snapshot.name == '\0') {
|
||||
g_free(snapshot.name);
|
||||
snapshot.name = atspi_accessible_get_description(parent, NULL);
|
||||
}
|
||||
}
|
||||
if (parent != NULL) g_object_unref(parent);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
if (captureDocumentMetadata) {
|
||||
cthulhu_capture_document_metrics(source, &snapshot);
|
||||
|
||||
@@ -27,7 +27,8 @@ func (adapter *Adapter) filterNativeBackgroundEvent(event *C.AtspiEvent, eventTy
|
||||
if C.cthulhu_may_be_background_notification(event.source) != 0 {
|
||||
return false
|
||||
}
|
||||
if foreground.Client.Window == 0 || adapter.foregroundWindow.Valid() && origin.BusName != adapter.foregroundWindow.BusName {
|
||||
if (foreground.Client.Window == 0 || adapter.foregroundWindow.Valid() && origin.BusName != adapter.foregroundWindow.BusName) &&
|
||||
adapter.applicationProcess(event.source) != "mate-panel" {
|
||||
return true
|
||||
}
|
||||
window := C.cthulhu_window_ancestor(event.source)
|
||||
|
||||
@@ -504,8 +504,10 @@ gboolean cthulhu_active_menu_window(AtspiAccessible *window) {
|
||||
AtspiStateSet *states = atspi_accessible_get_state_set(window);
|
||||
gboolean active = states != NULL && atspi_state_set_contains(states, ATSPI_STATE_ACTIVE);
|
||||
gboolean showing = states != NULL && atspi_state_set_contains(states, ATSPI_STATE_SHOWING);
|
||||
gboolean unavailable = states == NULL || atspi_state_set_contains(states, ATSPI_STATE_ICONIFIED) ||
|
||||
atspi_state_set_contains(states, ATSPI_STATE_DEFUNCT);
|
||||
if (states != NULL) g_object_unref(states);
|
||||
if (!active || !showing || atspi_accessible_get_child_count(window, NULL) != 1) return FALSE;
|
||||
if (!active || !showing || unavailable || atspi_accessible_get_child_count(window, NULL) != 1) return FALSE;
|
||||
AtspiAccessible *child = atspi_accessible_get_child_at_index(window, 0, NULL);
|
||||
if (child == NULL) return FALSE;
|
||||
AtspiRole childRole = atspi_accessible_get_role(child, NULL);
|
||||
|
||||
@@ -71,7 +71,7 @@ func (adapter *Adapter) matchForeground(window *C.AtspiAccessible, id ObjectID,
|
||||
if !state.Known {
|
||||
return true
|
||||
}
|
||||
if state.Window == 0 || window == nil {
|
||||
if window == nil {
|
||||
if check != nil {
|
||||
check.Reason = "no X11 foreground window or accessible window"
|
||||
}
|
||||
@@ -82,6 +82,18 @@ func (adapter *Adapter) matchForeground(window *C.AtspiAccessible, id ObjectID,
|
||||
check.AccessiblePID = pid
|
||||
}
|
||||
pid = adapter.foregroundProcessID(pid)
|
||||
// Desktop panel menus can grab the keyboard while X11 still names the
|
||||
// previous application. Orca accepts their active, showing menu window.
|
||||
// Keep the correlated X11 owner so dismissal can return to that client.
|
||||
if adapter.is_panel_menu(window) {
|
||||
return true
|
||||
}
|
||||
if state.Window == 0 {
|
||||
if check != nil {
|
||||
check.Reason = "no X11 foreground window or accessible window"
|
||||
}
|
||||
return false
|
||||
}
|
||||
if adapter.foregroundWindow.Valid() {
|
||||
if adapter.foregroundWindow == id {
|
||||
return true
|
||||
@@ -165,6 +177,17 @@ func (adapter *Adapter) matchForeground(window *C.AtspiAccessible, id ObjectID,
|
||||
return true
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Query fresh window state on the GLib owner; no popup state is retained.
|
||||
func (adapter *Adapter) is_panel_menu(window *C.AtspiAccessible) bool {
|
||||
if adapter.applicationProcess(window) != "mate-panel" {
|
||||
return false
|
||||
}
|
||||
C.atspi_accessible_clear_cache_single(window)
|
||||
return C.cthulhu_active_menu_window(window) != 0
|
||||
}
|
||||
|
||||
func (adapter *Adapter) replacesForegroundWindow(window *C.AtspiAccessible, id ObjectID, foregroundPID, pid int) bool {
|
||||
if foregroundPID <= 0 || pid != foregroundPID || id.BusName != adapter.foregroundWindow.BusName {
|
||||
return false
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package atspi
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNativeMatePanelMenuOutsideForeground(t *testing.T) {
|
||||
if os.Getenv("CTHULHU_NATIVE_PANEL_MENU") != "1" {
|
||||
t.Skip("requires private Xorg/AT-SPI session")
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name, processName string
|
||||
empty bool
|
||||
}{
|
||||
{"over editor", "mate-panel", false},
|
||||
{"empty desktop", "mate-panel", true},
|
||||
{"impostor", "impostor", false},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) { check_native_panel_menu(t, test.processName, test.empty) })
|
||||
}
|
||||
}
|
||||
|
||||
func check_native_panel_menu(t *testing.T, processName string, empty bool) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
adapter := New()
|
||||
if err := adapter.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer adapter.Stop(context.Background())
|
||||
start := func(name string) (io.WriteCloser, string) {
|
||||
t.Helper()
|
||||
command := exec.CommandContext(ctx, "/usr/bin/python3", "../../tests/gtk_panel_menu.py", name)
|
||||
input, err := command.StdinPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output, err := command.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
command.Stderr = os.Stderr
|
||||
if err := command.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { input.Close(); command.Process.Kill(); command.Wait() })
|
||||
line, err := bufio.NewReader(output).ReadString('\n')
|
||||
fields := strings.Fields(line)
|
||||
if err != nil || len(fields) != 2 || fields[0] != "ready" {
|
||||
t.Fatalf("fixture readiness: %q, %v", line, err)
|
||||
}
|
||||
return input, fields[1]
|
||||
}
|
||||
panel, _ := start(processName)
|
||||
editor, xid := start("editor")
|
||||
tree, err := adapter.CaptureDesktop(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check_native_panel_icons(t, tree)
|
||||
key := func(args ...string) {
|
||||
t.Helper()
|
||||
if output, err := exec.CommandContext(ctx, "xdotool", args...).CombinedOutput(); err != nil {
|
||||
t.Fatalf("xdotool: %s, %v", output, err)
|
||||
}
|
||||
}
|
||||
key("windowfocus", "--sync", xid)
|
||||
wait := func(predicate func(Event) bool) Event {
|
||||
t.Helper()
|
||||
var last Event
|
||||
for {
|
||||
select {
|
||||
case event := <-adapter.Events():
|
||||
last = event
|
||||
if predicate(event) {
|
||||
return event
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("native event missing: %v; filtered=%d last=%+v", ctx.Err(), adapter.backgroundEventsFiltered.Load(), last)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(editor, "edit Foreground ready")
|
||||
owned := wait(func(event Event) bool { return event.Payload == "Foreground ready" && !event.ForegroundMismatch })
|
||||
if empty {
|
||||
key("windowfocus", "0")
|
||||
for adapter.Foreground().Client.Window != 0 {
|
||||
select {
|
||||
case <-adapter.ForegroundChanges():
|
||||
case <-ctx.Done():
|
||||
t.Fatal("empty desktop foreground missing")
|
||||
}
|
||||
}
|
||||
owned.Foreground = adapter.Foreground()
|
||||
}
|
||||
fmt.Fprintln(panel, "popup")
|
||||
activation := wait(func(event Event) bool {
|
||||
return event.Type == WindowActivatedEvent && event.Source.ApplicationName == "mate-panel"
|
||||
})
|
||||
if processName != "mate-panel" {
|
||||
if !activation.ForegroundMismatch {
|
||||
t.Fatalf("accessible name enabled panel exception for another process: %+v", activation)
|
||||
}
|
||||
key("key", "Escape")
|
||||
return
|
||||
}
|
||||
if activation.ForegroundMismatch || activation.Foreground != owned.Foreground {
|
||||
t.Fatalf("panel popup rejected with unchanged X11 foreground: %+v", activation)
|
||||
}
|
||||
for _, step := range []struct{ key, name string }{
|
||||
{"Down", "First application"}, {"Down", "Utilities"}, {"Right", "Example utility"},
|
||||
{"Left", "Utilities"}, {"Down", "Last application"}, {"Up", "Utilities"},
|
||||
} {
|
||||
key("key", step.key)
|
||||
event := wait(func(event Event) bool {
|
||||
return event.Source.Name == step.name && (event.Type == FocusEvent || event.Type == FocusedStateEvent || event.Type == SelectionChangedEvent)
|
||||
})
|
||||
if event.ForegroundMismatch || event.Foreground != owned.Foreground || event.Source.ApplicationProcess != "mate-panel" {
|
||||
t.Fatalf("panel navigation lost ownership: %+v", event)
|
||||
}
|
||||
}
|
||||
key("key", "Escape")
|
||||
fmt.Fprintln(panel, "ordinary")
|
||||
ordinary := wait(func(event Event) bool {
|
||||
return event.Type == WindowActivatedEvent && event.Source.ApplicationName == "mate-panel"
|
||||
})
|
||||
if !ordinary.ForegroundMismatch {
|
||||
t.Fatalf("ordinary panel window borrowed menu exception: %+v", ordinary)
|
||||
}
|
||||
fmt.Fprintln(panel, "stale")
|
||||
wait(func(event Event) bool {
|
||||
if event.Source.Name == "First application" && (event.Type == FocusEvent || event.Type == FocusedStateEvent) {
|
||||
t.Fatalf("hidden menu item escaped filtering: %+v", event)
|
||||
}
|
||||
return event.Payload == "Stale barrier"
|
||||
})
|
||||
if empty {
|
||||
key("windowfocus", "--sync", xid)
|
||||
}
|
||||
fmt.Fprintln(editor, "edit Foreground restored")
|
||||
restored := wait(func(event Event) bool { return event.Payload == "Foreground restored" && !event.ForegroundMismatch })
|
||||
if restored.Window != owned.Window || !empty && restored.Foreground != owned.Foreground {
|
||||
t.Fatalf("panel dismissal changed editor ownership: %+v", restored)
|
||||
}
|
||||
}
|
||||
|
||||
func check_native_panel_icons(t *testing.T, tree *AccessibleTree) {
|
||||
t.Helper()
|
||||
tree.mu.RLock()
|
||||
defer tree.mu.RUnlock()
|
||||
icons := 0
|
||||
for _, node := range tree.nodes {
|
||||
icon := node.snapshot
|
||||
if icon.Role != "icon" {
|
||||
continue
|
||||
}
|
||||
icons++
|
||||
parent := tree.nodes[node.parent].snapshot
|
||||
want := ""
|
||||
switch parent.Name {
|
||||
case "Network applet":
|
||||
want = parent.Name
|
||||
case "Unused applet name":
|
||||
if icon.Description == "" {
|
||||
want = "Named icon"
|
||||
}
|
||||
}
|
||||
if icon.Name != want {
|
||||
t.Fatalf("panel icon name=%q description=%q parent=%q; want %q", icon.Name, icon.Description, parent.Name, want)
|
||||
}
|
||||
}
|
||||
if icons != 8 {
|
||||
t.Fatalf("captured %d panel icons; want four cases from each provider", icons)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user