|
|
|
|
@@ -63,41 +63,26 @@ class ActionList(Gtk.Window):
|
|
|
|
|
"""Window containing a list of accessible actions."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, presenter: ActionPresenter):
|
|
|
|
|
super().__init__()
|
|
|
|
|
super().__init__(window_position=Gtk.WindowPosition.MOUSE, transient_for=None)
|
|
|
|
|
self._presenter = presenter
|
|
|
|
|
self._actions = []
|
|
|
|
|
self._setup_gui()
|
|
|
|
|
|
|
|
|
|
def _setup_gui(self) -> None:
|
|
|
|
|
"""Sets up the GUI for the actions list."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.set_title(guilabels.KB_GROUP_ACTIONS)
|
|
|
|
|
self.set_modal(True)
|
|
|
|
|
self.set_decorated(False)
|
|
|
|
|
self.set_skip_taskbar_hint(True)
|
|
|
|
|
self.set_skip_pager_hint(True)
|
|
|
|
|
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
|
|
|
|
# Note: set_window_position is deprecated, using move() instead
|
|
|
|
|
self.move(100, 100) # Position window at reasonable location
|
|
|
|
|
self.set_default_size(400, 300)
|
|
|
|
|
|
|
|
|
|
# Create scrolled window
|
|
|
|
|
scrolled = Gtk.ScrolledWindow()
|
|
|
|
|
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
|
|
|
|
self.add(scrolled)
|
|
|
|
|
|
|
|
|
|
# Create list box
|
|
|
|
|
self._listbox = Gtk.ListBox()
|
|
|
|
|
self._listbox.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
|
|
|
|
self._listbox.connect("row-activated", self._on_row_activated)
|
|
|
|
|
scrolled.add(self._listbox)
|
|
|
|
|
self._listbox.set_margin_top(5)
|
|
|
|
|
self._listbox.set_margin_bottom(5)
|
|
|
|
|
self.add(self._listbox)
|
|
|
|
|
|
|
|
|
|
# Connect key events
|
|
|
|
|
self.connect("key-press-event", self._on_key_press)
|
|
|
|
|
self.connect("destroy", self._on_destroy)
|
|
|
|
|
|
|
|
|
|
self.show_all()
|
|
|
|
|
|
|
|
|
|
def _on_key_press(self, widget, event) -> bool:
|
|
|
|
|
"""Handles key press events."""
|
|
|
|
|
|
|
|
|
|
@@ -108,19 +93,17 @@ class ActionList(Gtk.Window):
|
|
|
|
|
|
|
|
|
|
def _on_row_activated(self, listbox, row) -> None:
|
|
|
|
|
"""Handles row activation (Enter or double-click)."""
|
|
|
|
|
|
|
|
|
|
if row is not None:
|
|
|
|
|
action_index = row.get_index()
|
|
|
|
|
if 0 <= action_index < len(self._actions):
|
|
|
|
|
action_name = self._actions[action_index]
|
|
|
|
|
self._presenter._perform_action(action_name)
|
|
|
|
|
|
|
|
|
|
action_name = getattr(row, "_action_name", None)
|
|
|
|
|
if action_name:
|
|
|
|
|
self._presenter._perform_action(action_name)
|
|
|
|
|
|
|
|
|
|
def _on_destroy(self, widget) -> None:
|
|
|
|
|
"""Handles window destruction."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
GLib.idle_add(self._presenter._clear_gui_and_restore_focus)
|
|
|
|
|
|
|
|
|
|
def populate_actions(self, actions: list[str]) -> None:
|
|
|
|
|
def populate_actions(self, actions: dict[str, str] | list[str]) -> None:
|
|
|
|
|
"""Populates the list with accessible actions."""
|
|
|
|
|
|
|
|
|
|
if isinstance(actions, dict):
|
|
|
|
|
@@ -128,22 +111,19 @@ class ActionList(Gtk.Window):
|
|
|
|
|
else:
|
|
|
|
|
items = [(action, action) for action in actions]
|
|
|
|
|
|
|
|
|
|
self._actions = [name for name, _label in items]
|
|
|
|
|
|
|
|
|
|
# Clear existing items
|
|
|
|
|
for child in self._listbox.get_children():
|
|
|
|
|
self._listbox.remove(child)
|
|
|
|
|
|
|
|
|
|
# Add actions to list
|
|
|
|
|
for _action, label_text in items:
|
|
|
|
|
label = Gtk.Label(label=label_text)
|
|
|
|
|
label.set_xalign(0.0) # Left align
|
|
|
|
|
label.set_margin_left(10)
|
|
|
|
|
label.set_margin_right(10)
|
|
|
|
|
label.set_margin_top(5)
|
|
|
|
|
label.set_margin_bottom(5)
|
|
|
|
|
|
|
|
|
|
self._listbox.add(label)
|
|
|
|
|
for action_name, label_text in items:
|
|
|
|
|
row = Gtk.ListBoxRow()
|
|
|
|
|
label = Gtk.Label(label=label_text, xalign=0)
|
|
|
|
|
label.set_margin_start(10)
|
|
|
|
|
label.set_margin_end(10)
|
|
|
|
|
row.add(label)
|
|
|
|
|
setattr(row, "_action_name", action_name)
|
|
|
|
|
self._listbox.add(row)
|
|
|
|
|
|
|
|
|
|
# Select first item
|
|
|
|
|
if actions:
|
|
|
|
|
@@ -152,7 +132,12 @@ class ActionList(Gtk.Window):
|
|
|
|
|
self._listbox.select_row(first_row)
|
|
|
|
|
first_row.grab_focus()
|
|
|
|
|
|
|
|
|
|
def show_gui(self) -> None:
|
|
|
|
|
"""Shows the window."""
|
|
|
|
|
|
|
|
|
|
self.show_all()
|
|
|
|
|
self.present_with_time(time.time())
|
|
|
|
|
self._listbox.grab_focus()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ActionPresenter:
|
|
|
|
|
@@ -264,7 +249,26 @@ class ActionPresenter:
|
|
|
|
|
"""Clears the GUI reference and then restores focus."""
|
|
|
|
|
|
|
|
|
|
self._gui = None
|
|
|
|
|
GLib.timeout_add(150, self._maybe_restore_focus)
|
|
|
|
|
|
|
|
|
|
def _maybe_restore_focus(self) -> bool:
|
|
|
|
|
"""Restores focus unless it already moved to another object in the target app."""
|
|
|
|
|
|
|
|
|
|
if not AXObject.is_valid(self._obj):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
manager = focus_manager.get_manager()
|
|
|
|
|
current_focus = manager.get_locus_of_focus()
|
|
|
|
|
if current_focus and AXObject.is_valid(current_focus):
|
|
|
|
|
target_app = AXObject.get_application(self._obj)
|
|
|
|
|
focus_app = AXObject.get_application(current_focus)
|
|
|
|
|
if target_app and focus_app == target_app and current_focus != self._obj:
|
|
|
|
|
tokens = ["ACTION PRESENTER: Skipping focus restore; focus now on", current_focus]
|
|
|
|
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
self._restore_focus()
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def _perform_action(self, action: str) -> None:
|
|
|
|
|
"""Attempts to perform the named action."""
|
|
|
|
|
@@ -327,6 +331,7 @@ class ActionPresenter:
|
|
|
|
|
|
|
|
|
|
self._gui = ActionList(self)
|
|
|
|
|
self._gui.populate_actions(actions)
|
|
|
|
|
self._gui.show_gui()
|
|
|
|
|
|
|
|
|
|
debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: GUI created successfully", True)
|
|
|
|
|
return True
|
|
|
|
|
|