// 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) } }