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 README/WHATS_NEW_zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# 本次更新 — AutoControl

## 本次更新 (2026-06-23) — 窗口铺排 / 版面几何规划器

计算应用程序窗口该放在哪里——半边、网格、层叠。完整参考:[`docs/source/Zh/doc/new_features/v133_features_doc.rst`](../docs/source/Zh/doc/new_features/v133_features_doc.rst)。

- **`tile_rect` / `grid_rects` / `cascade_rects`**(`AC_tile_rect`、`AC_grid_rects`、`AC_cascade_rects`):`save/restore_window_layout` 重播*精确*的已存位置、`snap_window` 移动*一个*窗口——没有任何功能能*计算*出全新的多窗口版面。此纯几何规划器在给定屏幕工作区下,返回半边、四分之一、三分之一、R×C 网格与错位层叠的目标矩形,让脚本能以确定性方式排列窗口。返回 `WindowRect`(`.as_tuple()` / `.to_dict()`);`gap` 内缩铺排间距;跨平台且完全无头可测;可与任何窗口移动后端组合。

## 本次更新 (2026-06-23) — 以边缘 / 轮廓定位 UI 元素(免模板)

在从未见过的画面上找出可点击的方框。完整参考:[`docs/source/Zh/doc/new_features/v132_features_doc.rst`](../docs/source/Zh/doc/new_features/v132_features_doc.rst)。
Expand Down
6 changes: 6 additions & 0 deletions README/WHATS_NEW_zh-TW.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# 本次更新 — AutoControl

## 本次更新 (2026-06-23) — 視窗鋪排 / 版面幾何規劃器

計算應用程式視窗該放在哪裡——半邊、網格、層疊。完整參考:[`docs/source/Zh/doc/new_features/v133_features_doc.rst`](../docs/source/Zh/doc/new_features/v133_features_doc.rst)。

- **`tile_rect` / `grid_rects` / `cascade_rects`**(`AC_tile_rect`、`AC_grid_rects`、`AC_cascade_rects`):`save/restore_window_layout` 重播*精確*的已存位置、`snap_window` 移動*一個*視窗——沒有任何功能能*計算*出全新的多視窗版面。此純幾何規劃器在給定螢幕工作區下,回傳半邊、四分之一、三分之一、R×C 網格與錯位層疊的目標矩形,讓腳本能以決定性方式排列視窗。回傳 `WindowRect`(`.as_tuple()` / `.to_dict()`);`gap` 內縮鋪排間距;跨平台且完全無頭可測;可與任何視窗移動後端組合。

## 本次更新 (2026-06-23) — 以邊緣 / 輪廓定位 UI 元素(免模板)

在從未見過的畫面上找出可點擊的方框。完整參考:[`docs/source/Zh/doc/new_features/v132_features_doc.rst`](../docs/source/Zh/doc/new_features/v132_features_doc.rst)。
Expand Down
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-23) — Window Tiling / Layout Geometry Planner

Compute where to place application windows — halves, grids, cascades. Full reference: [`docs/source/Eng/doc/new_features/v133_features_doc.rst`](docs/source/Eng/doc/new_features/v133_features_doc.rst).

- **`tile_rect` / `grid_rects` / `cascade_rects`** (`AC_tile_rect`, `AC_grid_rects`, `AC_cascade_rects`): `save/restore_window_layout` replay *exact* saved positions and `snap_window` moves *one* window — nothing *computes* a fresh multi-window layout. This pure-geometry planner returns the target rectangles for halves, quadrants, thirds, an R×C grid and a staggered cascade given a screen work area, so a script can lay out windows deterministically. Returns `WindowRect` (`.as_tuple()` / `.to_dict()`); `gap` insets tiles; cross-platform and fully headless-testable; composes with any window-move backend.

## What's new (2026-06-23) — Locate UI Elements by Edge / Contour (No Template)

Find the clickable boxes on a screen you have never seen. Full reference: [`docs/source/Eng/doc/new_features/v132_features_doc.rst`](docs/source/Eng/doc/new_features/v132_features_doc.rst).
Expand Down
47 changes: 47 additions & 0 deletions docs/source/Eng/doc/new_features/v133_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Window Tiling / Layout Geometry Planner
=======================================

``save_window_layout`` / ``restore_window_layout`` capture and replay the *exact*
positions a user already arranged, and ``snap_window`` moves *one* window to a half
or quarter. Nothing *computes* a fresh multi-window layout. ``tile_rect``,
``grid_rects`` and ``cascade_rects`` are a pure-geometry planner: given a screen
work area they return the target rectangles for the common tiling layouts — halves,
quadrants, thirds, an R×C grid, a staggered cascade — so a script can lay out
application windows deterministically.

