From c8ae1133554ae665ec51ecbc324a0880f4f8393e Mon Sep 17 00:00:00 2001 From: jimmy Date: Sun, 30 Nov 2025 17:23:32 +1300 Subject: [PATCH] Remove associated names label and always show n parameter inputs --- profiles/ring.json | 48 +++++++- settings.json | 4 +- src/main.py | 299 ++++++++++++++++++++++++++++----------------- 3 files changed, 238 insertions(+), 113 deletions(-) diff --git a/profiles/ring.json b/profiles/ring.json index 7f088eb..a5dbfb1 100644 --- a/profiles/ring.json +++ b/profiles/ring.json @@ -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": { "ring1": { "names": [ @@ -59,7 +106,6 @@ } } }, - "tab_password": "", "tab_order": [ "ring1", "ring2" diff --git a/settings.json b/settings.json index 2a03e8b..f67b21d 100644 --- a/settings.json +++ b/settings.json @@ -1,6 +1,6 @@ { - "tab_password": "qwerty1234", - "current_profile": "tt", + "tab_password": "", + "current_profile": "ring", "patterns": { "on": { "min_delay": 10, diff --git a/src/main.py b/src/main.py index cfa8a40..25f069c 100644 --- a/src/main.py +++ b/src/main.py @@ -109,114 +109,136 @@ class App: style.map("TNotebook.Tab", background=[("selected", active_bg_color)], foreground=[("selected", fg_color)]) style.configure("TFrame", background=bg_color) - # Tab management buttons frame (packed at bottom first to ensure it's always visible) - tab_management_frame = tk.Frame(self.root, bg=bg_color) - tab_management_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=self.scale_size(10), pady=self.scale_size(5)) + # 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 (packed after buttons, takes remaining space) + # Create Notebook for tabs self.notebook = ttk.Notebook(self.root) 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", self.scale_font(14)), - padx=self.scale_size(10), - pady=self.scale_size(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=self.scale_size(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", self.scale_font(14)), - padx=self.scale_size(10), - pady=self.scale_size(5) - ) - edit_tab_btn.pack(side=tk.LEFT, padx=self.scale_size(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", self.scale_font(14)), - padx=self.scale_size(10), - pady=self.scale_size(5) - ) - delete_tab_btn.pack(side=tk.LEFT, padx=self.scale_size(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", self.scale_font(14)), - padx=self.scale_size(10), - pady=self.scale_size(5) - ) - move_left_btn.pack(side=tk.LEFT, padx=self.scale_size(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", self.scale_font(14)), - padx=self.scale_size(10), - pady=self.scale_size(5) - ) - move_right_btn.pack(side=tk.LEFT, padx=self.scale_size(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", self.scale_font(14))).pack(side=tk.LEFT, padx=(self.scale_size(20), self.scale_size(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("", lambda e: btn.config(bg=active_bg_color)) + btn.bind("", 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("", 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", self.scale_font(14)), - width=self.scale_size(20), - state="readonly" - ) - self.profile_dropdown.pack(side=tk.LEFT, padx=self.scale_size(5)) - self.profile_dropdown.bind("<>", 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", self.scale_font(14)), - padx=self.scale_size(10), - pady=self.scale_size(5) - ) - new_profile_btn.pack(side=tk.LEFT, padx=self.scale_size(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", self.scale_font(14)), - padx=self.scale_size(10), - pady=self.scale_size(5) - ) - save_profile_btn.pack(side=tk.LEFT, padx=self.scale_size(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" @@ -362,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\nThis will replace your current settings.", + f"Load profile '{profile_name}'?\n\nThis 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""" @@ -583,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.""" @@ -958,7 +1038,6 @@ class App: # 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", self.scale_font(20)), bg=bg_color, fg=fg_color).pack(pady=self.scale_size(10)) # New inner frame for the IDs to be displayed horizontally ids_inner_frame = tk.Frame(ids_frame, bg=bg_color)