@@ -55,6 +55,27 @@ class App:
self . root = tk . Tk ( )
self . root . attributes ( " -fullscreen " , True )
self . root . configure ( bg = bg_color )
# Calculate scale factor based on screen resolution
# Reference resolution: 1920x1080
screen_width = self . root . winfo_screenwidth ( )
screen_height = self . root . winfo_screenheight ( )
ref_width = 1920
ref_height = 1080
# Use the smaller scale factor to ensure everything fits
self . scale_factor = min ( screen_width / ref_width , screen_height / ref_height )
# Clamp scale factor between 0.5 and 2.0 for reasonable scaling
self . scale_factor = max ( 0.5 , min ( 2.0 , self . scale_factor ) )
# Helper methods for scaling
def scale_font ( size ) :
return int ( size * self . scale_factor )
def scale_size ( size ) :
return int ( size * self . scale_factor )
self . scale_font = scale_font
self . scale_size = scale_size
# Debouncing variables (remain the same)
self . last_rgb_update_time = 0
@@ -77,125 +98,147 @@ class App:
self . websocket_client = WebSocketClient ( " ws://192.168.4.1:80/ws " )
self . root . after ( 100 , async_handler ( self . websocket_client . connect ) )
# Configure ttk style (unchang ed)
# Configure ttk style (scal ed)
style = ttk . Style ( )
style . theme_use ( " alt " )
style . configure ( " . " , background = bg_color , foreground = fg_color , font = ( " Arial " , 14 ) )
style . configure ( " . " , background = bg_color , foreground = fg_color , font = ( " Arial " , self . scale_font ( 14 ) ) )
style . configure ( " TNotebook " , background = bg_color , borderwidth = 0 )
style . configure (
" TNotebook.Tab " , background = bg_color , foreground = fg_color , font = ( " Arial " , 30 ) , padding = [ 10 , 5 ]
" TNotebook.Tab " , background = bg_color , foreground = fg_color , font = ( " Arial " , self . scale_font ( 30 ) ) , padding = [ self . scale_size ( 10 ) , self . scale_size ( 5 ) ]
)
style . map ( " TNotebook.Tab " , background = [ ( " selected " , active_bg_color ) ] , foreground = [ ( " selected " , fg_color ) ] )
style . configure ( " TFrame " , background = bg_color )
# Create Notebook for tabs (unchanged)
# Create a frame to hold notebook and menu button on same row
# The notebook tabs appear at the top, so we'll position the menu button there
top_frame = tk . Frame ( self . root , bg = bg_color )
top_frame . pack ( side = tk . TOP , fill = tk . X )
# Create Notebook for tabs
self . notebook = ttk . Notebook ( self . root )
self . notebook . pack ( expand = 1 , fill = " both " )
# Tab management buttons frame
tab_management_frame = tk . Frame ( self . root , bg = bg_color )
tab_management_frame . pack ( side = tk . TOP , fill = tk . X , padx = 10 , pady = 5 )
self . notebook . pack ( expand = True , fill = " both " )
add_tab_btn = tk . Button (
tab_management_frame ,
text = " + Add Tab " ,
command = self . add_tab_dialog ,
# Create menu button positioned to appear on same row as tabs
# Calculate approximate tab height: font size + padding
tab_font_size = self . scale_font ( 30 )
tab_padding = self . scale_size ( 5 ) * 2
tab_height = tab_font_size + tab_padding + self . scale_size ( 10 )
menu_btn = tk . Menubutton (
self . root ,
text = " ☰ Menu " ,
bg = active_bg_color ,
fg = fg_color ,
font = ( " Arial " , 14 ) ,
padx = 10 ,
pady = 5
font = ( " Arial " , self . scale_font ( 20 ) ) ,
padx = self . scale_size ( 20 ) ,
pady = self . scale_size ( 10 ) ,
relief = tk . RAISED ,
direction = " below "
)
add_tab_btn . pack ( side = tk . LEFT , padx = 5 )
# Position menu button aligned with tabs (vertically centered with tab row )
# The tabs are at the top of the notebook, so position button at same level
# We need to wait for the notebook to be rendered to get accurate positioning
def position_menu_button ( ) :
self . root . update_idletasks ( )
# Get the notebook's tab area position
notebook_y = self . notebook . winfo_y ( )
# Position button at the same vertical level as tabs (tabs are at top of notebook)
menu_btn . place ( relx = 1.0 , y = notebook_y + tab_height / / 2 , anchor = " e " , x = - self . scale_size ( 10 ) )
edit_tab_btn = tk . Button (
tab_management_frame ,
text = " ✎ Edit Tab " ,
command = self . edit_tab_dialog ,
bg = active_bg_color ,
fg = fg_color ,
font = ( " Arial " , 14 ) ,
padx = 10 ,
pady = 5
)
edit_tab_btn . pack ( side = tk . LEFT , padx = 5 )
# Position after initial layout
self . root . after ( 10 , position_menu_button )
# Also position immediately as fallback
menu_btn . place ( relx = 1.0 , y = tab_height / / 2 , anchor = " e " , x = - self . scale_size ( 10 ) )
delete_tab_btn = tk . Button (
tab_management_frame ,
text = " ✗ Delete Tab " ,
command = self . delete_tab_dialog ,
bg = active_bg_color ,
fg = fg_color ,
font = ( " Arial " , 14 ) ,
padx = 10 ,
pady = 5
)
delete_tab_btn . pack ( side = tk . LEFT , padx = 5 )
# Create the menu (bigger font)
menu = tk . Menu ( menu_btn , tearoff = 0 , bg = bg_color , fg = fg_color , font = ( " Arial " , self . scale_font ( 16 ) ) )
menu_btn . config ( menu = menu )
# Tab reorder buttons
move_left_btn = tk . Button (
tab_management_frame ,
text = " ← Move Left " ,
command = self . move_tab_left ,
bg = active_bg_color ,
fg = fg_color ,
font = ( " Arial " , 14 ) ,
padx = 10 ,
pady = 5
)
move_left_btn . pack ( side = tk . LEFT , padx = 5 )
# Add tab management items directly
menu . add_command ( label = " + Add Tab " , command = self . add_tab_dialog )
menu . add_command ( label = " ✎ Edit Tab " , command = self . edit_tab_dialog )
menu . add_command ( label = " ✗ Delete Tab " , command = self . delete_tab_dialog )
menu . add_separator ( )
menu . add_command ( label = " ← Move Tab Left " , command = self . move_tab_left )
menu . add_command ( label = " → Move Tab Right " , command = self . move_tab_right )
menu . add_separator ( )
move_right_btn = tk . Button (
tab_management_frame ,
text = " → Move Right " ,
command = self . move_tab_right ,
bg = active_bg_color ,
fg = fg_color ,
font = ( " Arial " , 14 ) ,
padx = 10 ,
pady = 5
)
move_right_btn . pack ( side = tk . LEFT , padx = 5 )
# Profile management - use a custom popup menu that opens to the left
# Store reference for the profile menu function
self . profile_menu_items = [ ]
# Profile dropdown
tk . Label ( tab_management_frame , text = " Profile: " , bg = bg_color , fg = fg_color , font = ( " Arial " , 14 ) ) . pack ( side = tk . LEFT , padx = ( 20 , 5 ) )
def show_profile_menu ( ) :
""" Show profile menu as a popup to the left """
# Create a popup menu window
popup = tk . Toplevel ( self . root )
popup . overrideredirect ( True ) # Remove window decorations
popup . configure ( bg = bg_color )
# Position to the left of the menu button
menu_btn . update_idletasks ( )
btn_x = menu_btn . winfo_x ( )
btn_y = menu_btn . winfo_y ( )
btn_height = menu_btn . winfo_height ( )
# Calculate popup position (to the left of menu button)
popup_width = self . scale_size ( 200 )
popup_x = btn_x - popup_width - self . scale_size ( 5 )
popup_y = btn_y
popup . geometry ( f " { popup_width } x { self . scale_size ( 400 ) } + { popup_x } + { popup_y } " )
# Create menu frame
menu_frame = tk . Frame ( popup , bg = bg_color )
menu_frame . pack ( fill = tk . BOTH , expand = True , padx = 2 , pady = 2 )
# Add menu items
def add_menu_item ( text , command = None , state = " normal " ) :
if state == " disabled " :
label = tk . Label ( menu_frame , text = text , bg = bg_color , fg = fg_color ,
font = ( " Arial " , self . scale_font ( 14 ) ) , anchor = " w " )
label . pack ( fill = tk . X , padx = self . scale_size ( 5 ) , pady = self . scale_size ( 2 ) )
else :
btn = tk . Button ( menu_frame , text = text , bg = bg_color , fg = fg_color ,
font = ( " Arial " , self . scale_font ( 14 ) ) , anchor = " w " ,
relief = tk . FLAT , command = lambda : ( command ( ) if command else None , popup . destroy ( ) ) )
btn . pack ( fill = tk . X , padx = self . scale_size ( 5 ) , pady = self . scale_size ( 2 ) )
btn . bind ( " <Enter> " , lambda e : btn . config ( bg = active_bg_color ) )
btn . bind ( " <Leave> " , lambda e : btn . config ( bg = bg_color ) )
add_menu_item ( " + New Profile " , self . new_profile_dialog )
add_menu_item ( " 💾 Save Profile " , self . save_profile_dialog )
add_menu_item ( " " , state = " disabled " ) # Separator
add_menu_item ( " Load Profile: " , state = " disabled " )
# Get profiles
profiles_dir = " profiles "
profiles = [ ]
if os . path . exists ( profiles_dir ) :
for filename in os . listdir ( profiles_dir ) :
if filename . endswith ( ' .json ' ) :
profiles . append ( filename [ : - 5 ] )
profiles . sort ( )
current_profile = self . settings . get ( " current_profile " , " " )
# Add profile list
for profile in profiles :
label = f " ✓ { profile } " if profile == current_profile else profile
add_menu_item ( label , lambda p = profile : ( self . on_profile_selected_menu ( p ) , popup . destroy ( ) ) )
# Close popup when clicking outside
def close_on_focus_out ( event ) :
if event . widget == popup :
popup . destroy ( )
popup . bind ( " <FocusOut> " , close_on_focus_out )
popup . focus_set ( )
self . profile_var = tk . StringVar ( )
self . profile_dropdown = ttk . Combobox (
tab_management_frame ,
textvariable = self . profile_var ,
font = ( " Arial " , 14 ) ,
width = 20 ,
state = " readonly "
)
self . profile_dropdown . pack ( side = tk . LEFT , padx = 5 )
self . profile_dropdown . bind ( " <<ComboboxSelected>> " , self . on_profile_selected )
# Add Profiles item that opens the custom popup menu
menu . add_command ( label = " Profiles → " , command = show_profile_menu )
# New profile button
new_profile_btn = tk . Button (
tab_management_frame ,
text = " + New Profile " ,
command = self . new_profile_dialog ,
bg = active_bg_color ,
fg = fg_color ,
font = ( " Arial " , 14 ) ,
padx = 10 ,
pady = 5
)
new_profile_btn . pack ( side = tk . LEFT , padx = 5 )
# Save profile button
save_profile_btn = tk . Button (
tab_management_frame ,
text = " 💾 Save Profile " ,
command = self . save_profile_dialog ,
bg = active_bg_color ,
fg = fg_color ,
font = ( " Arial " , 14 ) ,
padx = 10 ,
pady = 5
)
save_profile_btn . pack ( side = tk . LEFT , padx = 5 )
# Store menu references for updating
self . menu_btn = menu_btn
self . profile_var = tk . StringVar ( ) # Keep for compatibility
self . profile_menu = None # Not using standard menu for profiles
# Load profiles
self . profiles_dir = " profiles "
@@ -212,19 +255,31 @@ class App:
with open ( profile_path , ' r ' ) as file :
profile_data = json . load ( file )
# Update settings with profile data
# Load profile data into settings for use in the app
# Remove current_profile from profile data if it exists (shouldn't be in profiles)
profile_data . pop ( " current_profile " , None )
self . settings . clear ( )
# Preserve patterns and other settings.json-only data
patterns_backup = self . settings . get ( " patterns " , { } )
tab_password_backup = self . settings . get ( " tab_password " , " " )
# Update with profile data (lights, tab_order, tab_password from profile)
self . settings . update ( profile_data )
self . settings [ " current_profile " ] = current_profile # S tore in settings.json, not profile
# Res tore settings.json-only data
self . settings [ " patterns " ] = patterns_backup
self . settings [ " current_profile " ] = current_profile
# Only save current_profile to settings.json, not profile data
settings_to_save = {
" tab_password " : tab_password_backup ,
" current_profile " : current_profile ,
" patterns " : patterns_backup
}
with open ( " settings.json " , ' w ' ) as f :
json . dump ( settings_to_save , f , indent = 4 )
# Ensure tab_order exists in profile
if " tab_order " not in self . settings :
if " lights " in self . settings :
self . settings [ " tab_order " ] = list ( self . settings [ " lights " ] . keys ( ) )
else :
self . settings [ " tab_order " ] = [ ]
self . settings . save ( )
except Exception as e :
print ( f " Error loading current profile ' { current_profile } ' : { e } " )
@@ -329,32 +384,89 @@ class App:
if filename . endswith ( ' .json ' ) :
profiles . append ( filename [ : - 5 ] ) # Remove .json extension
profiles . sort ( )
self . profile_dropdown [ ' values ' ] = profiles
# Update profile menu - clear existing profile items
# Find "Load Profile:" label index
load_profile_label_idx = None
try :
menu_count = self . profile_menu . index ( tk . END )
for i in range ( menu_count + 1 ) :
try :
if self . profile_menu . type ( i ) == " command " :
label = self . profile_menu . entryconfig ( i , " label " ) [ 4 ]
if label == " Load Profile: " :
load_profile_label_idx = i
break
except :
pass
except :
pass
if load_profile_label_idx is not None :
# Delete all items after "Load Profile:" until we hit the end
try :
menu_count = self . profile_menu . index ( tk . END )
# Delete backwards from the end
items_to_delete = [ ]
for i in range ( menu_count , load_profile_label_idx , - 1 ) :
try :
item_type = self . profile_menu . type ( i )
if item_type == " command " :
label = self . profile_menu . entryconfig ( i , " label " ) [ 4 ]
if label not in [ " Load Profile: " , " + New Profile " , " 💾 Save Profile " ] :
items_to_delete . append ( i )
elif item_type == " separator " :
# Delete separators after "Load Profile:"
items_to_delete . append ( i )
except :
pass
# Delete items
for i in sorted ( items_to_delete , reverse = True ) :
try :
self . profile_menu . delete ( i )
except :
pass
except Exception as e :
print ( f " Error clearing profile menu: { e } " )
# Get current profile
current_profile = self . settings . get ( " current_profile " , " " )
# Add profile items to menu (after "Load Profile:" label)
if profiles :
# Try to load current profile name from settings
current_profile = self . settings . get ( " current_profile " , " " )
if current_profile in profiles :
self . profile_var . set ( current_ profile )
else :
self . profile_var . set ( " " )
insert_pos = load_profile_label_idx + 1 if load_profile_label_idx is not None else self . profile_menu . index ( tk . END ) + 1
# Add each profile as a menu item
for profile in profiles :
label = f " ✓ { profile } " if profile == current_profile else profile
self . profile_menu . insert_command (
insert_pos ,
label = label ,
command = lambda p = profile : self . on_profile_selected_menu ( p )
)
insert_pos + = 1
except Exception as e :
print ( f " Error refreshing profiles: { e } " )
def on_profile_selected ( self , event = None ) :
""" Handle profile selection from dropdown """
selected_profile = self . profile_var . get ( )
if not selected_profile :
""" Handle profile selection from dropdown (legacy, not used with menu) """
pass
def on_profile_selected_menu ( self , profile_name ) :
""" Handle profile selection from menu """
if not profile_name :
return
# Confirm before loading (will overwrite current settings)
result = messagebox . askyesno (
" Load Profile " ,
f " Load profile ' { selected_ profile} ' ? \n \n This will replace your current settings. " ,
f " Load profile ' { profile_name } ' ? \n \n This will replace your current settings. " ,
icon = " question "
)
if result :
self . load_profile ( selected_ profile)
self . load_profile ( profile_name )
def load_profile ( self , profile_name ) :
""" Load a profile from the profiles directory """
@@ -367,19 +479,31 @@ class App:
with open ( profile_path , ' r ' ) as file :
profile_data = json . load ( file )
# Update settings with profile data
# Load profile data into settings for use in the app
# Remove current_profile from profile data if it exists (shouldn't be in profiles)
profile_data . pop ( " current_profile " , None )
self . settings . clear ( )
# Preserve patterns and other settings.json-only data
patterns_backup = self . settings . get ( " patterns " , { } )
tab_password_backup = self . settings . get ( " tab_password " , " " )
# Update with profile data (lights, tab_order, tab_password from profile)
self . settings . update ( profile_data )
self . settings [ " current_profile " ] = profile_name # S tore in settings.json, not profile
# Res tore settings.json-only data
self . settings [ " patterns " ] = patterns_backup
self . settings [ " current_profile " ] = profile_name
# Only save current_profile to settings.json, not profile data
settings_to_save = {
" tab_password " : tab_password_backup ,
" current_profile " : profile_name ,
" patterns " : patterns_backup
}
with open ( " settings.json " , ' w ' ) as f :
json . dump ( settings_to_save , f , indent = 4 )
# Ensure tab_order exists in profile
if " tab_order " not in self . settings :
if " lights " in self . settings :
self . settings [ " tab_order " ] = list ( self . settings [ " lights " ] . keys ( ) )
else :
self . settings [ " tab_order " ] = [ ]
self . settings . save ( )
# Recreate tabs with new settings
self . create_tabs ( )
@@ -538,12 +662,13 @@ class App:
desc_name = self . get_n_parameter_name ( pattern_name , i )
if desc_name :
# Show the input and update label
# Show the input and update label with descriptive name
tab . widgets [ frame_key ] . grid ( )
tab . widgets [ label_key ] . config ( text = desc_name )
else :
# Hide the input if no description
tab . widgets [ frame_key ] . grid_remove ( )
# Show the input with default n{i} label if no description
tab . widgets [ frame_key ] . grid ( )
tab . widgets [ label_key ] . config ( text = f " n { i } " )
def get_pattern_settings ( self , tab_name , pattern_name ) :
""" Get pattern-specific settings (colors, delay, n params). Returns defaults if not found. """
@@ -647,8 +772,8 @@ class App:
self . tabs [ key ] = tab
def create_light_control_widgets ( self , tab , tab_name , ids , initial_settings ) :
slider_length = 600
slider_width = 50
slider_length = self . scale_size ( 600 )
slider_width = self . scale_size ( 50 )
# Get initial pattern and load pattern-specific settings
initial_pattern = initial_settings . get ( " pattern " , " on " )
@@ -668,11 +793,11 @@ class App:
# Main frame to hold everything within the tab
main_tab_frame = tk . Frame ( tab , bg = bg_color )
main_tab_frame . pack ( expand = True , fill = " both " , padx = 10 , pady = 10 )
main_tab_frame . pack ( expand = True , fill = " both " , padx = self . scale_size ( 10 ) , pady = self . scale_size ( 10 ) )
# Left panel container for sliders and n inputs
left_panel_container = tk . Frame ( main_tab_frame , bg = bg_color )
left_panel_container . pack ( side = tk . LEFT , padx = 10 , pady = 10 )
left_panel_container . pack ( side = tk . LEFT , padx = self . scale_size ( 10 ) , pady = self . scale_size ( 10 ) )
# Slider panel
slider_panel_frame = tk . Frame ( left_panel_container , bg = bg_color )
@@ -750,8 +875,8 @@ class App:
delay_slider . set ( initial_slider_pos )
# Create a custom label to show the actual delay value, positioned like the default Scale value
delay_value_label = tk . Label ( delay_container , text = f " { initial_delay } " , font = ( " Arial " , 12 ) , bg = bg_color , fg = fg_color , width = 5 , anchor = " e " )
delay_value_label . pack ( side = tk . LEFT , padx = ( 0 , 5 ) )
delay_value_label = tk . Label ( delay_container , text = f " { initial_delay } " , font = ( " Arial " , self . scale_font ( 12 ) ) , bg = bg_color , fg = fg_color , width = self . scale_size ( 5 ) , anchor = " e " )
delay_value_label . pack ( side = tk . LEFT , padx = ( 0 , self . scale_size ( 5 ) ) )
# Store min/max delay in tab widget for later use
tab . min_delay = min_delay
@@ -784,10 +909,10 @@ class App:
" from_ " : 0 ,
" to " : 255 ,
" increment " : 1 ,
" width " : 12 ,
" width " : self . scale_size ( 12 ) ,
" bg " : bg_color ,
" fg " : fg_color ,
" font " : ( " Arial " , 24 ) ,
" font " : ( " Arial " , self . scale_font ( 24 ) ) ,
" buttonbackground " : active_bg_color ,
}
@@ -798,8 +923,8 @@ class App:
n_frame . grid ( row = ( i - 1 ) / / 2 , column = ( i - 1 ) % 2 , padx = 10 , pady = 10 )
n_inputs [ f " n { i } _frame " ] = n_frame # Store frame reference for hiding/showing
n_label = tk . Label ( n_frame , text = f " n { i } " , font = ( " Arial " , 20 ) , bg = bg_color , fg = fg_color )
n_label . pack ( pady = ( 0 , 5 ) )
n_label = tk . Label ( n_frame , text = f " n { i } " , font = ( " Arial " , self . scale_font ( 20 ) ) , bg = bg_color , fg = fg_color )
n_label . pack ( pady = ( 0 , self . scale_size ( 5 ) ) )
n_inputs [ f " n { i } _label " ] = n_label # Store label reference
# Create a frame for the input with arrows on both sides
@@ -828,29 +953,29 @@ class App:
left_arrow = tk . Button (
input_container ,
text = " − " ,
font = ( " Arial " , 32 , " bold " ) ,
font = ( " Arial " , self . scale_font ( 32 ) , " bold " ) ,
bg = active_bg_color ,
fg = fg_color ,
relief = tk . FLAT ,
command = decrease_value ,
width = 3 ,
height = 1 ,
width = self . scale_size ( 3 ) ,
height = self . scale_size ( 1 ) ,
)
left_arrow . pack ( side = tk . LEFT , padx = 2 )
left_arrow . pack ( side = tk . LEFT , padx = self . scale_size ( 2 ) )
# Entry in the middle
n_entry = tk . Entry (
input_container ,
textvariable = n_var ,
font = ( " Arial " , 24 ) ,
font = ( " Arial " , self . scale_font ( 24 ) ) ,
bg = bg_color ,
fg = fg_color ,
width = 8 ,
width = self . scale_size ( 8 ) ,
justify = tk . CENTER ,
relief = tk . SUNKEN ,
bd = 2 ,
)
n_entry . pack ( side = tk . LEFT , padx = 2 , ipady = 8 )
n_entry . pack ( side = tk . LEFT , padx = self . scale_size ( 2 ) , ipady = self . scale_size ( 8 ) )
n_entry . bind ( " <KeyRelease> " , lambda event : self . schedule_update_n_params ( tab ) )
n_entry . bind ( " <FocusOut> " , lambda event : self . schedule_update_n_params ( tab , force_send = True ) )
@@ -864,15 +989,15 @@ class App:
right_arrow = tk . Button (
input_container ,
text = " + " ,
font = ( " Arial " , 32 , " bold " ) ,
font = ( " Arial " , self . scale_font ( 32 ) , " bold " ) ,
bg = active_bg_color ,
fg = fg_color ,
relief = tk . FLAT ,
command = increase_value ,
width = 3 ,
height = 1 ,
width = self . scale_size ( 3 ) ,
height = self . scale_size ( 1 ) ,
)
right_arrow . pack ( side = tk . LEFT , padx = 2 )
right_arrow . pack ( side = tk . LEFT , padx = self . scale_size ( 2 ) )
n_inputs [ f " n { i } " ] = n_entry
n_inputs [ f " n { i } _var " ] = n_var # Store the variable for later updates
@@ -908,30 +1033,29 @@ class App:
# Right panel for IDs, Patterns, and NEW Color Palette
right_panel_frame = tk . Frame ( main_tab_frame , bg = bg_color )
right_panel_frame . pack ( side = tk . LEFT , padx = 20 , pady = 10 , anchor = " n " , expand = True , fill = " both " )
right_panel_frame . pack ( side = tk . LEFT , padx = self . scale_size ( 20 ) , pady = self . scale_size ( 10 ) , anchor = " n " , expand = True , fill = " both " )
# IDs section - MODIFIED TO BE SIDE-BY-SIDE
ids_frame = tk . Frame ( right_panel_frame , bg = bg_color )
ids_frame . pack ( pady = 10 , fill = tk . X )
tk . Label ( ids_frame , text = " Associated Names: " , font = ( " Arial " , 20 ) , bg = bg_color , fg = fg_color ) . pack ( pady = 10 )
# New inner frame for the IDs to be displayed horizontally
ids_inner_frame = tk . Frame ( ids_frame , bg = bg_color )
ids_inner_frame . pack ( fill = tk . X , expand = True ) # Pack this frame to fill available width
for light_id in ids :
tk . Label ( ids_inner_frame , text = str ( light_id ) , font = ( " Arial " , 18 ) , bg = bg_color , fg = fg_color ) . pack (
side = tk . LEFT , padx = 5 , pady = 2
tk . Label ( ids_inner_frame , text = str ( light_id ) , font = ( " Arial " , self . scale_font ( 18 ) ) , bg = bg_color , fg = fg_color ) . pack (
side = tk . LEFT , padx = self . scale_size ( 5 ) , pady = self . scale_size ( 2 )
) # Pack labels horizontally
# --- New Frame to hold Patterns and Color Palette side-by-side ---
patterns_and_palette_frame = tk . Frame ( right_panel_frame , bg = bg_color )
patterns_and_palette_frame . pack ( pady = 20 , fill = tk . BOTH , expand = True )
patterns_and_palette_frame . pack ( pady = self . scale_size ( 20 ) , fill = tk . BOTH , expand = True )
# Patterns section
patterns_frame = tk . Frame ( patterns_and_palette_frame , bg = bg_color , bd = 2 , relief = tk . GROOVE )
patterns_frame . pack ( side = tk . LEFT , padx = 10 , pady = 5 , fill = tk . BOTH , expand = True ) # Pack to the left
tk . Label ( patterns_frame , text = " Patterns: " , font = ( " Arial " , 20 ) , bg = bg_color , fg = fg_color ) . pack ( pady = 10 )
patterns_frame . pack ( side = tk . LEFT , padx = self . scale_size ( 10 ) , pady = self . scale_size ( 5 ) , fill = tk . BOTH , expand = True ) # Pack to the left
tk . Label ( patterns_frame , text = " Patterns: " , font = ( " Arial " , self . scale_font ( 20 ) ) , bg = bg_color , fg = fg_color ) . pack ( pady = self . scale_size ( 10 ) )
tab . pattern_buttons = { }
patterns = list ( self . patterns . keys ( ) )
@@ -942,23 +1066,23 @@ class App:
command = lambda p = pattern_name : self . send_pattern ( tab_name , p ) ,
bg = active_bg_color ,
fg = fg_color ,
font = ( " Arial " , 18 ) ,
padx = 15 ,
pady = 5 ,
font = ( " Arial " , self . scale_font ( 18 ) ) ,
padx = self . scale_size ( 15 ) ,
pady = self . scale_size ( 5 ) ,
relief = tk . FLAT ,
)
button . pack ( pady = 5 , fill = tk . X )
button . pack ( pady = self . scale_size ( 5 ) , fill = tk . X )
tab . pattern_buttons [ pattern_name ] = button
self . highlight_pattern_button ( tab , initial_pattern )
# --- Color Palette Editor Section ---
color_palette_editor_frame = tk . Frame ( patterns_and_palette_frame , bg = bg_color , bd = 2 , relief = tk . GROOVE )
color_palette_editor_frame . pack ( side = tk . LEFT , padx = 10 , pady = 5 , fill = tk . BOTH , expand = True ) # Pack to the left
color_palette_editor_frame . pack ( side = tk . LEFT , padx = self . scale_size ( 10 ) , pady = self . scale_size ( 5 ) , fill = tk . BOTH , expand = True ) # Pack to the left
tab . color_palette_editor_frame = color_palette_editor_frame # Store reference for update_ui_for_pattern
tk . Label ( color_palette_editor_frame , text = " Color Palette: " , font = ( " Arial " , 20 ) , bg = bg_color , fg = fg_color ) . pack (
pady = 10
tk . Label ( color_palette_editor_frame , text = " Color Palette: " , font = ( " Arial " , self . scale_font ( 20 ) ) , bg = bg_color , fg = fg_color ) . pack (
pady = self . scale_size ( 10 )
)
# Frame to hold color swatches (will be dynamic)
@@ -975,9 +1099,9 @@ class App:
command = lambda t = tab : self . add_color_to_palette ( t ) ,
bg = active_bg_color ,
fg = fg_color ,
font = ( " Arial " , 16 ) ,
padx = 10 ,
pady = 5 ,
font = ( " Arial " , self . scale_font ( 16 ) ) ,
padx = self . scale_size ( 10 ) ,
pady = self . scale_size ( 5 ) ,
relief = tk . FLAT ,
)
add_color_button . pack ( side = tk . LEFT , expand = True , padx = 5 )
@@ -988,9 +1112,9 @@ class App:
command = lambda t = tab : self . remove_selected_color_from_palette ( t ) ,
bg = active_bg_color ,
fg = fg_color ,
font = ( " Arial " , 16 ) ,
padx = 10 ,
pady = 5 ,
font = ( " Arial " , self . scale_font ( 16 ) ) ,
padx = self . scale_size ( 10 ) ,
pady = self . scale_size ( 5 ) ,
relief = tk . FLAT ,
)
remove_color_button . pack ( side = tk . RIGHT , expand = True , padx = 5 )
@@ -1011,9 +1135,9 @@ class App:
for i , hex_color in enumerate ( tab . colors_in_palette ) :
swatch_frame = tk . Frame (
tab . color_swatches_container , bg = hex_color , width = 100 , height = 50 , bd = 2 , relief = tk . SOLID
tab . color_swatches_container , bg = hex_color , width = self . scale_size ( 100 ) , height = self . scale_size ( 50 ) , bd = 2 , relief = tk . SOLID
)
swatch_frame . pack ( pady = 3 , padx = 5 , fill = tk . X )
swatch_frame . pack ( pady = self . scale_size ( 3 ) , padx = self . scale_size ( 5 ) , fill = tk . X )
# Bind click to select this color for editing
swatch_frame . bind ( " <Button-1> " , lambda event , idx = i , t = tab : self . select_color_in_palette ( t , idx ) )
@@ -1023,9 +1147,9 @@ class App:
text = f " Color { i + 1 } " ,
bg = hex_color ,
fg = color_utils . get_contrast_text_color ( hex_color ) ,
font = ( " Arial " , 14 ) ,
width = 5 ,
height = 3 ,
font = ( " Arial " , self . scale_font ( 14 ) ) ,
width = self . scale_size ( 5 ) ,
height = self . scale_size ( 3 ) ,
)
swatch_label . pack ( expand = True , fill = tk . BOTH )
swatch_label . bind ( " <Button-1> " , lambda event , idx = i , t = tab : self . select_color_in_palette ( t , idx ) )