126 lines
4.9 KiB
Go
126 lines
4.9 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
package app
|
|
|
|
import (
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
|
"git.stormux.org/storm/cthulhu/internal/review"
|
|
"git.stormux.org/storm/cthulhu/internal/wine"
|
|
"log/slog"
|
|
)
|
|
|
|
type wineReviewFocusRegistry interface {
|
|
Focuses() []wine.ReviewFocusNotice
|
|
Current(atspi.ObjectID) bool
|
|
}
|
|
|
|
// Only the event loop calls this controller. Inputs are copied metadata;
|
|
// foreground queries read the adapter's atomic cache and perform no native I/O.
|
|
type wineReviewFocusController struct {
|
|
registry wineReviewFocusRegistry
|
|
logger *slog.Logger
|
|
state *eventLoopState
|
|
foreground foregroundMonitor
|
|
titles windowTitleSource
|
|
gate *focusGate
|
|
worker *review.Worker
|
|
clear func()
|
|
active wine.ReviewFocusNotice
|
|
epoch uint64
|
|
}
|
|
|
|
func wineReviewBoundsMatch(bounds atspi.Rectangle, client xorg.Foreground) bool {
|
|
return client.Known && client.Window != 0 && bounds.Valid() && ((int64(bounds.X) == int64(client.X) && int64(bounds.Y) == int64(client.Y) && int64(bounds.Width) == int64(client.Width) && int64(bounds.Height) == int64(client.Height)) ||
|
|
(int64(bounds.X) == int64(client.OuterX) && int64(bounds.Y) == int64(client.OuterY) && int64(bounds.Width) == int64(client.OuterWidth) && int64(bounds.Height) == int64(client.OuterHeight)))
|
|
}
|
|
func (c *wineReviewFocusController) eligible(notice wine.ReviewFocusNotice, fg atspi.ForegroundState, title atspi.ForegroundTitle) bool {
|
|
metadata := title.Title
|
|
if !fg.Client.Known || fg.Client.Window == 0 || title.Epoch != fg.Epoch || metadata.Window != fg.Client.Window || !metadata.Wine || !metadata.Focused || !equivalentWindowTitles(metadata.Title, notice.Title) {
|
|
return false
|
|
}
|
|
if metadata.TitleWindow == metadata.Window {
|
|
return wineReviewBoundsMatch(notice.Bounds, fg.Client)
|
|
}
|
|
if metadata.TitleWindow == 0 || metadata.Client.Window != metadata.TitleWindow {
|
|
return false
|
|
}
|
|
if wineReviewBoundsMatch(notice.Bounds, metadata.Client) {
|
|
return true
|
|
}
|
|
// Wine's virtual desktop uses desktop-local screen coordinates. Corroborate
|
|
// a translated rectangle against the actual focused X11 child, not a title.
|
|
return int64(notice.Bounds.X)+int64(fg.Client.X) == int64(metadata.Client.X) && int64(notice.Bounds.Y)+int64(fg.Client.Y) == int64(metadata.Client.Y) &&
|
|
int64(notice.Bounds.Width) == int64(metadata.Client.Width) && int64(notice.Bounds.Height) == int64(metadata.Client.Height) && notice.Bounds.Valid()
|
|
}
|
|
func (c *wineReviewFocusController) changed() {
|
|
if c == nil || c.registry == nil || c.worker == nil || c.foreground == nil || c.titles == nil || c.gate.Locked() {
|
|
if c != nil {
|
|
c.retire()
|
|
}
|
|
return
|
|
}
|
|
fg, title := c.foreground.Foreground(), c.titles.ForegroundTitle()
|
|
var selected wine.ReviewFocusNotice
|
|
count := 0
|
|
for _, notice := range c.registry.Focuses() {
|
|
eligible := c.eligible(notice, fg, title)
|
|
if !eligible && c.logger != nil {
|
|
c.logger.Debug("Wine review foreground mismatch", "sender", notice.Sender,
|
|
"window", notice.Window, "focus", notice.Focus, "revision", notice.Revision,
|
|
"source_bounds", notice.Bounds, "native_foreground", fg.Client,
|
|
"foreground_epoch", fg.Epoch, "title_epoch", title.Epoch, "title_metadata", title.Title)
|
|
}
|
|
if eligible {
|
|
selected = notice
|
|
count++
|
|
}
|
|
}
|
|
if count != 1 || !c.registry.Current(selected.ID()) {
|
|
c.retire()
|
|
return
|
|
}
|
|
c.gate.SetSuspended(false)
|
|
transition := c.active.ID() != selected.ID() || c.active.Revision != selected.Revision || c.active.Protected != selected.Protected || c.epoch != fg.Epoch
|
|
if transition {
|
|
if c.clear != nil {
|
|
c.clear()
|
|
} else {
|
|
c.worker.ClearFocus()
|
|
}
|
|
c.worker.SyncFocus(selected.ID())
|
|
}
|
|
c.active, c.epoch = selected, fg.Epoch
|
|
c.state.current = atspi.Snapshot{BusName: selected.Sender, Path: selected.ID().Path, Role: "text"}
|
|
if selected.Protected {
|
|
c.state.current.Role = "password text"
|
|
}
|
|
c.state.focusedID = selected.ID()
|
|
c.state.owner = windowOwner{applicationBus: selected.Sender, window: wine.ReviewID(selected.Sender, selected.Instance, selected.Window, selected.Window), focus: selected.ID(), activated: true, title: selected.Title}
|
|
}
|
|
func (c *wineReviewFocusController) foregroundChanged() { c.changed() }
|
|
func (c *wineReviewFocusController) retire() {
|
|
if c == nil || c.active.Sender == "" {
|
|
return
|
|
}
|
|
if c.worker != nil && c.worker.Focus() == c.active.ID() {
|
|
if c.clear != nil {
|
|
c.clear()
|
|
} else {
|
|
c.worker.ClearFocus()
|
|
}
|
|
}
|
|
if c.state != nil && c.state.owner.applicationBus == c.active.Sender {
|
|
c.state.owner.clear()
|
|
}
|
|
c.active = wine.ReviewFocusNotice{}
|
|
c.epoch = 0
|
|
}
|
|
func (c *wineReviewFocusController) owns(id atspi.ObjectID) bool {
|
|
return c != nil && c.active.Sender != "" && c.active.ID() == id && c.registry.Current(id)
|
|
}
|
|
func (c *wineReviewFocusController) resultCurrent(result review.Result) bool {
|
|
c.changed()
|
|
return c.owns(result.FlatFocus) && flatReviewResultCurrent(c.worker, result)
|
|
}
|