@@ -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