94 lines
3.9 KiB
Go
94 lines
3.9 KiB
Go
// 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/input"
|
|
"git.stormux.org/storm/cthulhu/internal/sound"
|
|
)
|
|
|
|
// handleAutocomplete may rewrite insertion payload before normal presentation.
|
|
// A true consumed result discards popup and address-bar echo events in place.
|
|
func (handler *accessibilityEventHandler) handleAutocomplete(event atspi.Event, activity input.Activity, intent input.Intent) (atspi.Event, speechRequest, bool) {
|
|
state := handler.state
|
|
live := handler.live
|
|
logger := handler.logger
|
|
speak := handler.speak
|
|
var autocompleteRequest speechRequest
|
|
state.autocompleteAvailable.focus(event)
|
|
if event.Type == atspi.TextInsertedEvent && supportsAutocomplete(event.Source) {
|
|
if live != nil {
|
|
available := state.presentationOptions.SoundCueAvailable
|
|
state.presentationOptions = presentationSettingsFromConfig(live.Load(), true)
|
|
state.presentationOptions.SoundCueAvailable = available
|
|
}
|
|
if address := state.autocompleteAvailable.offered(event, activity); address != "" {
|
|
autocompleteRequest = speechRequest{Text: address, Role: "autocomplete", Cause: "autocomplete-available"}
|
|
if state.presentationOptions.EnableSound && state.presentationOptions.RoleSoundPresentation != "speech_only" &&
|
|
(state.presentationOptions.SoundCueAvailable == nil || state.presentationOptions.SoundCueAvailable("autocomplete")) {
|
|
autocompleteRequest.Sound = []sound.Cue{{Name: "autocomplete"}}
|
|
}
|
|
}
|
|
}
|
|
if event.Source.AutocompletePopup {
|
|
explicitItem := isAutocompletePopupItem(event.Source.Role) &&
|
|
((isFocusGainEvent(event) && intent == input.LineIntent) ||
|
|
(event.Type == "object:state-changed:selected" && event.Detail1 != 0 && intent == input.LineIntent))
|
|
if explicitItem && event.Source.ID() != state.lastAutocompletePopupItem {
|
|
state.lastAutocompletePopupItem = event.Source.ID()
|
|
presentationEvent := event
|
|
presentationEvent.Type = atspi.FocusEvent
|
|
speak(presentationEvent)
|
|
}
|
|
logger.Debug("skipped autocomplete popup noise", "type", event.Type, "role", event.Source.Role)
|
|
return event, autocompleteRequest, true
|
|
}
|
|
if event.Source.Autocomplete {
|
|
if isFocusGainEvent(event) {
|
|
state.lastAutocompletePopupItem = atspi.ObjectID{}
|
|
}
|
|
if (isTextChangeEvent(event.Type) || event.Type == atspi.CaretEvent) && intent == input.LineIntent {
|
|
return event, autocompleteRequest, true // Up/Down presents the popup item, not the entry's replacement text.
|
|
}
|
|
if event.Type == atspi.TextDeletedEvent && intent == input.TypingIntent {
|
|
return event, autocompleteRequest, true // Replacing the selected URL or completion is not a user deletion.
|
|
}
|
|
if event.Type == atspi.TextInsertedEvent && intent == input.TypingIntent {
|
|
event.Payload = state.autocompleteTyping.insertion(event, activity)
|
|
if autocompleteRequest.Text != "" {
|
|
event.Payload = "" // The selected address replaces this insertion's character echo.
|
|
}
|
|
}
|
|
}
|
|
return event, autocompleteRequest, false
|
|
}
|
|
|
|
func isBrowserUINameNoise(event atspi.Event, focus atspi.Snapshot) bool {
|
|
if event.Type != atspi.AccessibleNameEvent {
|
|
return false
|
|
}
|
|
// Browser chrome labels, status bars, and frames can change independently
|
|
// of focus. Current controls and rows still need their new names presented.
|
|
// Orca's web UI noise filter likewise limits these ignored roles.
|
|
switch strings.ToLower(event.Source.ToolkitName) {
|
|
case "chromium", "gecko", "webkit", "webkitgtk":
|
|
return event.Source.Role == "status bar" || event.Source.Role == "label" ||
|
|
event.Source.Role == "frame"
|
|
}
|
|
if event.Source.AutocompletePopup {
|
|
return true
|
|
}
|
|
return focus.Autocomplete && event.Source.ID().BusName != "" &&
|
|
event.Source.ID().BusName == focus.ID().BusName
|
|
}
|
|
|
|
func isAutocompletePopupItem(role string) bool {
|
|
return role == "list item" || role == "menu item"
|
|
}
|