Try to make Cthulhu python 3.9 compatible. Fixed keybinding and notifications bugs.
This commit is contained in:
@@ -34,7 +34,7 @@ __copyright__ = "Copyright (c) 2023 Igalia, S.L."
|
||||
__license__ = "LGPL"
|
||||
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
import gi
|
||||
|
||||
@@ -103,7 +103,7 @@ class ActionList(Gtk.Window):
|
||||
|
||||
GLib.idle_add(self._presenter._clear_gui_and_restore_focus)
|
||||
|
||||
def populate_actions(self, actions: dict[str, str] | list[str]) -> None:
|
||||
def populate_actions(self, actions: Union[dict[str, str], list[str]]) -> None:
|
||||
"""Populates the list with accessible actions."""
|
||||
|
||||
if isinstance(actions, dict):
|
||||
|
||||
@@ -29,6 +29,7 @@ __date__ = "$Date$"
|
||||
__copyright__ = "Copyright (c) 2023 Igalia, S.L."
|
||||
__license__ = "LGPL"
|
||||
|
||||
from typing import Optional
|
||||
import time
|
||||
|
||||
import gi
|
||||
@@ -48,15 +49,15 @@ class AXCollection:
|
||||
# pylint: disable=R0913,R0914
|
||||
@staticmethod
|
||||
def create_match_rule(
|
||||
states: list[str] | None = None,
|
||||
states: Optional[list[str]] = None,
|
||||
state_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL,
|
||||
attributes: list[str] | None = None,
|
||||
attributes: Optional[list[str]] = None,
|
||||
attribute_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL,
|
||||
roles: list[str] | None = None,
|
||||
roles: Optional[list[str]] = None,
|
||||
role_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL,
|
||||
interfaces: list[str] | None = None,
|
||||
interfaces: Optional[list[str]] = None,
|
||||
interface_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL,
|
||||
invert: bool = False) -> Atspi.MatchRule | None:
|
||||
invert: bool = False) -> Optional[Atspi.MatchRule]:
|
||||
"""Creates a match rule based on the supplied criteria."""
|
||||
|
||||
if states is None:
|
||||
@@ -136,7 +137,7 @@ class AXCollection:
|
||||
obj: Atspi.Accessible,
|
||||
rule: Atspi.MatchRule,
|
||||
order: Atspi.CollectionSortOrder = Atspi.CollectionSortOrder.CANONICAL
|
||||
) -> Atspi.Accessible | None:
|
||||
) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the first object matching the specified rule."""
|
||||
|
||||
if not AXObject.supports_collection(obj):
|
||||
|
||||
@@ -34,6 +34,7 @@ __copyright__ = "Copyright (c) 2024 Igalia, S.L." \
|
||||
"Copyright (c) 2024 GNOME Foundation Inc."
|
||||
__license__ = "LGPL"
|
||||
|
||||
from typing import Optional
|
||||
import functools
|
||||
|
||||
import gi
|
||||
@@ -285,7 +286,7 @@ class AXComponent:
|
||||
@staticmethod
|
||||
def _find_descendant_at_point(
|
||||
obj: Atspi.Accessible, x: int, y: int
|
||||
) -> Atspi.Accessible | None:
|
||||
) -> Optional[Atspi.Accessible]:
|
||||
"""Checks each child to see if it has a descendant at the specified point."""
|
||||
|
||||
for child in AXObject.iter_children(obj):
|
||||
@@ -297,7 +298,7 @@ class AXComponent:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_object_at_point(obj: Atspi.Accessible, x: int, y: int) -> Atspi.Accessible | None:
|
||||
def _get_object_at_point(obj: Atspi.Accessible, x: int, y: int) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the child (or descendant?) of obj at the specified point."""
|
||||
|
||||
if not AXObject.supports_component(obj):
|
||||
@@ -317,7 +318,7 @@ class AXComponent:
|
||||
@staticmethod
|
||||
def _get_descendant_at_point(
|
||||
obj: Atspi.Accessible, x: int, y: int
|
||||
) -> Atspi.Accessible | None:
|
||||
) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the deepest descendant of obj at the specified point."""
|
||||
|
||||
child = AXComponent._get_object_at_point(obj, x, y)
|
||||
@@ -338,7 +339,7 @@ class AXComponent:
|
||||
@staticmethod
|
||||
def get_descendant_at_point(
|
||||
obj: Atspi.Accessible, x: int, y: int
|
||||
) -> Atspi.Accessible | None:
|
||||
) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the deepest descendant of obj at the specified point."""
|
||||
|
||||
result = AXComponent._get_descendant_at_point(obj, x, y)
|
||||
|
||||
@@ -34,6 +34,7 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
|
||||
"Copyright (c) 2018-2023 Igalia, S.L."
|
||||
__license__ = "LGPL"
|
||||
|
||||
from typing import Optional
|
||||
import gi
|
||||
gi.require_version("Atspi", "2.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
@@ -60,8 +61,8 @@ class AXEventSynthesizer:
|
||||
@staticmethod
|
||||
def _is_scrolled_off_screen(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None,
|
||||
ancestor: Atspi.Accessible | None = None
|
||||
offset: Optional[int] = None,
|
||||
ancestor: Optional[Atspi.Accessible] = None
|
||||
) -> bool:
|
||||
"""Returns true if obj, or the caret offset therein, is scrolled off-screen."""
|
||||
|
||||
@@ -119,7 +120,7 @@ class AXEventSynthesizer:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _mouse_event_on_character(obj: Atspi.Accessible, offset: int | None, event: str) -> bool:
|
||||
def _mouse_event_on_character(obj: Atspi.Accessible, offset: Optional[int], event: str) -> bool:
|
||||
"""Performs the specified mouse event on the current character in obj."""
|
||||
|
||||
if offset is None:
|
||||
@@ -164,7 +165,7 @@ class AXEventSynthesizer:
|
||||
return AXEventSynthesizer._generate_mouse_event(obj, relative_x, relative_y, event)
|
||||
|
||||
@staticmethod
|
||||
def route_to_character(obj: Atspi.Accessible, offset: int | None = None) -> bool:
|
||||
def route_to_character(obj: Atspi.Accessible, offset: Optional[int] = None) -> bool:
|
||||
"""Routes the pointer to the current character in obj."""
|
||||
|
||||
tokens = [f"AXEventSynthesizer: Attempting to route to offset {offset} in", obj]
|
||||
@@ -181,7 +182,7 @@ class AXEventSynthesizer:
|
||||
|
||||
@staticmethod
|
||||
def click_character(
|
||||
obj: Atspi.Accessible, offset: int | None = None, button: int = 1
|
||||
obj: Atspi.Accessible, offset: Optional[int] = None, button: int = 1
|
||||
) -> bool:
|
||||
"""Single click on the current character in obj using the specified button."""
|
||||
|
||||
@@ -198,7 +199,7 @@ class AXEventSynthesizer:
|
||||
@staticmethod
|
||||
def _scroll_to_location(
|
||||
obj: Atspi.Accessible, location: Atspi.ScrollType,
|
||||
start_offset: int | None = None, end_offset: int | None = None
|
||||
start_offset: Optional[int] = None, end_offset: Optional[int] = None
|
||||
) -> None:
|
||||
"""Attempts to scroll to the specified location."""
|
||||
|
||||
@@ -220,7 +221,7 @@ class AXEventSynthesizer:
|
||||
@staticmethod
|
||||
def _scroll_to_point(
|
||||
obj: Atspi.Accessible, x_coord: int, y_coord: int,
|
||||
start_offset: int | None = None, end_offset: int | None = None
|
||||
start_offset: Optional[int] = None, end_offset: Optional[int] = None
|
||||
) -> None:
|
||||
"""Attempts to scroll obj to the specified point."""
|
||||
|
||||
@@ -242,8 +243,8 @@ class AXEventSynthesizer:
|
||||
@staticmethod
|
||||
def scroll_into_view(
|
||||
obj: Atspi.Accessible,
|
||||
start_offset: int | None = None,
|
||||
end_offset: int | None = None
|
||||
start_offset: Optional[int] = None,
|
||||
end_offset: Optional[int] = None
|
||||
) -> None:
|
||||
"""Attempts to scroll obj into view."""
|
||||
|
||||
@@ -253,8 +254,8 @@ class AXEventSynthesizer:
|
||||
@staticmethod
|
||||
def scroll_to_center(
|
||||
obj: Atspi.Accessible,
|
||||
start_offset: int | None = None,
|
||||
end_offset: int | None = None
|
||||
start_offset: Optional[int] = None,
|
||||
end_offset: Optional[int] = None
|
||||
) -> None:
|
||||
"""Attempts to scroll obj to the center of its window."""
|
||||
|
||||
@@ -272,8 +273,8 @@ class AXEventSynthesizer:
|
||||
@staticmethod
|
||||
def scroll_to_top_edge(
|
||||
obj: Atspi.Accessible,
|
||||
start_offset: int | None = None,
|
||||
end_offset: int | None = None
|
||||
start_offset: Optional[int] = None,
|
||||
end_offset: Optional[int] = None
|
||||
) -> None:
|
||||
"""Attempts to scroll obj to the top edge."""
|
||||
|
||||
@@ -283,8 +284,8 @@ class AXEventSynthesizer:
|
||||
@staticmethod
|
||||
def scroll_to_top_left(
|
||||
obj: Atspi.Accessible,
|
||||
start_offset: int | None = None,
|
||||
end_offset: int | None = None
|
||||
start_offset: Optional[int] = None,
|
||||
end_offset: Optional[int] = None
|
||||
) -> None:
|
||||
"""Attempts to scroll obj to the top left."""
|
||||
|
||||
@@ -294,8 +295,8 @@ class AXEventSynthesizer:
|
||||
@staticmethod
|
||||
def scroll_to_left_edge(
|
||||
obj: Atspi.Accessible,
|
||||
start_offset: int | None = None,
|
||||
end_offset: int | None = None
|
||||
start_offset: Optional[int] = None,
|
||||
end_offset: Optional[int] = None
|
||||
) -> None:
|
||||
"""Attempts to scroll obj to the left edge."""
|
||||
|
||||
@@ -305,8 +306,8 @@ class AXEventSynthesizer:
|
||||
@staticmethod
|
||||
def scroll_to_bottom_edge(
|
||||
obj: Atspi.Accessible,
|
||||
start_offset: int | None = None,
|
||||
end_offset: int | None = None
|
||||
start_offset: Optional[int] = None,
|
||||
end_offset: Optional[int] = None
|
||||
) -> None:
|
||||
"""Attempts to scroll obj to the bottom edge."""
|
||||
|
||||
@@ -316,8 +317,8 @@ class AXEventSynthesizer:
|
||||
@staticmethod
|
||||
def scroll_to_bottom_right(
|
||||
obj: Atspi.Accessible,
|
||||
start_offset: int | None = None,
|
||||
end_offset: int | None = None
|
||||
start_offset: Optional[int] = None,
|
||||
end_offset: Optional[int] = None
|
||||
) -> None:
|
||||
"""Attempts to scroll obj to the bottom right."""
|
||||
|
||||
@@ -327,8 +328,8 @@ class AXEventSynthesizer:
|
||||
@staticmethod
|
||||
def scroll_to_right_edge(
|
||||
obj: Atspi.Accessible,
|
||||
start_offset: int | None = None,
|
||||
end_offset: int | None = None
|
||||
start_offset: Optional[int] = None,
|
||||
end_offset: Optional[int] = None
|
||||
) -> None:
|
||||
"""Attempts to scroll obj to the right edge."""
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ __date__ = "$Date$"
|
||||
__copyright__ = "Copyright (c) 2024 Igalia, S.L."
|
||||
__license__ = "LGPL"
|
||||
|
||||
from typing import Optional
|
||||
import os
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
@@ -66,7 +67,7 @@ class AXHypertext:
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
def _get_link_at_index(obj: Atspi.Accessible, index: int) -> Atspi.Hyperlink | None:
|
||||
def _get_link_at_index(obj: Atspi.Accessible, index: int) -> Optional[Atspi.Hyperlink]:
|
||||
"""Returns the hyperlink object at the specified index."""
|
||||
|
||||
if not AXObject.supports_hypertext(obj):
|
||||
@@ -202,7 +203,7 @@ class AXHypertext:
|
||||
return basename
|
||||
|
||||
@staticmethod
|
||||
def find_child_at_offset(obj: Atspi.Accessible, offset: int) -> Atspi.Accessible | None:
|
||||
def find_child_at_offset(obj: Atspi.Accessible, offset: int) -> Optional[Atspi.Accessible]:
|
||||
"""Attempts to correct for off-by-one brokenness in implementations"""
|
||||
|
||||
if child := AXHypertext.get_child_at_offset(obj, offset):
|
||||
@@ -227,7 +228,7 @@ class AXHypertext:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_child_at_offset(obj: Atspi.Accessible, offset: int) -> Atspi.Accessible | None:
|
||||
def get_child_at_offset(obj: Atspi.Accessible, offset: int) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the embedded-object child of obj at the specified offset."""
|
||||
|
||||
if not AXObject.supports_hypertext(obj):
|
||||
|
||||
+21
-21
@@ -34,7 +34,7 @@ __license__ = "LGPL"
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable, Generator
|
||||
from typing import Callable, Generator, Optional
|
||||
|
||||
import gi
|
||||
gi.require_version("Atspi", "2.0")
|
||||
@@ -105,7 +105,7 @@ class AXObject:
|
||||
return name.lower()
|
||||
|
||||
@staticmethod
|
||||
def get_application(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_application(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the accessible application associated with obj."""
|
||||
|
||||
if obj is None:
|
||||
@@ -313,7 +313,7 @@ class AXObject:
|
||||
return iface is not None
|
||||
|
||||
@staticmethod
|
||||
def find_real_app_and_window_for(obj: Atspi.Accessible, app: Atspi.Accessible | None = None):
|
||||
def find_real_app_and_window_for(obj: Atspi.Accessible, app: Optional[Atspi.Accessible] = None):
|
||||
"""Work around for window events coming from mutter-x11-frames."""
|
||||
|
||||
if app is None:
|
||||
@@ -545,7 +545,7 @@ class AXObject:
|
||||
return index
|
||||
|
||||
@staticmethod
|
||||
def get_parent(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_parent(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the accessible parent of obj. See also get_parent_checked."""
|
||||
|
||||
if not AXObject.is_valid(obj):
|
||||
@@ -571,7 +571,7 @@ class AXObject:
|
||||
return parent
|
||||
|
||||
@staticmethod
|
||||
def get_parent_checked(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_parent_checked(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the parent of obj, doing checks for tree validity"""
|
||||
|
||||
if not AXObject.is_valid(obj):
|
||||
@@ -619,7 +619,7 @@ class AXObject:
|
||||
def get_common_ancestor(
|
||||
obj1: Atspi.Accessible,
|
||||
obj2: Atspi.Accessible
|
||||
) -> Atspi.Accessible | None:
|
||||
) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the common ancestor of obj1 and obj2."""
|
||||
|
||||
tokens = ["AXObject: Looking for common ancestor of", obj1, "and", obj2]
|
||||
@@ -647,7 +647,7 @@ class AXObject:
|
||||
def find_ancestor_inclusive(
|
||||
obj: Atspi.Accessible,
|
||||
pred: Callable[[Atspi.Accessible], bool]
|
||||
) -> Atspi.Accessible | None:
|
||||
) -> Optional[Atspi.Accessible]:
|
||||
"""Returns obj, or the ancestor of obj, for which the function pred is true"""
|
||||
|
||||
if pred(obj):
|
||||
@@ -659,7 +659,7 @@ class AXObject:
|
||||
def find_ancestor(
|
||||
obj: Atspi.Accessible,
|
||||
pred: Callable[[Atspi.Accessible], bool]
|
||||
) -> Atspi.Accessible | None:
|
||||
) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the ancestor of obj if the function pred is true"""
|
||||
|
||||
if not AXObject.is_valid(obj):
|
||||
@@ -703,7 +703,7 @@ class AXObject:
|
||||
return AXObject.find_ancestor(obj, lambda x: x == ancestor) is not None
|
||||
|
||||
@staticmethod
|
||||
def get_child(obj: Atspi.Accessible, index: int) -> Atspi.Accessible | None:
|
||||
def get_child(obj: Atspi.Accessible, index: int) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the nth child of obj. See also get_child_checked."""
|
||||
|
||||
if not AXObject.is_valid(obj):
|
||||
@@ -736,7 +736,7 @@ class AXObject:
|
||||
@staticmethod
|
||||
def get_child_checked(
|
||||
obj: Atspi.Accessible, index: int
|
||||
) -> Atspi.Accessible | None:
|
||||
) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the nth child of obj, doing checks for tree validity"""
|
||||
|
||||
if not AXObject.is_valid(obj):
|
||||
@@ -757,7 +757,7 @@ class AXObject:
|
||||
def get_active_descendant_checked(
|
||||
container: Atspi.Accessible,
|
||||
reported_child: Atspi.Accessible
|
||||
) -> Atspi.Accessible | None:
|
||||
) -> Optional[Atspi.Accessible]:
|
||||
"""Checks the reported active descendant and return the real/valid one."""
|
||||
|
||||
if not AXObject.has_state(container, Atspi.StateType.MANAGES_DESCENDANTS):
|
||||
@@ -784,7 +784,7 @@ class AXObject:
|
||||
def _find_descendant(
|
||||
obj: Atspi.Accessible,
|
||||
pred: Callable[[Atspi.Accessible], bool]
|
||||
) -> Atspi.Accessible | None:
|
||||
) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the descendant of obj if the function pred is true"""
|
||||
|
||||
if not AXObject.is_valid(obj):
|
||||
@@ -806,7 +806,7 @@ class AXObject:
|
||||
def find_descendant(
|
||||
obj: Atspi.Accessible,
|
||||
pred: Callable[[Atspi.Accessible], bool]
|
||||
) -> Atspi.Accessible | None:
|
||||
) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the descendant of obj if the function pred is true"""
|
||||
|
||||
start = time.time()
|
||||
@@ -816,7 +816,7 @@ class AXObject:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def find_deepest_descendant(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def find_deepest_descendant(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the deepest descendant of obj"""
|
||||
|
||||
if not AXObject.is_valid(obj):
|
||||
@@ -831,8 +831,8 @@ class AXObject:
|
||||
@staticmethod
|
||||
def _find_all_descendants(
|
||||
obj: Atspi.Accessible,
|
||||
include_if: Callable[[Atspi.Accessible], bool] | None,
|
||||
exclude_if: Callable[[Atspi.Accessible], bool] | None,
|
||||
include_if: Optional[Callable[[Atspi.Accessible], bool]],
|
||||
exclude_if: Optional[Callable[[Atspi.Accessible], bool]],
|
||||
matches: list[Atspi.Accessible]
|
||||
) -> None:
|
||||
"""Returns all descendants which match the specified inclusion and exclusion"""
|
||||
@@ -852,8 +852,8 @@ class AXObject:
|
||||
@staticmethod
|
||||
def find_all_descendants(
|
||||
root: Atspi.Accessible,
|
||||
include_if: Callable[[Atspi.Accessible], bool] | None = None,
|
||||
exclude_if: Callable[[Atspi.Accessible], bool] | None = None
|
||||
include_if: Optional[Callable[[Atspi.Accessible], bool]] = None,
|
||||
exclude_if: Optional[Callable[[Atspi.Accessible], bool]] = None
|
||||
) -> list[Atspi.Accessible]:
|
||||
"""Returns all descendants which match the specified inclusion and exclusion"""
|
||||
|
||||
@@ -1044,7 +1044,7 @@ class AXObject:
|
||||
@staticmethod
|
||||
def iter_children(
|
||||
obj: Atspi.Accessible,
|
||||
pred: Callable[[Atspi.Accessible], bool] | None = None
|
||||
pred: Optional[Callable[[Atspi.Accessible], bool]] = None
|
||||
) -> Generator[Atspi.Accessible, None, None]:
|
||||
"""Generator to iterate through obj's children. If the function pred is
|
||||
specified, children for which pred is False will be skipped."""
|
||||
@@ -1068,7 +1068,7 @@ class AXObject:
|
||||
yield child
|
||||
|
||||
@staticmethod
|
||||
def get_previous_sibling(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_previous_sibling(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the previous sibling of obj, based on child indices"""
|
||||
|
||||
if not AXObject.is_valid(obj):
|
||||
@@ -1091,7 +1091,7 @@ class AXObject:
|
||||
return sibling
|
||||
|
||||
@staticmethod
|
||||
def get_next_sibling(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_next_sibling(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the next sibling of obj, based on child indices"""
|
||||
|
||||
if not AXObject.is_valid(obj):
|
||||
|
||||
@@ -28,6 +28,7 @@ __date__ = "$Date$"
|
||||
__copyright__ = "Copyright (c) 2023 Igalia, S.L."
|
||||
__license__ = "LGPL"
|
||||
|
||||
from typing import Optional
|
||||
import gi
|
||||
gi.require_version("Atspi", "2.0")
|
||||
from gi.repository import Atspi
|
||||
@@ -60,7 +61,7 @@ class AXSelection:
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
def get_selected_child(obj: Atspi.Accessible, index: int) -> Atspi.Accessible | None:
|
||||
def get_selected_child(obj: Atspi.Accessible, index: int) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the nth selected child of obj."""
|
||||
|
||||
n_children = AXSelection.get_selected_child_count(obj)
|
||||
|
||||
+27
-27
@@ -35,7 +35,7 @@ __license__ = "LGPL"
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import Generator
|
||||
from typing import Generator, Optional
|
||||
|
||||
import gi
|
||||
gi.require_version("Atspi", "2.0")
|
||||
@@ -62,11 +62,11 @@ class AXTable:
|
||||
PHYSICAL_SPANS_FROM_TABLE: dict[int, tuple[int, int]] = {}
|
||||
PHYSICAL_COLUMN_COUNT: dict[int, int] = {}
|
||||
PHYSICAL_ROW_COUNT: dict[int, int] = {}
|
||||
PRESENTABLE_COORDINATES: dict[int, tuple[str | None, str | None]] = {}
|
||||
PRESENTABLE_COORDINATES: dict[int, tuple[Optional[str], Optional[str]]] = {}
|
||||
PRESENTABLE_COORDINATES_LABELS: dict[int, str] = {}
|
||||
PRESENTABLE_SPANS: dict[int, tuple[str | None, str | None]] = {}
|
||||
PRESENTABLE_COLUMN_COUNT: dict[int, int | None] = {}
|
||||
PRESENTABLE_ROW_COUNT: dict[int, int | None] = {}
|
||||
PRESENTABLE_SPANS: dict[int, tuple[Optional[str], Optional[str]]] = {}
|
||||
PRESENTABLE_COLUMN_COUNT: dict[int, Optional[int]] = {}
|
||||
PRESENTABLE_ROW_COUNT: dict[int, Optional[int]] = {}
|
||||
COLUMN_HEADERS_FOR_CELL: dict[int, list[Atspi.Accessible]] = {}
|
||||
ROW_HEADERS_FOR_CELL: dict[int, list[Atspi.Accessible]] = {}
|
||||
|
||||
@@ -121,7 +121,7 @@ class AXTable:
|
||||
AXTable._clear_all_dictionaries(reason)
|
||||
|
||||
@staticmethod
|
||||
def get_caption(table: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_caption(table: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the accessible object containing the caption of table."""
|
||||
|
||||
if not AXObject.supports_table(table):
|
||||
@@ -171,7 +171,7 @@ class AXTable:
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
def _get_column_count_from_attribute(table: Atspi.Accessible) -> int | None:
|
||||
def _get_column_count_from_attribute(table: Atspi.Accessible) -> Optional[int]:
|
||||
"""Returns the value of the 'colcount' object attribute or None if not found."""
|
||||
|
||||
if hash(table) in AXTable.PRESENTABLE_COLUMN_COUNT:
|
||||
@@ -217,7 +217,7 @@ class AXTable:
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
def _get_row_count_from_attribute(table: Atspi.Accessible) -> int | None:
|
||||
def _get_row_count_from_attribute(table: Atspi.Accessible) -> Optional[int]:
|
||||
"""Returns the value of the 'rowcount' object attribute or None if not found."""
|
||||
|
||||
if hash(table) in AXTable.PRESENTABLE_ROW_COUNT:
|
||||
@@ -344,7 +344,7 @@ class AXTable:
|
||||
return AXTable.get_selected_column_count(table) == cols
|
||||
|
||||
@staticmethod
|
||||
def get_cell_at(table: Atspi.Accessible, row: int, column: int) -> Atspi.Accessible | None:
|
||||
def get_cell_at(table: Atspi.Accessible, row: int, column: int) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the cell at the 0-indexed row and column."""
|
||||
|
||||
if not AXObject.supports_table(table):
|
||||
@@ -405,7 +405,7 @@ class AXTable:
|
||||
@staticmethod
|
||||
def _get_cell_spans_from_attribute(
|
||||
cell: Atspi.Accessible
|
||||
) -> tuple[str | None, str | None]:
|
||||
) -> tuple[Optional[str], Optional[str]]:
|
||||
"""Returns the row and column spans exposed via object attribute, or None, None."""
|
||||
|
||||
if hash(cell) in AXTable.PRESENTABLE_SPANS:
|
||||
@@ -606,7 +606,7 @@ class AXTable:
|
||||
@staticmethod
|
||||
def get_new_row_headers(
|
||||
cell: Atspi.Accessible,
|
||||
old_cell: Atspi.Accessible | None
|
||||
old_cell: Optional[Atspi.Accessible]
|
||||
) -> list[Atspi.Accessible]:
|
||||
"""Returns row headers of cell that are not also headers of old_cell. """
|
||||
|
||||
@@ -623,7 +623,7 @@ class AXTable:
|
||||
@staticmethod
|
||||
def get_new_column_headers(
|
||||
cell: Atspi.Accessible,
|
||||
old_cell: Atspi.Accessible | None
|
||||
old_cell: Optional[Atspi.Accessible]
|
||||
) -> list[Atspi.Accessible]:
|
||||
"""Returns column headers of cell that are not also headers of old_cell. """
|
||||
|
||||
@@ -880,7 +880,7 @@ class AXTable:
|
||||
@staticmethod
|
||||
def _get_cell_coordinates_from_attribute(
|
||||
cell: Atspi.Accessible
|
||||
) -> tuple[str | None, str | None]:
|
||||
) -> tuple[Optional[str], Optional[str]]:
|
||||
"""Returns the 1-based indices for cell exposed via object attribute, or None, None."""
|
||||
|
||||
if cell is None:
|
||||
@@ -941,7 +941,7 @@ class AXTable:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_table(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_table(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns obj if it is a table, otherwise returns the ancestor table of obj."""
|
||||
|
||||
if obj is None:
|
||||
@@ -981,21 +981,21 @@ class AXTable:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_first_cell(table: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_first_cell(table: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the first cell in table."""
|
||||
|
||||
row, col = 0, 0
|
||||
return AXTable.get_cell_at(table, row, col)
|
||||
|
||||
@staticmethod
|
||||
def get_last_cell(table: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_last_cell(table: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the last cell in table."""
|
||||
|
||||
row, col = AXTable.get_row_count(table) - 1, AXTable.get_column_count(table) - 1
|
||||
return AXTable.get_cell_at(table, row, col)
|
||||
|
||||
@staticmethod
|
||||
def get_cell_above(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_cell_above(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the cell above cell in table."""
|
||||
|
||||
row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
|
||||
@@ -1003,7 +1003,7 @@ class AXTable:
|
||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
||||
|
||||
@staticmethod
|
||||
def get_cell_below(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_cell_below(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the cell below cell in table."""
|
||||
|
||||
row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
|
||||
@@ -1011,7 +1011,7 @@ class AXTable:
|
||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
||||
|
||||
@staticmethod
|
||||
def get_cell_on_left(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_cell_on_left(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the cell to the left."""
|
||||
|
||||
row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
|
||||
@@ -1019,7 +1019,7 @@ class AXTable:
|
||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
||||
|
||||
@staticmethod
|
||||
def get_cell_on_right(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_cell_on_right(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the cell to the right."""
|
||||
|
||||
row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
|
||||
@@ -1027,14 +1027,14 @@ class AXTable:
|
||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
||||
|
||||
@staticmethod
|
||||
def get_start_of_row(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_start_of_row(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the cell at the start of cell's row."""
|
||||
|
||||
row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0]
|
||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, 0)
|
||||
|
||||
@staticmethod
|
||||
def get_end_of_row(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_end_of_row(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the cell at the end of cell's row."""
|
||||
|
||||
row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0]
|
||||
@@ -1043,14 +1043,14 @@ class AXTable:
|
||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
||||
|
||||
@staticmethod
|
||||
def get_top_of_column(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_top_of_column(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the cell at the top of cell's column."""
|
||||
|
||||
col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1]
|
||||
return AXTable.get_cell_at(AXTable.get_table(cell), 0, col)
|
||||
|
||||
@staticmethod
|
||||
def get_bottom_of_column(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_bottom_of_column(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the cell at the bottom of cell's column."""
|
||||
|
||||
col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1]
|
||||
@@ -1059,7 +1059,7 @@ class AXTable:
|
||||
return AXTable.get_cell_at(AXTable.get_table(cell), row, col)
|
||||
|
||||
@staticmethod
|
||||
def get_cell_formula(cell: Atspi.Accessible) -> str | None:
|
||||
def get_cell_formula(cell: Atspi.Accessible) -> Optional[str]:
|
||||
"""Returns the formula associated with this cell."""
|
||||
|
||||
attrs = AXObject.get_attributes_dict(cell, use_cache=False)
|
||||
@@ -1192,7 +1192,7 @@ class AXTable:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_dynamic_row_header(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_dynamic_row_header(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the user-set row header for cell."""
|
||||
|
||||
table = AXTable.get_table(cell)
|
||||
@@ -1207,7 +1207,7 @@ class AXTable:
|
||||
return AXTable.get_cell_at(table, cell_row, headers_column)
|
||||
|
||||
@staticmethod
|
||||
def get_dynamic_column_header(cell: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_dynamic_column_header(cell: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the user-set column header for cell."""
|
||||
|
||||
table = AXTable.get_table(cell)
|
||||
|
||||
+32
-32
@@ -44,7 +44,7 @@ __license__ = "LGPL"
|
||||
import enum
|
||||
import locale
|
||||
import re
|
||||
from typing import Generator
|
||||
from typing import Generator, Optional
|
||||
|
||||
import gi
|
||||
gi.require_version("Atspi", "2.0")
|
||||
@@ -104,7 +104,7 @@ class AXTextAttribute(enum.Enum):
|
||||
WRITING_MODE = ("writing-mode", False)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, string: str) -> "AXTextAttribute" | None:
|
||||
def from_string(cls, string: str) -> Optional['AXTextAttribute']:
|
||||
"""Returns the AXTextAttribute for the specified string."""
|
||||
|
||||
for attribute in cls:
|
||||
@@ -114,7 +114,7 @@ class AXTextAttribute(enum.Enum):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_localized_string(cls, string: str) -> "AXTextAttribute" | None:
|
||||
def from_localized_string(cls, string: str) -> Optional['AXTextAttribute']:
|
||||
"""Returns the AXTextAttribute for the specified localized string."""
|
||||
|
||||
for attribute in cls:
|
||||
@@ -192,7 +192,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_character_at_offset(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the (character, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -239,7 +239,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_next_character(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the next (character, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -264,7 +264,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_previous_character(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the previous (character, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -291,7 +291,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def iter_character(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> Generator[tuple[str, int, int], None, None]:
|
||||
"""Generator to iterate by character in obj starting with the character at offset."""
|
||||
|
||||
@@ -312,7 +312,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_word_at_offset(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the (word, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -354,7 +354,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_next_word(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the next (word, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -379,7 +379,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_previous_word(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the previous (word, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -406,7 +406,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def iter_word(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> Generator[tuple[str, int, int], None, None]:
|
||||
"""Generator to iterate by word in obj starting with the word at offset."""
|
||||
|
||||
@@ -427,7 +427,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_line_at_offset(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the (line, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -495,7 +495,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_next_line(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the next (line, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -520,7 +520,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_previous_line(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the previous (line, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -549,7 +549,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def iter_line(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> Generator[tuple[str, int, int], None, None]:
|
||||
"""Generator to iterate by line in obj starting with the line at offset."""
|
||||
|
||||
@@ -606,7 +606,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def _get_sentence_at_offset_fallback(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Fallback sentence detection for broken implementations."""
|
||||
|
||||
@@ -633,7 +633,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_sentence_at_offset(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the (sentence, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -682,7 +682,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_next_sentence(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the next (sentence, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -707,7 +707,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_previous_sentence(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the previous (sentence, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -734,7 +734,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def iter_sentence(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> Generator[tuple[str, int, int], None, None]:
|
||||
"""Generator to iterate by sentence in obj starting with the sentence at offset."""
|
||||
|
||||
@@ -759,7 +759,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_paragraph_at_offset(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the (paragraph, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -801,7 +801,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_next_paragraph(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the next (paragraph, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -826,7 +826,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_previous_paragraph(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[str, int, int]:
|
||||
"""Returns the previous (paragraph, start, end) for the current or specified offset."""
|
||||
|
||||
@@ -852,7 +852,7 @@ class AXText:
|
||||
|
||||
@staticmethod
|
||||
def iter_paragraph(
|
||||
obj: Atspi.Accessible, offset: int | None = None
|
||||
obj: Atspi.Accessible, offset: Optional[int] = None
|
||||
) -> Generator[tuple[str, int, int], None, None]:
|
||||
"""Generator to iterate by paragraph in obj starting with the paragraph at offset."""
|
||||
|
||||
@@ -1206,7 +1206,7 @@ class AXText:
|
||||
@staticmethod
|
||||
def get_text_attributes_at_offset(
|
||||
obj: Atspi.Accessible,
|
||||
offset: int | None = None
|
||||
offset: Optional[int] = None
|
||||
) -> tuple[dict[str, str], int, int]:
|
||||
"""Returns a (dict, start, end) tuple for attributes at offset in obj."""
|
||||
|
||||
@@ -1303,7 +1303,7 @@ class AXText:
|
||||
return offset
|
||||
|
||||
@staticmethod
|
||||
def get_character_rect(obj: Atspi.Accessible, offset: int | None = None) -> Atspi.Rect:
|
||||
def get_character_rect(obj: Atspi.Accessible, offset: Optional[int] = None) -> Atspi.Rect:
|
||||
"""Returns the Atspi rect of the character at the specified offset in obj."""
|
||||
|
||||
if not AXObject.supports_text(obj):
|
||||
@@ -1487,7 +1487,7 @@ class AXText:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def string_has_spelling_error(obj: Atspi.Accessible, offset: int | None = None) -> bool:
|
||||
def string_has_spelling_error(obj: Atspi.Accessible, offset: Optional[int] = None) -> bool:
|
||||
"""Returns True if the text attributes indicate a spelling error."""
|
||||
|
||||
attributes = AXText.get_text_attributes_at_offset(obj, offset)[0]
|
||||
@@ -1502,7 +1502,7 @@ class AXText:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def string_has_grammar_error(obj: Atspi.Accessible, offset: int | None = None) -> bool:
|
||||
def string_has_grammar_error(obj: Atspi.Accessible, offset: Optional[int] = None) -> bool:
|
||||
"""Returns True if the text attributes indicate a grammar error."""
|
||||
|
||||
attributes = AXText.get_text_attributes_at_offset(obj, offset)[0]
|
||||
@@ -1552,8 +1552,8 @@ class AXText:
|
||||
obj: Atspi.Accessible,
|
||||
x: int,
|
||||
y: int,
|
||||
start_offset: int | None = None,
|
||||
end_offset: int | None = None
|
||||
start_offset: Optional[int] = None,
|
||||
end_offset: Optional[int] = None
|
||||
) -> bool:
|
||||
"""Attempts to scroll obj to the specified point."""
|
||||
|
||||
@@ -1583,8 +1583,8 @@ class AXText:
|
||||
def scroll_substring_to_location(
|
||||
obj: Atspi.Accessible,
|
||||
location: Atspi.ScrollType,
|
||||
start_offset: int | None = None,
|
||||
end_offset: int | None = None
|
||||
start_offset: Optional[int] = None,
|
||||
end_offset: Optional[int] = None
|
||||
) -> bool:
|
||||
"""Attempts to scroll the substring to the specified Atspi.ScrollType location."""
|
||||
|
||||
|
||||
+13
-12
@@ -34,6 +34,7 @@ __date__ = "$Date$"
|
||||
__copyright__ = "Copyright (c) 2023-2025 Igalia, S.L."
|
||||
__license__ = "LGPL"
|
||||
|
||||
from typing import Optional
|
||||
import functools
|
||||
import inspect
|
||||
import queue
|
||||
@@ -97,7 +98,7 @@ class AXUtilities:
|
||||
AXUtilities.IS_LAYOUT_ONLY.clear()
|
||||
|
||||
@staticmethod
|
||||
def clear_all_cache_now(obj: Atspi.Accessible | None = None, reason: str = "") -> None:
|
||||
def clear_all_cache_now(obj: Optional[Atspi.Accessible] = None, reason: str = "") -> None:
|
||||
"""Clears all cached information immediately."""
|
||||
|
||||
AXUtilities._clear_all_dictionaries(reason)
|
||||
@@ -152,7 +153,7 @@ class AXUtilities:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def find_active_window() -> Atspi.Accessible | None:
|
||||
def find_active_window() -> Optional[Atspi.Accessible]:
|
||||
"""Tries to locate the active window; may or may not succeed."""
|
||||
|
||||
candidates = []
|
||||
@@ -192,7 +193,7 @@ class AXUtilities:
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
return filtered[0]
|
||||
|
||||
guess: Atspi.Accessible | None = None
|
||||
guess: Optional[Atspi.Accessible] = None
|
||||
if filtered:
|
||||
tokens = ["AXUtilities: Still have multiple active windows:", filtered]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
@@ -266,7 +267,7 @@ class AXUtilities:
|
||||
return AXObject.find_all_descendants(obj, is_match)
|
||||
|
||||
@staticmethod
|
||||
def get_default_button(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_default_button(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the default button descendant of obj"""
|
||||
|
||||
result = None
|
||||
@@ -278,7 +279,7 @@ class AXUtilities:
|
||||
return AXObject.find_descendant(obj, AXUtilitiesRole.is_default_button)
|
||||
|
||||
@staticmethod
|
||||
def get_focused_object(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_focused_object(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the focused descendant of obj"""
|
||||
|
||||
result = None
|
||||
@@ -290,7 +291,7 @@ class AXUtilities:
|
||||
return AXObject.find_descendant(obj, AXUtilitiesState.is_focused)
|
||||
|
||||
@staticmethod
|
||||
def get_info_bar(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_info_bar(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the info bar descendant of obj"""
|
||||
|
||||
result = None
|
||||
@@ -302,7 +303,7 @@ class AXUtilities:
|
||||
return AXObject.find_descendant(obj, AXUtilitiesRole.is_info_bar)
|
||||
|
||||
@staticmethod
|
||||
def get_status_bar(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_status_bar(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the status bar descendant of obj"""
|
||||
|
||||
result = None
|
||||
@@ -748,7 +749,7 @@ class AXUtilities:
|
||||
return len(ancestors)
|
||||
|
||||
@staticmethod
|
||||
def get_next_object(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_next_object(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the next object (depth first, unless there's a flows-to relation)"""
|
||||
|
||||
if not AXObject.is_valid(obj):
|
||||
@@ -778,7 +779,7 @@ class AXUtilities:
|
||||
return next_object
|
||||
|
||||
@staticmethod
|
||||
def get_previous_object(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_previous_object(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the previous object (depth first, unless there's a flows-from relation)"""
|
||||
|
||||
if not AXObject.is_valid(obj):
|
||||
@@ -810,7 +811,7 @@ class AXUtilities:
|
||||
@staticmethod
|
||||
def is_on_screen(
|
||||
obj: Atspi.Accessible,
|
||||
bounding_box: Atspi.Rect | None = None
|
||||
bounding_box: Optional[Atspi.Rect] = None
|
||||
) -> bool:
|
||||
"""Returns true if obj should be treated as being on screen."""
|
||||
|
||||
@@ -887,7 +888,7 @@ class AXUtilities:
|
||||
def _get_on_screen_objects(
|
||||
root: Atspi.Accessible,
|
||||
cancellation_event: threading.Event,
|
||||
bounding_box: Atspi.Rect | None = None
|
||||
bounding_box: Optional[Atspi.Rect] = None
|
||||
) -> list:
|
||||
|
||||
tokens = ["AXUtilities: Getting on-screen objects in", root, f"({hex(id(root))})"]
|
||||
@@ -939,7 +940,7 @@ class AXUtilities:
|
||||
@staticmethod
|
||||
def get_on_screen_objects(
|
||||
root: Atspi.Accessible,
|
||||
bounding_box: Atspi.Rect | None = None,
|
||||
bounding_box: Optional[Atspi.Rect] = None,
|
||||
timeout: float = 5.0
|
||||
) -> list:
|
||||
"""Returns a list of onscreen objects in the given root."""
|
||||
|
||||
@@ -30,6 +30,7 @@ __copyright__ = "Copyright (c) 2023-2024 Igalia, S.L." \
|
||||
"Copyright (c) 2024 GNOME Foundation Inc."
|
||||
__license__ = "LGPL"
|
||||
|
||||
from typing import Optional
|
||||
import subprocess
|
||||
|
||||
import gi
|
||||
@@ -82,7 +83,7 @@ class AXUtilitiesApplication:
|
||||
return list(AXObject.iter_children(desktop, pred))
|
||||
|
||||
@staticmethod
|
||||
def get_application(obj: Atspi.Accessible) -> Atspi.Accessible | None:
|
||||
def get_application(obj: Atspi.Accessible) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the accessible application associated with obj"""
|
||||
|
||||
if obj is None:
|
||||
@@ -131,7 +132,7 @@ class AXUtilitiesApplication:
|
||||
return version
|
||||
|
||||
@staticmethod
|
||||
def get_application_with_pid(pid: int) -> Atspi.Accessible | None:
|
||||
def get_application_with_pid(pid: int) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the accessible application with the specified pid"""
|
||||
|
||||
applications = AXUtilitiesApplication.get_all_applications()
|
||||
@@ -144,7 +145,7 @@ class AXUtilitiesApplication:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_desktop() -> Atspi.Accessible | None:
|
||||
def get_desktop() -> Optional[Atspi.Accessible]:
|
||||
"""Returns the accessible desktop"""
|
||||
|
||||
try:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@ __copyright__ = "Copyright (c) 2024 Igalia, S.L." \
|
||||
"Copyright (c) 2024 GNOME Foundation Inc."
|
||||
__license__ = "LGPL"
|
||||
|
||||
from typing import Optional
|
||||
import threading
|
||||
import time
|
||||
|
||||
@@ -118,7 +119,7 @@ class AXUtilitiesRelation:
|
||||
def _get_relation(
|
||||
obj: Atspi.Accessible,
|
||||
relation_type: Atspi.RelationType
|
||||
) -> Atspi.Relation | None:
|
||||
) -> Optional[Atspi.Relation]:
|
||||
"""Returns the specified Atspi.Relation for obj"""
|
||||
|
||||
for relation in AXUtilitiesRelation.get_relations(obj):
|
||||
|
||||
+242
-241
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@ __copyright__ = "Copyright (c) 2024 Igalia, S.L." \
|
||||
"Copyright (c) 2024 GNOME Foundation Inc."
|
||||
__license__ = "LGPL"
|
||||
|
||||
from typing import Optional
|
||||
import threading
|
||||
import time
|
||||
|
||||
@@ -149,7 +150,7 @@ class AXValue:
|
||||
return f"{current:.{decimal_places}f}"
|
||||
|
||||
@staticmethod
|
||||
def get_value_as_percent(obj: Atspi.Accessible) -> int | None:
|
||||
def get_value_as_percent(obj: Atspi.Accessible) -> Optional[int]:
|
||||
"""Returns the current value as a percent, or None if that is not applicable."""
|
||||
|
||||
if not AXObject.supports_value(obj):
|
||||
|
||||
@@ -3571,9 +3571,8 @@
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<items>
|
||||
<item translatable="yes">Claude Code (Enhanced)</item>
|
||||
<item translatable="yes">Claude (Anthropic)</item>
|
||||
<item translatable="yes">ChatGPT (OpenAI)</item>
|
||||
<item translatable="yes">Claude Code (CLI)</item>
|
||||
<item translatable="yes">Codex (CLI)</item>
|
||||
<item translatable="yes">Gemini (Google)</item>
|
||||
<item translatable="yes">Ollama (Local - Free)</item>
|
||||
</items>
|
||||
|
||||
@@ -57,6 +57,7 @@ from . import cthulhu_state
|
||||
from . import settings
|
||||
from . import settings_manager
|
||||
from . import input_event
|
||||
from . import input_event_manager
|
||||
from . import keybindings
|
||||
from . import pronunciation_dict
|
||||
from . import braille
|
||||
@@ -64,6 +65,7 @@ from . import speech
|
||||
from . import speechserver
|
||||
from . import text_attribute_names
|
||||
from . import sound_theme_manager
|
||||
from . import script_manager
|
||||
from .ax_object import AXObject
|
||||
|
||||
_settingsManager = settings_manager.getManager()
|
||||
@@ -1874,10 +1876,12 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
# Set provider combo
|
||||
provider = prefs.get("aiProvider", settings.aiProvider)
|
||||
providerIndex = 0 # Default to Claude Code
|
||||
if provider == settings.AI_PROVIDER_GEMINI:
|
||||
if provider == settings.AI_PROVIDER_CODEX:
|
||||
providerIndex = 1
|
||||
elif provider == settings.AI_PROVIDER_OLLAMA:
|
||||
elif provider == settings.AI_PROVIDER_GEMINI:
|
||||
providerIndex = 2
|
||||
elif provider == settings.AI_PROVIDER_OLLAMA:
|
||||
providerIndex = 3
|
||||
self.aiProviderCombo.set_active(providerIndex)
|
||||
|
||||
# Set API key file
|
||||
@@ -1947,6 +1951,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
# Update labels based on provider
|
||||
if provider == settings.AI_PROVIDER_CLAUDE_CODE:
|
||||
self.aiApiKeyEntry.set_placeholder_text("No API key needed - uses Claude Code CLI")
|
||||
elif provider == settings.AI_PROVIDER_CODEX:
|
||||
self.aiApiKeyEntry.set_placeholder_text("No API key needed - uses Codex CLI")
|
||||
elif provider == settings.AI_PROVIDER_OLLAMA:
|
||||
self.aiApiKeyEntry.set_placeholder_text("No API key needed - uses local Ollama")
|
||||
else:
|
||||
@@ -3065,6 +3071,14 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
|
||||
self._presentMessage(messages.KB_ENTER_NEW_KEY)
|
||||
cthulhu_state.capturingKeys = True
|
||||
try:
|
||||
script_manager.get_manager().get_active_script().removeKeyGrabs()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
input_event_manager.get_manager().unmap_all_modifiers()
|
||||
except Exception:
|
||||
pass
|
||||
editable.connect('key-press-event', self.kbKeyPressed)
|
||||
return
|
||||
|
||||
@@ -3073,6 +3087,10 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
|
||||
cthulhu_state.capturingKeys = False
|
||||
self._capturedKey = []
|
||||
try:
|
||||
script_manager.get_manager().get_active_script().refreshKeyGrabs()
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
def _processKeyCaptured(self, keyPressedEvent):
|
||||
@@ -3090,9 +3108,17 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
entries = entries_for_keycode[-1]
|
||||
eventString = Gdk.keyval_name(entries[0])
|
||||
eventState = keyPressedEvent.state
|
||||
eventKeyvalName = Gdk.keyval_name(keyPressedEvent.keyval)
|
||||
|
||||
cthulhuMods = settings.cthulhuModifierKeys
|
||||
if eventString in cthulhuMods:
|
||||
if eventKeyvalName in cthulhuMods:
|
||||
eventString = eventKeyvalName
|
||||
self._capturedKey = ['', keybindings.CTHULHU_MODIFIER_MASK, 0]
|
||||
return False
|
||||
if eventKeyvalName == "KP_0" \
|
||||
and "KP_Insert" in cthulhuMods \
|
||||
and eventState & Gdk.ModifierType.SHIFT_MASK:
|
||||
eventString = "KP_Insert"
|
||||
self._capturedKey = ['', keybindings.CTHULHU_MODIFIER_MASK, 0]
|
||||
return False
|
||||
|
||||
@@ -3180,6 +3206,10 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
|
||||
cthulhu_state.capturingKeys = False
|
||||
self._capturedKey = []
|
||||
try:
|
||||
script_manager.get_manager().get_active_script().refreshKeyGrabs()
|
||||
except Exception:
|
||||
pass
|
||||
myiter = treeModel.get_iter_from_string(path)
|
||||
try:
|
||||
originalBinding = treeModel.get_value(myiter, text)
|
||||
@@ -3793,7 +3823,12 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
|
||||
def aiProviderChanged(self, widget):
|
||||
"""AI Provider combo box changed handler"""
|
||||
providers = [settings.AI_PROVIDER_CLAUDE_CODE, settings.AI_PROVIDER_GEMINI, settings.AI_PROVIDER_OLLAMA]
|
||||
providers = [
|
||||
settings.AI_PROVIDER_CLAUDE_CODE,
|
||||
settings.AI_PROVIDER_CODEX,
|
||||
settings.AI_PROVIDER_GEMINI,
|
||||
settings.AI_PROVIDER_OLLAMA
|
||||
]
|
||||
activeIndex = widget.get_active()
|
||||
if 0 <= activeIndex < len(providers):
|
||||
provider = providers[activeIndex]
|
||||
@@ -3970,4 +4005,3 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
||||
"""OCR copy to clipboard checkbox toggled handler"""
|
||||
self.prefsDict["ocrCopyToClipboard"] = widget.get_active()
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ __license__ = "LGPL"
|
||||
|
||||
import enum
|
||||
import inspect
|
||||
from typing import Callable
|
||||
from typing import Callable, Optional
|
||||
|
||||
try:
|
||||
from dasbus.connection import SessionMessageBus
|
||||
@@ -156,7 +156,7 @@ class _HandlerInfo:
|
||||
description: str,
|
||||
action: Callable[..., bool],
|
||||
handler_type: 'HandlerType' = HandlerType.COMMAND,
|
||||
parameters: list[tuple[str, str]] | None = None
|
||||
parameters: Optional[list[tuple[str, str]]] = None
|
||||
):
|
||||
self.python_function_name: str = python_function_name
|
||||
self.description: str = description
|
||||
@@ -548,10 +548,10 @@ class CthulhuRemoteController:
|
||||
OBJECT_PATH = "/org/stormux/Cthulhu/Service"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._dbus_service_interface: CthulhuDBusServiceInterface | None = None
|
||||
self._dbus_service_interface: Optional[CthulhuDBusServiceInterface] = None
|
||||
self._is_running: bool = False
|
||||
self._bus: SessionMessageBus | None = None
|
||||
self._event_loop: EventLoop | None = None
|
||||
self._bus: Optional[SessionMessageBus] = None
|
||||
self._event_loop: Optional[EventLoop] = None
|
||||
self._pending_registrations: dict[str, object] = {}
|
||||
self._total_commands: int = 0
|
||||
self._total_getters: int = 0
|
||||
|
||||
@@ -40,7 +40,7 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
|
||||
"Copyright (c) 2016-2023 Igalia, S.L."
|
||||
__license__ = "LGPL"
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import gi
|
||||
gi.require_version("Atspi", "2.0")
|
||||
@@ -75,13 +75,13 @@ class FocusManager:
|
||||
"""Manages the focused object, window, etc."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._window: Atspi.Accessible | None = cthulhu_state.activeWindow
|
||||
self._focus: Atspi.Accessible | None = cthulhu_state.locusOfFocus
|
||||
self._object_of_interest: Atspi.Accessible | None = cthulhu_state.objOfInterest
|
||||
self._active_mode: str | None = cthulhu_state.activeMode
|
||||
self._window: Optional[Atspi.Accessible] = cthulhu_state.activeWindow
|
||||
self._focus: Optional[Atspi.Accessible] = cthulhu_state.locusOfFocus
|
||||
self._object_of_interest: Optional[Atspi.Accessible] = cthulhu_state.objOfInterest
|
||||
self._active_mode: Optional[str] = cthulhu_state.activeMode
|
||||
self._last_cell_coordinates: tuple[int, int] = (-1, -1)
|
||||
self._last_cursor_position: tuple[Atspi.Accessible | None, int] = (None, -1)
|
||||
self._penultimate_cursor_position: tuple[Atspi.Accessible | None, int] = (None, -1)
|
||||
self._last_cursor_position: tuple[Optional[Atspi.Accessible], int] = (None, -1)
|
||||
self._penultimate_cursor_position: tuple[Optional[Atspi.Accessible], int] = (None, -1)
|
||||
|
||||
msg = "FOCUS MANAGER: Registering D-Bus commands."
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
@@ -104,7 +104,7 @@ class FocusManager:
|
||||
cthulhu_state.objOfInterest = None
|
||||
cthulhu_state.activeMode = None
|
||||
|
||||
def find_focused_object(self) -> Atspi.Accessible | None:
|
||||
def find_focused_object(self) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the focused object in the active window."""
|
||||
|
||||
result = _get_ax_utilities().get_focused_object(self._window)
|
||||
@@ -147,9 +147,9 @@ class FocusManager:
|
||||
|
||||
def emit_region_changed(
|
||||
self, obj: Atspi.Accessible,
|
||||
start_offset: int | None = None,
|
||||
end_offset: int | None = None,
|
||||
mode: str | None = None
|
||||
start_offset: Optional[int] = None,
|
||||
end_offset: Optional[int] = None,
|
||||
mode: Optional[str] = None
|
||||
) -> None:
|
||||
"""Notifies interested clients that the current region of interest has changed."""
|
||||
|
||||
@@ -192,7 +192,7 @@ class FocusManager:
|
||||
|
||||
def get_active_mode_and_object_of_interest(
|
||||
self
|
||||
) -> tuple[str | None, Atspi.Accessible | None]:
|
||||
) -> tuple[Optional[str], Optional[Atspi.Accessible]]:
|
||||
"""Returns the current mode and associated object of interest"""
|
||||
|
||||
tokens = ["FOCUS MANAGER: Active mode:", self._active_mode,
|
||||
@@ -200,7 +200,7 @@ class FocusManager:
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
return self._active_mode, self._object_of_interest
|
||||
|
||||
def get_penultimate_cursor_position(self) -> tuple[Atspi.Accessible | None, int]:
|
||||
def get_penultimate_cursor_position(self) -> tuple[Optional[Atspi.Accessible], int]:
|
||||
"""Returns the penultimate cursor position as a tuple of (object, offset)."""
|
||||
|
||||
obj, offset = self._penultimate_cursor_position
|
||||
@@ -208,7 +208,7 @@ class FocusManager:
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
return obj, offset
|
||||
|
||||
def get_last_cursor_position(self) -> tuple[Atspi.Accessible | None, int]:
|
||||
def get_last_cursor_position(self) -> tuple[Optional[Atspi.Accessible], int]:
|
||||
"""Returns the last cursor position as a tuple of (object, offset)."""
|
||||
|
||||
obj, offset = self._last_cursor_position
|
||||
@@ -216,7 +216,7 @@ class FocusManager:
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
return obj, offset
|
||||
|
||||
def set_last_cursor_position(self, obj: Atspi.Accessible | None, offset: int) -> None:
|
||||
def set_last_cursor_position(self, obj: Optional[Atspi.Accessible], offset: int) -> None:
|
||||
"""Sets the last cursor position as a tuple of (object, offset)."""
|
||||
|
||||
tokens = ["FOCUS MANAGER: Setting last cursor position to", obj, offset]
|
||||
@@ -239,7 +239,7 @@ class FocusManager:
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
self._last_cell_coordinates = row, column
|
||||
|
||||
def get_locus_of_focus(self) -> Atspi.Accessible | None:
|
||||
def get_locus_of_focus(self) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the current locus of focus (i.e. the object with visual focus)."""
|
||||
|
||||
tokens = ["FOCUS MANAGER: Locus of focus is", self._focus]
|
||||
@@ -248,8 +248,8 @@ class FocusManager:
|
||||
|
||||
def set_locus_of_focus(
|
||||
self,
|
||||
event: Atspi.Event | None,
|
||||
obj: Atspi.Accessible | None,
|
||||
event: Optional[Atspi.Event],
|
||||
obj: Optional[Atspi.Accessible],
|
||||
notify_script: bool = True,
|
||||
force: bool = False
|
||||
) -> None:
|
||||
@@ -340,7 +340,7 @@ class FocusManager:
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
return is_active
|
||||
|
||||
def get_active_window(self) -> Atspi.Accessible | None:
|
||||
def get_active_window(self) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the currently-active window (i.e. without searching or verifying)."""
|
||||
|
||||
tokens = ["FOCUS MANAGER: Active window is", self._window]
|
||||
@@ -349,8 +349,8 @@ class FocusManager:
|
||||
|
||||
def set_active_window(
|
||||
self,
|
||||
frame: Atspi.Accessible | None,
|
||||
app: Atspi.Accessible | None = None,
|
||||
frame: Optional[Atspi.Accessible],
|
||||
app: Optional[Atspi.Accessible] = None,
|
||||
set_window_as_focus: bool = False,
|
||||
notify_script: bool = False
|
||||
) -> None:
|
||||
@@ -390,7 +390,7 @@ class FocusManager:
|
||||
def toggle_presentation_mode(
|
||||
self,
|
||||
script: default.Script,
|
||||
event: InputEvent | None = None,
|
||||
event: Optional[InputEvent] = None,
|
||||
notify_user: bool = True
|
||||
) -> bool:
|
||||
"""Switches between browse mode and focus mode (web content only)."""
|
||||
@@ -401,7 +401,7 @@ class FocusManager:
|
||||
def toggle_layout_mode(
|
||||
self,
|
||||
script: default.Script,
|
||||
event: InputEvent | None = None,
|
||||
event: Optional[InputEvent] = None,
|
||||
notify_user: bool = True
|
||||
) -> bool:
|
||||
"""Switches between object mode and layout mode for line presentation (web content only)."""
|
||||
@@ -412,7 +412,7 @@ class FocusManager:
|
||||
def enable_sticky_browse_mode(
|
||||
self,
|
||||
script: default.Script,
|
||||
event: InputEvent | None = None,
|
||||
event: Optional[InputEvent] = None,
|
||||
notify_user: bool = True
|
||||
) -> bool:
|
||||
"""Enables sticky browse mode (web content only)."""
|
||||
@@ -423,7 +423,7 @@ class FocusManager:
|
||||
def enable_sticky_focus_mode(
|
||||
self,
|
||||
script: default.Script,
|
||||
event: InputEvent | None = None,
|
||||
event: Optional[InputEvent] = None,
|
||||
notify_user: bool = True
|
||||
) -> bool:
|
||||
"""Enables sticky focus mode (web content only)."""
|
||||
|
||||
@@ -39,7 +39,7 @@ __copyright__ = "Copyright (c) 2024 Igalia, S.L." \
|
||||
"Copyright (c) 2024 GNOME Foundation Inc."
|
||||
__license__ = "LGPL"
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import gi
|
||||
gi.require_version("Atspi", "2.0")
|
||||
@@ -50,6 +50,7 @@ from . import focus_manager
|
||||
from . import input_event
|
||||
from . import script_manager
|
||||
from . import settings
|
||||
from . import cthulhu_state
|
||||
from .ax_object import AXObject
|
||||
from .ax_utilities import AXUtilities
|
||||
|
||||
@@ -60,9 +61,9 @@ class InputEventManager:
|
||||
"""Provides utilities for managing input events."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._last_input_event: input_event.InputEvent | None = None
|
||||
self._last_non_modifier_key_event: input_event.KeyboardEvent | None = None
|
||||
self._device: Atspi.Device | None = None
|
||||
self._last_input_event: Optional[input_event.InputEvent] = None
|
||||
self._last_non_modifier_key_event: Optional[input_event.KeyboardEvent] = None
|
||||
self._device: Optional[Atspi.Device] = None
|
||||
self._mapped_keycodes: list[int] = []
|
||||
self._mapped_keysyms: list[int] = []
|
||||
self._grabbed_bindings: dict[int, keybindings.KeyBinding] = {}
|
||||
@@ -267,6 +268,10 @@ class InputEventManager:
|
||||
msg = "INPUT EVENT MANAGER: Keyboard event processing is paused."
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
return False
|
||||
if cthulhu_state.capturingKeys:
|
||||
msg = "INPUT EVENT MANAGER: Capturing keys; ignoring keyboard event."
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
return False
|
||||
|
||||
event = input_event.KeyboardEvent(pressed, keycode, keysym, modifiers, text)
|
||||
if event in [self._last_input_event, self._last_non_modifier_key_event]:
|
||||
|
||||
@@ -38,6 +38,8 @@ import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Gdk
|
||||
|
||||
from . import cmdnames
|
||||
from . import debug
|
||||
@@ -275,16 +277,20 @@ class NotificationPresenter:
|
||||
class NotificationListGUI:
|
||||
"""The dialog containing the notifications list."""
|
||||
|
||||
RESPONSE_COPY = 1
|
||||
|
||||
def __init__(self, script, title, column_headers, rows):
|
||||
self._script = script
|
||||
self._model = None
|
||||
self._tree = None
|
||||
self._gui = self._create_dialog(title, column_headers, rows)
|
||||
|
||||
def _create_dialog(self, title, column_headers, rows):
|
||||
dialog = Gtk.Dialog(title,
|
||||
None,
|
||||
Gtk.DialogFlags.MODAL,
|
||||
(Gtk.STOCK_CLEAR, Gtk.ResponseType.APPLY,
|
||||
(Gtk.STOCK_COPY, self.RESPONSE_COPY,
|
||||
Gtk.STOCK_CLEAR, Gtk.ResponseType.APPLY,
|
||||
Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE))
|
||||
dialog.set_default_size(600, 400)
|
||||
|
||||
@@ -315,6 +321,11 @@ class NotificationListGUI:
|
||||
self._model.set_value(row_iter, i, cell)
|
||||
|
||||
tree.set_model(self._model)
|
||||
selection = tree.get_selection()
|
||||
selection.set_mode(Gtk.SelectionMode.SINGLE)
|
||||
if self._model.iter_n_children(None) > 0:
|
||||
selection.select_path(0)
|
||||
self._tree = tree
|
||||
dialog.connect("response", self.on_response)
|
||||
return dialog
|
||||
|
||||
@@ -325,6 +336,10 @@ class NotificationListGUI:
|
||||
self._gui.destroy()
|
||||
return
|
||||
|
||||
if response == self.RESPONSE_COPY:
|
||||
self._copy_selected_notification()
|
||||
return
|
||||
|
||||
if response == Gtk.ResponseType.APPLY and self._model is not None:
|
||||
self._model.clear()
|
||||
getPresenter().clear_list()
|
||||
@@ -336,10 +351,39 @@ class NotificationListGUI:
|
||||
"""Shows the notifications list dialog."""
|
||||
|
||||
self._gui.show_all()
|
||||
time_stamp = cthulhu_state.lastInputEvent.timestamp
|
||||
if time_stamp == 0:
|
||||
time_stamp = Gtk.get_current_event_time()
|
||||
self._gui.present_with_time(time_stamp)
|
||||
time_stamp = Gtk.get_current_event_time()
|
||||
if not time_stamp or time_stamp > 0xFFFFFFFF:
|
||||
time_stamp = Gdk.CURRENT_TIME
|
||||
self._gui.present_with_time(int(time_stamp))
|
||||
|
||||
def _copy_selected_notification(self):
|
||||
if self._model is None or self._tree is None:
|
||||
return
|
||||
|
||||
selection = self._tree.get_selection()
|
||||
model, paths = selection.get_selected_rows()
|
||||
if not paths and self._model.iter_n_children(None) > 0:
|
||||
selection.select_path(0)
|
||||
model, paths = selection.get_selected_rows()
|
||||
|
||||
if not paths:
|
||||
return
|
||||
|
||||
iter_for_path = model.get_iter(paths[0])
|
||||
if iter_for_path is None:
|
||||
return
|
||||
|
||||
message = model.get_value(iter_for_path, 0)
|
||||
timestamp = model.get_value(iter_for_path, 1)
|
||||
if timestamp:
|
||||
text = f"{message}\t{timestamp}"
|
||||
else:
|
||||
text = f"{message}"
|
||||
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
clipboard.set_text(text, -1)
|
||||
clipboard.store()
|
||||
self._script.presentMessage(messages.CLIPBOARD_COPIED_FULL)
|
||||
|
||||
_presenter = None
|
||||
def getPresenter():
|
||||
|
||||
@@ -59,9 +59,9 @@ class Plugin:
|
||||
self.plugin_info = plugin_info
|
||||
if plugin_info:
|
||||
self.module_name = getattr(plugin_info, 'module_name', '')
|
||||
self.name = getattr(plugin_info, 'name', '')
|
||||
self.version = getattr(plugin_info, 'version', '')
|
||||
self.description = getattr(plugin_info, 'description', '')
|
||||
self.name = plugin_info.get_name()
|
||||
self.version = plugin_info.get_version()
|
||||
self.description = plugin_info.get_description()
|
||||
|
||||
@cthulhu_hookimpl
|
||||
def activate(self, plugin=None):
|
||||
|
||||
@@ -12,6 +12,7 @@ import os
|
||||
import inspect
|
||||
import importlib.util
|
||||
import logging
|
||||
import configparser
|
||||
from enum import IntEnum
|
||||
|
||||
# Import pluggy if available
|
||||
@@ -29,6 +30,12 @@ logger = logging.getLogger(__name__)
|
||||
if PLUGIN_DEBUG:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
_manager = None
|
||||
|
||||
def getManager():
|
||||
"""Return the shared PluginSystemManager instance."""
|
||||
return _manager
|
||||
|
||||
class PluginType(IntEnum):
|
||||
"""Types of plugins we support."""
|
||||
SYSTEM = 1
|
||||
@@ -78,8 +85,10 @@ class PluginSystemManager:
|
||||
"""Cthulhu Plugin Manager using pluggy."""
|
||||
|
||||
def __init__(self, app):
|
||||
global _manager
|
||||
self.app = app
|
||||
logger.info("Initializing PluginSystemManager")
|
||||
_manager = self
|
||||
|
||||
# Initialize plugin manager
|
||||
if PLUGGY_AVAILABLE:
|
||||
@@ -378,15 +387,38 @@ class PluginSystemManager:
|
||||
|
||||
if os.path.isfile(metadata_file):
|
||||
try:
|
||||
with open(metadata_file, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
with open(metadata_file, 'r', encoding='utf-8') as f:
|
||||
contents = f.read()
|
||||
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
metadata[key.strip()] = value.strip()
|
||||
has_section_header = False
|
||||
for line in contents.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith('#') or stripped.startswith(';'):
|
||||
continue
|
||||
if stripped.startswith('[') and stripped.endswith(']'):
|
||||
has_section_header = True
|
||||
break
|
||||
|
||||
if has_section_header:
|
||||
parser = configparser.ConfigParser()
|
||||
try:
|
||||
parser.read_string(contents)
|
||||
if parser.sections():
|
||||
for section in parser.sections():
|
||||
for key, value in parser[section].items():
|
||||
metadata[key.strip().lower()] = value.strip()
|
||||
return metadata
|
||||
except configparser.Error as e:
|
||||
logger.warning(f"Plugin metadata INI parse failed for {metadata_file}: {e}")
|
||||
|
||||
for line in contents.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or line.startswith(';') or line.startswith('['):
|
||||
continue
|
||||
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
metadata[key.strip().lower()] = value.strip()
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading plugin metadata: {e}")
|
||||
|
||||
|
||||
@@ -334,6 +334,172 @@ Keep descriptions informative and well-structured."""
|
||||
return base_prompt
|
||||
|
||||
|
||||
class CodexProvider(AIProvider):
|
||||
"""Codex CLI provider - uses installed Codex CLI."""
|
||||
|
||||
def __init__(self, codex_path=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.codex_path = codex_path or self._resolve_codex_path()
|
||||
|
||||
def describe_screen(self, screenshot_data, accessibility_data):
|
||||
"""Generate a description using Codex CLI."""
|
||||
try:
|
||||
prompt = self._build_prompt("describe", None, accessibility_data)
|
||||
return self._call_codex(prompt, screenshot_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Codex describe error: {e}")
|
||||
return f"Error getting screen description: {e}"
|
||||
|
||||
def answer_question(self, question, screenshot_data, accessibility_data):
|
||||
"""Answer a question using Codex CLI."""
|
||||
try:
|
||||
prompt = self._build_prompt("question", question, accessibility_data)
|
||||
return self._call_codex(prompt, screenshot_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Codex question error: {e}")
|
||||
return f"Error answering question: {e}"
|
||||
|
||||
def suggest_actions(self, request, screenshot_data, accessibility_data):
|
||||
"""Suggest actions using Codex CLI."""
|
||||
try:
|
||||
prompt = self._build_prompt("action", request, accessibility_data)
|
||||
return self._call_codex(prompt, screenshot_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Codex action error: {e}")
|
||||
return f"Error suggesting actions: {e}"
|
||||
|
||||
def analyze_images(self, user_question, screenshot_data, accessibility_data):
|
||||
"""Analyze images visible on screen using Codex CLI."""
|
||||
try:
|
||||
prompt = self._build_prompt("image", user_question, accessibility_data)
|
||||
return self._call_codex(prompt, screenshot_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Codex image analysis error: {e}")
|
||||
return f"Error analyzing images: {e}"
|
||||
|
||||
def _build_prompt(self, task_type, user_input, accessibility_data):
|
||||
"""Build the complete prompt for Codex CLI."""
|
||||
system_prompt = self._prepare_system_prompt(task_type)
|
||||
|
||||
if task_type == "image":
|
||||
if user_input == "ANALYZE_SINGLE_IMAGE_FILE":
|
||||
prompt = (
|
||||
f"{system_prompt}\n\nAnalyze and describe the single image file provided. "
|
||||
"Focus on visual content only - describe what you see in the image: objects, "
|
||||
"people, scenery, colors, text, composition, and any other visual details."
|
||||
)
|
||||
else:
|
||||
prompt = f"{system_prompt}\n\nCurrent screen context (focus on images):\n"
|
||||
if user_input:
|
||||
prompt += f"User question about images: {user_input}\n\n"
|
||||
prompt += "Analyze and describe any images visible on this screen. Focus on visual content, not UI elements."
|
||||
else:
|
||||
prompt = (
|
||||
f"{system_prompt}\n\nCurrent accessibility information:\n"
|
||||
f"```json\n{json.dumps(accessibility_data, indent=2)}\n```\n\n"
|
||||
)
|
||||
|
||||
if task_type == "describe":
|
||||
prompt += "Please describe what's on this screen."
|
||||
elif task_type == "question":
|
||||
prompt += f"User question: {user_input}"
|
||||
elif task_type == "action":
|
||||
prompt += f"User wants to: {user_input}\n\nProvide the action analysis in the required format."
|
||||
|
||||
return prompt
|
||||
|
||||
def _resolve_codex_path(self):
|
||||
import shutil
|
||||
import os
|
||||
|
||||
codex_path = shutil.which('codex')
|
||||
if not codex_path and os.path.isfile('/usr/bin/codex'):
|
||||
codex_path = '/usr/bin/codex'
|
||||
return codex_path
|
||||
|
||||
def _call_codex(self, prompt, screenshot_data):
|
||||
"""Call Codex CLI with the prompt and optional image."""
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
import base64
|
||||
|
||||
if not self.codex_path:
|
||||
return "Codex CLI not found"
|
||||
|
||||
output_path = None
|
||||
image_path = None
|
||||
try:
|
||||
# Write screenshot to a temp file if available
|
||||
if screenshot_data:
|
||||
image_format = screenshot_data.get('format', 'png')
|
||||
suffix = f".{image_format}"
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
|
||||
image_data = base64.b64decode(screenshot_data['data'])
|
||||
temp_file.write(image_data)
|
||||
image_path = temp_file.name
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False) as output_file:
|
||||
output_path = output_file.name
|
||||
|
||||
cmd = [
|
||||
self.codex_path,
|
||||
'exec',
|
||||
'--skip-git-repo-check',
|
||||
'--color',
|
||||
'never',
|
||||
'--output-last-message',
|
||||
output_path,
|
||||
'-'
|
||||
]
|
||||
|
||||
if image_path:
|
||||
cmd.extend(['-i', image_path])
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
input=prompt,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
error_msg = result.stderr.strip() or result.stdout.strip() or "Codex CLI error"
|
||||
logger.error(error_msg)
|
||||
return f"Codex CLI error: {error_msg}"
|
||||
|
||||
response_text = ""
|
||||
if output_path and os.path.exists(output_path):
|
||||
with open(output_path, 'r') as f:
|
||||
response_text = f.read().strip()
|
||||
|
||||
if not response_text:
|
||||
response_text = result.stdout.strip()
|
||||
|
||||
return response_text or "No response from Codex"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
error_msg = "Codex CLI timed out"
|
||||
logger.error(error_msg)
|
||||
return error_msg
|
||||
except Exception as e:
|
||||
error_msg = f"Error calling Codex CLI: {e}"
|
||||
logger.error(error_msg)
|
||||
return error_msg
|
||||
finally:
|
||||
if image_path and os.path.exists(image_path):
|
||||
try:
|
||||
os.unlink(image_path)
|
||||
except Exception:
|
||||
pass
|
||||
if output_path and os.path.exists(output_path):
|
||||
try:
|
||||
os.unlink(output_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class OllamaProvider(AIProvider):
|
||||
"""Ollama local AI provider."""
|
||||
|
||||
@@ -563,9 +729,11 @@ def create_provider(provider_type, **kwargs):
|
||||
"""Factory function to create AI providers."""
|
||||
if provider_type == "claude_code":
|
||||
return ClaudeCodeProvider(**kwargs)
|
||||
elif provider_type == "codex":
|
||||
return CodexProvider(**kwargs)
|
||||
elif provider_type == "ollama":
|
||||
return OllamaProvider(**kwargs)
|
||||
elif provider_type == "gemini":
|
||||
return GeminiProvider(**kwargs)
|
||||
else:
|
||||
raise ValueError(f"Unknown provider type: {provider_type}")
|
||||
raise ValueError(f"Unknown provider type: {provider_type}")
|
||||
|
||||
@@ -204,6 +204,11 @@ class AIAssistant(Plugin):
|
||||
result = self._check_claude_code_availability()
|
||||
logger.info(f"Claude Code availability check result: {result}")
|
||||
return result
|
||||
elif self._provider_type == settings.AI_PROVIDER_CODEX:
|
||||
logger.info("Checking Codex CLI availability")
|
||||
result = self._check_codex_availability()
|
||||
logger.info(f"Codex CLI availability check result: {result}")
|
||||
return result
|
||||
elif self._provider_type == settings.AI_PROVIDER_GEMINI:
|
||||
logger.info("Checking Gemini API key")
|
||||
if not self._api_key:
|
||||
@@ -258,12 +263,43 @@ class AIAssistant(Plugin):
|
||||
except Exception as e:
|
||||
logger.warning(f"Claude Code CLI not available: {e}")
|
||||
return False
|
||||
|
||||
def _check_codex_availability(self):
|
||||
"""Check if Codex CLI is available."""
|
||||
try:
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
codex_path = shutil.which('codex')
|
||||
if not codex_path and os.path.isfile('/usr/bin/codex'):
|
||||
codex_path = '/usr/bin/codex'
|
||||
|
||||
if not codex_path:
|
||||
logger.warning("Codex CLI not found in PATH")
|
||||
return False
|
||||
|
||||
result = subprocess.run([codex_path, '--version'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
logger.info("Codex CLI is available")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"Codex CLI not responding: {result.stderr}")
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("Codex CLI timeout")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning(f"Codex CLI not available: {e}")
|
||||
return False
|
||||
|
||||
def _initialize_ai_provider(self):
|
||||
"""Initialize the AI provider based on settings."""
|
||||
try:
|
||||
if self._provider_type == settings.AI_PROVIDER_CLAUDE_CODE:
|
||||
self._ai_provider = create_provider("claude_code")
|
||||
elif self._provider_type == settings.AI_PROVIDER_CODEX:
|
||||
self._ai_provider = create_provider("codex")
|
||||
elif self._provider_type == settings.AI_PROVIDER_OLLAMA:
|
||||
self._ai_provider = create_provider("ollama", model=self._ollama_model, base_url=self._ollama_endpoint)
|
||||
elif self._provider_type == settings.AI_PROVIDER_GEMINI:
|
||||
|
||||
@@ -5,10 +5,10 @@ ocrdesktop_python_sources = files([
|
||||
|
||||
python3.install_sources(
|
||||
ocrdesktop_python_sources,
|
||||
subdir: 'cthulhu/plugins/OCRDesktop'
|
||||
subdir: 'cthulhu/plugins/OCR'
|
||||
)
|
||||
|
||||
install_data(
|
||||
'plugin.info',
|
||||
install_dir: python3.get_install_dir() / 'cthulhu' / 'plugins' / 'OCRDesktop'
|
||||
)
|
||||
install_dir: python3.get_install_dir() / 'cthulhu' / 'plugins' / 'OCR'
|
||||
)
|
||||
|
||||
@@ -223,7 +223,7 @@ class PluginManager(Plugin):
|
||||
checkbox.connect("toggled", self._on_plugin_toggled, plugin_name)
|
||||
|
||||
# Create plugin info label
|
||||
info_text = f"<b>{plugin_name}</b>"
|
||||
info_text = f"<b>{plugin_info.get('name', plugin_name)}</b>"
|
||||
if plugin_info.get('description'):
|
||||
info_text += f"\n{plugin_info['description']}"
|
||||
if plugin_info.get('version'):
|
||||
@@ -257,17 +257,17 @@ class PluginManager(Plugin):
|
||||
from cthulhu import plugin_system_manager
|
||||
|
||||
# Use existing plugin manager to get plugins
|
||||
if hasattr(plugin_system_manager, '_manager') and plugin_system_manager._manager:
|
||||
manager = plugin_system_manager._manager
|
||||
manager = plugin_system_manager.getManager()
|
||||
if manager:
|
||||
manager.rescanPlugins()
|
||||
|
||||
for plugin_info in manager.plugins:
|
||||
plugin_name = plugin_info.get_module_name()
|
||||
plugins[plugin_name] = {
|
||||
'name': plugin_name,
|
||||
'description': getattr(plugin_info, 'description', ''),
|
||||
'version': getattr(plugin_info, 'version', ''),
|
||||
'path': getattr(plugin_info, 'module_dir', '')
|
||||
'name': plugin_info.get_name(),
|
||||
'description': plugin_info.get_description(),
|
||||
'version': plugin_info.get_version(),
|
||||
'path': plugin_info.get_module_dir()
|
||||
}
|
||||
else:
|
||||
# Fallback: manually scan plugin directories
|
||||
|
||||
@@ -210,6 +210,7 @@ CHAT_SPEAK_FOCUSED_CHANNEL = 2
|
||||
|
||||
# AI Assistant constants - simplified to providers that don't need complex API key management
|
||||
AI_PROVIDER_CLAUDE_CODE = "claude_code"
|
||||
AI_PROVIDER_CODEX = "codex"
|
||||
AI_PROVIDER_GEMINI = "gemini"
|
||||
AI_PROVIDER_OLLAMA = "ollama"
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ falls off the end, it may be added again later.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
import threading
|
||||
from collections import deque
|
||||
|
||||
@@ -76,7 +77,7 @@ def is_capture_paused() -> bool:
|
||||
return _pauseCount > 0
|
||||
|
||||
|
||||
def add(text: str | None, source: str = "") -> bool:
|
||||
def add(text: Optional[str], source: str = "") -> bool:
|
||||
"""Add text to speech history if it's not already present.
|
||||
|
||||
Returns True if the item was added; False otherwise.
|
||||
@@ -141,7 +142,7 @@ def get_items() -> list[str]:
|
||||
return list(_historyItems)
|
||||
|
||||
|
||||
def remove(text: str | None) -> bool:
|
||||
def remove(text: Optional[str]) -> bool:
|
||||
"""Remove an item from the history (if present)."""
|
||||
if text is None:
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user