Skip to content

Commit ed66db2

Browse files
authored
Merge pull request #454 from Integration-Automation/feat/minimal-menu-ui
Redesign GUI around a menu-driven, low-button layout
2 parents dcf86b7 + 124a4a9 commit ed66db2

52 files changed

Lines changed: 770 additions & 785 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

WHATS_NEW.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# What's New — AutoControl
22

3+
## What's new (2026-07-02)
4+
5+
### Menu-Driven GUI: the Actions Menu Replaces In-Tab Buttons
6+
7+
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.
8+
9+
- **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.
10+
311
## What's new (2026-06-26)
412

513
### Trial and Force Action Modes (Playwright-style)

je_auto_control/gui/_auto_click_tab.py

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from PySide6.QtGui import QIntValidator
22
from PySide6.QtWidgets import (
3-
QWidget, QLineEdit, QComboBox, QPushButton, QVBoxLayout, QLabel,
3+
QWidget, QLineEdit, QComboBox, QVBoxLayout, QLabel,
44
QGridLayout, QHBoxLayout, QRadioButton, QButtonGroup, QMessageBox,
55
QGroupBox,
66
)
@@ -100,29 +100,18 @@ def _build_auto_click_tab(self) -> QWidget:
100100
rh.addWidget(self.repeat_count_input)
101101
grid.addLayout(rh, row, 0, 1, 2)
102102

103-
row += 1
104-
btn_h = QHBoxLayout()
105-
self.start_button = self._tr(QPushButton(), "start")
106-
self.start_button.clicked.connect(self._start_auto_click)
107-
self.stop_button = self._tr(QPushButton(), "stop")
108-
self.stop_button.clicked.connect(self._stop_auto_click)
109-
btn_h.addWidget(self.start_button)
110-
btn_h.addWidget(self.stop_button)
111-
grid.addLayout(btn_h, row, 0, 1, 2)
112-
113103
click_group.setLayout(grid)
114104
outer.addWidget(click_group)
115105

106+
# Start/stop, position probe, hotkey, write, and scroll commands all
107+
# run from the Actions menu; the tab keeps only their inputs.
116108
pos_group = self._tr(QGroupBox(), "get_position")
117109
pos_layout = QHBoxLayout()
118-
self.pos_btn = self._tr(QPushButton(), "get_position")
119-
self.pos_btn.clicked.connect(self._get_mouse_pos)
120110
self.pos_label = QLabel()
121111
self._pos_label_suffix = " --"
122112
self.pos_label.setText(
123113
self._translate("current_position") + self._pos_label_suffix,
124114
)
125-
pos_layout.addWidget(self.pos_btn)
126115
pos_layout.addWidget(self.pos_label)
127116
pos_group.setLayout(pos_layout)
128117
outer.addWidget(pos_group)
@@ -131,20 +120,14 @@ def _build_auto_click_tab(self) -> QWidget:
131120
hk_layout = QHBoxLayout()
132121
self.hotkey_input = QLineEdit()
133122
self.hotkey_input.setPlaceholderText("ctrl,a")
134-
self.hotkey_btn = self._tr(QPushButton(), "hotkey_send")
135-
self.hotkey_btn.clicked.connect(self._send_hotkey)
136123
hk_layout.addWidget(self.hotkey_input)
137-
hk_layout.addWidget(self.hotkey_btn)
138124
hotkey_group.setLayout(hk_layout)
139125
outer.addWidget(hotkey_group)
140126

141127
write_group = self._tr(QGroupBox(), "write_label")
142128
wr_layout = QHBoxLayout()
143129
self.write_input = QLineEdit()
144-
self.write_btn = self._tr(QPushButton(), "write_send")
145-
self.write_btn.clicked.connect(self._send_write)
146130
wr_layout.addWidget(self.write_input)
147-
wr_layout.addWidget(self.write_btn)
148131
write_group.setLayout(wr_layout)
149132
outer.addWidget(write_group)
150133

@@ -160,9 +143,6 @@ def _build_auto_click_tab(self) -> QWidget:
160143
sc_layout.addWidget(self.scroll_dir_combo)
161144
else:
162145
self.scroll_dir_combo = None
163-
self.scroll_btn = self._tr(QPushButton(), "scroll_send")
164-
self.scroll_btn.clicked.connect(self._send_scroll)
165-
sc_layout.addWidget(self.scroll_btn)
166146
scroll_group.setLayout(sc_layout)
167147
outer.addWidget(scroll_group)
168148

