Files

70 lines
2.6 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package atspi
// IsEchoEditField requires positive editable metadata. Text roles alone also
// describe labels and read-only documents; terminals are not edit fields.
func IsEchoEditField(snapshot Snapshot) bool {
return snapshot.Editable && !snapshot.ReadOnly && !snapshot.Defunct &&
snapshot.Role != "password text" && snapshot.Role != "terminal"
}
type echoFocus struct {
object, origin, window ObjectID
epoch uint64
editable bool
}
// EditFieldFocused reads copied metadata only. The GLib owner publishes it;
// foreground epochs reject stale focus even before queued events are consumed.
func (adapter *Adapter) EditFieldFocused() bool {
focus := adapter.echoFocus.Load()
foreground := adapter.Foreground()
return !adapter.secure.Load() && focus != nil && focus.editable &&
focus.epoch == foreground.Epoch && (!foreground.Client.Known || foreground.Client.Window != 0)
}
func (adapter *Adapter) observeEchoFocus(event Event) {
if event.ForegroundMismatch || event.Foreground.Epoch != adapter.Foreground().Epoch {
return
}
current := adapter.echoFocus.Load()
if event.Type == WindowActivatedEvent && !event.Source.Focused {
if current != nil && event.Window != current.window {
adapter.echoFocus.Store(nil)
}
return
}
gain := event.Type == FocusEvent || event.Type == FocusedStateEvent && event.Detail1 != 0 ||
event.Type == WindowActivatedEvent && event.Source.Focused
if !gain && (current == nil || current.object != event.Source.ID()) {
return
}
if !event.Source.ID().Valid() {
return
}
origin := NewObjectID(event.Source.BusName, event.OriginPath)
if !gain && current != nil {
origin = current.origin
}
adapter.echoFocus.Store(&echoFocus{object: event.Source.ID(), origin: origin,
window: event.Window, epoch: event.Foreground.Epoch,
editable: IsEchoEditField(event.Source) && event.Source.Focused})
}
// Retire before filtering focus losses or dead objects out of speech delivery.
func (adapter *Adapter) retireEchoFocus(kind string, detail int, source ObjectID) {
current := adapter.echoFocus.Load()
if current == nil {
return
}
matched := source == current.object || source == current.origin || source == current.window
if (kind == FocusedStateEvent && detail == 0 || kind == DefunctStateEvent && detail != 0 ||
kind == WindowDestroyedEvent || kind == WindowDeactivatedEvent) && matched ||
(kind == FocusEvent || kind == FocusedStateEvent && detail != 0) && source.BusName == current.object.BusName {
adapter.echoFocus.Store(nil)
}
}