Skip to content

Commit 1554a2f

Browse files
committed
feat: Add fullscreen operator view with large controls and PASS/FAIL indicator
Major operator view enhancements: - Fullscreen mode with Escape key to exit - Large control buttons (18 char width, 28pt font) with mirrored state management - Massive PASS/FAIL indicator (180pt) showing script execution status - Centered vertical layout using spacer technique - Visible exit button in top-left corner - Button states synchronized with main script controls - Color-coded status: READY (gray), RUNNING (blue), PASS (green), FAIL (red)
1 parent 433703e commit 1554a2f

File tree

6 files changed

+709
-24
lines changed

6 files changed

+709
-24
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
## [1.18.0] - 2025-11-26
8+
9+
### Added
10+
- **Operator View Infrastructure**: Support for double-click to open views, shared button state management
11+
- **Script Error Tracking**: Added `had_errors` flag to track warnings/errors for PASS/FAIL logic
12+
- **FONT_FAMILY Export**: Added `theme.FONT_FAMILY` for custom font construction in device modules
13+
14+
### Changed
15+
- **Script Control Sharing**: Run/Hold/Reset handlers now shared with operator views via `shared_gui_refs`
16+
- **Button State Synchronization**: Operator view buttons update simultaneously with main GUI buttons
17+
718
## [1.17.0] - 2025-11-26
819

920
### Added

_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.17.0"
1+
__version__ = "1.18.0"

src/device/panel.py

Lines changed: 230 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ def __init__(self, parent, script_editor_widget, device_manager, **kwargs):
161161
self.text.tag_configure('events_header', foreground=theme.SUCCESS_GREEN, font=theme.FONT_BOLD)
162162
self.text.tag_configure('warnings_header', foreground=theme.ERROR_RED, font=theme.FONT_BOLD)
163163
self.text.tag_configure('warning_part', foreground=theme.ERROR_RED)
164+
self.text.tag_configure('views_header', foreground='#B0A3D4', font=theme.FONT_BOLD) # Lavender
165+
self.text.tag_configure('view_name', foreground='#B0A3D4') # Lavender
164166
self.text.tag_configure('folder_icon', foreground=theme.COMMENT_COLOR)
165167
self.text.tag_configure('variable_info', foreground=theme.COMMENT_COLOR)
166168
self.text.tag_configure('parameter_name', foreground=theme.PARAMETER_COLOR) # Orange/yellow for parameter names
@@ -182,6 +184,9 @@ def __init__(self, parent, script_editor_widget, device_manager, **kwargs):
182184
# Track hover line for highlighting
183185
self.current_hover_line = None
184186

