Try to improve window title reading.

This commit is contained in:
Storm Dragon
2026-05-31 00:48:44 -04:00
parent 6cd745dc57
commit 43996e7b8c
3 changed files with 221 additions and 6 deletions
+100 -6
View File
@@ -24,6 +24,7 @@ import logging
from gi.repository import GLib
from cthulhu.ax_object import AXObject
from cthulhu import debug
from cthulhu import dbus_service
from cthulhu.plugin import Plugin, cthulhu_hookimpl
@@ -53,6 +54,9 @@ class WindowTitleReader(Plugin):
self._enabled = False
self._pollSourceId = None
self._pollIntervalMs = 100
self._fallbackDelayMs = 250
self._pendingFallbackSourceId = None
self._lastActiveWindowId = None
self._lastTitle = None
self._display = None
self._root = None
@@ -71,6 +75,13 @@ class WindowTitleReader(Plugin):
return True
self._register_keybinding()
self.app.getDynamicApiManager().registerAPI(
"WindowTitleReader",
self,
overwrite=True,
)
if xlibAvailable:
self._start_tracking()
self._activated = True
debug.printMessage(debug.LEVEL_INFO, "WindowTitleReader: Activated", True)
return True
@@ -81,6 +92,8 @@ class WindowTitleReader(Plugin):
return
self._stop_tracking()
if self.app:
self.app.getDynamicApiManager().unregisterAPI("WindowTitleReader")
self._activated = False
debug.printMessage(debug.LEVEL_INFO, "WindowTitleReader: Deactivated", True)
return True
@@ -118,7 +131,8 @@ class WindowTitleReader(Plugin):
def _toggle_tracking(self, script=None, inputEvent=None):
if self._enabled:
self._stop_tracking()
self._enabled = False
self._lastTitle = None
self._present_message("Window title reader off")
return True
@@ -132,6 +146,8 @@ class WindowTitleReader(Plugin):
return True
if self._start_tracking():
self._enabled = True
self._poll_window_title()
self._present_message("Window title reader on")
else:
self._present_message("Window title reader unavailable")
@@ -162,6 +178,8 @@ class WindowTitleReader(Plugin):
return False
if self._start_tracking():
self._enabled = True
self._poll_window_title()
if notify_user:
self._present_message("Window title reader on")
return True
@@ -171,7 +189,8 @@ class WindowTitleReader(Plugin):
return False
if self._enabled:
self._stop_tracking()
self._enabled = False
self._lastTitle = None
if notify_user:
self._present_message("Window title reader off")
@@ -195,7 +214,7 @@ class WindowTitleReader(Plugin):
pluginLogger.exception("WindowTitleReader: Failed to present message")
def _start_tracking(self):
if self._enabled:
if self._pollSourceId is not None:
return True
try:
@@ -203,7 +222,6 @@ class WindowTitleReader(Plugin):
self._root = self._display.screen().root
self._init_atoms()
self._pollSourceId = GLib.timeout_add(self._pollIntervalMs, self._poll_window_title)
self._enabled = True
self._poll_window_title()
return True
except Exception as error:
@@ -217,11 +235,16 @@ class WindowTitleReader(Plugin):
return False
def _stop_tracking(self):
if self._pendingFallbackSourceId is not None:
GLib.source_remove(self._pendingFallbackSourceId)
self._pendingFallbackSourceId = None
if self._pollSourceId is not None:
GLib.source_remove(self._pollSourceId)
self._pollSourceId = None
self._enabled = False
self._lastActiveWindowId = None
self._lastTitle = None
self._cleanup_display()
@@ -246,7 +269,7 @@ class WindowTitleReader(Plugin):
}
def _poll_window_title(self):
if not self._enabled:
if self._pollSourceId is None:
return False
try:
@@ -260,7 +283,14 @@ class WindowTitleReader(Plugin):
self._lastTitle = None
return True
if windowTitle != self._lastTitle:
activeWindowId = activeWindow.id
if self._lastActiveWindowId is None:
self._lastActiveWindowId = activeWindowId
elif activeWindowId != self._lastActiveWindowId:
self._lastActiveWindowId = activeWindowId
self._schedule_fallback_title()
if self._enabled and windowTitle != self._lastTitle:
self._present_title(windowTitle)
self._lastTitle = windowTitle
except Exception as error:
@@ -269,6 +299,70 @@ class WindowTitleReader(Plugin):
return True
def _schedule_fallback_title(self):
if self._enabled:
return
if self._pendingFallbackSourceId is not None:
GLib.source_remove(self._pendingFallbackSourceId)
self._pendingFallbackSourceId = GLib.timeout_add(
self._fallbackDelayMs,
self._present_pending_fallback_title,
)
def _present_pending_fallback_title(self):
self._pendingFallbackSourceId = None
titleText = self.get_fallback_title()
if titleText:
self._present_title(titleText)
return False
def get_fallback_title(self, atspiTitle=None):
"""Returns the X11 title when AT-SPI has not exposed an equivalent title."""
if not xlibAvailable and self._display is None:
return ""
startedTracking = self._pollSourceId is not None
if not startedTracking and not self._start_tracking():
return ""
activeWindow = self._get_active_window()
titleText = self._get_current_title(activeWindow) if activeWindow else ""
if not titleText:
return ""
if atspiTitle is None:
atspiTitle = self._get_atspi_title()
if self._titles_match(atspiTitle, titleText):
return ""
return titleText
def _get_atspi_title(self):
if not self.app:
return ""
appState = self.app.getDynamicApiManager().getAPI("CthulhuState")
if not appState:
return ""
return AXObject.get_name(appState.activeWindow) or ""
def _titles_match(self, atspiTitle, fallbackTitle):
if not atspiTitle or not fallbackTitle:
return False
if self._is_wine_desktop_title(atspiTitle):
return False
normalizedAtspiTitle = " ".join(atspiTitle.casefold().split())
normalizedFallbackTitle = " ".join(fallbackTitle.casefold().split())
return normalizedAtspiTitle in normalizedFallbackTitle \
or normalizedFallbackTitle in normalizedAtspiTitle
def _present_title(self, titleText):
if not self.app:
return
+24
View File
@@ -33,6 +33,7 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
__license__ = "LGPL"
from . import cmdnames
from . import cthulhu
from . import debug
from . import input_event
from . import keybindings
@@ -319,10 +320,33 @@ class WhereAmIPresenter:
return True
title = script.speechGenerator.generateTitle(obj)
fallbackTitle = self._get_fallback_title(title)
if fallbackTitle:
script.presentMessage(fallbackTitle)
return True
for (string, voice) in title:
script.presentMessage(string, voice=voice)
return True
@staticmethod
def _get_fallback_title(generatedTitle):
"""Returns a non-AT-SPI title when the active title reader can provide one."""
try:
reader = cthulhu.getManager().getDynamicApiManager().getAPI("WindowTitleReader")
except Exception:
return ""
if reader is None:
return ""
atspiTitle = " ".join(
str(item[0]) for item in generatedTitle
if isinstance(item, (list, tuple)) and item
)
return reader.get_fallback_title(atspiTitle)
def _present_default_button(self, script, event=None, dialog=None, error_messages=True):
"""Presents the default button of the current dialog."""