Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,130 @@
|
||||
// Cthulhu screen reader.
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/braille"
|
||||
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
||||
"git.stormux.org/storm/cthulhu/internal/webscripts"
|
||||
)
|
||||
|
||||
// acceptFocusOwnership keeps native window focus, XTerm handoff, and navigation
|
||||
// echoes ahead of input intent and normal presentation.
|
||||
func (handler *accessibilityEventHandler) acceptFocusOwnership(event atspi.Event, notification bool) bool {
|
||||
state := handler.state
|
||||
logger := handler.logger
|
||||
speech := handler.speech
|
||||
focusGate := handler.focusGate
|
||||
objectReview := handler.objectReview
|
||||
brailleOutput := handler.brailleOutput
|
||||
ownedAutocompletePopup := state.ownedAutocompletePopup
|
||||
if isFocusGainEvent(event) {
|
||||
if event.Type == atspi.WindowActivatedEvent {
|
||||
focusBeforeActivation := state.owner.window.Valid() && state.owner.window == eventWindow(event) &&
|
||||
state.owner.focus.Valid() && !state.owner.activated
|
||||
// Window ownership and object focus are independent. Providers can
|
||||
// repeat activation for the same top-level around an X11 key grab,
|
||||
// and the activation snapshot can be the frame while the focused
|
||||
// object is one of its descendants. Treating that source mismatch as
|
||||
// a new activation interrupts review speech and resets its cursor.
|
||||
if state.owner.activate(event) {
|
||||
logger.Debug("skipped duplicate accessible window activation", "application", event.Source.ApplicationName, "bus", state.owner.window.BusName, "window_path", state.owner.window.Path, "focus_path", event.Source.Path)
|
||||
return true
|
||||
}
|
||||
state.lastFileManagerStatus, state.lastFileManagerStatusWindow = "", atspi.ObjectID{}
|
||||
logger.Debug("activated accessible window", "application", event.Source.ApplicationName, "bus", state.owner.window.BusName, "window_path", state.owner.window.Path, "focus_path", event.Source.Path)
|
||||
if (event.Source.Role == "alert" || event.Source.Role == "frame") &&
|
||||
state.pendingPromptTextAnnounced && state.pendingPromptWindow.Valid() && event.Window == state.pendingPromptWindow {
|
||||
// The prompt's window:create already queued its title and
|
||||
// message. Activation must not cut that announcement short.
|
||||
return true
|
||||
}
|
||||
if focusBeforeActivation {
|
||||
text := strings.TrimSpace(event.WindowTitle)
|
||||
if (event.Source.Role == "button" || event.Source.Role == "tree table" || event.Source.Role == "table cell") && event.WindowMessage != "" {
|
||||
if text != "" {
|
||||
text += ", "
|
||||
}
|
||||
text += event.WindowMessage
|
||||
}
|
||||
if text != "" {
|
||||
speech.Interrupt()
|
||||
speech.Enqueue(speechRequest{Text: text, Role: "frame", Cause: event.Type,
|
||||
Window: eventWindow(event), IntroducesWindow: true,
|
||||
ExcludeFromHistory: state.historyWindow.Valid() && eventWindow(event) == state.historyWindow})
|
||||
}
|
||||
return true
|
||||
}
|
||||
} else if !ownedAutocompletePopup(event) && !state.owner.follow(event) {
|
||||
logger.Debug("skipped focus outside active window", "type", event.Type, "application", event.Source.ApplicationName, "bus", event.Window.BusName, "window_path", event.Window.Path, "focus_path", event.Source.Path)
|
||||
return true
|
||||
}
|
||||
if isXTermFocus(event) {
|
||||
state.brailleSnapshot = atspi.Snapshot{}
|
||||
if brailleOutput != nil {
|
||||
brailleOutput.EnqueueFrame(braille.Frame{Cursor: -1})
|
||||
}
|
||||
state.pending.clear()
|
||||
focusGate.SetSuspended(true)
|
||||
state.focusedID = event.Source.ID()
|
||||
state.current = event.Source
|
||||
if objectReview != nil {
|
||||
objectReview.SyncFocus(event.Source.ID())
|
||||
}
|
||||
logger.Info("suspended for XTerm focus handoff")
|
||||
return true
|
||||
}
|
||||
if !notification {
|
||||
focusGate.SetSuspended(false)
|
||||
}
|
||||
}
|
||||
if focusGate.Suspended() && !notification {
|
||||
logger.Debug("skipped event during XTerm focus handoff", "type", event.Type, "role", event.Source.Role)
|
||||
return true
|
||||
}
|
||||
if event.NavigationEcho {
|
||||
return true
|
||||
}
|
||||
// The native adapter already filters focused-state losses. Keep the
|
||||
// event loop defensive for alternate adapters and tests: Python does
|
||||
// not present or follow the object which just lost focus.
|
||||
if event.Type == atspi.FocusedStateEvent && event.Detail1 == 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isXTermSnapshot(snapshot atspi.Snapshot) bool {
|
||||
return strings.EqualFold(snapshot.ApplicationName, "xterm")
|
||||
}
|
||||
|
||||
func isXTermFocus(event atspi.Event) bool {
|
||||
if isXTermSnapshot(event.Source) {
|
||||
return true
|
||||
}
|
||||
return event.Source.Role == "terminal" && event.Source.ApplicationName == "" && xorg.ActiveWindowIsXTerm()
|
||||
}
|
||||
|
||||
func eventBypassesXTermHandoff(event atspi.Event) bool {
|
||||
if webscripts.HandlesNotification(event.Source.ApplicationProcess, event.Type, event.Detail1, event.WebNotification) {
|
||||
return true
|
||||
}
|
||||
role := strings.ToLower(event.Source.Role)
|
||||
if role == "notification" || role == "alert" {
|
||||
return true
|
||||
}
|
||||
switch event.Type {
|
||||
case "window:create", "object:property-change:accessible-name", atspi.AccessibleValueEvent:
|
||||
switch strings.ToLower(event.Source.ApplicationName) {
|
||||
case "notification-daemon", "notify-osd", "xfce4-notifyd", "mate-notification-daemon":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in new issue
Block a user