187+
# Create tooltip for the main text widget
188+
self.tooltip = Tooltip(self.text)
189+
185190
# Add device button at bottom
186191
add_device_btn = ttk.Button(self,
187192
text="+ Add Device...",
@@ -755,19 +760,67 @@ def refresh(self):
755760
else:
756761
self.text.insert(f"{line_num}.end", f"{full_warning_name}", "warning_part")
757762

758-
# Add description info
763+
self.text.insert(f"{line_num}.end", "\n")
764+
765+
# Add tooltip for description
759766
description = warning_details.get('description', '')
760767
if description:
761-
self.text.insert(f"{line_num}.end", f" - {description}", "variable_info")
762-
763-
self.text.insert(f"{line_num}.end", "\n")
768+
warn_key = f"warn_{full_warning_name}"
769+
self.text.tag_add(warn_key, f"{line_num}.0", f"{line_num}.end")
770+
self.text.tag_bind(warn_key, "<Enter>",
771+
lambda e, desc=description: self._show_tooltip(e, desc, self.tooltip))
772+
self.text.tag_bind(warn_key, "<Leave>", lambda e: self._hide_tooltip(self.tooltip))
764773

765774
self.line_items[line_num] = {
766775
'type': 'warning',
767776
'name': full_warning_name,
768777
'data': warning_details
769778
}
770779
line_num += 1
780+
781+
# Views folder
782+
views = device_data.get('views_data', {})
783+
views_folder_key = (device_name, 'views_folder')
784+
views_collapsed = views_folder_key in self.collapsed_folders
785+
views_icon = " ► " if views_collapsed else " ▼ "
786+
787+
self.text.insert(f"{line_num}.0", views_icon, "folder_icon")
788+
self.text.insert(f"{line_num}.end", f"views ({len(views)})\n", "views_header")
789+
self.line_items[line_num] = {'type': 'views_folder', 'device': device_name, 'line_start': line_num}
790+
line_num += 1
791+
792+
# Add individual views if not collapsed
793+
if not views_collapsed:
794+
for view_key, view_details in sorted(views.items()):
795+
view_name = view_details.get('name', view_key)
796+
view_icon = view_details.get('icon', '')
797+
798+
self.text.insert(f"{line_num}.0", " ", "folder_icon")
799+
# Only add icon if it's not empty
800+
if view_icon and view_icon.strip():
801+
self.text.insert(f"{line_num}.end", f"{view_icon} {view_name}", "view_name")
802+
else:
803+
self.text.insert(f"{line_num}.end", f"{view_name}", "view_name")
804+
805+
self.text.insert(f"{line_num}.end", "\n")
806+
807+
# Add tooltip for description
808+
description = view_details.get('description', '')
809+
if description:
810+
view_tag_key = f"view_{device_name}_{view_key}"
811+
self.text.tag_add(view_tag_key, f"{line_num}.0", f"{line_num}.end")
812+
self.text.tag_bind(view_tag_key, "<Enter>",
813+
lambda e, desc=description: self._show_tooltip(e, desc, self.tooltip))
814+
self.text.tag_bind(view_tag_key, "<Leave>", lambda e: self._hide_tooltip(self.tooltip))
815+
816+
self.line_items[line_num] = {
817+
'type': 'view',
818+
'view_id': view_key,
819+
'name': view_key,
820+
'device': device_name,
821+
'data': view_details
822+
}
823+
line_num += 1
771824

772825
def _get_default_collapsed_folders(self):
773826
"""Get the default set of collapsed folders (all folders except Script Commands)."""
@@ -781,6 +834,7 @@ def _get_default_collapsed_folders(self):
781834
collapsed.add((device_name, 'variables_folder'))
782835
collapsed.add((device_name, 'events_folder'))
783836
collapsed.add((device_name, 'warnings_folder'))
837+
collapsed.add((device_name, 'views_folder'))
784838

785839
# Keep script commands expanded by default
786840
# collapsed.add(('', 'script_folder'))
@@ -799,9 +853,9 @@ def on_text_motion(self, event):
799853
item_data = self.line_items.get(line_num, {})
800854

801855
# Only highlight if it's an actual item (not empty or folder header in some cases)
802-
is_item = item_data.get('type') in ['command', 'variable', 'event', 'warning', 'device',
856+
is_item = item_data.get('type') in ['command', 'variable', 'event', 'warning', 'view', 'device',
803857
'commands_folder', 'variables_folder',
804-
'events_folder', 'warnings_folder', 'script_folder', 'script_command']
858+
'events_folder', 'warnings_folder', 'views_folder', 'script_folder', 'script_command']
805859

806860
if is_item and line_num != self.current_hover_line:
807861
# Remove previous hover
@@ -832,7 +886,7 @@ def on_text_single_click(self, event):
832886
self.text.tag_remove(tk.SEL, "1.0", tk.END)
833887

834888
# Toggle folder collapse/expand
835-
if item_type in ['commands_folder', 'variables_folder', 'events_folder', 'warnings_folder', 'script_folder', 'device']:
889+
if item_type in ['commands_folder', 'variables_folder', 'events_folder', 'warnings_folder', 'views_folder', 'script_folder', 'device']:
836890
# Use device name + type as stable key (not line number which changes)
837891
folder_key = (item_data.get('device', item_data.get('name', '')), item_type)
838892
if folder_key in self.collapsed_folders:
@@ -877,8 +931,27 @@ def on_text_double_click(self, event):
877931
if event_name:
878932
self.script_editor_widget.insert(tk.INSERT, f"WAIT_FOR {event_name}\n")
879933

934+
# Views: show view
935+
elif item_type == 'view':
936+
device_name = item_data.get('device')
937+
view_id = item_data.get('view_id')
938+
if device_name and view_id:
939+
from src.device.views import show_operator_view
940+
from src.config import load_config
941+
device_data = self.device_manager.devices.get(device_name)
942+
script_runner = self.device_manager.shared_gui_refs.get('script_runner')
943+
if device_data:
944+
show_operator_view(
945+
self.winfo_toplevel(),
946+
device_name,
947+
device_data,
948+
self.device_manager.shared_gui_refs,
949+
script_runner,
950+
view_id
951+
)
952+
880953
# Folders: toggle open/close
881-
elif item_type in ['commands_folder', 'variables_folder', 'events_folder', 'warnings_folder', 'script_folder', 'device']:
954+
elif item_type in ['commands_folder', 'variables_folder', 'events_folder', 'warnings_folder', 'views_folder', 'script_folder', 'device']:
882955
# Use device name + type as stable key (not line number which changes)
883956
folder_key = (item_data.get('device', item_data.get('name', '')), item_type)
884957
if folder_key in self.collapsed_folders:
@@ -1035,6 +1108,42 @@ def clear_cache():
10351108
if queued_vars or has_active_logs:
10361109
self.context_menu.add_separator()
10371110

1111+
# Check if device has an operator view
1112+
device_data = self.device_manager.devices.get(device_name, {})
1113+
operator_view_module = device_data.get('modules', {}).get('operator_view')
1114+
has_operator_view = operator_view_module and hasattr(operator_view_module, 'create_operator_view')
1115+
1116+
if has_operator_view:
1117+
from src.device.views import show_operator_view, get_operator_view_settings, save_operator_view_settings
1118+
from src.config import load_config, save_config
1119+
1120+
def open_operator_view():
1121+
script_runner = self.device_manager.shared_gui_refs.get('script_runner')
1122+
show_operator_view(self.winfo_toplevel(), device_name, device_data,
1123+
self.device_manager.shared_gui_refs, script_runner)
1124+
1125+
self.context_menu.add_command(label="Open Operator View...", command=open_operator_view)
1126+
1127+
# Add checkbutton for "Display on startup"
1128+
config_data = load_config()
1129+
settings = get_operator_view_settings(config_data, device_name)
1130+
show_on_startup = settings.get('show_on_startup', False)
1131+
1132+
def toggle_startup():
1133+
config_data = load_config()
1134+
settings = get_operator_view_settings(config_data, device_name)
1135+
new_value = not settings.get('show_on_startup', False)
1136+
save_operator_view_settings(config_data, device_name, new_value)
1137+
save_config(config_data)
1138+
status = "enabled" if new_value else "disabled"
1139+
from tkinter import messagebox
1140+
messagebox.showinfo("Operator View",
1141+
f"Operator view on startup {status} for {device_name}")
1142+
1143+
check_label = "☑ Display on Startup" if show_on_startup else "☐ Display on Startup"
1144+
self.context_menu.add_command(label=check_label, command=toggle_startup)
1145+
self.context_menu.add_separator()
1146+
10381147
self.context_menu.add_command(label="Refresh Device", command=lambda: self.refresh_device(device_name))
10391148
self.context_menu.add_separator()
10401149
self.context_menu.add_command(label="More Info...", command=self.show_device_info)
@@ -1144,6 +1253,54 @@ def clear_cache():
11441253
self.context_menu.add_command(label="Delete Warning", command=self.delete_warning,
11451254
foreground=theme.ERROR_RED)
11461255

1256+
elif item_type == 'views_folder':
1257+
device_name = item_data.get('device')
1258+
self.context_menu.add_command(label="Add View...",
1259+
command=lambda: self.show_add_view_dialog_for_device(device_name))
1260+
1261+
elif item_type == 'view':
1262+
view_id = item_data.get('view_id', '')
1263+
view_name = item_data.get('name', '')
1264+
device_name = item_data.get('device', '')
1265+
view_data = item_data.get('data', {})
1266+
1267+
self.context_menu.add_command(label="Open View", command=lambda: self.open_view(device_name, view_id, view_data))
1268+
self.context_menu.add_separator()
1269+
1270+
# Display on startup option
1271+
from src.config import load_config, save_config
1272+
config_data = load_config()
1273+
startup_views = config_data.get('startup_views', {})
1274+
is_startup = startup_views.get(device_name) == view_id
1275+
1276+
def toggle_startup_view():
1277+
config_data = load_config()
1278+
startup_views = config_data.get('startup_views', {})
1279+
if startup_views.get(device_name) == view_id:
1280+
# Remove from startup
1281+
startup_views.pop(device_name, None)
1282+
status = "disabled"
1283+
else:
1284+
# Add to startup
1285+
if 'startup_views' not in config_data:
1286+
config_data['startup_views'] = {}
1287+
config_data['startup_views'][device_name] = view_id
1288+
status = "enabled"
1289+
save_config(config_data)
1290+
from tkinter import messagebox
1291+
messagebox.showinfo("View Settings",
1292+
f"Display on startup {status} for {view_data.get('name', view_id)}")
1293+
1294+
check_label = "☑ Display on Startup" if is_startup else "☐ Display on Startup"
1295+
self.context_menu.add_command(label=check_label, command=toggle_startup_view)
1296+
self.context_menu.add_separator()
1297+
1298+
self.context_menu.add_command(label="Edit View...", command=self.edit_view)
1299+
self.context_menu.add_command(label="More Info...", command=self.show_view_info)
1300+
self.context_menu.add_separator()
1301+
self.context_menu.add_command(label="Delete View", command=self.delete_view,
1302+
foreground=theme.ERROR_RED)
1303+
11471304
# Show menu
11481305
if self.context_menu.index('end') is not None: # If menu has items
11491306
self.context_menu.post(event.x_root, event.y_root)
@@ -3575,6 +3732,71 @@ def delete_warning(self):
35753732
messagebox.showerror("Error", f"Warning '{warn_key}' not found in JSON.")
35763733
except Exception as e:
35773734
messagebox.showerror("Error", f"Failed to delete warning:\n{str(e)}")
3735+
3736+
# ===== VIEW METHODS =====
3737+
3738+
def open_view(self, device_name, view_id, view_data):
3739+
"""Open a view for the device."""
3740+
print(f"[DEBUG] Opening view: {device_name}.{view_id}")
3741+
print(f"[DEBUG] View data: {view_data}")
3742+
3743+
from src.device.views import show_operator_view
3744+
3745+
device_data = self.device_manager.devices.get(device_name)
3746+
script_runner = self.device_manager.shared_gui_refs.get('script_runner')
3747+
3748+
if device_data:
3749+
show_operator_view(
3750+
self.winfo_toplevel(),
3751+
device_name,
3752+
device_data,
3753+
self.device_manager.shared_gui_refs,
3754+
script_runner,
3755+
view_id
3756+
)
3757+
3758+
def show_add_view_dialog_for_device(self, device_name):
3759+
"""Show dialog to add a new view."""
3760+
from tkinter import messagebox
3761+
messagebox.showinfo("Add View", "View editor coming soon!")
3762+
3763+
def edit_view(self):
3764+
"""Edit selected view."""
3765+
from tkinter import messagebox
3766+
line_num = self._get_selected_line_num()
3767+
if not line_num:
3768+
return
3769+
item_data = self.line_items.get(line_num, {})
3770+
view_name = item_data.get('name', '')
3771+
view_data = item_data.get('data', {})
3772+
messagebox.showinfo("Edit View", f"View editor coming soon for {view_name}!")
3773+
3774+
def show_view_info(self):
3775+
"""Show info for selected view."""
3776+
from tkinter import messagebox
3777+
line_num = self._get_selected_line_num()
3778+
if not line_num:
3779+
return
3780+
item_data = self.line_items.get(line_num, {})
3781+
view_name = item_data.get('name', '')
3782+
device_name = item_data.get('device', '')
3783+
view_data = item_data.get('data', {})
3784+
description = view_data.get('description', 'No description available.')
3785+
3786+
messagebox.showinfo(f"View: {view_data.get('name', view_name)}",
3787+
f"Device: {device_name}\n" +
3788+
f"Key: {view_name}\n\n" +
3789+
f"Description:\n{description}")
3790+
3791+
def delete_view(self):
3792+
"""Delete selected view."""
3793+
from tkinter import messagebox
3794+
line_num = self._get_selected_line_num()
3795+
if not line_num:
3796+
return
3797+
item_data = self.line_items.get(line_num, {})
3798+
view_name = item_data.get('name', '')
3799+
messagebox.showinfo("Delete View", f"View deletion coming soon for {view_name}!")
35783800

35793801
# ===== ADD/EDIT DIALOGS =====
35803802

0 commit comments

Comments
 (0)