Fixes for mumble check/unchecked items and for menus that were not reading properly.

This commit is contained in:
Storm Dragon committed 2026-10-18 12:39:26 -04:00
1 parent 3db88abddb
commit 919b9e013b
19 files changed
+574 -7

No files matched your search

+1 -1
View File
@@ -163,7 +163,7 @@ func braille_object_frame(s atspi.Snapshot, options config.BrailleOptions, confi
if frame, handled := libreOfficeBrailleFrame(s, options); handled {
return frame
}
name, role := strings.TrimSpace(s.Name), s.Role
name, role := strings.TrimSpace(s.Name), s.ControlRole()
if role == "terminal" || brailleTextRole(role) && s.TextCaretKnown {
label := name
if role == "terminal" || role == "heading" || role == "static" {
@@ -161,6 +161,9 @@ func TestBraillePythonControlFormats(t *testing.T) {
{"toggle", atspi.Snapshot{Name: "Bold", Role: "toggle button", Checked: true}, "&=y Bold toggle button", 0},
{"button", atspi.Snapshot{Name: "More", Role: "push button", Expandable: true}, "More collapsed push button", 0},
{"menu", atspi.Snapshot{Name: "Save", Role: "menu item", SensitiveKnown: true}, "Save grayed", 0},
{"unchecked generic menu", atspi.Snapshot{Name: "Updates", Role: "menu item", Checkable: true}, "< > Updates check menu item", 0},
{"checked generic menu", atspi.Snapshot{Name: "Updates", Role: "menu item", Checked: true}, "<x> Updates check menu item", 0},
{"mixed generic menu", atspi.Snapshot{Name: "Updates", Role: "menu item", Indeterminate: true}, "<-> Updates check menu item", 0},
{"combo", atspi.Snapshot{Name: "Color", Role: "combo box", ValueText: "Blue"}, "Color Blue combo box", 6},
{"slider", atspi.Snapshot{Name: "Volume", Role: "slider", ValueKnown: true, CurrentValue: 1.25, MinimumIncrement: 0.01}, "Volume 1.25 slider", 0},
{"progress", atspi.Snapshot{Name: "Copy", Role: "progress bar", ValueKnown: true, CurrentValue: 25, MaximumValue: 100}, "Copy 25% progress bar", 0},
+18 -2
View File
@@ -18,7 +18,7 @@ import (
)
func TestRepeatedControlFocusAfterStateChange(t *testing.T) {
for _, role := range []string{"radio button", "check box", "toggle button"} {
for _, role := range []string{"radio button", "check box", "toggle button", "check menu item", "menu item"} {
for _, mode := range []string{"speech_only", "sound_and_speech", "sound_only"} {
t.Run(role+"/"+mode, func(t *testing.T) {
queue := &fakeSpeechQueue{spoken: make(chan string, 16), requests: make(chan speechRequest, 16)}
@@ -39,6 +39,7 @@ func TestRepeatedControlFocusAfterStateChange(t *testing.T) {
return true
}
control := atspi.Snapshot{BusName: ":1.7", Path: "/option", Role: role, Name: "Some", RadioGroupName: "Punctuation Level"}
control.Checkable = role == "menu item"
focus := atspi.NewEvent(atspi.FocusedStateEvent, control)
focus.Detail1 = 1
present(focus)
@@ -54,7 +55,7 @@ func TestRepeatedControlFocusAfterStateChange(t *testing.T) {
t.Fatalf("state sound = %+v", changed)
}
} else {
want := map[string]string{"radio button": "selected", "check box": "checked", "toggle button": "pressed"}[role]
want := map[string]string{"radio button": "selected", "check box": "checked", "toggle button": "pressed", "check menu item": "checked", "menu item": "checked"}[role]
if changed.Text != want {
t.Fatalf("state speech = %q, want %q", changed.Text, want)
}
@@ -67,6 +68,21 @@ func TestRepeatedControlFocusAfterStateChange(t *testing.T) {
if frame := <-frames; frame.Text != checkedFrame.Text || !state.current.Checked || !state.brailleSnapshot.Checked {
t.Fatalf("repeated focus lost the checked state: %+v", frame)
}
control.Checked = false
present(atspi.NewEvent("object:state-changed:checked", control))
changed = <-queue.requests
if mode == "sound_only" {
if changed.Text != "" || len(changed.Sound) != 1 {
t.Fatalf("unchecked state sound = %+v", changed)
}
} else {
want := map[string]string{"radio button": "not selected", "check box": "not checked", "toggle button": "not pressed", "check menu item": "not checked", "menu item": "not checked"}[role]
if changed.Text != want {
t.Fatalf("unchecked state speech = %q, want %q", changed.Text, want)
}
}
<-frames
focus.Source = control
// New semantics at the same identity still need an announcement.
focus.Source.Name = "Most"
if !present(focus) || len(queue.requests) != 1 {
+1
View File
@@ -90,6 +90,7 @@ func (handler *accessibilityEventHandler) handle(event atspi.Event) {
"window_recovered", event.WindowRecovered,
"focus_outside_document", event.FocusOutsideDocument.Path,
"role", event.Source.Role,
"focused", event.Source.Focused,
"browser_tab", event.Source.BrowserTab,
"selected", event.Source.Selected,
"radio_group_name", event.Source.RadioGroupName,
+220
View File
@@ -0,0 +1,220 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"log/slog"
"os"
"os/exec"
"strings"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/braille"
)
func TestNativeQtControlStates(t *testing.T) {
version := os.Getenv("CTHULHU_NATIVE_QT_CONTROLS")
if version != "5" && version != "6" {
t.Skip("requires private Qt control fixture")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
native := atspi.New()
if err := native.Start(ctx); err != nil {
t.Fatal(err)
}
defer native.Stop(context.Background())
adapter := &menuRecordingAdapter{Adapter: native, events: make(chan atspi.Event, 256)}
observed := make(chan atspi.Event, 256)
pumpDone := make(chan struct{})
go func() {
defer close(pumpDone)
for {
select {
case event := <-native.Events():
select {
case observed <- event:
case <-ctx.Done():
return
}
select {
case adapter.events <- event:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}()
queue := &autocompleteRequestQueue{requests: make(chan speechRequest, 256)}
frames := make(chan braille.Frame, 256)
done := make(chan struct{})
go func() {
defer close(done)
output := outputQueue{speech: queue, braille: frameRecorder{frames: frames}}
processEventsWithInput(ctx, slog.New(slog.DiscardHandler), output, adapter, nil, nil, 0,
nil, nil, nil, nil)
}()
defer func() { cancel(); <-done; <-pumpDone }()
keys := func(chord string) {
t.Helper()
if out, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", chord).CombinedOutput(); err != nil {
t.Fatalf("keys: %v %s", err, out)
}
}
waitEvent := func(name, eventType string, checked bool) atspi.Event {
t.Helper()
for {
select {
case event := <-observed:
if event.Source.Name == name {
t.Logf("event: %s detail=%d checked=%v checkable=%v focused=%v window=%s", event.Type, event.Detail1, event.Source.Checked, event.Source.Checkable, event.Source.Focused, event.Window.Path)
}
if event.Source.Name == name && event.Type == eventType && event.Source.Checked == checked &&
(eventType != atspi.FocusedStateEvent || event.Detail1 == 1) {
return event
}
case <-ctx.Done():
t.Fatalf("missing %s event for %s checked=%v", eventType, name, checked)
}
}
}
waitSpeech := func(want string) {
t.Helper()
timer := time.NewTimer(2 * time.Second)
defer timer.Stop()
for {
select {
case request := <-queue.requests:
if request.Text == want || strings.HasSuffix(request.Text, ", "+want) {
t.Logf("speech: %q", request.Text)
return
}
case <-timer.C:
t.Fatalf("missing speech %q", want)
}
}
}
waitBraille := func(want string) {
t.Helper()
timer := time.NewTimer(2 * time.Second)
defer timer.Stop()
for {
select {
case frame := <-frames:
if frame.Text == want || strings.HasSuffix(frame.Text, " "+want) {
t.Logf("Braille: %q", frame.Text)
return
}
case <-timer.C:
t.Fatalf("missing Braille %q", want)
}
}
}
// Qt 5 omits STATE_CHECKABLE. Checked-state evidence must survive the
// next uncheck without assigning a state to unrelated menu commands.
for _, probe := range []struct{ key, name string }{{"F6", "Optional feature"}, {"F8", "Context option"}} {
for step, checked := range []bool{false, true, false} {
keys(probe.key)
event := waitEvent(probe.name, atspi.FocusedStateEvent, checked)
if event.Source.Checkable != (version == "6" || step > 0) {
t.Fatalf("Qt %s checkable metadata = %v", version, event.Source.Checkable)
}
role, suffix, indicator := "check menu item", ", not checked", "< > "
if checked {
suffix, indicator = ", checked", "<x> "
} else if version == "5" && step == 0 {
role, suffix, indicator = "menu item", "", ""
}
waitSpeech(probe.name + ", " + role + suffix)
brailleText := indicator + probe.name
if role == "check menu item" {
brailleText += " check menu item"
}
waitBraille(brailleText)
if step < 2 {
keys("Return")
} else {
keys("Escape")
}
}
}
keys("F7")
waitEvent("Ordinary action", atspi.FocusedStateEvent, false)
waitSpeech("Ordinary action, menu item")
waitBraille("Ordinary action")
keys("Escape")
// Traverse between context-only entries and actions shared with the
// menu bar, in both directions and after reopening the popup.
for range 2 {
keys("F8")
waitEvent("Context option", atspi.FocusedStateEvent, false)
waitSpeech("Context option, check menu item, not checked")
for _, step := range []struct{ key, name string }{
{"Down", "Ordinary action"}, {"Down", "Context command"},
{"Down", "Optional feature"}, {"Up", "Context command"},
{"Up", "Ordinary action"}, {"Up", "Context option"},
} {
keys(step.key)
waitEvent(step.name, atspi.FocusedStateEvent, false)
wantSpeech, wantBraille := step.name+", menu item", step.name
if step.name == "Optional feature" || step.name == "Context option" {
wantSpeech = step.name + ", check menu item, not checked"
wantBraille = "< > " + step.name + " check menu item"
}
waitSpeech(wantSpeech)
waitBraille(wantBraille)
}
keys("Escape")
}
if version == "6" {
// When the provider exports CHECKABLE, withdrawing that capability is
// authoritative and must retire the prior evidence.
keys("F12")
keys("F6")
event := waitEvent("Optional feature", atspi.FocusedStateEvent, false)
if event.Source.Checkable {
t.Fatal("explicit removal of checkability was ignored")
}
waitSpeech("Optional feature, menu item")
waitBraille("Optional feature")
keys("Escape")
}
// Replacing an action with an ordinary action of the same name must not
// inherit the removed accessible's checkability evidence.
keys("F11")
keys("F6")
event := waitEvent("Optional feature", atspi.FocusedStateEvent, false)
if event.Source.Checkable {
t.Fatal("replacement action inherited checkability")
}
waitSpeech("Optional feature, menu item")
waitBraille("Optional feature")
keys("Escape")
for _, probe := range []struct{ key, name string }{{"F9", "Updates"}, {"F10", "Power"}} {
if probe.key == "F9" {
keys("F10")
} else {
keys("F9")
}
keys(probe.key)
waitEvent(probe.name, atspi.FocusedStateEvent, false)
waitSpeech(probe.name + ", check box, not checked")
for _, checked := range []bool{true, false} {
keys("space")
waitEvent(probe.name, "object:state-changed:checked", checked)
want, indicator := "not checked", "< > "
if checked {
want, indicator = "checked", "<x> "
}
waitSpeech(want)
waitBraille(indicator + probe.name + " check box")
}
}
}
+2
View File
@@ -362,6 +362,7 @@ cthulhu_snapshot cthulhu_capture_snapshot(AtspiAccessible *source, gboolean capt
snapshot.focused = atspi_state_set_contains(states, ATSPI_STATE_FOCUSED);
snapshot.hasPopup = atspi_state_set_contains(states, ATSPI_STATE_HAS_POPUP);
snapshot.sensitiveKnown = TRUE;
snapshot.checkable = atspi_state_set_contains(states, ATSPI_STATE_CHECKABLE);
snapshot.checked = atspi_state_set_contains(states, ATSPI_STATE_CHECKED) || atspi_state_set_contains(states, ATSPI_STATE_PRESSED);
snapshot.defunct = atspi_state_set_contains(states, ATSPI_STATE_DEFUNCT);
snapshot.editable = atspi_state_set_contains(states, ATSPI_STATE_EDITABLE);
@@ -388,6 +389,7 @@ cthulhu_snapshot cthulhu_capture_snapshot(AtspiAccessible *source, gboolean capt
g_error_free(error);
error = NULL;
}
cthulhu_capture_menu_checkability(source, &snapshot);
if (!snapshot.defunct && (g_strcmp0(snapshot.role, "list") == 0 ||
g_strcmp0(snapshot.role, "list box") == 0)) {
snapshot.childCount = atspi_accessible_get_child_count(source, &error);
@@ -268,7 +268,7 @@ func (adapter *Adapter) enrichNativeWindowEvent(event *C.AtspiEvent, target *C.A
}
focusEvent.Foreground = adapter.Foreground()
focusedMenuItem := (eventType == FocusEvent || eventType == FocusedStateEvent) &&
snapshot.Role == "menu item" && snapshot.Focused
(snapshot.Role == "menu item" || snapshot.Role == "check menu item" || snapshot.Role == "radio menu item") && snapshot.Focused
if eventType == WindowActivatedEvent {
focusEvent.ForegroundMismatch = !adapter.matchForeground(window, focusEvent.Window, &focusEvent.ForegroundCheck, false)
} else {
@@ -410,6 +410,9 @@ func cthulhuAtspiEventCallback(event *C.AtspiEvent, userData unsafe.Pointer) {
}
eventType := canonicalTextChangeEventType(nativeEventType)
originID := nativeObjectID(event.source)
if eventType == AccessibleNameEvent || eventType == DefunctStateEvent && C.cthulhu_event_detail1(event) != 0 {
C.cthulhu_forget_menu_checkability(event.source)
}
adapter.retireEchoFocus(eventType, int(C.cthulhu_event_detail1(event)), originID)
if eventType != DefunctStateEvent || C.cthulhu_event_detail1(event) != 0 {
adapter.retireLibreOfficeHeaders(event.source, eventType, originID)
@@ -417,6 +420,10 @@ func cthulhuAtspiEventCallback(event *C.AtspiEvent, userData unsafe.Pointer) {
if adapter.secure.Load() {
return
}
if eventType == "object:state-changed:checked" &&
C.atspi_accessible_get_role(event.source, nil) == C.ATSPI_ROLE_MENU_ITEM {
C.cthulhu_note_menu_checkability(event.source)
}
adapter.applySecureReset()
adapter.refreshForeground()
@@ -457,6 +464,12 @@ func cthulhuAtspiEventCallback(event *C.AtspiEvent, userData unsafe.Pointer) {
if target == nil {
return
}
if (eventType == FocusEvent || eventType == FocusedStateEvent) &&
C.atspi_accessible_get_role(target, nil) == C.ATSPI_ROLE_MENU_ITEM {
// Qt menus can change their checked state without a state-change
// event. Reopening the menu must capture the current provider state.
C.atspi_accessible_clear_cache_single(target)
}
if (eventType == SelectedStateEvent || eventType == SelectionChangedEvent) &&
C.atspi_accessible_get_role(target, nil) == C.ATSPI_ROLE_PAGE_TAB {
// Firefox can emit selected-state events from the previous tab;
@@ -470,6 +483,17 @@ func cthulhuAtspiEventCallback(event *C.AtspiEvent, userData unsafe.Pointer) {
}
payload := C.cthulhu_event_payload(event)
snapshot, popupFor := adapter.captureNativeEventSnapshot(event, target, eventType, originID)
// Shared Qt actions can report focus through their original, hidden menu
// even while another menu emits focus-gained for that very accessible.
// The explicit event establishes focus; the queried state may disagree.
// Do not transfer this evidence to a redirected descendant or a dead item.
if snapshot.ID() == originID && !snapshot.Defunct &&
(eventType == FocusEvent || eventType == FocusedStateEvent && C.cthulhu_event_detail1(event) == 1) {
switch snapshot.Role {
case "menu item", "check menu item", "radio menu item":
snapshot.Focused = true
}
}
var inlineMenuID ObjectID
if eventType == SelectedStateEvent && C.cthulhu_event_detail1(event) == 1 && !snapshot.Defunct &&
(snapshot.Role == "button" || snapshot.Role == "push button" || snapshot.Role == "toggle button" ||
+48
View File
@@ -0,0 +1,48 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
#include "adapter_native.h"
// Owned by the native accessible, on the AT-SPI GLib thread. No accessible
// references or application labels are retained. Destruction frees the data;
// semantic identity changes and explicit capability removal retire it too.
static const gchar *menu_checkability_key = "cthulhu-menu-checkability";
enum { CHECKABILITY_INFERRED = 1, CHECKABILITY_EXPLICIT = 2 };
void cthulhu_forget_menu_checkability(AtspiAccessible *source) {
if (source != NULL)
g_object_set_data(G_OBJECT(source), menu_checkability_key, NULL);
}
void cthulhu_note_menu_checkability(AtspiAccessible *source) {
if (source != NULL &&
GPOINTER_TO_INT(g_object_get_data(G_OBJECT(source), menu_checkability_key)) != CHECKABILITY_EXPLICIT)
g_object_set_data(G_OBJECT(source), menu_checkability_key,
GINT_TO_POINTER(CHECKABILITY_INFERRED));
}
void cthulhu_capture_menu_checkability(AtspiAccessible *source, cthulhu_snapshot *snapshot) {
if (snapshot->defunct || g_strcmp0(snapshot->role, "menu item") != 0) {
cthulhu_forget_menu_checkability(source);
return;
}
if (!snapshot->statesKnown) return;
gint evidence = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(source), menu_checkability_key));
if (snapshot->checkable) {
g_object_set_data(G_OBJECT(source), menu_checkability_key,
GINT_TO_POINTER(CHECKABILITY_EXPLICIT));
} else if (snapshot->checked || snapshot->indeterminate) {
cthulhu_note_menu_checkability(source);
snapshot->checkable = TRUE;
} else if (evidence == CHECKABILITY_INFERRED) {
// Providers such as Qt 5 omit CHECKABLE in both states. Once CHECKED
// or a checked-state event proves this object's capability, retain it
// when the value becomes false. Absence alone proves nothing.
snapshot->checkable = TRUE;
} else {
// A provider which supplied CHECKABLE can explicitly withdraw it.
cthulhu_forget_menu_checkability(source);
}
}
+5
View File
@@ -29,6 +29,7 @@ typedef struct {
gboolean focusable;
gboolean focused;
gboolean hasPopup;
gboolean checkable;
gboolean checked;
gboolean defunct;
gboolean editable;
@@ -76,6 +77,10 @@ typedef struct {
gboolean referenceGeometryKnown;
} cthulhu_snapshot;
void cthulhu_capture_menu_checkability(AtspiAccessible *source, cthulhu_snapshot *snapshot);
void cthulhu_note_menu_checkability(AtspiAccessible *source);
void cthulhu_forget_menu_checkability(AtspiAccessible *source);
typedef struct {
uintptr_t callback;
} cthulhu_callback_data;
@@ -112,6 +112,7 @@ func snapshotFromNativeWithOptions(source *C.AtspiAccessible, text, caretCharact
snapshot.Focusable = captured.focusable != 0
snapshot.Focused = captured.focused != 0
snapshot.HasPopup = captured.hasPopup != 0
snapshot.Checkable = captured.checkable != 0
snapshot.Checked = captured.checked != 0
snapshot.Defunct = captured.defunct != 0
snapshot.Editable = captured.editable != 0
+16
View File
@@ -83,6 +83,7 @@ type Snapshot struct {
Focused bool
HasPopup bool
HeadingLevel int
Checkable bool
Checked bool
Defunct bool
Editable bool
@@ -144,6 +145,16 @@ type Snapshot struct {
SoundGeometry SoundGeometry
}
// ControlRole includes checkable menu semantics even when a provider exposes
// the ordinary menu-item role. Checked and mixed states also establish this
// capability when the provider omits STATE_CHECKABLE.
func (snapshot Snapshot) ControlRole() string {
if snapshot.Role == "menu item" && (snapshot.Checkable || snapshot.Checked || snapshot.Indeterminate) {
return "check menu item"
}
return snapshot.Role
}
// TextAttributeRun is the native attribute range at the caret, copied without
// retaining an AT-SPI object. Offsets are Unicode character offsets.
type TextAttributeRun struct {
@@ -312,6 +323,11 @@ func NewSnapshotWithCaretCharacter(path, name, role, description, text, caretCha
func NewEvent(eventType string, source Snapshot) Event {
eventType = sanitizeSnapshotField(eventType)
// A checked-state transition proves checkability even when the provider
// omits STATE_CHECKABLE and the new checked value is false.
if eventType == "object:state-changed:checked" && source.Role == "menu item" {
source.Checkable = true
}
event := Event{Type: canonicalTextChangeEventType(eventType), Source: source, OriginPath: source.Path}
if event.Type != eventType {
event.NativeType = eventType
+1 -1
View File
@@ -88,7 +88,7 @@ func controlValue(snapshot atspi.Snapshot) string {
func controlStates(snapshot atspi.Snapshot) []string {
var states []string
switch snapshot.Role {
switch snapshot.ControlRole() {
case "check box", "check menu item":
switch {
case snapshot.Indeterminate:
@@ -39,6 +39,10 @@ func TestFocusTextPresentsControlStates(t *testing.T) {
{name: "collapsed item", snapshot: atspi.Snapshot{Name: "Documents", Role: "tree item", Expandable: true}, want: "Documents, tree item, collapsed"},
{name: "expanded selected item", snapshot: atspi.Snapshot{Name: "Documents", Role: "tree item", Expandable: true, Expanded: true, Selected: true}, want: "Documents, tree item, expanded, selected"},
{name: "selected menu item", snapshot: atspi.Snapshot{Name: "Open", Role: "menu item", Selected: true}, want: "Open, menu item"},
{name: "unchecked generic menu", snapshot: atspi.Snapshot{Name: "Updates", Role: "menu item", Checkable: true}, want: "Updates, check menu item, not checked"},
{name: "checked generic menu without marker", snapshot: atspi.Snapshot{Name: "Updates", Role: "menu item", Checked: true}, want: "Updates, check menu item, checked"},
{name: "mixed generic menu", snapshot: atspi.Snapshot{Name: "Updates", Role: "menu item", Indeterminate: true}, want: "Updates, check menu item, partially checked"},
{name: "ordinary menu", snapshot: atspi.Snapshot{Name: "Open", Role: "menu item"}, want: "Open, menu item"},
{name: "selected table cell", snapshot: atspi.Snapshot{Name: "First post", Role: "table cell", Selected: true}, want: "First post"},
{name: "field requirements", snapshot: atspi.Snapshot{Name: "Account", Role: "text", ReadOnly: true, Required: true, Invalid: true}, want: "Account, text, read only, required, invalid entry"},
{name: "disabled control", snapshot: atspi.Snapshot{Name: "Apply", Role: "push button", SensitiveKnown: true}, want: "Apply, push button, grayed"},
@@ -61,6 +65,8 @@ func TestEventTextPresentsOnlyChangedState(t *testing.T) {
want string
}{
{name: "checked", event: atspi.NewEvent("object:state-changed:checked", atspi.Snapshot{Name: "Updates", Role: "check box", Checked: true}), want: "checked"},
{name: "generic menu checked", event: atspi.NewEvent("object:state-changed:checked", atspi.Snapshot{Name: "Updates", Role: "menu item", Checkable: true, Checked: true}), want: "checked"},
{name: "generic menu unchecked without marker", event: atspi.NewEvent("object:state-changed:checked", atspi.Snapshot{Name: "Updates", Role: "menu item"}), want: "not checked"},
{name: "expanded", event: atspi.NewEvent("object:state-changed:expanded", atspi.Snapshot{Name: "Files", Role: "tree item", Expanded: true}), want: "expanded"},
{name: "menu selection omitted", event: atspi.NewEvent("object:state-changed:selected", atspi.Snapshot{Name: "Open", Role: "menu item", Selected: true}), want: ""},
{name: "tree selection", event: atspi.NewEvent("object:state-changed:selected", atspi.Snapshot{Name: "Files", Role: "tree item", Selected: true}), want: "selected"},
+1 -1
View File
@@ -86,7 +86,7 @@ func FocusSpeechWithOptions(snapshot atspi.Snapshot, options Options) Speech {
}
reading := reading_options(options)
var parts []string
role := snapshot.Role
role := snapshot.ControlRole()
if role == "entry" && snapshot.Autocomplete {
role = "autocomplete"
}
+1 -1
View File
@@ -151,7 +151,7 @@ func stateSoundCue(snapshot atspi.Snapshot, options Options) (sound.Cue, bool) {
"radio menu item": "radiobutton",
"toggle button": "togglebutton",
"switch": "switch",
}[snapshot.Role]
}[snapshot.ControlRole()]
switch {
case stateBase == "":
case snapshot.Indeterminate && stateBase == "checkbox":
+4
View File
@@ -36,6 +36,10 @@ func TestRoleAndStateSoundPolicy(t *testing.T) {
{atspi.Snapshot{Role: "check box", Checked: true}, []string{"checkbox_checked"}},
{atspi.Snapshot{Role: "check box", Indeterminate: true}, []string{"checkbox_mixed"}},
{atspi.Snapshot{Role: "check box"}, []string{"checkbox_unchecked"}},
{atspi.Snapshot{Role: "menu item", Checkable: true}, []string{"checkbox_unchecked"}},
{atspi.Snapshot{Role: "menu item", Checked: true}, []string{"checkbox_checked"}},
{atspi.Snapshot{Role: "menu item", Indeterminate: true}, []string{"checkbox_mixed"}},
{atspi.Snapshot{Role: "menu item"}, nil},
{atspi.Snapshot{Role: "toggle button", Checked: true}, []string{"button", "togglebutton_checked"}},
{atspi.Snapshot{Role: "link", Visited: true}, []string{"link_visited"}},
{atspi.Snapshot{Role: "entry"}, nil},