Messages read as you tab through discord again.

This commit is contained in:
lilmike committed 2026-11-01 14:52:44 -08:00
1 parent 6ed0e36960
commit 9374b5a2e5
10 files changed
+162 -3

No files matched your search

+15 -1
View File
@@ -220,8 +220,22 @@ func (presenter *eventPresenter) speak(event atspi.Event, capturedIntent ...inpu
line = &speechLine{Object: event.Source.ID(), Raw: event.Source.BrailleLine}
}
browseContext := browser.speech_context_enabled() && event.Source.BusName == browser.document.BusName
context := event.Source.SpeechContext
if !browseContext && isFocusGainEvent(event) {
// Named articles label a focused descendant even in focus mode (for
// example, a chat message's Reply button), including while browser
// context discovery is pending. Other container context retains its
// browse-mode policy.
context = nil
for _, ancestor := range event.Source.SpeechContext {
if ancestor.Role == "article" && ancestor.Name != "" {
context = append(context, ancestor)
}
}
browseContext = len(context) != 0
}
speech.Enqueue(speechRequest{
Context: event.Source.SpeechContext, ResetContext: !browseContext, VoiceType: ancestor_voice(event.Source.SpeechContext),
Context: context, ResetContext: !browseContext, VoiceType: ancestor_voice(event.Source.SpeechContext),
Window: eventWindow(event),
IntroducesWindow: (event.Type == atspi.WindowActivatedEvent || event.Type == atspi.WindowCreateEvent) &&
activationTitle(event) != "",
+3 -1
View File
@@ -59,7 +59,9 @@ func (worker *speechWorker) context_messages_for(ancestors []atspi.Snapshot, say
enabled = presentation.SayAllContextEnabled
}
for _, item := range previous {
if !contains(current, item) && enabled(presentation.ContextType(item), settings) {
// Article labels can contain an entire chat message. Python announces
// them on entry only; repeating the label on exit obscures the next one.
if item.Role != "article" && !contains(current, item) && enabled(presentation.ContextType(item), settings) {
appendMessage(item, "Leaving "+presentation.ContextLabel(item)+".")
}
}
+38
View File
@@ -94,3 +94,41 @@ func TestReviewContextSuppressedOutsideBrowseMode(t *testing.T) {
}
}
}
func TestMessageArticleAnnouncedOnceWhenTabbingIntoControls(t *testing.T) {
for name, mode := range map[string]review.DocumentMode{"browse": review.BrowseMode, "focus": review.FocusMode, "resolving": review.InactiveMode} {
t.Run(name, func(t *testing.T) {
logger := slog.New(slog.DiscardHandler)
queue := &fakeSpeechQueue{spoken: make(chan string, 16), requests: make(chan speechRequest, 16)}
speaker := &spellingTestSpeaker{}
worker := newSpeechWorker(logger, speaker)
worker.reading = func() config.ReadingSettings { return config.DefaultSettings().Reading }
browser := &browserModeController{ready: mode != review.InactiveMode, mode: mode, document: atspi.NewObjectID(":1.42", "/document")}
state := &eventLoopState{}
state.presentationOptions = presentation.DefaultOptions()
presenter := eventPresenter{state: state, logger: logger, speech: queue, browser: browser}
article := atspi.Snapshot{BusName: ":1.42", Path: "/message1", Role: "article", Name: "Author: First message"}
check := func(path, name string, ancestors []atspi.Snapshot, want []string) {
t.Helper()
control := atspi.Snapshot{BusName: ":1.42", Path: path, Role: "button", Name: name, SpeechContext: ancestors}
presenter.speak(atspi.NewEvent(atspi.FocusEvent, control))
speaker.parts = nil
if len(queue.requests) != 1 {
t.Fatalf("queued %d requests", len(queue.requests))
}
if err := worker.deliver(context.Background(), <-queue.requests); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(speaker.parts, want) {
t.Fatalf("speech = %q, want %q", speaker.parts, want)
}
}
check("/reply1", "Reply", []atspi.Snapshot{article}, []string{"Author: First message article", "Reply", "button"})
check("/forward1", "Forward", []atspi.Snapshot{article}, []string{"Forward", "button"})
article.Path, article.Name = "/message2", "Author: Second message"
check("/reply2", "Reply", []atspi.Snapshot{article}, []string{"Author: Second message article", "Reply", "button"})
check("/outside", "Outside", []atspi.Snapshot{}, []string{"Outside", "button"})
check("/reply2", "Reply", []atspi.Snapshot{article}, []string{"Author: Second message article", "Reply", "button"})
})
}
}