%PDF- %PDF-
| Direktori : /lib/python3/dist-packages/orca/ |
| Current File : //lib/python3/dist-packages/orca/focus_manager.py |
# Orca
#
# Copyright 2005-2008 Sun Microsystems Inc.
# Copyright 2016-2023 Igalia, S.L.
#
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA.
"""Module to manage the focused object, window, etc."""
__id__ = "$Id$"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
"Copyright (c) 2016-2023 Igalia, S.L."
__license__ = "LGPL"
from . import braille
from . import debug
from . import script_manager
from .ax_object import AXObject
from .ax_utilities import AXUtilities
CARET_TRACKING = "caret-tracking"
FOCUS_TRACKING = "focus-tracking"
FLAT_REVIEW = "flat-review"
MOUSE_REVIEW = "mouse-review"
OBJECT_NAVIGATOR = "object-navigator"
SAY_ALL = "say-all"
class FocusManager:
"""Manages the focused object, window, etc."""
def __init__(self):
self._window = None
self._focus = None
self._object_of_interest = None
self._active_mode = None
def clear_state(self, reason=""):
"""Clears everything we're tracking."""
msg = "FOCUS MANAGER: Clearing all state"
if reason:
msg += f": {reason}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._focus = None
self._window = None
self._object_of_interest = None
self._active_mode = None
def find_focused_object(self):
"""Returns the focused object in the active window."""
result = AXUtilities.get_focused_object(self._window)
tokens = ["FOCUS MANAGER: Focused object in", self._window, "is", result]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return result
def focus_and_window_are_unknown(self):
"""Returns True if we have no knowledge about what is focused."""
result = self._focus is None and self._window is None
if result:
msg = "FOCUS MANAGER: Focus and window are unknown"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return result
def focus_is_dead(self):
"""Returns True if the locus of focus is dead."""
if not AXObject.is_dead(self._focus):
return False
msg = "FOCUS MANAGER: Focus is dead"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
def focus_is_active_window(self):
"""Returns True if the locus of focus is the active window."""
if self._focus is None:
return False
return self._focus == self._window
def focus_is_in_active_window(self):
"""Returns True if the locus of focus is inside the current window."""
return self._focus is not None and AXObject.is_ancestor(self._focus, self._window)
def emit_region_changed(self, obj, start_offset=None, end_offset=None, mode=None):
"""Notifies interested clients that the current region of interest has changed."""
if start_offset is None:
start_offset = 0
if end_offset is None:
end_offset = start_offset
if mode is None:
mode = FOCUS_TRACKING
try:
obj.emit("mode-changed::" + mode, 1, "")
except Exception as error:
msg = f"FOCUS MANAGER: Exception emitting mode-changed notification: {error}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
if mode != self._active_mode:
tokens = ["FOCUS MANAGER: Switching mode from", self._active_mode, "to", mode]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
self._active_mode = mode
if mode == FLAT_REVIEW:
braille.setBrlapiPriority(braille.BRLAPI_PRIORITY_HIGH)
else:
braille.setBrlapiPriority()
try:
tokens = ["FOCUS MANAGER: Region of interest:", obj, f"({start_offset}, {end_offset})"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
obj.emit("region-changed", start_offset, end_offset)
except Exception as error:
msg = f"FOCUS MANAGER: Exception emitting region-changed notification: {error}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
if obj != self._object_of_interest:
tokens = ["FOCUS MANAGER: Switching object of interest from",
self._object_of_interest, "to", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
self._object_of_interest = obj
def get_active_mode_and_object_of_interest(self):
"""Returns the current mode and associated object of interest"""
tokens = ["FOCUS MANAGER: Active mode:", self._active_mode,
"Object of interest:", self._object_of_interest]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return self._active_mode, self._object_of_interest
def get_locus_of_focus(self):
"""Returns the current locus of focus (i.e. the object with visual focus)."""
tokens = ["FOCUS MANAGER: Locus of focus is", self._focus]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return self._focus
def set_locus_of_focus(self, event, obj, notify_script=True, force=False):
"""Sets the locus of focus (i.e., the object with visual focus)."""
tokens = ["FOCUS MANAGER: Request to set locus of focus to", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True, True)
# We clear the cache on the locus of focus because too many apps and toolkits fail
# to emit the correct accessibility events. We do so recursively on table cells
# to handle bugs like https://gitlab.gnome.org/GNOME/nautilus/-/issues/3253.
recursive = AXUtilities.is_table_cell(obj)
AXObject.clear_cache(obj, recursive, "Setting locus of focus.")
if not force and obj == self._focus:
msg = "FOCUS MANAGER: Setting locus of focus to existing locus of focus"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return
# TODO - JD: Consider always updating the active script here.
script = script_manager.getManager().getActiveScript()
if event and (script and not script.app):
app = AXObject.get_application(event.source)
script = script_manager.getManager().getScript(app, event.source)
script_manager.getManager().setActiveScript(script, "Setting locus of focus")
old_focus = self._focus
if AXObject.is_dead(old_focus):
old_focus = None
if obj is None:
msg = "FOCUS MANAGER: New locus of focus is null (being cleared)"
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._focus = None
return
if AXObject.is_dead(obj):
tokens = ["FOCUS MANAGER: New locus of focus (", obj, ") is dead. Not updating."]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return
if script is not None:
if not AXObject.is_valid(obj):
tokens = ["FOCUS MANAGER: New locus of focus (", obj, ") is invalid. Not updating."]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return
tokens = ["FOCUS MANAGER: Changing locus of focus from", old_focus,
"to", obj, ". Notify:", notify_script]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
self._focus = obj
self.emit_region_changed(obj, mode=FOCUS_TRACKING)
if not notify_script:
return
if script is None:
msg = "FOCUS MANAGER: Cannot notify active script because there isn't one"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return
script.locusOfFocusChanged(event, old_focus, self._focus)
def active_window_is_active(self):
"""Returns True if the window we think is currently active is actually active."""
AXObject.clear_cache(self._window, False, "Ensuring the active window is really active.")
is_active = AXUtilities.is_active(self._window)
tokens = ["FOCUS MANAGER:", self._window, "is active:", is_active]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return is_active
def _is_desktop_frame(self, window):
"""Returns True if this object is the desktop frame"""
if not AXUtilities.is_frame(window):
return False
return AXObject.get_attributes_dict(window).get("is-desktop") == "true"
def can_be_active_window(self, window):
"""Returns True if window can be the active window based on its state."""
if window is None:
return False
AXObject.clear_cache(window, False, "Checking if window can be the active window")
app = AXObject.get_application(window)
tokens = ["FOCUS MANAGER:", window, "from", app]
if not AXUtilities.is_active(window):
tokens.append("lacks active state")
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return False
if not AXUtilities.is_showing(window):
tokens.append("lacks showing state")
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return False
if AXUtilities.is_iconified(window):
tokens.append("is iconified")
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return False
if AXObject.get_name(app) == "mutter-x11-frames":
tokens.append("is from app that cannot have the real active window")
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return False
tokens.append("can be active window")
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return True
def find_active_window(self, *apps):
"""Tries to locate the active window; may or may not succeed."""
candidates = []
apps = apps or AXUtilities.get_all_applications(must_have_window=True)
for app in apps:
candidates.extend(list(AXObject.iter_children(app, self.can_be_active_window)))
if not candidates:
tokens = ["FOCUS MANAGER: Unable to find active window from", apps]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return None
if len(candidates) == 1:
tokens = ["FOCUS MANAGER: Active window is", candidates[0]]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return candidates[0]
tokens = ["FOCUS MANAGER: These windows all claim to be active:", candidates]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
# Some electron apps running in the background claim to be active even when they
# are not. These are the ones we know about. We can add others as we go.
suspect_app_names = ["slack",
"discord",
"outline-client",
"whatsapp-desktop-linux"]
filtered = []
for frame in candidates:
if AXObject.get_name(AXObject.get_application(frame)) in suspect_app_names:
tokens = ["FOCUS MANAGER: Suspecting", frame, "is a non-active Electron app"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
else:
filtered.append(frame)
if len(filtered) == 1:
tokens = ["FOCUS MANAGER: Active window is believed to be", filtered[0]]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return filtered[0]
guess = None
if filtered:
tokens = ["FOCUS MANAGER: Still have multiple active windows:", filtered]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
guess = filtered[0]
tokens = ["FOCUS MANAGER: Returning", guess, "as active window"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return guess
def get_active_window(self):
"""Returns the currently-active window (i.e. without searching or verifying)."""
tokens = ["FOCUS MANAGER: Active window is", self._window]
debug.printTokens(debug.LEVEL_INFO, tokens, True, True)
return self._window
def set_active_window(self, frame, app=None, set_window_as_focus=False, notify_script=False):
"""Sets the active window."""
tokens = ["FOCUS MANAGER: Request to set active window to", frame]
if app is not None:
tokens.extend(["in", app])
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if frame == self._window:
msg = "FOCUS MANAGER: Setting active window to existing active window"
debug.printMessage(debug.LEVEL_INFO, msg, True)
elif frame is None:
self._window = None
else:
real_app, real_frame = AXObject.find_real_app_and_window_for(frame, app)
if real_frame != frame:
tokens = ["FOCUS MANAGER: Correcting active window to", real_frame, "in", real_app]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
self._window = real_frame
else:
self._window = frame
if set_window_as_focus:
self.set_locus_of_focus(None, self._window, notify_script)
elif self._window and self._focus and not self.focus_is_in_active_window():
tokens = ["FOCUS MANAGER: Focus", self._focus, "is not in", self._window]
debug.printTokens(debug.LEVEL_INFO, tokens, True, True)
self.set_locus_of_focus(None, self._window, notify_script=True)
_manager = FocusManager()
def getManager():
return _manager