Files

155 lines
4.4 KiB
Go

// 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"
)
// windowOwner keeps application, top-level window, and object focus as
// separate lifecycles. The event loop remains the sole owner of this state.
type windowOwner struct {
applicationBus string
window atspi.ObjectID
focus atspi.ObjectID
activated bool
title string
}
func (owner *windowOwner) activate(event atspi.Event) (duplicate bool) {
window := eventWindow(event)
if window.Valid() && window == owner.window {
if owner.activated {
return true
}
owner.activated = true
owner.title = activationTitle(event)
return false
}
owner.window = window
owner.applicationBus = window.BusName
owner.focus = event.Source.ID()
owner.activated = true
owner.title = activationTitle(event)
return false
}
func (owner *windowOwner) follow(event atspi.Event) bool {
window := eventWindow(event)
if owner.window.Valid() && window.Valid() && window != owner.window {
// A focused object can precede window:activate during an actual X11
// handoff. The native adapter has already rejected objects in a known
// inactive window, so confirmed focused state may establish the new owner.
if !event.Source.Focused {
return false
}
owner.window = window
owner.applicationBus = window.BusName
owner.activated = false
owner.title = ""
}
if !owner.window.Valid() && window.Valid() {
owner.window = window
owner.applicationBus = window.BusName
owner.activated = false
owner.title = ""
}
owner.focus = event.Source.ID()
return true
}
func (owner *windowOwner) end(event atspi.Event, consumedCommand bool) bool {
window := eventWindow(event)
if !owner.window.Valid() || !window.Valid() || window != owner.window {
return false
}
// Python rechecks active state because passive X11 grabs can emit a
// transient deactivate/activate pair while the same window stays active.
if event.Type == atspi.WindowDeactivatedEvent {
if consumedCommand || event.Source.StatesKnown && event.Source.Active {
return false
}
}
owner.clear()
return true
}
// containsLoad permits a document replacement only inside the existing focus
// context. A load completion is not a window activation, even when a provider
// still reports focused or active state for a background document.
func (owner *windowOwner) containsLoad(event atspi.Event) bool {
window := eventWindow(event)
if owner.window.Valid() {
// The old document may become defunct before its replacement loads.
// Its window still owns the context even though object focus is gone.
return window == owner.window && event.Source.BusName == owner.applicationBus
}
return owner.focus.Valid() && event.Source.BusName == owner.focus.BusName
}
func (owner *windowOwner) defunct(event atspi.Event) (windowEnded, focusEnded bool) {
if event.Detail1 == 0 {
return false, false
}
id := event.Source.ID()
if owner.window.Valid() && id == owner.window {
owner.clear()
return true, true
}
if owner.focus.Valid() && id == owner.focus {
owner.focus = atspi.ObjectID{}
return false, true
}
return false, false
}
func (owner *windowOwner) clear() {
owner.applicationBus = ""
owner.window = atspi.ObjectID{}
owner.focus = atspi.ObjectID{}
owner.activated = false
owner.title = ""
}
func activationTitle(event atspi.Event) string {
if title := strings.TrimSpace(event.WindowTitle); title != "" {
return title
}
if event.Source.ID() == eventWindow(event) {
return strings.TrimSpace(event.Source.Name)
}
return ""
}
// Track empty names as well so restoring an earlier title is a real change.
func (owner *windowOwner) changeTitle(event atspi.Event) bool {
if !owner.window.Valid() || event.Source.ID() != owner.window {
return false
}
title := strings.TrimSpace(event.Source.Name)
if title == owner.title {
return false
}
owner.title = title
return title != ""
}
func eventWindow(event atspi.Event) atspi.ObjectID {
if event.Window.Valid() {
return event.Window
}
// Window lifecycle fixtures and older adapters identify the top-level in
// OriginPath. For object events OriginPath is the object itself, so it must
// not be promoted to a window owner.
switch event.Type {
case atspi.WindowActivatedEvent, atspi.WindowDeactivatedEvent, atspi.WindowDestroyedEvent:
return atspi.NewObjectID(event.Source.BusName, event.OriginPath)
default:
return atspi.ObjectID{}
}
}