Remove associated names label and always show n parameter inputs

This commit is contained in:
2025-11-30 17:23:32 +13:00
parent 2db2d9e120
commit c8ae113355
3 changed files with 238 additions and 113 deletions

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": {
"ring1": {
"names": [
@@ -59,7 +106,6 @@
}
}
},
"tab_password": "",
"tab_order": [
"ring1",
"ring2"

View File

@@ -1,6 +1,6 @@
{
"tab_password": "qwerty1234",
"current_profile": "tt",
"tab_password": "",
"current_profile": "ring",
"patterns": {
"on": {
"min_delay": 10,

View File

@@ -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("<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", 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("<<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", 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)