je_auto_control/gui/_report_tab.py

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Report-generation tab builder (extracted mixin)."""
22
from PySide6.QtWidgets import (
3-
QGroupBox, QHBoxLayout, QLabel, QLineEdit, QPushButton,
3+
QGroupBox, QHBoxLayout, QLabel, QLineEdit,
44
QTextEdit, QVBoxLayout, QWidget,
55
)
66

@@ -21,15 +21,11 @@ def _build_report_tab(self) -> QWidget:
2121
tab = QWidget()
2222
layout = QVBoxLayout()
2323

24+
# Enable/disable recording and report generation run from the
25+
# Actions menu; the tab keeps only status, name input, and result.
2426
tr_group = self._tr(QGroupBox(), "test_record_status")
2527
tr_h = QHBoxLayout()
26-
self.tr_enable_btn = self._tr(QPushButton(), "enable_test_record")
27-
self.tr_enable_btn.clicked.connect(lambda: self._set_test_record(True))
28-
self.tr_disable_btn = self._tr(QPushButton(), "disable_test_record")
29-
self.tr_disable_btn.clicked.connect(lambda: self._set_test_record(False))
3028
self.tr_status_label = QLabel("OFF")
31-
tr_h.addWidget(self.tr_enable_btn)
32-
tr_h.addWidget(self.tr_disable_btn)
3329
tr_h.addWidget(self.tr_status_label)
3430
tr_group.setLayout(tr_h)
3531
layout.addWidget(tr_group)
@@ -40,18 +36,6 @@ def _build_report_tab(self) -> QWidget:
4036
name_h.addWidget(self.report_name_input)
4137
layout.addLayout(name_h)
4238

43-
btn_h = QHBoxLayout()
44-
self.html_report_btn = self._tr(QPushButton(), "generate_html_report")
45-
self.html_report_btn.clicked.connect(self._gen_html)
46-
self.json_report_btn = self._tr(QPushButton(), "generate_json_report")
47-
self.json_report_btn.clicked.connect(self._gen_json)
48-
self.xml_report_btn = self._tr(QPushButton(), "generate_xml_report")
49-
self.xml_report_btn.clicked.connect(self._gen_xml)
50-
btn_h.addWidget(self.html_report_btn)
51-
btn_h.addWidget(self.json_report_btn)
52-
btn_h.addWidget(self.xml_report_btn)
53-
layout.addLayout(btn_h)
54-
5539
layout.addWidget(self._tr(QLabel(), "report_result"))
5640
self.report_result_text = QTextEdit()
5741
self.report_result_text.setReadOnly(True)
@@ -64,6 +48,12 @@ def _set_test_record(self, enable: bool):
6448
test_record_instance.set_record_enable(enable)
6549
self.tr_status_label.setText("ON" if enable else "OFF")
6650

51+
def _enable_test_record(self):
52+
self._set_test_record(True)
53+
54+
def _disable_test_record(self):
55+
self._set_test_record(False)
56+
6757
def _gen_html(self):
6858
try:
6959
name = self.report_name_input.text() or "autocontrol_report"

je_auto_control/gui/a11y_audit_tab.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from PySide6.QtCore import Qt
99
from PySide6.QtWidgets import (
10-
QAbstractItemView, QHBoxLayout, QLabel, QLineEdit, QPushButton,
10+
QAbstractItemView, QHBoxLayout, QLabel, QLineEdit,
1111
QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
1212
)
1313

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

5757
def _build_layout(self) -> None:
58+
# Audit/contrast commands run from the Actions menu; the tab keeps
59+
# only the inputs, the issue table, and the summary line.
5860
root = QVBoxLayout(self)
5961
row = QHBoxLayout()
6062
row.addWidget(QLabel(_t("audit_app")))
6163
row.addWidget(self._app, stretch=1)
62-
run_btn = self._tr(QPushButton(), "audit_run")
63-
run_btn.clicked.connect(self._on_run)
64-
row.addWidget(run_btn)
6564
root.addLayout(row)
6665
crow = QHBoxLayout()
6766
crow.addWidget(QLabel(_t("audit_contrast_label")))
6867
crow.addWidget(self._fg)
6968
crow.addWidget(self._bg)
70-
contrast_btn = self._tr(QPushButton(), "audit_contrast_run")
71-
contrast_btn.clicked.connect(self._on_contrast)
72-
crow.addWidget(contrast_btn)
7369
root.addLayout(crow)
7470
root.addWidget(self._table, stretch=1)
7571
root.addWidget(self._summary)
7672

73+
def menu_actions(self) -> list:
74+
"""Expose tab commands to the window-level Actions menu."""
75+
return [
76+
("audit_run", self._on_run),
77+
("audit_contrast_run", self._on_contrast),
78+
]
79+
7780
def _on_run(self) -> None:
7881
app = self._app.text().strip() or None
7982
try:

je_auto_control/gui/accessibility_tab.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from PySide6.QtCore import Qt
55
from PySide6.QtWidgets import (
66
QAbstractItemView, QHBoxLayout, QHeaderView, QLabel, QLineEdit,
7-
QMessageBox, QPushButton, QTableWidget, QTableWidgetItem,
7+
QMessageBox, QTableWidget, QTableWidgetItem,
88
QVBoxLayout, QWidget,
99
)
1010

@@ -59,6 +59,8 @@ def _apply_table_headers(self) -> None:
5959
])
6060

6161
def _build_layout(self) -> None:
62+
# Refresh/click commands run from the Actions menu; the tab keeps
63+
# only the filter inputs, the element table, and the status line.
6264
root = QVBoxLayout(self)
6365
row = QHBoxLayout()
6466
row.addWidget(self._tr(QLabel(), "a11y_app_label"))
@@ -67,19 +69,17 @@ def _build_layout(self) -> None:
6769
row.addWidget(self._tr(QLabel(), "a11y_name_label"))
6870
self._name_filter.setPlaceholderText(_t("a11y_name_placeholder"))
6971
row.addWidget(self._name_filter, stretch=1)
70-
refresh = self._tr(QPushButton(), "a11y_refresh")
71-
refresh.clicked.connect(self._refresh)
72-
row.addWidget(refresh)
7372
root.addLayout(row)
7473
root.addWidget(self._table, stretch=1)
75-
action_row = QHBoxLayout()
76-
click_btn = self._tr(QPushButton(), "a11y_click_selected")
77-
click_btn.clicked.connect(self._click_selected)
78-
action_row.addWidget(click_btn)
79-
action_row.addStretch()
80-
root.addLayout(action_row)
8174
root.addWidget(self._status)
8275

76+
def menu_actions(self) -> list:
77+
"""Expose tab commands to the window-level Actions menu."""
78+
return [
79+
("a11y_refresh", self._refresh),
80+
("a11y_click_selected", self._click_selected),
81+
]
82+
8383
def _refresh(self) -> None:
8484
app = self._app_filter.text().strip() or None
8585
try:

je_auto_control/gui/admin_console_tab.py

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from PySide6.QtGui import QIcon, QImage, QPixmap
77
from PySide6.QtWidgets import (
88
QGroupBox, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QListWidget,
9-
QListWidgetItem, QMessageBox, QPushButton, QSpinBox, QTableWidget,
9+
QListWidgetItem, QMessageBox, QSpinBox, QTableWidget,
1010
QTableWidgetItem, QTextEdit, QVBoxLayout, QWidget,
1111
)
1212

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

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

117+
def menu_actions(self) -> list:
118+
"""Expose tab commands to the window-level Actions menu."""
119+
return [
120+
("admin_add", self._on_add),
121+
("admin_remove", self._on_remove),
122+
("admin_refresh", self._on_refresh),
123+
("admin_thumb_refresh_now", self._refresh_thumbnails),
124+
("admin_broadcast_run", self._on_broadcast),
125+
]
126+
116127
def _build_thumbnails_group(self) -> QGroupBox:
117128
group = self._tr(QGroupBox(), "admin_thumb_group")
118129
layout = QVBoxLayout(group)
119130
controls = QHBoxLayout()
120131
controls.addWidget(self._tr(QLabel(), "admin_thumb_interval"))
121132
controls.addWidget(self._thumb_interval)
122-
refresh = self._tr(QPushButton(), "admin_thumb_refresh_now")
123-
refresh.clicked.connect(self._refresh_thumbnails)
124-
controls.addWidget(refresh)
125133
controls.addStretch(1)
126134
layout.addLayout(controls)
127135
layout.addWidget(self._thumbnails, stretch=1)
@@ -136,31 +144,13 @@ def _build_add_group(self) -> QGroupBox:
136144
form.addWidget(self._url_input, stretch=1)
137145
form.addWidget(self._tr(QLabel(), "admin_token"))
138146
form.addWidget(self._token_input)
139-
add = self._tr(QPushButton(), "admin_add")
140-
add.clicked.connect(self._on_add)
141-
form.addWidget(add)
142147
return group
143148

144-
def _build_button_row(self) -> QHBoxLayout:
145-
row = QHBoxLayout()
146-
for key, handler in (
147-
("admin_remove", self._on_remove),
148-
("admin_refresh", self._on_refresh),
149-
):
150-
btn = self._tr(QPushButton(), key)
151-
btn.clicked.connect(handler)
152-
row.addWidget(btn)
153-
row.addStretch(1)
154-
return row
155-
156149
def _build_broadcast_group(self) -> QGroupBox:
157150
group = self._tr(QGroupBox(), "admin_broadcast_group")
158151
form = QVBoxLayout(group)
159152
form.addWidget(self._tr(QLabel(), "admin_actions_label"))
160153
form.addWidget(self._actions_input)
161-
run = self._tr(QPushButton(), "admin_broadcast_run")
162-
run.clicked.connect(self._on_broadcast)
163-
form.addWidget(run)
164154
form.addWidget(self._tr(QLabel(), "admin_results_label"))
165155
form.addWidget(self._broadcast_output, stretch=1)
166156
return group

je_auto_control/gui/assertions_tab.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Any, Dict, Optional
88

99
from PySide6.QtWidgets import (
10-
QCheckBox, QComboBox, QHBoxLayout, QLabel, QLineEdit, QPushButton,
10+
QCheckBox, QComboBox, QHBoxLayout, QLabel, QLineEdit,
1111
QVBoxLayout, QWidget,
1212
)
1313

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

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

7274
root.addWidget(self._expect)
7375
root.addWidget(self._regex)
74-
run_btn = self._tr(QPushButton(), "assert_run")
75-
run_btn.clicked.connect(self._on_run)
76-
root.addWidget(run_btn)
7776
root.addWidget(self._result)
7877
root.addStretch()
7978

79+
def menu_actions(self) -> list:
80+
"""Expose tab commands to the window-level Actions menu."""
81+
return [
82+
("assert_run", self._on_run),
83+
]
84+
8085
def _current_kind(self) -> str:
8186
return _KINDS[self._kind.currentIndex()]
8287

je_auto_control/gui/audit_log_tab.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from PySide6.QtWidgets import (
66
QComboBox, QGroupBox, QHBoxLayout, QHeaderView, QLabel, QLineEdit,
7-
QMessageBox, QPushButton, QSpinBox, QTableWidget, QTableWidgetItem,
7+
QMessageBox, QSpinBox, QTableWidget, QTableWidgetItem,
88
QVBoxLayout, QWidget,
99
)
1010

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

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

82+
def menu_actions(self) -> list:
83+
"""Expose tab commands to the window-level Actions menu."""
84+
return [
85+
("audit_refresh", self._refresh),
86+
("audit_verify", self._verify),
87+
("audit_clear", self._clear),
88+
]
89+
8190
def _build_filter_group(self) -> QGroupBox:
8291
group = self._tr(QGroupBox(), "audit_filter_group")
8392
row = QHBoxLayout(group)
@@ -89,19 +98,6 @@ def _build_filter_group(self) -> QGroupBox:
8998
row.addWidget(self._limit_input)
9099
return group
91100

92-
def _build_button_row(self) -> QHBoxLayout:
93-
row = QHBoxLayout()
94-
for key, handler in (
95-
("audit_refresh", self._refresh),
96-
("audit_verify", self._verify),
97-
("audit_clear", self._clear),
98-
):
99-
btn = self._tr(QPushButton(), key)
100-
btn.clicked.connect(handler)
101-
row.addWidget(btn)
102-
row.addStretch(1)
103-
return row
104-
105101
def _refresh(self) -> None:
106102
self._apply_table_headers()
107103
# Pull a wide window so the dropdown reflects everything the user

0 commit comments

Comments
 (0)