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
8 changes: 8 additions & 0 deletions WHATS_NEW.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# What's New — AutoControl

## What's new (2026-07-02)

### Menu-Driven GUI: the Actions Menu Replaces In-Tab Buttons

Every tab's commands now live in one predictable place. The window menu bar gains a dynamic **Actions** menu that rebuilds for the active tab; tabs keep only their inputs, tables, and result/status views instead of rows of buttons.

- **Window-level Actions menu**: core tabs declare their commands at registration; feature tabs expose a `menu_actions()` hook returning `(label_key, handler)` pairs. 46 of 48 registered tabs now surface their commands this way — Script Builder and Remote Desktop intentionally keep their interactive panel layouts, and the menu shows a placeholder there. Buttons a window-level menu cannot replace stay in place (per-page browse buttons inside stacked trigger forms, the visibility-toggled data-source browse button, stateful auto-refresh checkboxes). A headless regression test guards the contract so no tab can silently lose its commands.

## What's new (2026-06-26)

### Trial and Force Action Modes (Playwright-style)
Expand Down
26 changes: 3 additions & 23 deletions je_auto_control/gui/_auto_click_tab.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from PySide6.QtGui import QIntValidator
from PySide6.QtWidgets import (
QWidget, QLineEdit, QComboBox, QPushButton, QVBoxLayout, QLabel,
QWidget, QLineEdit, QComboBox, QVBoxLayout, QLabel,
QGridLayout, QHBoxLayout, QRadioButton, QButtonGroup, QMessageBox,
QGroupBox,
)
Expand Down Expand Up @@ -100,29 +100,18 @@ def _build_auto_click_tab(self) -> QWidget:
rh.addWidget(self.repeat_count_input)
grid.addLayout(rh, row, 0, 1, 2)

row += 1
btn_h = QHBoxLayout()
self.start_button = self._tr(QPushButton(), "start")
self.start_button.clicked.connect(self._start_auto_click)
self.stop_button = self._tr(QPushButton(), "stop")
self.stop_button.clicked.connect(self._stop_auto_click)
btn_h.addWidget(self.start_button)
btn_h.addWidget(self.stop_button)
grid.addLayout(btn_h, row, 0, 1, 2)

click_group.setLayout(grid)
outer.addWidget(click_group)

# Start/stop, position probe, hotkey, write, and scroll commands all
# run from the Actions menu; the tab keeps only their inputs.
pos_group = self._tr(QGroupBox(), "get_position")
pos_layout = QHBoxLayout()
self.pos_btn = self._tr(QPushButton(), "get_position")
self.pos_btn.clicked.connect(self._get_mouse_pos)
self.pos_label = QLabel()
self._pos_label_suffix = " --"
self.pos_label.setText(
self._translate("current_position") + self._pos_label_suffix,
)
pos_layout.addWidget(self.pos_btn)
pos_layout.addWidget(self.pos_label)
pos_group.setLayout(pos_layout)
outer.addWidget(pos_group)
Expand All @@ -131,20 +120,14 @@ def _build_auto_click_tab(self) -> QWidget:
hk_layout = QHBoxLayout()
self.hotkey_input = QLineEdit()
self.hotkey_input.setPlaceholderText("ctrl,a")
self.hotkey_btn = self._tr(QPushButton(), "hotkey_send")
self.hotkey_btn.clicked.connect(self._send_hotkey)
hk_layout.addWidget(self.hotkey_input)
hk_layout.addWidget(self.hotkey_btn)
hotkey_group.setLayout(hk_layout)
outer.addWidget(hotkey_group)

write_group = self._tr(QGroupBox(), "write_label")
wr_layout = QHBoxLayout()
self.write_input = QLineEdit()
self.write_btn = self._tr(QPushButton(), "write_send")
self.write_btn.clicked.connect(self._send_write)
wr_layout.addWidget(self.write_input)
wr_layout.addWidget(self.write_btn)
write_group.setLayout(wr_layout)
outer.addWidget(write_group)

Expand All @@ -160,9 +143,6 @@ def _build_auto_click_tab(self) -> QWidget:
sc_layout.addWidget(self.scroll_dir_combo)
else:
self.scroll_dir_combo = None
self.scroll_btn = self._tr(QPushButton(), "scroll_send")
self.scroll_btn.clicked.connect(self._send_scroll)
sc_layout.addWidget(self.scroll_btn)
scroll_group.setLayout(sc_layout)
outer.addWidget(scroll_group)

