155 lines
4.4 KiB
Go
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{}
|
|
}
|
|
}
|