The planner is cross-platform and has no device dependency, so it is fully
unit-testable; the rectangles it returns compose with any window-move backend.
Imports no ``PySide6``.

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

.. code-block:: python

from je_auto_control import tile_rect, grid_rects, cascade_rects

left = tile_rect((0, 0, 1920, 1080), "left_third", gap=8)
print(left.as_tuple()) # (8, 8, 624, 1064)

for cell in grid_rects((0, 0, 1920, 1080), rows=2, cols=3):
window_move("Editor", *cell.as_tuple()) # 6-up grid

plan = cascade_rects((0, 0, 1920, 1080), count=4, offset=40)

``tile_rect`` returns a ``WindowRect`` (``x, y, width, height`` with ``.as_tuple()``
and ``.to_dict()``) for a named ``slot`` — see :func:`available_slots`
(``left``, ``top_right``, ``center``, ``left_third`` …); ``gap`` insets all sides
for a margin between tiles. ``grid_rects`` returns one rectangle per cell of an
``rows`` × ``cols`` grid, row-major. ``cascade_rects`` returns ``count`` staggered,
overlapping rectangles clamped to the screen (``size`` defaults to 60% of the work
area). Unknown slots / non-positive grid dimensions raise ``ValueError``.

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

``AC_tile_rect`` (``slot`` / ``screen`` / ``gap`` → ``{rect}``), ``AC_grid_rects``
(``rows`` / ``cols`` / ``screen`` / ``gap`` → ``{count, rects}``) and
``AC_cascade_rects`` (``count`` / ``screen`` / ``offset`` / ``size`` →
``{count, rects}``). ``screen`` defaults to the live primary screen work area. They
are exposed as the MCP tools ``ac_tile_rect`` / ``ac_grid_rects`` /
``ac_cascade_rects`` and as Script Builder commands under **Window**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v130_features_doc
doc/new_features/v131_features_doc
doc/new_features/v132_features_doc
doc/new_features/v133_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
37 changes: 37 additions & 0 deletions docs/source/Zh/doc/new_features/v133_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
視窗鋪排 / 版面幾何規劃器
==========================

``save_window_layout`` / ``restore_window_layout``擷取並重播使用者已經排好的*精確*位置,``snap_window`` 把*一個*
視窗移到一半或四分之一。沒有任何功能能*計算*出全新的多視窗版面。``tile_rect``、``grid_rects`` 與 ``cascade_rects``
是純幾何規劃器:給定螢幕工作區,回傳常見鋪排版面的目標矩形——左右半、四分之一、三分之一、R×C 網格、錯位層疊——
讓腳本能以決定性方式排列應用程式視窗。

此規劃器跨平台且無裝置相依,因此完全可單元測試;它回傳的矩形可與任何視窗移動後端組合。不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import tile_rect, grid_rects, cascade_rects

left = tile_rect((0, 0, 1920, 1080), "left_third", gap=8)
print(left.as_tuple()) # (8, 8, 624, 1064)

for cell in grid_rects((0, 0, 1920, 1080), rows=2, cols=3):
window_move("Editor", *cell.as_tuple()) # 6 格網格

plan = cascade_rects((0, 0, 1920, 1080), count=4, offset=40)

``tile_rect`` 為具名 ``slot`` 回傳 ``WindowRect``(``x, y, width, height``,含 ``.as_tuple()`` 與 ``.to_dict()``)
——見 :func:`available_slots`(``left``、``top_right``、``center``、``left_third`` …);``gap`` 內縮各邊作為鋪排間距。
``grid_rects`` 為 ``rows`` × ``cols`` 網格的每格(列優先)回傳一個矩形。``cascade_rects`` 回傳 ``count`` 個錯位、
重疊且被夾在螢幕內的矩形(``size`` 預設為工作區的 60%)。未知 slot / 非正網格維度會丟出 ``ValueError``。

執行器命令
----------

