Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions WHATS_NEW.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# What's New — AutoControl

## What's new (2026-06-25) — Container Selection + View Switching (Selection / MultipleView)

Read what's selected in a listbox/grid, and switch Explorer-style views. Full reference: [`docs/source/Eng/doc/new_features/v201_features_doc.rst`](docs/source/Eng/doc/new_features/v201_features_doc.rst).

- **`get_selection` / `list_views` / `set_view`** (`AC_get_selection`, `AC_list_views`, `AC_set_view`): `select_control_item` selects *one* item, but the container-level `SelectionPattern` answers "what is currently selected, and may it select multiple?" — the assertion target after selecting. `MultipleViewPattern` switches a control between its views (Explorer's list / details / tile / thumbnail), a precondition that otherwise needs fragile menu clicking. `get_selection` returns `{items, can_select_multiple, is_required}`, `list_views` returns `{current, views}`, and `set_view` switches by view name. Dispatched through the injectable accessibility backend seam (headless-testable via a fake backend; real UIA in the Windows backend). No `PySide6`.

## What's new (2026-06-25) — Advanced TextPattern (find / select / read attributes)

Search a control's text, select a match to replace it, and read font/colour formatting. Full reference: [`docs/source/Eng/doc/new_features/v200_features_doc.rst`](docs/source/Eng/doc/new_features/v200_features_doc.rst).
Expand Down
45 changes: 45 additions & 0 deletions docs/source/Eng/doc/new_features/v201_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Container Selection + View Switching (Selection / MultipleView)
===============================================================

``select_control_item`` (SelectionItemPattern) selects *one* item; the
container-level ``SelectionPattern`` answers the natural follow-up — **what is
currently selected** in a listbox / grid / tab, and **may it select multiple?** —
the assertion target after selecting. ``MultipleViewPattern`` switches a control
between its views (Explorer's list / details / tile / thumbnail), a common
precondition that otherwise needs fragile menu clicking.

* :func:`get_selection` — ``{items, can_select_multiple, is_required}``,
* :func:`list_views` — ``{current, views: [...]}``,
* :func:`set_view` — switch to a named view.

Each is a thin dispatch onto the injectable ``accessibility.backends.get_backend()``
seam — headless-testable via a fake backend; the real UIA calls live in the
Windows backend. Imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import get_selection, list_views, set_view

get_selection(name="File List")
# {"items": ["report.pdf", "notes.txt"], "can_select_multiple": True,
# "is_required": False}

list_views(name="File List")
# {"current": "Details", "views": ["List", "Details", "Tiles"]}
set_view("Tiles", name="File List") # switch the view

The control is located by ``name`` / ``role`` / ``app_name`` / ``automation_id``
(same as the other native-control actions). ``get_selection`` / ``list_views``
return their dict (or ``None`` if the control or pattern isn't found);
``set_view`` returns ``bool`` (False when the named view isn't supported).

Executor commands
-----------------

``AC_get_selection`` (``{found, selection}``), ``AC_list_views`` (``{found,
views}``) and ``AC_set_view`` (``view``). They are exposed as the matching
``ac_*`` MCP tools (the reads read-only, ``set_view`` destructive) and as Script
Builder commands under **Native UI**.
40 changes: 40 additions & 0 deletions docs/source/Zh/doc/new_features/v201_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
容器選取 + 檢視切換(Selection / MultipleView)
==============================================

``select_control_item``(SelectionItemPattern)選取*單一*項目;容器層級的 ``SelectionPattern``
回答自然的後續問題——listbox / grid / tab 中**目前選了什麼**,以及**是否可多選?**——這正是選取
之後的斷言目標。``MultipleViewPattern`` 在控制項的各檢視之間切換(檔案總管的清單 / 詳細資料 /
並排 / 縮圖),這是個常見前置條件,否則就得靠脆弱的選單點擊。

* :func:`get_selection` ——``{items, can_select_multiple, is_required}``,
* :func:`list_views` ——``{current, views: [...]}``,
* :func:`set_view` ——切換到具名的檢視。

每個都是對可注入的 ``accessibility.backends.get_backend()`` 接縫的薄分派——可透過注入 fake
backend 進行無頭測試;真正的 UIA 呼叫位於 Windows 後端。不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import get_selection, list_views, set_view

get_selection(name="File List")
# {"items": ["report.pdf", "notes.txt"], "can_select_multiple": True,
# "is_required": False}

list_views(name="File List")
# {"current": "Details", "views": ["List", "Details", "Tiles"]}
set_view("Tiles", name="File List") # 切換檢視

控制項以 ``name`` / ``role`` / ``app_name`` / ``automation_id`` 定位(與其他原生控制動作相同)。
``get_selection`` / ``list_views`` 回傳其字典(找不到控制項或模式則為 ``None``);``set_view``
回傳 ``bool``(具名檢視不支援時為 False)。

執行器指令
----------

``AC_get_selection``(``{found, selection}``)、``AC_list_views``(``{found, views}``)與
``AC_set_view``(``view``)。皆以對應的 ``ac_*`` MCP 工具(讀取類為唯讀、``set_view`` 為破壞性)
及 Script Builder 指令(位於 **Native UI** 分類下)形式提供。
5 changes: 5 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
from je_auto_control.utils.legacy_accessible import (
legacy_default_action, legacy_info,
)
# Container selection state + view switching (Selection / MultipleView patterns)
from je_auto_control.utils.selection_view import (
get_selection, list_views, set_view,
)
# Rich clipboard formats — RTF + CSV/TSV codecs and Windows get / set
from je_auto_control.utils.clipboard_rich_formats import (
build_rtf, csv_to_rows, get_clipboard_csv, get_clipboard_rtf, rows_to_csv,
Expand Down Expand Up @@ -1692,6 +1696,7 @@ def start_autocontrol_gui(*args, **kwargs):
"move_element", "resize_element", "set_window_state",
"window_interaction_state",
"legacy_info", "legacy_default_action",
"get_selection", "list_views", "set_view",
"build_rtf", "rtf_to_text", "rows_to_csv", "csv_to_rows",
"set_clipboard_rtf", "get_clipboard_rtf",
"set_clipboard_csv", "get_clipboard_csv",
Expand Down
15 changes: 15 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,21 @@ def _add_native_control_specs(specs: List[CommandSpec]) -> None:
fields=fields,
description="Fire an old control's MSAA default action (fallback).",
))
specs.append(CommandSpec(
"AC_get_selection", "Native UI", "Get Container Selection",
fields=fields,
description="Read a container's selection (SelectionPattern).",
))
specs.append(CommandSpec(
"AC_list_views", "Native UI", "List Control Views",
fields=fields,
description="List a control's selectable views (MultipleViewPattern).",
))
specs.append(CommandSpec(
"AC_set_view", "Native UI", "Set Control View",
fields=(FieldSpec("view", FieldType.STRING),) + fields,
description="Switch a control to the named view (MultipleViewPattern).",
))
specs.append(CommandSpec(
"AC_get_control_text", "Native UI", "Get Control Text",
fields=fields,
Expand Down
24 changes: 24 additions & 0 deletions je_auto_control/utils/accessibility/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,30 @@
success — the fallback when Value / Invoke / Toggle all do nothing."""
self._unsupported("legacy_default_action")

# --- container selection + views (Selection / MultipleView patterns) ----

def get_selection(self, name: Optional[str] = None, role: Optional[str] = None,

Check warning on line 262 in je_auto_control/utils/accessibility/backends/base.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "role".

See more on https://sonarcloud.io/project/issues?id=Integration-Automation_AutoControlGUI&issues=AZ77uCsW83tBz8AQY1lQ&open=AZ77uCsW83tBz8AQY1lQ&pullRequest=422

Check warning on line 262 in je_auto_control/utils/accessibility/backends/base.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "name".

See more on https://sonarcloud.io/project/issues?id=Integration-Automation_AutoControlGUI&issues=AZ77uCsW83tBz8AQY1lO&open=AZ77uCsW83tBz8AQY1lO&pullRequest=422
app_name: Optional[str] = None,

Check warning on line 263 in je_auto_control/utils/accessibility/backends/base.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "app_name".

See more on https://sonarcloud.io/project/issues?id=Integration-Automation_AutoControlGUI&issues=AZ77uCsW83tBz8AQY1lN&open=AZ77uCsW83tBz8AQY1lN&pullRequest=422
automation_id: Optional[str] = None,

Check warning on line 264 in je_auto_control/utils/accessibility/backends/base.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "automation_id".

See more on https://sonarcloud.io/project/issues?id=Integration-Automation_AutoControlGUI&issues=AZ77uCsW83tBz8AQY1lP&open=AZ77uCsW83tBz8AQY1lP&pullRequest=422
) -> Optional[Dict[str, Any]]:
"""Return a container's selection state — ``{items, can_select_multiple,
is_required}`` (SelectionPattern), or None."""
self._unsupported("get_selection")

def list_views(self, name: Optional[str] = None, role: Optional[str] = None,

Check warning on line 270 in je_auto_control/utils/accessibility/backends/base.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "role".

See more on https://sonarcloud.io/project/issues?id=Integration-Automation_AutoControlGUI&issues=AZ77uCsW83tBz8AQY1lR&open=AZ77uCsW83tBz8AQY1lR&pullRequest=422

Check warning on line 270 in je_auto_control/utils/accessibility/backends/base.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "name".

See more on https://sonarcloud.io/project/issues?id=Integration-Automation_AutoControlGUI&issues=AZ77uCsW83tBz8AQY1lU&open=AZ77uCsW83tBz8AQY1lU&pullRequest=422
app_name: Optional[str] = None,

Check warning on line 271 in je_auto_control/utils/accessibility/backends/base.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "app_name".

See more on https://sonarcloud.io/project/issues?id=Integration-Automation_AutoControlGUI&issues=AZ77uCsW83tBz8AQY1lT&open=AZ77uCsW83tBz8AQY1lT&pullRequest=422
automation_id: Optional[str] = None,

Check warning on line 272 in je_auto_control/utils/accessibility/backends/base.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "automation_id".

See more on https://sonarcloud.io/project/issues?id=Integration-Automation_AutoControlGUI&issues=AZ77uCsW83tBz8AQY1lS&open=AZ77uCsW83tBz8AQY1lS&pullRequest=422
) -> Optional[Dict[str, Any]]:
"""Return a control's selectable views — ``{current, views: [...]}``
(MultipleViewPattern: list / details / tile / …), or None."""
self._unsupported("list_views")

def set_view(self, view: str = "", name: Optional[str] = None,

Check warning on line 278 in je_auto_control/utils/accessibility/backends/base.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "name".

See more on https://sonarcloud.io/project/issues?id=Integration-Automation_AutoControlGUI&issues=AZ77uCsW83tBz8AQY1lW&open=AZ77uCsW83tBz8AQY1lW&pullRequest=422
role: Optional[str] = None, app_name: Optional[str] = None,

Check warning on line 279 in je_auto_control/utils/accessibility/backends/base.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "role".

See more on https://sonarcloud.io/project/issues?id=Integration-Automation_AutoControlGUI&issues=AZ77uCsW83tBz8AQY1lX&open=AZ77uCsW83tBz8AQY1lX&pullRequest=422

Check warning on line 279 in je_auto_control/utils/accessibility/backends/base.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "app_name".

See more on https://sonarcloud.io/project/issues?id=Integration-Automation_AutoControlGUI&issues=AZ77uCsW83tBz8AQY1lY&open=AZ77uCsW83tBz8AQY1lY&pullRequest=422
automation_id: Optional[str] = None) -> bool:

Check warning on line 280 in je_auto_control/utils/accessibility/backends/base.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "automation_id".

See more on https://sonarcloud.io/project/issues?id=Integration-Automation_AutoControlGUI&issues=AZ77uCsW83tBz8AQY1lV&open=AZ77uCsW83tBz8AQY1lV&pullRequest=422
"""Switch a control to the named view (MultipleViewPattern); True on success."""
self._unsupported("set_view")

def _unsupported(self, operation: str):
"""Raise a clear error for an action this backend can't perform."""
raise AccessibilityNotAvailableError(
Expand Down
58 changes: 58 additions & 0 deletions je_auto_control/utils/accessibility/backends/windows_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
_UIA_TRANSFORM_PATTERN_ID = 10016
_UIA_WINDOW_PATTERN_ID = 10009
_UIA_LEGACYIACCESSIBLE_PATTERN_ID = 10018
_UIA_SELECTION_PATTERN_ID = 10001
_UIA_MULTIPLEVIEW_PATTERN_ID = 10008
_UIA_AUTOMATIONID_PROPERTY = 30011
_EXPAND_STATES = {0: "collapsed", 1: "expanded", 2: "partial", 3: "leaf"}
_WINDOW_VISUAL_STATES = {"normal": 0, "maximized": 1, "minimized": 2}
Expand Down Expand Up @@ -370,6 +372,54 @@ def legacy_default_action(self, name=None, role=None, app_name=None,
"IUIAutomationLegacyIAccessiblePattern",
lambda pattern: pattern.DoDefaultAction())

def get_selection(self, name=None, role=None, app_name=None,
automation_id=None) -> Optional[Dict[str, Any]]:
raw = self._find_raw(name, role, app_name, automation_id)
pattern = self._pattern(raw, _UIA_SELECTION_PATTERN_ID,
"IUIAutomationSelectionPattern") if raw else None
if pattern is None:
return None
try:
items = _header_names(pattern.GetCurrentSelection())
can_multiple = bool(pattern.CurrentCanSelectMultiple)
required = bool(pattern.CurrentIsSelectionRequired)
except (OSError, AttributeError):
return None
return {"items": items, "can_select_multiple": can_multiple,
"is_required": required}

def _multiple_view(self, name, role, app_name, automation_id):
raw = self._find_raw(name, role, app_name, automation_id)
return self._pattern(raw, _UIA_MULTIPLEVIEW_PATTERN_ID,
"IUIAutomationMultipleViewPattern") if raw else None

def list_views(self, name=None, role=None, app_name=None,
automation_id=None) -> Optional[Dict[str, Any]]:
pattern = self._multiple_view(name, role, app_name, automation_id)
if pattern is None:
return None
try:
view_ids = list(pattern.GetCurrentSupportedViews())
current = int(pattern.CurrentCurrentView)
except (OSError, AttributeError, ValueError, TypeError):
return None
return {"current": _view_name(pattern, current),
"views": [_view_name(pattern, view_id) for view_id in view_ids]}

def set_view(self, view="", name=None, role=None, app_name=None,
automation_id=None):
pattern = self._multiple_view(name, role, app_name, automation_id)
if pattern is None:
return False
try:
for view_id in pattern.GetCurrentSupportedViews():
if _view_name(pattern, view_id) == str(view):
pattern.SetCurrentView(int(view_id))
return True
except (OSError, AttributeError, ValueError, TypeError):
return False
return False

def get_table_headers(self, name=None, role=None, app_name=None,
automation_id=None) -> Optional[Dict[str, Any]]:
raw = self._find_raw(name, role, app_name, automation_id)
Expand Down Expand Up @@ -511,6 +561,14 @@ def _read_row(pattern, row: int, cols: int):
return cells


def _view_name(pattern, view_id) -> str:
"""Return a MultipleViewPattern view's name, or '' on failure."""
try:
return str(pattern.GetViewName(int(view_id)) or "")
except (OSError, AttributeError, ValueError, TypeError):
return ""


def _header_names(array) -> List[str]:
"""Read an IUIAutomationElementArray of header elements into name strings."""
names: List[str] = []
Expand Down
32 changes: 32 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2573,6 +2573,35 @@ def _legacy_default_action(name: Optional[str] = None, role: Optional[str] = Non
automation_id=automation_id)


def _get_selection(name: Optional[str] = None, role: Optional[str] = None,
app_name: Optional[str] = None,
automation_id: Optional[str] = None) -> Dict[str, Any]:
"""Adapter: a container's selection state (SelectionPattern)."""
from je_auto_control.utils.selection_view import get_selection
selection = get_selection(name=name, role=role, app_name=app_name,
automation_id=automation_id)
return {"found": selection is not None, "selection": selection}


def _list_views(name: Optional[str] = None, role: Optional[str] = None,
app_name: Optional[str] = None,
automation_id: Optional[str] = None) -> Dict[str, Any]:
"""Adapter: a control's selectable views (MultipleViewPattern)."""
from je_auto_control.utils.selection_view import list_views
views = list_views(name=name, role=role, app_name=app_name,
automation_id=automation_id)
return {"found": views is not None, "views": views}


def _set_view(view: str, name: Optional[str] = None, role: Optional[str] = None,
app_name: Optional[str] = None,
automation_id: Optional[str] = None) -> bool:
"""Adapter: switch a control to a named view (MultipleViewPattern)."""
from je_auto_control.utils.selection_view import set_view
return set_view(str(view), name=name, role=role, app_name=app_name,
automation_id=automation_id)


def _get_control_text(name: Optional[str] = None, role: Optional[str] = None,
app_name: Optional[str] = None,
automation_id: Optional[str] = None) -> Dict[str, Any]:
Expand Down Expand Up @@ -6559,6 +6588,9 @@ def __init__(self):
"AC_window_interaction_state": _window_interaction_state,
"AC_legacy_info": _legacy_info,
"AC_legacy_default_action": _legacy_default_action,
"AC_get_selection": _get_selection,
"AC_list_views": _list_views,
"AC_set_view": _set_view,
"AC_get_control_text": _get_control_text,
"AC_find_control_text": _find_control_text,
"AC_select_control_text": _select_control_text,
Expand Down
27 changes: 27 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,33 @@ def a11y_control_tools() -> List[MCPTool]:
handler=h.legacy_default_action,
annotations=DESTRUCTIVE,
),
MCPTool(
name="ac_get_selection",
description=("Read a container's selection via SelectionPattern: "
"{found, selection:{items:[names], can_select_multiple, "
"is_required}} — what's selected in a listbox/grid/tab."),
input_schema=schema(dict(_M)),
handler=h.get_selection,
annotations=READ_ONLY,
),
MCPTool(
name="ac_list_views",
description=("List a control's selectable views via "
"MultipleViewPattern: {found, views:{current, views:"
"[names]}} — e.g. Explorer list / details / tile."),
input_schema=schema(dict(_M)),
handler=h.list_views,
annotations=READ_ONLY,
),
MCPTool(
name="ac_set_view",
description=("Switch a control to the named 'view' via "
"MultipleViewPattern. Returns True on success."),
input_schema=schema({"view": {"type": "string"}, **_M},
required=["view"]),
handler=h.set_view,
annotations=DESTRUCTIVE,
),
MCPTool(
name="ac_get_control_text",
description=("Read a control's full text via TextPattern: "
Expand Down
15 changes: 15 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,21 @@ def legacy_default_action(name=None, role=None, app_name=None,
return _legacy_default_action(name, role, app_name, automation_id)


def get_selection(name=None, role=None, app_name=None, automation_id=None):
from je_auto_control.utils.executor.action_executor import _get_selection
return _get_selection(name, role, app_name, automation_id)


def list_views(name=None, role=None, app_name=None, automation_id=None):
from je_auto_control.utils.executor.action_executor import _list_views
return _list_views(name, role, app_name, automation_id)


def set_view(view, name=None, role=None, app_name=None, automation_id=None):
from je_auto_control.utils.executor.action_executor import _set_view
return _set_view(view, name, role, app_name, automation_id)


def get_selected_text(name=None, role=None, app_name=None, automation_id=None):
from je_auto_control.utils.executor.action_executor import _get_selected_text
return _get_selected_text(name, role, app_name, automation_id)
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/selection_view/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Container selection state + view switching (Selection / MultipleView patterns)."""
from je_auto_control.utils.selection_view.selection_view import (
get_selection, list_views, set_view,
)

__all__ = ["get_selection", "list_views", "set_view"]
Loading
Loading