7 Commits

5 changed files with 425 additions and 196 deletions

View File

@@ -1,25 +0,0 @@
{
"on": {},
"off": {},
"rainbow": {
"Step Rate": "n1"
},
"transition": {},
"chase": {
"Colour 1 Length": "n1",
"Colour 2 Length": "n2",
"Step 1": "n3",
"Step 2": "n4"
},
"pulse": {
"Attack": "n1",
"Hold": "n2",
"Decay": "n3"
},
"circle": {
"Head Rate": "n1",
"Max Length": "n2",
"Tail Rate": "n3",
"Min Length": "n4"
}
}

View File

@@ -1,4 +1,51 @@
{ {
"tab_password": "",
"patterns": {
"on": {
"min_delay": 10,
"max_delay": 10000
},
"off": {
"min_delay": 10,
"max_delay": 10000
},
"rainbow": {
"Step Rate": "n1",
"min_delay": 10,
"max_delay": 10000
},
"transition": {
"min_delay": 10,
"max_delay": 10000
},
"chase": {
"Colour 1 Length": "n1",
"Colour 2 Length": "n2",
"Step 1": "n3",
"Step 2": "n4",
"min_delay": 10,
"max_delay": 10000
},
"pulse": {
"Attack": "n1",
"Hold": "n2",
"Decay": "n3",
"min_delay": 10,
"max_delay": 10000
},
"circle": {
"Head Rate": "n1",
"Max Length": "n2",
"Tail Rate": "n3",
"Min Length": "n4",
"min_delay": 10,
"max_delay": 10000
},
"blink": {
"min_delay": 10,
"max_delay": 10000
}
},
"lights": { "lights": {
"ring1": { "ring1": {
"names": [ "names": [
@@ -59,7 +106,6 @@
} }
} }
}, },
"tab_password": "",
"tab_order": [ "tab_order": [
"ring1", "ring1",
"ring2" "ring2"

View File

@@ -1,4 +1,50 @@
{ {
"tab_password": "qwerty1234", "tab_password": "",
"current_profile": "tt" "current_profile": "ring",
"patterns": {
"on": {
"min_delay": 10,
"max_delay": 10000
},
"off": {
"min_delay": 10,
"max_delay": 10000
},
"rainbow": {
"Step Rate": "n1",
"min_delay": 10,
"max_delay": 10000
},
"transition": {
"min_delay": 10,
"max_delay": 10000
},
"chase": {
"Colour 1 Length": "n1",
"Colour 2 Length": "n2",
"Step 1": "n3",
"Step 2": "n4",
"min_delay": 10,
"max_delay": 10000
},
"pulse": {
"Attack": "n1",
"Hold": "n2",
"Decay": "n3",
"min_delay": 10,
"max_delay": 10000
},
"circle": {
"Head Rate": "n1",
"Max Length": "n2",
"Tail Rate": "n3",
"Min Length": "n4",
"min_delay": 10,
"max_delay": 10000
},
"blink": {
"min_delay": 10,
"max_delay": 10000
}
}
} }

View File

@@ -56,6 +56,27 @@ class App:
self.root.attributes("-fullscreen", True) self.root.attributes("-fullscreen", True)
self.root.configure(bg=bg_color) 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) # Debouncing variables (remain the same)
self.last_rgb_update_time = 0 self.last_rgb_update_time = 0
self.rgb_update_interval_ms = 100 self.rgb_update_interval_ms = 100
@@ -77,125 +98,147 @@ class App:
self.websocket_client = WebSocketClient("ws://192.168.4.1:80/ws") self.websocket_client = WebSocketClient("ws://192.168.4.1:80/ws")
self.root.after(100, async_handler(self.websocket_client.connect)) self.root.after(100, async_handler(self.websocket_client.connect))
# Configure ttk style (unchanged) # Configure ttk style (scaled)
style = ttk.Style() style = ttk.Style()
style.theme_use("alt") 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", background=bg_color, borderwidth=0)
style.configure( 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.map("TNotebook.Tab", background=[("selected", active_bg_color)], foreground=[("selected", fg_color)])
style.configure("TFrame", background=bg_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 = ttk.Notebook(self.root)
self.notebook.pack(expand=1, fill="both") self.notebook.pack(expand=True, fill="both")
# Tab management buttons frame # Create menu button positioned to appear on same row as tabs
tab_management_frame = tk.Frame(self.root, bg=bg_color) # Calculate approximate tab height: font size + padding
tab_management_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=5) 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)
add_tab_btn = tk.Button( menu_btn = tk.Menubutton(
tab_management_frame, self.root,
text="+ Add Tab", text="☰ Menu",
command=self.add_tab_dialog,
bg=active_bg_color, bg=active_bg_color,
fg=fg_color, fg=fg_color,
font=("Arial", 14), font=("Arial", self.scale_font(20)),
padx=10, padx=self.scale_size(20),
pady=5 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( # Position after initial layout
tab_management_frame, self.root.after(10, position_menu_button)
text="✎ Edit Tab", # Also position immediately as fallback
command=self.edit_tab_dialog, menu_btn.place(relx=1.0, y=tab_height//2, anchor="e", x=-self.scale_size(10))
bg=active_bg_color,
fg=fg_color,
font=("Arial", 14),
padx=10,
pady=5
)
edit_tab_btn.pack(side=tk.LEFT, padx=5)
delete_tab_btn = tk.Button( # Create the menu (bigger font)
tab_management_frame, menu = tk.Menu(menu_btn, tearoff=0, bg=bg_color, fg=fg_color, font=("Arial", self.scale_font(16)))
text="✗ Delete Tab", menu_btn.config(menu=menu)
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)
# Tab reorder buttons # Add tab management items directly
move_left_btn = tk.Button( menu.add_command(label="+ Add Tab", command=self.add_tab_dialog)
tab_management_frame, menu.add_command(label="✎ Edit Tab", command=self.edit_tab_dialog)
text="← Move Left", menu.add_command(label="✗ Delete Tab", command=self.delete_tab_dialog)
command=self.move_tab_left, menu.add_separator()
bg=active_bg_color, menu.add_command(label="← Move Tab Left", command=self.move_tab_left)
fg=fg_color, menu.add_command(label="→ Move Tab Right", command=self.move_tab_right)
font=("Arial", 14), menu.add_separator()
padx=10,
pady=5
)
move_left_btn.pack(side=tk.LEFT, padx=5)
move_right_btn = tk.Button( # Profile management - use a custom popup menu that opens to the left
tab_management_frame, # Store reference for the profile menu function
text="→ Move Right", self.profile_menu_items = []
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 dropdown def show_profile_menu():
tk.Label(tab_management_frame, text="Profile:", bg=bg_color, fg=fg_color, font=("Arial", 14)).pack(side=tk.LEFT, padx=(20, 5)) """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)
self.profile_var = tk.StringVar() # Position to the left of the menu button
self.profile_dropdown = ttk.Combobox( menu_btn.update_idletasks()
tab_management_frame, btn_x = menu_btn.winfo_x()
textvariable=self.profile_var, btn_y = menu_btn.winfo_y()
font=("Arial", 14), btn_height = menu_btn.winfo_height()
width=20,
state="readonly"
)
self.profile_dropdown.pack(side=tk.LEFT, padx=5)
self.profile_dropdown.bind("<<ComboboxSelected>>", self.on_profile_selected)
# New profile button # Calculate popup position (to the left of menu button)
new_profile_btn = tk.Button( popup_width = self.scale_size(200)
tab_management_frame, popup_x = btn_x - popup_width - self.scale_size(5)
text="+ New Profile", popup_y = btn_y
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 popup.geometry(f"{popup_width}x{self.scale_size(400)}+{popup_x}+{popup_y}")
save_profile_btn = tk.Button(
tab_management_frame, # Create menu frame
text="💾 Save Profile", menu_frame = tk.Frame(popup, bg=bg_color)
command=self.save_profile_dialog, menu_frame.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
bg=active_bg_color,
fg=fg_color, # Add menu items
font=("Arial", 14), def add_menu_item(text, command=None, state="normal"):
padx=10, if state == "disabled":
pady=5 label = tk.Label(menu_frame, text=text, bg=bg_color, fg=fg_color,
) font=("Arial", self.scale_font(14)), anchor="w")
save_profile_btn.pack(side=tk.LEFT, padx=5) 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()
# Add Profiles item that opens the custom popup menu
menu.add_command(label="Profiles →", command=show_profile_menu)
# 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 # Load profiles
self.profiles_dir = "profiles" self.profiles_dir = "profiles"
@@ -212,19 +255,31 @@ class App:
with open(profile_path, 'r') as file: with open(profile_path, 'r') as file:
profile_data = json.load(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) # Remove current_profile from profile data if it exists (shouldn't be in profiles)
profile_data.pop("current_profile", None) 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.update(profile_data)
self.settings["current_profile"] = current_profile # Store in settings.json, not profile # Restore 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 # Ensure tab_order exists in profile
if "tab_order" not in self.settings: if "tab_order" not in self.settings:
if "lights" in self.settings: if "lights" in self.settings:
self.settings["tab_order"] = list(self.settings["lights"].keys()) self.settings["tab_order"] = list(self.settings["lights"].keys())
else: else:
self.settings["tab_order"] = [] self.settings["tab_order"] = []
self.settings.save()
except Exception as e: except Exception as e:
print(f"Error loading current profile '{current_profile}': {e}") print(f"Error loading current profile '{current_profile}': {e}")
@@ -329,32 +384,89 @@ class App:
if filename.endswith('.json'): if filename.endswith('.json'):
profiles.append(filename[:-5]) # Remove .json extension profiles.append(filename[:-5]) # Remove .json extension
profiles.sort() 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: if profiles:
# Try to load current profile name from settings insert_pos = load_profile_label_idx + 1 if load_profile_label_idx is not None else self.profile_menu.index(tk.END) + 1
current_profile = self.settings.get("current_profile", "")
if current_profile in profiles: # Add each profile as a menu item
self.profile_var.set(current_profile) for profile in profiles:
else: label = f"{profile}" if profile == current_profile else profile
self.profile_var.set("") 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: except Exception as e:
print(f"Error refreshing profiles: {e}") print(f"Error refreshing profiles: {e}")
def on_profile_selected(self, event=None): def on_profile_selected(self, event=None):
"""Handle profile selection from dropdown""" """Handle profile selection from dropdown (legacy, not used with menu)"""
selected_profile = self.profile_var.get() pass
if not selected_profile:
def on_profile_selected_menu(self, profile_name):
"""Handle profile selection from menu"""
if not profile_name:
return return
# Confirm before loading (will overwrite current settings) # Confirm before loading (will overwrite current settings)
result = messagebox.askyesno( result = messagebox.askyesno(
"Load Profile", "Load Profile",
f"Load profile '{selected_profile}'?\n\nThis will replace your current settings.", f"Load profile '{profile_name}'?\n\nThis will replace your current settings.",
icon="question" icon="question"
) )
if result: if result:
self.load_profile(selected_profile) self.load_profile(profile_name)
def load_profile(self, profile_name): def load_profile(self, profile_name):
"""Load a profile from the profiles directory""" """Load a profile from the profiles directory"""
@@ -367,19 +479,31 @@ class App:
with open(profile_path, 'r') as file: with open(profile_path, 'r') as file:
profile_data = json.load(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) # Remove current_profile from profile data if it exists (shouldn't be in profiles)
profile_data.pop("current_profile", None) 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.update(profile_data)
self.settings["current_profile"] = profile_name # Store in settings.json, not profile # Restore 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 # Ensure tab_order exists in profile
if "tab_order" not in self.settings: if "tab_order" not in self.settings:
if "lights" in self.settings: if "lights" in self.settings:
self.settings["tab_order"] = list(self.settings["lights"].keys()) self.settings["tab_order"] = list(self.settings["lights"].keys())
else: else:
self.settings["tab_order"] = [] self.settings["tab_order"] = []
self.settings.save()
# Recreate tabs with new settings # Recreate tabs with new settings
self.create_tabs() self.create_tabs()
@@ -538,12 +662,13 @@ class App:
desc_name = self.get_n_parameter_name(pattern_name, i) desc_name = self.get_n_parameter_name(pattern_name, i)
if desc_name: 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[frame_key].grid()
tab.widgets[label_key].config(text=desc_name) tab.widgets[label_key].config(text=desc_name)
else: else:
# Hide the input if no description # Show the input with default n{i} label if no description
tab.widgets[frame_key].grid_remove() tab.widgets[frame_key].grid()
tab.widgets[label_key].config(text=f"n{i}")
def get_pattern_settings(self, tab_name, pattern_name): def get_pattern_settings(self, tab_name, pattern_name):
"""Get pattern-specific settings (colors, delay, n params). Returns defaults if not found.""" """Get pattern-specific settings (colors, delay, n params). Returns defaults if not found."""
@@ -647,8 +772,8 @@ class App:
self.tabs[key] = tab self.tabs[key] = tab
def create_light_control_widgets(self, tab, tab_name, ids, initial_settings): def create_light_control_widgets(self, tab, tab_name, ids, initial_settings):
slider_length = 600 slider_length = self.scale_size(600)
slider_width = 50 slider_width = self.scale_size(50)
# Get initial pattern and load pattern-specific settings # Get initial pattern and load pattern-specific settings
initial_pattern = initial_settings.get("pattern", "on") initial_pattern = initial_settings.get("pattern", "on")
@@ -668,11 +793,11 @@ class App:
# Main frame to hold everything within the tab # Main frame to hold everything within the tab
main_tab_frame = tk.Frame(tab, bg=bg_color) 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 for sliders and n inputs
left_panel_container = tk.Frame(main_tab_frame, bg=bg_color) 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
slider_panel_frame = tk.Frame(left_panel_container, bg=bg_color) slider_panel_frame = tk.Frame(left_panel_container, bg=bg_color)
@@ -750,8 +875,8 @@ class App:
delay_slider.set(initial_slider_pos) delay_slider.set(initial_slider_pos)
# Create a custom label to show the actual delay value, positioned like the default Scale value # 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 = 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, 5)) delay_value_label.pack(side=tk.LEFT, padx=(0, self.scale_size(5)))
# Store min/max delay in tab widget for later use # Store min/max delay in tab widget for later use
tab.min_delay = min_delay tab.min_delay = min_delay
@@ -784,10 +909,10 @@ class App:
"from_": 0, "from_": 0,
"to": 255, "to": 255,
"increment": 1, "increment": 1,
"width": 12, "width": self.scale_size(12),
"bg": bg_color, "bg": bg_color,
"fg": fg_color, "fg": fg_color,
"font": ("Arial", 24), "font": ("Arial", self.scale_font(24)),
"buttonbackground": active_bg_color, "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_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_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 = 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, 5)) n_label.pack(pady=(0, self.scale_size(5)))
n_inputs[f"n{i}_label"] = n_label # Store label reference n_inputs[f"n{i}_label"] = n_label # Store label reference
# Create a frame for the input with arrows on both sides # Create a frame for the input with arrows on both sides
@@ -828,29 +953,29 @@ class App:
left_arrow = tk.Button( left_arrow = tk.Button(
input_container, input_container,
text="", text="",
font=("Arial", 32, "bold"), font=("Arial", self.scale_font(32), "bold"),
bg=active_bg_color, bg=active_bg_color,
fg=fg_color, fg=fg_color,
relief=tk.FLAT, relief=tk.FLAT,
command=decrease_value, command=decrease_value,
width=3, width=self.scale_size(3),
height=1, 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 # Entry in the middle
n_entry = tk.Entry( n_entry = tk.Entry(
input_container, input_container,
textvariable=n_var, textvariable=n_var,
font=("Arial", 24), font=("Arial", self.scale_font(24)),
bg=bg_color, bg=bg_color,
fg=fg_color, fg=fg_color,
width=8, width=self.scale_size(8),
justify=tk.CENTER, justify=tk.CENTER,
relief=tk.SUNKEN, relief=tk.SUNKEN,
bd=2, 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("<KeyRelease>", lambda event: self.schedule_update_n_params(tab))
n_entry.bind("<FocusOut>", lambda event: self.schedule_update_n_params(tab, force_send=True)) 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( right_arrow = tk.Button(
input_container, input_container,
text="+", text="+",
font=("Arial", 32, "bold"), font=("Arial", self.scale_font(32), "bold"),
bg=active_bg_color, bg=active_bg_color,
fg=fg_color, fg=fg_color,
relief=tk.FLAT, relief=tk.FLAT,
command=increase_value, command=increase_value,
width=3, width=self.scale_size(3),
height=1, 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}"] = n_entry
n_inputs[f"n{i}_var"] = n_var # Store the variable for later updates 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 for IDs, Patterns, and NEW Color Palette
right_panel_frame = tk.Frame(main_tab_frame, bg=bg_color) 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 section - MODIFIED TO BE SIDE-BY-SIDE
ids_frame = tk.Frame(right_panel_frame, bg=bg_color) ids_frame = tk.Frame(right_panel_frame, bg=bg_color)
ids_frame.pack(pady=10, fill=tk.X) 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 # New inner frame for the IDs to be displayed horizontally
ids_inner_frame = tk.Frame(ids_frame, bg=bg_color) 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 ids_inner_frame.pack(fill=tk.X, expand=True) # Pack this frame to fill available width
for light_id in ids: for light_id in ids:
tk.Label(ids_inner_frame, text=str(light_id), font=("Arial", 18), bg=bg_color, fg=fg_color).pack( 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=5, pady=2 side=tk.LEFT, padx=self.scale_size(5), pady=self.scale_size(2)
) # Pack labels horizontally ) # Pack labels horizontally
# --- New Frame to hold Patterns and Color Palette side-by-side --- # --- 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 = 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 section
patterns_frame = tk.Frame(patterns_and_palette_frame, bg=bg_color, bd=2, relief=tk.GROOVE) 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 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", 20), bg=bg_color, fg=fg_color).pack(pady=10) 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 = {} tab.pattern_buttons = {}
patterns = list(self.patterns.keys()) patterns = list(self.patterns.keys())
@@ -942,23 +1066,23 @@ class App:
command=lambda p=pattern_name: self.send_pattern(tab_name, p), command=lambda p=pattern_name: self.send_pattern(tab_name, p),
bg=active_bg_color, bg=active_bg_color,
fg=fg_color, fg=fg_color,
font=("Arial", 18), font=("Arial", self.scale_font(18)),
padx=15, padx=self.scale_size(15),
pady=5, pady=self.scale_size(5),
relief=tk.FLAT, 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 tab.pattern_buttons[pattern_name] = button
self.highlight_pattern_button(tab, initial_pattern) self.highlight_pattern_button(tab, initial_pattern)
# --- Color Palette Editor Section --- # --- 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 = 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 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( tk.Label(color_palette_editor_frame, text="Color Palette:", font=("Arial", self.scale_font(20)), bg=bg_color, fg=fg_color).pack(
pady=10 pady=self.scale_size(10)
) )
# Frame to hold color swatches (will be dynamic) # Frame to hold color swatches (will be dynamic)
@@ -975,9 +1099,9 @@ class App:
command=lambda t=tab: self.add_color_to_palette(t), command=lambda t=tab: self.add_color_to_palette(t),
bg=active_bg_color, bg=active_bg_color,
fg=fg_color, fg=fg_color,
font=("Arial", 16), font=("Arial", self.scale_font(16)),
padx=10, padx=self.scale_size(10),
pady=5, pady=self.scale_size(5),
relief=tk.FLAT, relief=tk.FLAT,
) )
add_color_button.pack(side=tk.LEFT, expand=True, padx=5) 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), command=lambda t=tab: self.remove_selected_color_from_palette(t),
bg=active_bg_color, bg=active_bg_color,
fg=fg_color, fg=fg_color,
font=("Arial", 16), font=("Arial", self.scale_font(16)),
padx=10, padx=self.scale_size(10),
pady=5, pady=self.scale_size(5),
relief=tk.FLAT, relief=tk.FLAT,
) )
remove_color_button.pack(side=tk.RIGHT, expand=True, padx=5) 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): for i, hex_color in enumerate(tab.colors_in_palette):
swatch_frame = tk.Frame( 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 # 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)) 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}", text=f"Color {i+1}",
bg=hex_color, bg=hex_color,
fg=color_utils.get_contrast_text_color(hex_color), fg=color_utils.get_contrast_text_color(hex_color),
font=("Arial", 14), font=("Arial", self.scale_font(14)),
width=5, width=self.scale_size(5),
height=3, height=self.scale_size(3),
) )
swatch_label.pack(expand=True, fill=tk.BOTH) 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)) swatch_label.bind("<Button-1>", lambda event, idx=i, t=tab: self.select_color_in_palette(t, idx))

View File

@@ -10,7 +10,11 @@ class Settings(dict):
def save(self): def save(self):
try: try:
# Create a copy without lights and tab_order (these belong in profiles, not settings.json) # Create a copy without lights and tab_order (these belong in profiles, not settings.json)
# But keep patterns, tab_password, and current_profile
settings_to_save = {k: v for k, v in self.items() if k not in ["lights", "tab_order"]} settings_to_save = {k: v for k, v in self.items() if k not in ["lights", "tab_order"]}
# Ensure patterns are always included if they exist
if "patterns" in self:
settings_to_save["patterns"] = self["patterns"]
j = json.dumps(settings_to_save, indent=4) j = json.dumps(settings_to_save, indent=4)
with open(self.SETTINGS_FILE, 'w') as file: with open(self.SETTINGS_FILE, 'w') as file:
file.write(j) file.write(j)
@@ -23,6 +27,40 @@ class Settings(dict):
with open(self.SETTINGS_FILE, 'r') as file: with open(self.SETTINGS_FILE, 'r') as file:
loaded_settings = json.load(file) loaded_settings = json.load(file)
self.update(loaded_settings) self.update(loaded_settings)
# Ensure patterns exist (they should always be in settings.json)
if "patterns" not in self:
# Initialize with default patterns if missing
self["patterns"] = {
"on": {"min_delay": 10, "max_delay": 10000},
"off": {"min_delay": 10, "max_delay": 10000},
"rainbow": {"Step Rate": "n1", "min_delay": 10, "max_delay": 10000},
"transition": {"min_delay": 10, "max_delay": 10000},
"chase": {
"Colour 1 Length": "n1",
"Colour 2 Length": "n2",
"Step 1": "n3",
"Step 2": "n4",
"min_delay": 10,
"max_delay": 10000
},
"pulse": {
"Attack": "n1",
"Hold": "n2",
"Decay": "n3",
"min_delay": 10,
"max_delay": 10000
},
"circle": {
"Head Rate": "n1",
"Max Length": "n2",
"Tail Rate": "n3",
"Min Length": "n4",
"min_delay": 10,
"max_delay": 10000
},
"blink": {"min_delay": 10, "max_delay": 10000}
}
self.save() # Save to persist the default patterns
print("Settings loaded successfully.") print("Settings loaded successfully.")
except Exception as e: except Exception as e:
print(f"Error loading settings {e}") print(f"Error loading settings {e}")