Expand Down
28 changes: 9 additions & 19 deletions je_auto_control/gui/_report_tab.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Report-generation tab builder (extracted mixin)."""
from PySide6.QtWidgets import (
QGroupBox, QHBoxLayout, QLabel, QLineEdit, QPushButton,
QGroupBox, QHBoxLayout, QLabel, QLineEdit,
QTextEdit, QVBoxLayout, QWidget,
)

Expand All @@ -21,15 +21,11 @@ def _build_report_tab(self) -> QWidget:
tab = QWidget()
layout = QVBoxLayout()

# Enable/disable recording and report generation run from the
# Actions menu; the tab keeps only status, name input, and result.
tr_group = self._tr(QGroupBox(), "test_record_status")
tr_h = QHBoxLayout()
self.tr_enable_btn = self._tr(QPushButton(), "enable_test_record")
self.tr_enable_btn.clicked.connect(lambda: self._set_test_record(True))
self.tr_disable_btn = self._tr(QPushButton(), "disable_test_record")
self.tr_disable_btn.clicked.connect(lambda: self._set_test_record(False))
self.tr_status_label = QLabel("OFF")
tr_h.addWidget(self.tr_enable_btn)
tr_h.addWidget(self.tr_disable_btn)
tr_h.addWidget(self.tr_status_label)
tr_group.setLayout(tr_h)
layout.addWidget(tr_group)
Expand All @@ -40,18 +36,6 @@ def _build_report_tab(self) -> QWidget:
name_h.addWidget(self.report_name_input)
layout.addLayout(name_h)

btn_h = QHBoxLayout()
self.html_report_btn = self._tr(QPushButton(), "generate_html_report")
self.html_report_btn.clicked.connect(self._gen_html)
self.json_report_btn = self._tr(QPushButton(), "generate_json_report")
self.json_report_btn.clicked.connect(self._gen_json)
self.xml_report_btn = self._tr(QPushButton(), "generate_xml_report")
self.xml_report_btn.clicked.connect(self._gen_xml)
btn_h.addWidget(self.html_report_btn)
btn_h.addWidget(self.json_report_btn)
btn_h.addWidget(self.xml_report_btn)
layout.addLayout(btn_h)

layout.addWidget(self._tr(QLabel(), "report_result"))
self.report_result_text = QTextEdit()
self.report_result_text.setReadOnly(True)
Expand All @@ -64,6 +48,12 @@ def _set_test_record(self, enable: bool):
test_record_instance.set_record_enable(enable)
self.tr_status_label.setText("ON" if enable else "OFF")

def _enable_test_record(self):
self._set_test_record(True)

def _disable_test_record(self):
self._set_test_record(False)

def _gen_html(self):
try:
name = self.report_name_input.text() or "autocontrol_report"
Expand Down
17 changes: 10 additions & 7 deletions je_auto_control/gui/a11y_audit_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QAbstractItemView, QHBoxLayout, QLabel, QLineEdit, QPushButton,
QAbstractItemView, QHBoxLayout, QLabel, QLineEdit,
QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
)

Expand Down Expand Up @@ -55,25 +55,28 @@ def _apply_headers(self) -> None:
self._table.setHorizontalHeaderLabels([_t(k) for k in _COLS])

def _build_layout(self) -> None:
# Audit/contrast commands run from the Actions menu; the tab keeps
# only the inputs, the issue table, and the summary line.
root = QVBoxLayout(self)
row = QHBoxLayout()
row.addWidget(QLabel(_t("audit_app")))
row.addWidget(self._app, stretch=1)
run_btn = self._tr(QPushButton(), "audit_run")
run_btn.clicked.connect(self._on_run)
row.addWidget(run_btn)
root.addLayout(row)
crow = QHBoxLayout()
crow.addWidget(QLabel(_t("audit_contrast_label")))
crow.addWidget(self._fg)
crow.addWidget(self._bg)
contrast_btn = self._tr(QPushButton(), "audit_contrast_run")
contrast_btn.clicked.connect(self._on_contrast)
crow.addWidget(contrast_btn)
root.addLayout(crow)
root.addWidget(self._table, stretch=1)
root.addWidget(self._summary)

def menu_actions(self) -> list:
"""Expose tab commands to the window-level Actions menu."""
return [
("audit_run", self._on_run),
("audit_contrast_run", self._on_contrast),
]

def _on_run(self) -> None:
app = self._app.text().strip() or None
try:
Expand Down
20 changes: 10 additions & 10 deletions je_auto_control/gui/accessibility_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QAbstractItemView, QHBoxLayout, QHeaderView, QLabel, QLineEdit,
QMessageBox, QPushButton, QTableWidget, QTableWidgetItem,
QMessageBox, QTableWidget, QTableWidgetItem,
QVBoxLayout, QWidget,
)

Expand Down Expand Up @@ -59,6 +59,8 @@ def _apply_table_headers(self) -> None:
])

def _build_layout(self) -> None:
# Refresh/click commands run from the Actions menu; the tab keeps
# only the filter inputs, the element table, and the status line.
root = QVBoxLayout(self)
row = QHBoxLayout()
row.addWidget(self._tr(QLabel(), "a11y_app_label"))
Expand All @@ -67,19 +69,17 @@ def _build_layout(self) -> None:
row.addWidget(self._tr(QLabel(), "a11y_name_label"))
self._name_filter.setPlaceholderText(_t("a11y_name_placeholder"))
row.addWidget(self._name_filter, stretch=1)
refresh = self._tr(QPushButton(), "a11y_refresh")
refresh.clicked.connect(self._refresh)
row.addWidget(refresh)
root.addLayout(row)
root.addWidget(self._table, stretch=1)
action_row = QHBoxLayout()
click_btn = self._tr(QPushButton(), "a11y_click_selected")
click_btn.clicked.connect(self._click_selected)
action_row.addWidget(click_btn)
action_row.addStretch()
root.addLayout(action_row)
root.addWidget(self._status)

def menu_actions(self) -> list:
"""Expose tab commands to the window-level Actions menu."""
return [
("a11y_refresh", self._refresh),
("a11y_click_selected", self._click_selected),
]

def _refresh(self) -> None:
app = self._app_filter.text().strip() or None
try:
Expand Down
36 changes: 13 additions & 23 deletions je_auto_control/gui/admin_console_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from PySide6.QtGui import QIcon, QImage, QPixmap
from PySide6.QtWidgets import (
QGroupBox, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QListWidget,
QListWidgetItem, QMessageBox, QPushButton, QSpinBox, QTableWidget,
QListWidgetItem, QMessageBox, QSpinBox, QTableWidget,
QTableWidgetItem, QTextEdit, QVBoxLayout, QWidget,
)

Expand Down Expand Up @@ -106,22 +106,30 @@ def __init__(self, parent: Optional[QWidget] = None) -> None:
self._apply_thumb_interval()

def _build_layout(self) -> None:
# Add/remove/refresh/thumbnail/broadcast commands run from the
# Actions menu; the tab keeps only the inputs, tables, and output.
root = QVBoxLayout(self)
root.addWidget(self._build_add_group())
root.addWidget(self._table, stretch=1)
root.addLayout(self._build_button_row())
root.addWidget(self._build_thumbnails_group(), stretch=1)
root.addWidget(self._build_broadcast_group(), stretch=1)

def menu_actions(self) -> list:
"""Expose tab commands to the window-level Actions menu."""
return [
("admin_add", self._on_add),
("admin_remove", self._on_remove),
("admin_refresh", self._on_refresh),
("admin_thumb_refresh_now", self._refresh_thumbnails),
("admin_broadcast_run", self._on_broadcast),
]

def _build_thumbnails_group(self) -> QGroupBox:
group = self._tr(QGroupBox(), "admin_thumb_group")
layout = QVBoxLayout(group)
controls = QHBoxLayout()
controls.addWidget(self._tr(QLabel(), "admin_thumb_interval"))
controls.addWidget(self._thumb_interval)
refresh = self._tr(QPushButton(), "admin_thumb_refresh_now")
refresh.clicked.connect(self._refresh_thumbnails)
controls.addWidget(refresh)
controls.addStretch(1)
layout.addLayout(controls)
layout.addWidget(self._thumbnails, stretch=1)
Expand All @@ -136,31 +144,13 @@ def _build_add_group(self) -> QGroupBox:
form.addWidget(self._url_input, stretch=1)
form.addWidget(self._tr(QLabel(), "admin_token"))
form.addWidget(self._token_input)
add = self._tr(QPushButton(), "admin_add")
add.clicked.connect(self._on_add)
form.addWidget(add)
return group

def _build_button_row(self) -> QHBoxLayout:
row = QHBoxLayout()
for key, handler in (
("admin_remove", self._on_remove),
("admin_refresh", self._on_refresh),
):
btn = self._tr(QPushButton(), key)
btn.clicked.connect(handler)
row.addWidget(btn)
row.addStretch(1)
return row

def _build_broadcast_group(self) -> QGroupBox:
group = self._tr(QGroupBox(), "admin_broadcast_group")
form = QVBoxLayout(group)
form.addWidget(self._tr(QLabel(), "admin_actions_label"))
form.addWidget(self._actions_input)
run = self._tr(QPushButton(), "admin_broadcast_run")
run.clicked.connect(self._on_broadcast)
form.addWidget(run)
form.addWidget(self._tr(QLabel(), "admin_results_label"))
form.addWidget(self._broadcast_output, stretch=1)
return group
Expand Down
13 changes: 9 additions & 4 deletions je_auto_control/gui/assertions_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Any, Dict, Optional

from PySide6.QtWidgets import (
QCheckBox, QComboBox, QHBoxLayout, QLabel, QLineEdit, QPushButton,
QCheckBox, QComboBox, QHBoxLayout, QLabel, QLineEdit,
QVBoxLayout, QWidget,
)

Expand Down Expand Up @@ -51,6 +51,8 @@ def __init__(self, parent: Optional[QWidget] = None) -> None:
self._sync_visibility()

def _build_layout(self) -> None:
# The run command runs from the Actions menu; the tab keeps only
# the assertion form and the result label.
root = QVBoxLayout(self)
krow = QHBoxLayout()
krow.addWidget(QLabel(_t("assert_kind")))
Expand All @@ -71,12 +73,15 @@ def _build_layout(self) -> None:

root.addWidget(self._expect)
root.addWidget(self._regex)
run_btn = self._tr(QPushButton(), "assert_run")
run_btn.clicked.connect(self._on_run)
root.addWidget(run_btn)
root.addWidget(self._result)
root.addStretch()

def menu_actions(self) -> list:
"""Expose tab commands to the window-level Actions menu."""
return [
("assert_run", self._on_run),
]

def _current_kind(self) -> str:
return _KINDS[self._kind.currentIndex()]

Expand Down
26 changes: 11 additions & 15 deletions je_auto_control/gui/audit_log_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from PySide6.QtWidgets import (
QComboBox, QGroupBox, QHBoxLayout, QHeaderView, QLabel, QLineEdit,
QMessageBox, QPushButton, QSpinBox, QTableWidget, QTableWidgetItem,
QMessageBox, QSpinBox, QTableWidget, QTableWidgetItem,
QVBoxLayout, QWidget,
)

Expand Down Expand Up @@ -72,12 +72,21 @@ def __init__(self, parent: Optional[QWidget] = None) -> None:
self._refresh()

def _build_layout(self) -> None:
# Refresh/verify/clear commands run from the Actions menu; the
# tab keeps only the filter inputs, the table, and the status.
root = QVBoxLayout(self)
root.addWidget(self._build_filter_group())
root.addWidget(self._table, stretch=1)
root.addLayout(self._build_button_row())
root.addWidget(self._verify_status)

def menu_actions(self) -> list:
"""Expose tab commands to the window-level Actions menu."""
return [
("audit_refresh", self._refresh),
("audit_verify", self._verify),
("audit_clear", self._clear),
]

def _build_filter_group(self) -> QGroupBox:
group = self._tr(QGroupBox(), "audit_filter_group")
row = QHBoxLayout(group)
Expand All @@ -89,19 +98,6 @@ def _build_filter_group(self) -> QGroupBox:
row.addWidget(self._limit_input)
return group

def _build_button_row(self) -> QHBoxLayout:
row = QHBoxLayout()
for key, handler in (
("audit_refresh", self._refresh),
("audit_verify", self._verify),
("audit_clear", self._clear),
):
btn = self._tr(QPushButton(), key)
btn.clicked.connect(handler)
row.addWidget(btn)
row.addStretch(1)
return row

def _refresh(self) -> None:
self._apply_table_headers()
# Pull a wide window so the dropdown reflects everything the user
Expand Down
Loading
Loading