``AC_tile_rect``(``slot`` / ``screen`` / ``gap`` → ``{rect}``)、``AC_grid_rects``(``rows`` / ``cols`` / ``screen``
/ ``gap`` → ``{count, rects}``)與 ``AC_cascade_rects``(``count`` / ``screen`` / ``offset`` / ``size`` →
``{count, rects}``)。``screen`` 預設為實際主螢幕工作區。它們以 MCP 工具 ``ac_tile_rect`` / ``ac_grid_rects`` /
``ac_cascade_rects`` 以及 Script Builder 中 **Window** 分類下的命令提供。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v130_features_doc
doc/new_features/v131_features_doc
doc/new_features/v132_features_doc
doc/new_features/v133_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
9 changes: 9 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@
from je_auto_control.utils.shape_locator import (
find_rectangles, find_shapes,
)
# Window tiling/layout geometry planner (halves, quadrants, grids, cascade)
from je_auto_control.utils.window_layout import (
WindowRect, available_slots, cascade_rects, grid_rects, tile_rect,
)
# CI workflow annotations (GitHub Actions)
from je_auto_control.utils.ci_annotations import (
emit_annotations, format_annotation,
Expand Down Expand Up @@ -1097,6 +1101,11 @@ def start_autocontrol_gui(*args, **kwargs):
"FeatureMatch",
"find_shapes",
"find_rectangles",
"WindowRect",
"available_slots",
"tile_rect",
"grid_rects",
"cascade_rects",
"emit_annotations", "format_annotation",
"ClipboardHistory", "default_clipboard_history",
"analyze_heal_log", "heal_stats", "scan_secrets",
Expand Down
37 changes: 37 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,43 @@
fields=(FieldSpec("layout", FieldType.FILE_PATH),),
description="Move windows back to a saved layout file.",
))
specs.append(CommandSpec(
"AC_tile_rect", "Window", "Tile Rect (compute)",
fields=(
FieldSpec("slot", FieldType.ENUM,
choices=("full", "left", "right", "top", "bottom",
"top_left", "top_right", "bottom_left",
"bottom_right", "center", "left_third",
"center_third", "right_third"), default="left"),
FieldSpec("screen", FieldType.STRING, optional=True,
placeholder="[x, y, width, height]"),

Check failure on line 564 in je_auto_control/gui/script_builder/command_schema.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "[x, y, width, height]" 3 times.

See more on https://sonarcloud.io/project/issues?id=Integration-Automation_AutoControlGUI&issues=AZ7xBfgYAHjVPX4tZl_E&open=AZ7xBfgYAHjVPX4tZl_E&pullRequest=343
FieldSpec("gap", FieldType.INT, optional=True, default=0),
),
description="Compute the rectangle for a tiling slot of the screen.",
))
specs.append(CommandSpec(
"AC_grid_rects", "Window", "Grid Rects (compute)",
fields=(
FieldSpec("rows", FieldType.INT, default=2),
FieldSpec("cols", FieldType.INT, default=2),
FieldSpec("screen", FieldType.STRING, optional=True,
placeholder="[x, y, width, height]"),
FieldSpec("gap", FieldType.INT, optional=True, default=0),
),
description="Compute the cell rectangles of an R×C screen grid.",
))
specs.append(CommandSpec(
"AC_cascade_rects", "Window", "Cascade Rects (compute)",
fields=(
FieldSpec("count", FieldType.INT, default=3),
FieldSpec("screen", FieldType.STRING, optional=True,
placeholder="[x, y, width, height]"),
FieldSpec("offset", FieldType.INT, optional=True, default=30),
FieldSpec("size", FieldType.STRING, optional=True,
placeholder="[width, height]"),
),
description="Compute staggered, overlapping window rectangles.",
))


def _add_flow_specs(specs: List[CommandSpec]) -> None:
Expand Down
42 changes: 42 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3345,6 +3345,45 @@ def _find_rectangles(region: Any = None, min_area: Any = 400, max_area: Any = No
return {"count": len(rects), "rectangles": rects}


def _resolve_screen(screen: Any) -> list:
"""Parse a JSON screen rect, or default to the live primary screen work area."""
import json
if isinstance(screen, str):
screen = json.loads(screen) if screen.strip() else None
if screen:
return list(screen)
from je_auto_control.wrapper.auto_control_screen import screen_size
width, height = screen_size()
return [0, 0, int(width), int(height)]


def _tile_rect(slot: str, screen: Any = None, gap: Any = 0) -> Dict[str, Any]:
"""Adapter: rectangle for a named tiling slot of the screen work area."""
from je_auto_control.utils.window_layout import tile_rect
rect = tile_rect(_resolve_screen(screen), str(slot), gap=int(gap))
return {"rect": rect.to_dict()}


def _grid_rects(rows: Any, cols: Any, screen: Any = None,
gap: Any = 0) -> Dict[str, Any]:
"""Adapter: one rectangle per cell of an rows x cols grid over the screen."""
from je_auto_control.utils.window_layout import grid_rects
rects = grid_rects(_resolve_screen(screen), int(rows), int(cols), gap=int(gap))
return {"count": len(rects), "rects": [rect.to_dict() for rect in rects]}


def _cascade_rects(count: Any, screen: Any = None, offset: Any = 30,
size: Any = None) -> Dict[str, Any]:
"""Adapter: count staggered, overlapping window rectangles (a cascade)."""
import json
from je_auto_control.utils.window_layout import cascade_rects
if isinstance(size, str):
size = json.loads(size) if size.strip() else None
rects = cascade_rects(_resolve_screen(screen), int(count), offset=int(offset),
size=tuple(size) if size else None)
return {"count": len(rects), "rects": [rect.to_dict() for rect in rects]}


def _with_modifiers(modifiers: Any, actions: Any) -> Dict[str, Any]:
"""Adapter: run nested actions while modifier keys are held down."""
import json
Expand Down Expand Up @@ -5071,6 +5110,9 @@ def __init__(self):
"AC_feature_match": _feature_match,
"AC_find_shapes": _find_shapes,
"AC_find_rectangles": _find_rectangles,
"AC_tile_rect": _tile_rect,
"AC_grid_rects": _grid_rects,
"AC_cascade_rects": _cascade_rects,
"AC_find_color_region": _find_color_region,
"AC_detect_drift": _detect_drift,
"AC_categorical_drift": _categorical_drift,
Expand Down
53 changes: 52 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -2709,6 +2709,57 @@ def shape_locator_tools() -> List[MCPTool]:
]


