728 lines
33 KiB
Python
728 lines
33 KiB
Python
import asyncio
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox # Import messagebox for confirmations
|
|
import json
|
|
from async_tkinter_loop import async_handler, async_mainloop
|
|
from networking import WebSocketClient
|
|
import color_utils
|
|
from settings import Settings
|
|
import time
|
|
|
|
# Dark theme colors (unchanged)
|
|
bg_color = "#2e2e2e"
|
|
fg_color = "white"
|
|
trough_color_red = "#4a0000"
|
|
trough_color_green = "#004a00"
|
|
trough_color_blue = "#00004a"
|
|
trough_color_brightness = "#4a4a4a"
|
|
trough_color_delay = "#4a4a4a"
|
|
active_bg_color = "#4a4a4a"
|
|
highlight_pattern_color = "#6a5acd"
|
|
# New color for active color in palette
|
|
active_palette_color_border = "#FFD700" # Gold color
|
|
|
|
|
|
class App:
|
|
def __init__(self) -> None:
|
|
self.settings = Settings()
|
|
self.root = tk.Tk()
|
|
self.root.attributes("-fullscreen", True)
|
|
self.root.configure(bg=bg_color)
|
|
|
|
# Debouncing variables (remain the same)
|
|
self.last_rgb_update_time = 0
|
|
self.rgb_update_interval_ms = 100
|
|
self.pending_rgb_update_id = None
|
|
|
|
self.last_brightness_update_time = 0
|
|
self.brightness_update_interval_ms = 100
|
|
self.pending_brightness_update_id = None
|
|
|
|
self.last_delay_update_time = 0
|
|
self.delay_update_interval_ms = 100
|
|
self.pending_delay_update_id = None
|
|
|
|
# --- WebSocketClient ---
|
|
self.websocket_client = WebSocketClient("ws://192.168.4.1:80/ws")
|
|
self.root.after(100, async_handler(self.websocket_client.connect))
|
|
|
|
# Configure ttk style (unchanged)
|
|
style = ttk.Style()
|
|
style.theme_use("alt")
|
|
style.configure(".", background=bg_color, foreground=fg_color, font=("Arial", 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]
|
|
)
|
|
style.map("TNotebook.Tab", background=[("selected", active_bg_color)], foreground=[("selected", fg_color)])
|
|
style.configure("TFrame", background=bg_color)
|
|
|
|
# Create Notebook for tabs (unchanged)
|
|
self.notebook = ttk.Notebook(self.root)
|
|
self.notebook.pack(expand=1, fill="both")
|
|
|
|
self.tabs = {}
|
|
self.create_tabs()
|
|
|
|
self.notebook.bind("<<NotebookTabChanged>>", self.on_tab_change)
|
|
|
|
# Add Reload Config Button (unchanged)
|
|
reload_button = tk.Button(
|
|
self.root,
|
|
text="Reload Config",
|
|
command=self.reload_config,
|
|
bg=active_bg_color,
|
|
fg=fg_color,
|
|
font=("Arial", 20),
|
|
padx=20,
|
|
pady=10,
|
|
relief=tk.FLAT,
|
|
)
|
|
reload_button.pack(side=tk.BOTTOM, pady=20)
|
|
|
|
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
|
|
|
async_mainloop(self.root)
|
|
|
|
def on_closing(self):
|
|
print("Closing application...")
|
|
asyncio.create_task(self.websocket_client.close())
|
|
self.root.destroy()
|
|
|
|
def create_tabs(self):
|
|
for tab_name in list(self.tabs.keys()):
|
|
self.notebook.forget(self.tabs[tab_name])
|
|
del self.tabs[tab_name]
|
|
|
|
for key, value in self.settings["lights"].items():
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text=key)
|
|
self.create_light_control_widgets(tab, key, value["names"], value["settings"])
|
|
self.tabs[key] = tab
|
|
|
|
def create_light_control_widgets(self, tab, tab_name, ids, initial_settings):
|
|
slider_length = 800
|
|
slider_width = 50
|
|
|
|
# Extract initial color, brightness, and delay
|
|
# The 'colors' entry can now be a list. We'll pick the first one for initial display.
|
|
initial_colors = initial_settings.get("colors", ["#000000"])
|
|
initial_hex_color = initial_colors[0] if initial_colors else "#000000"
|
|
initial_brightness = initial_settings.get("brightness", 127)
|
|
initial_delay = initial_settings.get("delay", 0)
|
|
initial_pattern = initial_settings.get("pattern", "on")
|
|
|
|
initial_r, initial_g, initial_b = color_utils.hex_to_rgb(initial_hex_color)
|
|
|
|
# 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)
|
|
|
|
# Left panel for sliders
|
|
slider_panel_frame = tk.Frame(main_tab_frame, bg=bg_color)
|
|
slider_panel_frame.pack(side=tk.LEFT, padx=10, pady=10)
|
|
|
|
# Common slider configuration
|
|
slider_config = {
|
|
"from_": 255,
|
|
"to": 0,
|
|
"orient": tk.VERTICAL,
|
|
"length": slider_length,
|
|
"width": slider_width,
|
|
"bg": bg_color,
|
|
"fg": fg_color,
|
|
"highlightbackground": bg_color,
|
|
"activebackground": active_bg_color,
|
|
"resolution": 1,
|
|
"sliderlength": 70,
|
|
}
|
|
|
|
# Red Slider
|
|
red_slider = tk.Scale(slider_panel_frame, label="Red", troughcolor=trough_color_red, **slider_config)
|
|
red_slider.set(initial_r)
|
|
red_slider.pack(side=tk.LEFT, padx=10)
|
|
red_slider.bind("<B1-Motion>", lambda _: self.schedule_update_rgb(tab))
|
|
red_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_rgb(tab, force_send=True))
|
|
|
|
# Green Slider
|
|
green_slider = tk.Scale(slider_panel_frame, label="Green", troughcolor=trough_color_green, **slider_config)
|
|
green_slider.set(initial_g)
|
|
green_slider.pack(side=tk.LEFT, padx=10)
|
|
green_slider.bind("<B1-Motion>", lambda _: self.schedule_update_rgb(tab))
|
|
green_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_rgb(tab, force_send=True))
|
|
|
|
# Blue Slider
|
|
blue_slider = tk.Scale(slider_panel_frame, label="Blue", troughcolor=trough_color_blue, **slider_config)
|
|
blue_slider.set(initial_b)
|
|
blue_slider.pack(side=tk.LEFT, padx=10)
|
|
blue_slider.bind("<B1-Motion>", lambda _: self.schedule_update_rgb(tab))
|
|
blue_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_rgb(tab, force_send=True))
|
|
|
|
# Brightness Slider
|
|
brightness_slider = tk.Scale(
|
|
slider_panel_frame, label="Brightness", troughcolor=trough_color_brightness, **slider_config
|
|
)
|
|
brightness_slider.set(initial_brightness)
|
|
brightness_slider.pack(side=tk.LEFT, padx=10)
|
|
brightness_slider.bind("<B1-Motion>", lambda _: self.schedule_update_brightness(tab))
|
|
brightness_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_brightness(tab, force_send=True))
|
|
|
|
# Delay Slider
|
|
delay_slider_config = slider_config.copy()
|
|
delay_slider_config.update(
|
|
{
|
|
"from_": 1000,
|
|
"to": 0,
|
|
"resolution": 10,
|
|
"label": "Delay (ms)",
|
|
"troughcolor": trough_color_delay,
|
|
}
|
|
)
|
|
delay_slider = tk.Scale(slider_panel_frame, **delay_slider_config)
|
|
delay_slider.set(initial_delay)
|
|
delay_slider.pack(side=tk.LEFT, padx=10)
|
|
delay_slider.bind("<B1-Motion>", lambda _: self.schedule_update_delay(tab))
|
|
delay_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_delay(tab, force_send=True))
|
|
|
|
# Store references to widgets for this tab
|
|
tab.widgets = {
|
|
"red_slider": red_slider,
|
|
"green_slider": green_slider,
|
|
"blue_slider": blue_slider,
|
|
"brightness_slider": brightness_slider,
|
|
"delay_slider": delay_slider,
|
|
"selected_color_index": 0, # Default to the first color
|
|
}
|
|
tab.colors_in_palette = initial_colors.copy() # Store the list of hex colors for this tab
|
|
tab.color_swatch_frames = [] # To hold references to the color swatches
|
|
|
|
# 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")
|
|
|
|
# IDs section (unchanged)
|
|
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)
|
|
for light_id in ids:
|
|
tk.Label(ids_frame, text=str(light_id), font=("Arial", 18), bg=bg_color, fg=fg_color).pack(pady=2)
|
|
|
|
# --- 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 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)
|
|
|
|
tab.pattern_buttons = {}
|
|
patterns = self.settings.get("patterns", [])
|
|
for pattern_name in patterns:
|
|
button = tk.Button(
|
|
patterns_frame,
|
|
text=pattern_name,
|
|
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,
|
|
relief=tk.FLAT,
|
|
)
|
|
button.pack(pady=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
|
|
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
|
|
)
|
|
|
|
# Frame to hold color swatches (will be dynamic)
|
|
tab.color_swatches_container = tk.Frame(color_palette_editor_frame, bg=bg_color)
|
|
tab.color_swatches_container.pack(pady=5, fill=tk.BOTH, expand=True)
|
|
|
|
# Buttons for Add/Remove Color
|
|
palette_buttons_frame = tk.Frame(color_palette_editor_frame, bg=bg_color)
|
|
palette_buttons_frame.pack(pady=10, fill=tk.X)
|
|
|
|
add_color_button = tk.Button(
|
|
palette_buttons_frame,
|
|
text="Add Color",
|
|
command=lambda t=tab: self.add_color_to_palette(t),
|
|
bg=active_bg_color,
|
|
fg=fg_color,
|
|
font=("Arial", 16),
|
|
padx=10,
|
|
pady=5,
|
|
relief=tk.FLAT,
|
|
)
|
|
add_color_button.pack(side=tk.LEFT, expand=True, padx=5)
|
|
|
|
remove_color_button = tk.Button(
|
|
palette_buttons_frame,
|
|
text="Remove Selected",
|
|
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,
|
|
relief=tk.FLAT,
|
|
)
|
|
remove_color_button.pack(side=tk.RIGHT, expand=True, padx=5)
|
|
|
|
# Initial population of the color palette
|
|
self.refresh_color_palette_display(tab)
|
|
|
|
# The initial call to update_ui_for_pattern now only sets slider values and highlights
|
|
self.update_ui_for_pattern(tab, initial_pattern)
|
|
|
|
def refresh_color_palette_display(self, tab):
|
|
"""Clears and repopulates the color swatches in the palette display."""
|
|
# Clear existing swatches
|
|
for frame in tab.color_swatch_frames:
|
|
frame.destroy()
|
|
tab.color_swatch_frames.clear()
|
|
|
|
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
|
|
)
|
|
swatch_frame.pack(pady=2, padx=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))
|
|
|
|
# Add a label inside to make it clickable too
|
|
swatch_label = tk.Label(
|
|
swatch_frame,
|
|
text=f"Color {i+1}",
|
|
bg=hex_color,
|
|
fg=color_utils.get_contrast_text_color(hex_color),
|
|
font=("Arial", 14),
|
|
width=10,
|
|
height=2,
|
|
)
|
|
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))
|
|
|
|
tab.color_swatch_frames.append(swatch_frame)
|
|
|
|
# Re-highlight the currently selected color
|
|
self._highlight_selected_color_swatch(tab)
|
|
|
|
def _highlight_selected_color_swatch(self, tab):
|
|
"""Applies/removes highlight border to the selected color swatch."""
|
|
current_index = tab.widgets["selected_color_index"]
|
|
for i, swatch_frame in enumerate(tab.color_swatch_frames):
|
|
if i == current_index:
|
|
swatch_frame.config(highlightbackground=active_palette_color_border, highlightthickness=3)
|
|
else:
|
|
swatch_frame.config(highlightbackground=swatch_frame.cget("bg"), highlightthickness=0) # Reset to no highlight
|
|
|
|
def select_color_in_palette(self, tab, index: int):
|
|
"""Selects a color in the palette, updates sliders, and highlights swatch."""
|
|
if not (0 <= index < len(tab.colors_in_palette)):
|
|
return
|
|
|
|
tab.widgets["selected_color_index"] = index
|
|
self._highlight_selected_color_swatch(tab)
|
|
|
|
# Update RGB sliders with the selected color
|
|
hex_color = tab.colors_in_palette[index]
|
|
r, g, b = color_utils.hex_to_rgb(hex_color)
|
|
tab.widgets["red_slider"].set(r)
|
|
tab.widgets["green_slider"].set(g)
|
|
tab.widgets["blue_slider"].set(b)
|
|
|
|
print(f"Selected color index {index}: {hex_color}")
|
|
|
|
def add_color_to_palette(self, tab):
|
|
"""Adds a new black color to the palette and selects it, with a limit of 10 colors."""
|
|
MAX_COLORS = 10 # Define the maximum number of colors allowed
|
|
|
|
if len(tab.colors_in_palette) >= MAX_COLORS:
|
|
messagebox.showwarning("Color Limit Reached", f"You can add a maximum of {MAX_COLORS} colors to the palette.")
|
|
return
|
|
|
|
tab.colors_in_palette.append("#000000") # Add black as default
|
|
self.refresh_color_palette_display(tab)
|
|
# Select the newly added color
|
|
self.select_color_in_palette(tab, len(tab.colors_in_palette) - 1)
|
|
self.save_current_tab_settings() # Save changes to settings.json
|
|
|
|
def remove_selected_color_from_palette(self, tab):
|
|
"""Removes the currently selected color from the palette."""
|
|
current_index = tab.widgets["selected_color_index"]
|
|
if len(tab.colors_in_palette) <= 1:
|
|
messagebox.showwarning("Cannot Remove", "There must be at least one color in the palette.")
|
|
return
|
|
|
|
if messagebox.askyesno("Confirm Delete", f"Are you sure you want to remove Color {current_index + 1}?"):
|
|
del tab.colors_in_palette[current_index]
|
|
# Adjust selected index if the removed color was the last one
|
|
if current_index >= len(tab.colors_in_palette):
|
|
tab.widgets["selected_color_index"] = len(tab.colors_in_palette) - 1
|
|
if tab.widgets["selected_color_index"] < 0: # Should not happen if check above works
|
|
tab.widgets["selected_color_index"] = 0
|
|
|
|
self.refresh_color_palette_display(tab)
|
|
# Update sliders with the new selected color (if any)
|
|
if tab.colors_in_palette:
|
|
self.select_color_in_palette(tab, tab.widgets["selected_color_index"])
|
|
else: # If palette became empty (shouldn't happen with 1-color check)
|
|
tab.widgets["red_slider"].set(0)
|
|
tab.widgets["green_slider"].set(0)
|
|
tab.widgets["blue_slider"].set(0)
|
|
|
|
self.save_current_tab_settings() # Save changes to settings.json
|
|
|
|
def update_ui_for_pattern(self, tab, current_pattern: str):
|
|
"""
|
|
Manages the state of the UI elements based on the selected pattern.
|
|
The Color Palette Editor is now always visible when 'transition' is selected,
|
|
and RGB sliders update based on the selected color in the palette.
|
|
When not 'transition', RGB sliders revert to the first color in settings.
|
|
"""
|
|
# The color_palette_editor_frame is now *always* packed in create_light_control_widgets
|
|
# when patterns_and_palette_frame is created with side-by-side packing.
|
|
# So we no longer need to pack/pack_forget it here.
|
|
# Its visibility is handled by its initial creation and packing alongside the patterns.
|
|
|
|
# If the pattern is "transition", select the current color in the palette
|
|
# and ensure the RGB sliders reflect that color.
|
|
if current_pattern == "transition":
|
|
# This handles refreshing the display and setting sliders to the selected color
|
|
self.refresh_color_palette_display(tab)
|
|
self.select_color_in_palette(tab, tab.widgets["selected_color_index"])
|
|
else:
|
|
# When switching away from transition, ensure RGB sliders show the first color from settings
|
|
# and reset selected_color_index.
|
|
initial_colors = self.settings["lights"][self.notebook.tab(self.notebook.select(), "text")]["settings"].get(
|
|
"colors", ["#000000"]
|
|
)
|
|
initial_hex_color = initial_colors[0] if initial_colors else "#000000"
|
|
r, g, b = color_utils.hex_to_rgb(initial_hex_color)
|
|
tab.widgets["red_slider"].set(r)
|
|
tab.widgets["green_slider"].set(g)
|
|
tab.widgets["blue_slider"].set(b)
|
|
tab.widgets["selected_color_index"] = 0 # Reset selected color index to the first (default)
|
|
self._highlight_selected_color_swatch(
|
|
tab
|
|
) # Remove highlight if active color is no longer relevant for editing
|
|
# (or just highlight the 0th if you want)
|
|
|
|
# Brightness and Delay sliders are always visible.
|
|
|
|
def highlight_pattern_button(self, tab_widget, active_pattern_name):
|
|
if hasattr(tab_widget, "pattern_buttons"):
|
|
for pattern_name, button in tab_widget.pattern_buttons.items():
|
|
if pattern_name == active_pattern_name:
|
|
button.config(bg=highlight_pattern_color)
|
|
else:
|
|
button.config(bg=active_bg_color)
|
|
|
|
def on_tab_change(self, event):
|
|
selected_tab_name = self.notebook.tab(self.notebook.select(), "text")
|
|
current_tab_widget = self.notebook.nametowidget(self.notebook.select())
|
|
|
|
initial_settings = self.settings["lights"][selected_tab_name]["settings"]
|
|
|
|
# Ensure current_tab_widget has the necessary attributes
|
|
if not hasattr(current_tab_widget, "colors_in_palette"):
|
|
# This tab might not have been fully initialized yet, or recreated
|
|
# In a full reload, create_tabs ensures it is.
|
|
return
|
|
|
|
# Update the local colors_in_palette list for the tab
|
|
current_tab_widget.colors_in_palette = initial_settings.get("colors", ["#000000"]).copy()
|
|
current_tab_widget.widgets["selected_color_index"] = 0 # Default to first color
|
|
|
|
# Refresh the color palette display and select the first color
|
|
self.refresh_color_palette_display(current_tab_widget)
|
|
if current_tab_widget.colors_in_palette:
|
|
self.select_color_in_palette(current_tab_widget, 0)
|
|
else: # If palette became empty (shouldn't happen with default ["#000000"])
|
|
current_tab_widget.widgets["red_slider"].set(0)
|
|
current_tab_widget.widgets["green_slider"].set(0)
|
|
current_tab_widget.widgets["blue_slider"].set(0)
|
|
|
|
# Update brightness and delay sliders
|
|
current_tab_widget.widgets["brightness_slider"].set(initial_settings.get("brightness", 127))
|
|
current_tab_widget.widgets["delay_slider"].set(initial_settings.get("delay", 0))
|
|
|
|
# Highlight the active pattern button
|
|
initial_pattern = initial_settings.get("pattern", "on")
|
|
self.highlight_pattern_button(current_tab_widget, initial_pattern)
|
|
|
|
# Update UI visibility based on the current pattern
|
|
self.update_ui_for_pattern(current_tab_widget, initial_pattern)
|
|
|
|
def reload_config(self):
|
|
print("Reloading configuration...")
|
|
self.settings = Settings()
|
|
self.create_tabs()
|
|
# After recreating, ensure the currently selected tab's sliders are updated
|
|
# Trigger on_tab_change manually for the currently selected tab
|
|
self.on_tab_change(None)
|
|
|
|
# --- Debouncing functions (no change to core logic, just how they call update_rgb) ---
|
|
def schedule_update_rgb(self, tab, force_send=False):
|
|
current_time = time.time() * 1000
|
|
if force_send:
|
|
if self.pending_rgb_update_id:
|
|
self.root.after_cancel(self.pending_rgb_update_id)
|
|
self.pending_rgb_update_id = None
|
|
self.update_rgb(tab)
|
|
self.last_rgb_update_time = current_time
|
|
elif current_time - self.last_rgb_update_time >= self.rgb_update_interval_ms:
|
|
if self.pending_rgb_update_id:
|
|
self.root.after_cancel(self.pending_rgb_update_id)
|
|
self.pending_rgb_update_id = None
|
|
self.update_rgb(tab)
|
|
self.last_rgb_update_time = current_time
|
|
else:
|
|
if self.pending_rgb_update_id:
|
|
self.root.after_cancel(self.pending_rgb_update_id)
|
|
time_to_wait = int(self.rgb_update_interval_ms - (current_time - self.last_rgb_update_time))
|
|
self.pending_rgb_update_id = self.root.after(time_to_wait, lambda: self.update_rgb(tab))
|
|
|
|
def schedule_update_brightness(self, tab, force_send=False):
|
|
current_time = time.time() * 1000
|
|
if force_send:
|
|
if self.pending_brightness_update_id:
|
|
self.root.after_cancel(self.pending_brightness_update_id)
|
|
self.pending_brightness_update_id = None
|
|
self.update_brightness(tab)
|
|
self.last_brightness_update_time = current_time
|
|
elif current_time - self.last_brightness_update_time >= self.brightness_update_interval_ms:
|
|
if self.pending_brightness_update_id:
|
|
self.root.after_cancel(self.pending_brightness_update_id)
|
|
self.pending_brightness_update_id = None
|
|
self.update_brightness(tab)
|
|
self.last_brightness_update_time = current_time
|
|
else:
|
|
if self.pending_brightness_update_id:
|
|
self.root.after_cancel(self.pending_brightness_update_id)
|
|
time_to_wait = int(self.brightness_update_interval_ms - (current_time - self.last_brightness_update_time))
|
|
self.pending_brightness_update_id = self.root.after(time_to_wait, lambda: self.update_brightness(tab))
|
|
|
|
def schedule_update_delay(self, tab, force_send=False):
|
|
current_time = time.time() * 1000
|
|
if force_send:
|
|
if self.pending_delay_update_id:
|
|
self.root.after_cancel(self.pending_delay_update_id)
|
|
self.pending_delay_update_id = None
|
|
self.update_delay(tab)
|
|
self.last_delay_update_time = current_time
|
|
elif current_time - self.last_delay_update_time >= self.delay_update_interval_ms:
|
|
if self.pending_delay_update_id:
|
|
self.root.after_cancel(self.pending_delay_update_id)
|
|
self.pending_delay_update_id = None
|
|
self.update_delay(tab)
|
|
self.last_delay_update_time = current_time
|
|
else:
|
|
if self.pending_delay_update_id:
|
|
self.root.after_cancel(self.pending_delay_update_id)
|
|
time_to_wait = int(self.delay_update_interval_ms - (current_time - self.last_delay_update_time))
|
|
self.pending_delay_update_id = self.root.after(time_to_wait, lambda: self.update_delay(tab))
|
|
|
|
# --- Asynchronous Update Functions ---
|
|
@async_handler
|
|
async def update_rgb(self, tab):
|
|
"""Update the currently selected color in the palette and send to the server."""
|
|
try:
|
|
red_slider = tab.widgets["red_slider"]
|
|
green_slider = tab.widgets["green_slider"]
|
|
blue_slider = tab.widgets["blue_slider"]
|
|
|
|
r = red_slider.get()
|
|
g = green_slider.get()
|
|
b = blue_slider.get()
|
|
|
|
hex_color = f"#{r:02x}{g:02x}{b:02x}"
|
|
print(f"Updating selected color to: {hex_color}")
|
|
|
|
selected_color_index = tab.widgets["selected_color_index"]
|
|
if 0 <= selected_color_index < len(tab.colors_in_palette):
|
|
tab.colors_in_palette[selected_color_index] = hex_color
|
|
self.refresh_color_palette_display(tab) # Update swatch immediately
|
|
|
|
selected_server = self.notebook.tab(self.notebook.select(), "text")
|
|
names = self.settings["lights"][selected_server]["names"]
|
|
|
|
# ALWAYS send the full current palette, or at least the first color,
|
|
# along with other relevant settings, when an RGB slider is moved.
|
|
# The device firmware will interpret 'colors' based on its current pattern.
|
|
|
|
# Determine which colors to send. It's generally safest to send the
|
|
# full current palette, as the device might need all of them.
|
|
colors_to_send = tab.colors_in_palette.copy() # Send a copy to be safe
|
|
|
|
payload = {
|
|
"save": True, # Always save this change to config
|
|
"names": names,
|
|
"settings": {
|
|
"colors": colors_to_send,
|
|
# We might also want to send the current brightness and delay
|
|
# to ensure the device has the complete state, or at least
|
|
# ensures these don't get 'unset' if they weren't explicitly changed.
|
|
# This depends on your firmware's expected payload.
|
|
"brightness": tab.widgets["brightness_slider"].get(),
|
|
"delay": tab.widgets["delay_slider"].get(),
|
|
# Also include the current pattern, so the device knows how to apply colors
|
|
"pattern": self.settings["lights"][selected_server]["settings"].get("pattern", "on"),
|
|
},
|
|
}
|
|
|
|
# Update the settings object with the new color list (and potentially other synced values)
|
|
self.settings["lights"][selected_server]["settings"]["colors"] = tab.colors_in_palette.copy()
|
|
# Also ensure brightness, delay, and pattern are up-to-date in settings before saving
|
|
self.settings["lights"][selected_server]["settings"]["brightness"] = tab.widgets["brightness_slider"].get()
|
|
self.settings["lights"][selected_server]["settings"]["delay"] = tab.widgets["delay_slider"].get()
|
|
# Pattern is generally set by the pattern buttons, but including it in the save here
|
|
# for completeness might be useful depending on your app's state management.
|
|
# self.settings["lights"][selected_server]["settings"]["pattern"] = current_pattern # Already updated by send_pattern
|
|
self.settings.save()
|
|
|
|
await self.websocket_client.send_data(payload)
|
|
print(f"Sent RGB payload: {payload}")
|
|
|
|
except Exception as e:
|
|
print(f"Error updating RGB: {e}")
|
|
|
|
@async_handler
|
|
async def update_brightness(self, tab):
|
|
try:
|
|
brightness_slider = tab.widgets["brightness_slider"]
|
|
brightness = brightness_slider.get()
|
|
print(f"Brightness: {brightness}")
|
|
|
|
selected_server = self.notebook.tab(self.notebook.select(), "text")
|
|
names = self.settings["lights"][selected_server]["names"]
|
|
|
|
payload = {
|
|
"save": True,
|
|
"names": names,
|
|
"settings": {
|
|
"brightness": brightness,
|
|
},
|
|
}
|
|
# Update the settings object with the new brightness
|
|
self.settings["lights"][selected_server]["settings"]["brightness"] = brightness
|
|
self.settings.save()
|
|
await self.websocket_client.send_data(payload)
|
|
print(f"Sent brightness payload: {payload}")
|
|
except Exception as e:
|
|
print(f"Error updating brightness: {e}")
|
|
|
|
@async_handler
|
|
async def update_delay(self, tab):
|
|
try:
|
|
delay_slider = tab.widgets["delay_slider"]
|
|
delay = delay_slider.get()
|
|
print(f"Delay: {delay}")
|
|
|
|
selected_server = self.notebook.tab(self.notebook.select(), "text")
|
|
names = self.settings["lights"][selected_server]["names"]
|
|
payload = {
|
|
"save": True,
|
|
"names": names,
|
|
"settings": {
|
|
"delay": delay,
|
|
},
|
|
}
|
|
# Update the settings object with the new delay
|
|
self.settings["lights"][selected_server]["settings"]["delay"] = delay
|
|
self.settings.save()
|
|
await self.websocket_client.send_data(payload)
|
|
print(f"Sent delay payload: {payload}")
|
|
except Exception as e:
|
|
print(f"Error updating delay: {e}")
|
|
|
|
@async_handler
|
|
async def send_pattern(self, tab_name: str, pattern_name: str):
|
|
try:
|
|
names = self.settings["lights"][tab_name]["names"]
|
|
# Get the actual tab widget to access its `colors_in_palette` and other attributes
|
|
current_tab_widget = None
|
|
for key, tab_widget in self.tabs.items():
|
|
if key == tab_name:
|
|
current_tab_widget = tab_widget
|
|
break
|
|
|
|
if not current_tab_widget:
|
|
print(f"Error: Could not find tab widget for {tab_name}")
|
|
return
|
|
|
|
current_settings_for_tab = self.settings["lights"][tab_name]["settings"]
|
|
|
|
payload_settings = {
|
|
"pattern": pattern_name,
|
|
"brightness": current_settings_for_tab.get("brightness", 127),
|
|
"delay": current_settings_for_tab.get("delay", 0),
|
|
}
|
|
|
|
# Only include "colors" in the payload if the pattern specifically uses them
|
|
# For "transition", send the entire palette
|
|
# For "on", "off", "blink", usually just the first color from the palette is relevant
|
|
if pattern_name == "transition":
|
|
# Ensure we send the *current state* of the palette
|
|
payload_settings["colors"] = current_tab_widget.colors_in_palette
|
|
elif pattern_name in ["on", "blink"]: # Add other patterns that use a single color here
|
|
if current_tab_widget.colors_in_palette:
|
|
payload_settings["colors"] = [current_tab_widget.colors_in_palette[0]]
|
|
else:
|
|
payload_settings["colors"] = ["#000000"] # Default if palette is empty
|
|
# For patterns like "off" or "rainbow", "colors" might not be needed or handled differently
|
|
|
|
payload = {
|
|
"save": True,
|
|
"names": names,
|
|
"settings": payload_settings,
|
|
}
|
|
|
|
# Update the settings object with the new pattern and current colors/brightness/delay
|
|
# It's important to save the state that *was active* when the pattern was set,
|
|
# or the state that should *persist* with the pattern.
|
|
self.settings["lights"][tab_name]["settings"]["pattern"] = pattern_name
|
|
# Ensure the saved colors are always the palette's current state
|
|
self.settings["lights"][tab_name]["settings"]["colors"] = current_tab_widget.colors_in_palette
|
|
self.settings.save()
|
|
|
|
self.highlight_pattern_button(current_tab_widget, pattern_name)
|
|
self.update_ui_for_pattern(current_tab_widget, pattern_name) # Update visibility
|
|
|
|
await self.websocket_client.send_data(payload)
|
|
print(f"Sent pattern payload: {payload}")
|
|
except Exception as e:
|
|
print(f"Error sending pattern: {e}")
|
|
|
|
def save_current_tab_settings(self):
|
|
"""Saves the current state of the active tab's settings (colors, brightness, delay, pattern) to config."""
|
|
selected_server = self.notebook.tab(self.notebook.select(), "text")
|
|
current_tab_widget = self.notebook.nametowidget(self.notebook.select())
|
|
|
|
if not hasattr(current_tab_widget, "colors_in_palette"):
|
|
return # Tab not fully initialized yet
|
|
|
|
# Update settings for the current tab in the self.settings object
|
|
self.settings["lights"][selected_server]["settings"]["colors"] = current_tab_widget.colors_in_palette
|
|
self.settings["lights"][selected_server]["settings"]["brightness"] = current_tab_widget.widgets["brightness_slider"].get()
|
|
self.settings["lights"][selected_server]["settings"]["delay"] = current_tab_widget.widgets["delay_slider"].get()
|
|
# The pattern is updated in send_pattern already, but ensure consistency
|
|
# For simplicity, we assume send_pattern is the primary way to change pattern.
|
|
|
|
self.settings.save()
|
|
print(f"Saved settings for {selected_server}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = App()
|