def window_layout_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_tile_rect",
description=("Compute the rectangle for a named tiling 'slot' of the "
"screen work area: full/left/right/top/bottom/top_left/"
"top_right/bottom_left/bottom_right/center/left_third/"
"center_third/right_third. 'screen' [x,y,w,h] defaults to "
"the live primary screen; 'gap' insets all sides. Returns "
"{rect:{x,y,width,height}} — feed to a window-move."),
input_schema=schema({
"slot": {"type": "string"},
"screen": {"type": "array", "items": {"type": "integer"}},
"gap": {"type": "integer"}},
required=["slot"]),
handler=h.tile_rect,
annotations=READ_ONLY,
),
MCPTool(
name="ac_grid_rects",
description=("Split the screen work area into an 'rows' x 'cols' grid; "
"return {count, rects:[{x,y,width,height}]} row-major. "
"'screen' [x,y,w,h] defaults to the live screen; 'gap' "
"insets each cell."),
input_schema=schema({
"rows": {"type": "integer"},
"cols": {"type": "integer"},
"screen": {"type": "array", "items": {"type": "integer"}},
"gap": {"type": "integer"}},
required=["rows", "cols"]),
handler=h.grid_rects,
annotations=READ_ONLY,
),
MCPTool(
name="ac_cascade_rects",
description=("Compute 'count' staggered, overlapping window rectangles "
"(a cascade), each 'offset' px down-right of the previous, "
"clamped to the screen. 'size' [w,h] defaults to 60% of "
"the work area. Returns {count, rects}."),
input_schema=schema({
"count": {"type": "integer"},
"screen": {"type": "array", "items": {"type": "integer"}},
"offset": {"type": "integer"},
"size": {"type": "array", "items": {"type": "integer"}}},
required=["count"]),
handler=h.cascade_rects,
annotations=READ_ONLY,
),
]


def ssim_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -6211,7 +6262,7 @@ def media_assert_tools() -> List[MCPTool]:
key_hold_tools, mouse_relative_tools, text_unicode_tools,
modifier_state_tools, grid_locator_tools, visual_match_tools,
color_region_tools, ssim_tools, feature_match_tools, shape_locator_tools,
plugin_sdk_tools, governance_tools,
window_layout_tools, plugin_sdk_tools, governance_tools,
credential_lease_tools, egress_tools, approval_testing_tools,
trajectory_eval_tools, compliance_tools, agent_trace_tools,
video_report_tools, fuzzy_tools, artifact_store_tools, image_dedup_tools,
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 @@ -2130,6 +2130,21 @@ def find_rectangles(region=None, min_area=400, max_area=None, aspect_range=None,
return _find_rectangles(region, min_area, max_area, aspect_range, epsilon)


def tile_rect(slot, screen=None, gap=0):
from je_auto_control.utils.executor.action_executor import _tile_rect
return _tile_rect(slot, screen, gap)


def grid_rects(rows, cols, screen=None, gap=0):
from je_auto_control.utils.executor.action_executor import _grid_rects
return _grid_rects(rows, cols, screen, gap)


def cascade_rects(count, screen=None, offset=30, size=None):
from je_auto_control.utils.executor.action_executor import _cascade_rects
return _cascade_rects(count, screen, offset, size)


def detect_drift(reference, current, threshold=0.25, bins=10):
from je_auto_control.utils.executor.action_executor import _detect_drift
return _detect_drift(reference, current, threshold, bins)
Expand Down
7 changes: 7 additions & 0 deletions je_auto_control/utils/window_layout/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Window tiling/layout geometry planner (halves, quadrants, grids, cascade)."""
from je_auto_control.utils.window_layout.window_layout import (
WindowRect, available_slots, cascade_rects, grid_rects, tile_rect,
)

__all__ = ["WindowRect", "available_slots", "cascade_rects", "grid_rects",
"tile_rect"]
Loading
Loading