Files
text-to-mic/utils/tone_presets_manager.py
2025-03-22 12:18:40 +00:00

455 lines
20 KiB
Python

import tkinter as tk
from tkinter import ttk, messagebox
import json
from pathlib import Path
import customtkinter as ctk
import os
class TonePresetsManager:
def __init__(self, parent):
self.parent = parent
self.dialog = tk.Toplevel(parent)
self.dialog.title("Spoken Tone Presets")
self.dialog.geometry("800x650")
self.dialog.resizable(False, False)
self.dialog.transient(parent)
self.dialog.grab_set()
# Set the dialog background color
self.dialog.configure(background="#f0f0f0")
# Center the dialog on the screen
self.center_dialog()
# Add a variable to track the currently selected tone
self.current_selected_tone = None
# Set style for a cleaner look
self.setup_styles()
self.create_dialog()
self.parent.after(100, self.update_content)
def setup_styles(self):
"""Configure ttk styles for a clean, modern appearance"""
style = ttk.Style()
# Use namespaced styles with "TonePresets." prefix to avoid affecting global styles
# Base frame style that will be used by all frames
style.configure("TonePresets.TFrame", background="#f0f0f0")
# Update labelframe style with bold and centered title
style.configure("TonePresets.TLabelframe", borderwidth=0, relief="flat", background="#f0f0f0")
style.configure("TonePresets.TLabelframe.Label",
foreground="#333333",
background="#f0f0f0",
font=("Arial", 10, "bold"),
anchor="center")
# Namespaced button style
style.configure("TonePresets.TButton", foreground="#333333", background="#f0f0f0", font=("Arial", 10))
style.map("TonePresets.TButton",
background=[("active", "#e0e0e0"), ("pressed", "#d0d0d0")])
# Namespaced label style
style.configure("TonePresets.TLabel", foreground="#333333", background="#f0f0f0", font=("Arial", 10))
# Listbox and Text widget styling will be applied directly to those widgets
def center_dialog(self):
# Get the parent window position and dimensions
parent_x = self.parent.winfo_x()
parent_y = self.parent.winfo_y()
parent_width = self.parent.winfo_width()
parent_height = self.parent.winfo_height()
# Calculate position for the dialog
dialog_width = 800 # Width from geometry
dialog_height = 650 # Height from geometry
position_x = parent_x + (parent_width - dialog_width) // 2
position_y = parent_y + (parent_height - dialog_height) // 2
# Set the position
self.dialog.geometry(f"{dialog_width}x{dialog_height}+{position_x}+{position_y}")
def create_dialog(self):
# Main container frame with padding
main_frame = ttk.Frame(self.dialog, padding="10", style="TonePresets.TFrame")
main_frame.pack(fill=tk.BOTH, expand=True)
# Create left panel (tone selection)
self.create_left_panel(main_frame)
# Create right panel (tone content)
self.create_right_panel(main_frame)
# Create bottom frame for main action button
self.create_bottom_frame()
def create_left_panel(self, main_frame):
left_panel = ttk.Frame(main_frame, width=200, style="TonePresets.TFrame")
left_panel.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
left_panel.pack_propagate(False)
# Create labelframe with centered title
select_frame = ttk.LabelFrame(left_panel, text="Tone Selection", padding="5", style="TonePresets.TLabelframe")
select_frame.pack(fill=tk.BOTH, expand=True)
# Use grid_propagate to ensure the title is properly centered
for child in select_frame.winfo_children():
if child.winfo_class() == 'TLabel':
child.configure(anchor='center')
child.place(relx=0.5, rely=0, anchor='n')
# Listbox for tones with scrollbar
self.tone_list = tk.Listbox(select_frame, height=8, selectmode=tk.BROWSE,
bg="#ffffff", fg="#333333",
selectbackground="#0078d7", selectforeground="#ffffff",
font=("Arial", 10))
tone_scrollbar = ttk.Scrollbar(select_frame, orient=tk.VERTICAL, command=self.tone_list.yview)
self.tone_list.config(yscrollcommand=tone_scrollbar.set)
self.tone_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, pady=5)
tone_scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
# Populate listbox with "None" option plus tone presets
all_tones = ["None"] + list(self.parent.tone_presets.keys())
for tone in all_tones:
self.tone_list.insert(tk.END, tone)
if tone == self.parent.current_tone_name:
self.tone_list.selection_set(all_tones.index(tone))
# Button frame
button_frame = ttk.Frame(left_panel, style="TonePresets.TFrame")
button_frame.pack(fill=tk.X, pady=5)
# Action buttons
self.new_button = ttk.Button(button_frame, text="New Tone", command=self.create_new_tone, style="TonePresets.TButton")
self.delete_button = ttk.Button(button_frame, text="Delete", command=self.delete_current_tone, style="TonePresets.TButton")
self.new_button.pack(side=tk.LEFT, padx=2)
self.delete_button.pack(side=tk.LEFT, padx=2)
# Bind selection event
self.tone_list.bind('<<ListboxSelect>>', self.on_tone_select)
def on_tone_select(self, event):
"""Handle tone selection and update the current selection."""
selected_indices = self.tone_list.curselection()
if selected_indices:
self.current_selected_tone = self.tone_list.get(selected_indices[0])
self.update_content()
def create_right_panel(self, main_frame):
self.right_panel = ttk.Frame(main_frame, style="TonePresets.TFrame")
self.right_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Create labelframe with centered title
content_frame = ttk.LabelFrame(self.right_panel, text="Tone Description", padding="5", style="TonePresets.TLabelframe")
content_frame.pack(fill=tk.BOTH, expand=True)
# Use grid_propagate to ensure the title is properly centered
for child in content_frame.winfo_children():
if child.winfo_class() == 'TLabel':
child.configure(anchor='center')
child.place(relx=0.5, rely=0, anchor='n')
self.edit_status_label = ttk.Label(content_frame, foreground="blue", style="TonePresets.TLabel")
self.edit_status_label.pack(anchor=tk.W, padx=5, pady=(5,0))
# Create a frame to contain the text widget and scrollbar
text_frame = ttk.Frame(content_frame, style="TonePresets.TFrame")
text_frame.pack(fill=tk.BOTH, expand=True, pady=5)
# Create vertical scrollbar only
v_scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL)
v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Create the text widget with word wrap and vertical scrollbar
self.content_text = tk.Text(text_frame, wrap=tk.WORD,
yscrollcommand=v_scrollbar.set,
bg="#ffffff", fg="#333333",
font=("Arial", 10),
relief="solid", borderwidth=1)
self.content_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Configure the scrollbar
v_scrollbar.config(command=self.content_text.yview)
self.save_changes_button = ttk.Button(self.right_panel, text="Save Changes",
command=self.save_content_changes, state='disabled', style="TonePresets.TButton")
self.save_changes_button.pack(pady=(5, 0), anchor=tk.E)
def create_bottom_frame(self):
bottom_frame = ttk.Frame(self.dialog, style="TonePresets.TFrame")
bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=10, padx=10)
save_button = ctk.CTkButton(
bottom_frame,
text="Save Selection and Exit",
corner_radius=20,
height=35,
fg_color="#058705",
hover_color="#046a38",
font=("Arial", 13, "bold"),
command=self.save_and_exit
)
save_button.pack(side=tk.BOTTOM, fill=tk.X)
def update_content(self):
"""Update the content area with the selected tone."""
if not self.current_selected_tone:
selected_indices = self.tone_list.curselection()
if selected_indices:
self.current_selected_tone = self.tone_list.get(selected_indices[0])
else:
return
# Enable content_text before any operations
self.content_text.config(state='normal')
self.content_text.delete('1.0', tk.END)
if self.current_selected_tone == "None":
self.content_text.insert('1.0', "No tone modifier will be applied to the speech.")
self.content_text.config(state='disabled')
self.save_changes_button.config(state='disabled')
self.delete_button.config(state='disabled')
self.edit_status_label.config(
text="(No tone modifier)",
foreground="gray"
)
else:
self.content_text.insert('1.0', self.parent.tone_presets[self.current_selected_tone])
self.content_text.config(state='normal')
self.save_changes_button.config(state='normal')
self.delete_button.config(state='normal')
self.edit_status_label.config(
text=f"{self.current_selected_tone}",
foreground="blue"
)
def save_content_changes(self):
"""Save changes to the current tone content."""
if not self.current_selected_tone or self.current_selected_tone == "None":
return
new_content = self.content_text.get("1.0", tk.END).strip()
if not new_content:
messagebox.showerror("Error", "Tone description cannot be empty")
return
self.parent.tone_presets[self.current_selected_tone] = new_content
self.parent.save_tone_presets(self.parent.tone_presets)
messagebox.showinfo("Success", "Tone changes saved successfully")
def create_new_tone(self):
"""Open dialog for creating a new tone preset."""
tone_dialog = tk.Toplevel(self.dialog)
tone_dialog.title("Create New Tone Preset")
tone_dialog.geometry("600x400")
tone_dialog.transient(self.dialog)
tone_dialog.grab_set()
# Set background color for a cleaner look
tone_dialog.configure(background="#f0f0f0")
# Center the new tone dialog
dialog_width = 600
dialog_height = 400
position_x = self.dialog.winfo_x() + (self.dialog.winfo_width() - dialog_width) // 2
position_y = self.dialog.winfo_y() + (self.dialog.winfo_height() - dialog_height) // 2
tone_dialog.geometry(f"{dialog_width}x{dialog_height}+{position_x}+{position_y}")
# Name entry
name_frame = ttk.Frame(tone_dialog, padding="10", style="TonePresets.TFrame")
name_frame.pack(fill=tk.X)
ttk.Label(name_frame, text="Tone Name:", style="TonePresets.TLabel").pack(side=tk.LEFT)
name_entry = ttk.Entry(name_frame)
name_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 0))
# Tone description with consistent styling
desc_frame = ttk.LabelFrame(tone_dialog, text="Tone Description", padding="10", style="TonePresets.TLabelframe")
desc_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# Ensure the title is centered
for child in desc_frame.winfo_children():
if child.winfo_class() == 'TLabel':
child.configure(anchor='center')
child.place(relx=0.5, rely=0, anchor='n')
# Create a frame for the text widget and scrollbar
text_frame = ttk.Frame(desc_frame, style="TonePresets.TFrame")
text_frame.pack(fill=tk.BOTH, expand=True)
# Create vertical scrollbar only
v_scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL)
v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Create the text widget with word wrap and vertical scrollbar
new_tone_text = tk.Text(text_frame, wrap=tk.WORD, height=15,
yscrollcommand=v_scrollbar.set,
bg="#ffffff", fg="#333333",
font=("Arial", 10),
relief="solid", borderwidth=1)
new_tone_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Configure the scrollbar
v_scrollbar.config(command=new_tone_text.yview)
def save_new_tone():
new_name = name_entry.get().strip()
if not new_name:
messagebox.showerror("Error", "Please enter a tone name")
return
if len(new_name) > 25:
messagebox.showerror("Error", "Tone name must be 25 characters or less")
return
new_content = new_tone_text.get("1.0", tk.END).strip()
if not new_content:
messagebox.showerror("Error", "Please enter a tone description")
return
# Save tone
self.parent.tone_presets[new_name] = new_content
self.parent.save_tone_presets(self.parent.tone_presets)
# Update listbox and select the new tone
self.tone_list.delete(0, tk.END)
all_tones = ["None"] + list(self.parent.tone_presets.keys())
for t in all_tones:
self.tone_list.insert(tk.END, t)
# Find and select the new tone
new_tone_index = all_tones.index(new_name)
self.tone_list.selection_clear(0, tk.END)
self.tone_list.selection_set(new_tone_index)
self.tone_list.see(new_tone_index)
# Set the current selected tone and update content
self.current_selected_tone = new_name
self.update_content()
tone_dialog.destroy()
# Buttons
button_frame = ttk.Frame(tone_dialog, style="TonePresets.TFrame")
button_frame.pack(fill=tk.X, pady=10, padx=10)
ttk.Button(button_frame, text="Save",
command=save_new_tone, style="TonePresets.TButton").pack(side=tk.RIGHT, padx=5)
ttk.Button(button_frame, text="Cancel",
command=tone_dialog.destroy, style="TonePresets.TButton").pack(side=tk.RIGHT)
def delete_current_tone(self):
"""Delete the currently selected tone."""
selected_indices = self.tone_list.curselection()
if not selected_indices:
return
tone_name = self.tone_list.get(selected_indices[0])
if tone_name == "None":
return
if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete '{tone_name}'?"):
if tone_name == self.parent.current_tone_name:
self.parent.current_tone_name = "None"
self.parent.save_current_tone_to_settings()
self.parent.update_tone_selection()
del self.parent.tone_presets[tone_name]
self.parent.save_tone_presets(self.parent.tone_presets)
# Update listbox
self.tone_list.delete(0, tk.END)
for t in ["None"] + list(self.parent.tone_presets.keys()):
self.tone_list.insert(tk.END, t)
# Set current_selected_tone to "None" before selecting it in the listbox
self.current_selected_tone = "None"
self.tone_list.selection_set(0)
self.update_content()
def save_and_exit(self):
"""Save the selected tone and close the dialog."""
if not self.current_selected_tone:
messagebox.showwarning("No Selection", "Please select a tone before saving")
return
tone_name = self.current_selected_tone
# First save any content changes if it's not "None"
if tone_name != "None":
new_content = self.content_text.get("1.0", tk.END).strip()
if new_content:
self.parent.tone_presets[tone_name] = new_content
self.parent.save_tone_presets(self.parent.tone_presets)
# Then save the selection
self.parent.current_tone_name = tone_name
self.parent.save_current_tone_to_settings()
self.parent.update_tone_selection()
self.dialog.destroy()
messagebox.showinfo("Success", f"Now using tone: {tone_name}")
@staticmethod
def load_tone_presets(app_instance):
"""Load tone presets from JSON file, creating from template if needed."""
# Create config directory if it doesn't exist
config_dir = Path("config")
config_dir.mkdir(parents=True, exist_ok=True)
tone_presets_file = config_dir / "tone_presets.json"
example_path = app_instance.resource_path("assets/tone_presets.example.json")
# If tone presets file doesn't exist, copy from example
if not tone_presets_file.exists():
try:
# Check if example file exists
if not os.path.exists(example_path):
# Create default tone presets
default_presets = {
"Cheerful": "Speak in a cheerful, upbeat tone with enthusiasm.",
"Angry": "Sound like you're angry and frustrated.",
"Bedtime Story": "Say it like you're reading a bedtime story to a child, soft and soothing.",
"News Anchor": "Speak in a professional news anchor tone, clear and formal.",
"Excited": "Sound extremely excited and enthusiastic."
}
# Create and save default presets
with open(tone_presets_file, "w", encoding="utf-8") as f:
json.dump({"tone_presets": default_presets}, f, indent=2)
else:
# Copy example presets to config directory
with open(example_path, "r", encoding="utf-8") as example_file:
with open(tone_presets_file, "w", encoding="utf-8") as config_file:
config_file.write(example_file.read())
except Exception as e:
print(f"Failed to create tone presets file: {e}")
return {}
# Load presets from file
try:
with open(tone_presets_file, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("tone_presets", {})
except Exception as e:
print(f"Error loading tone presets: {e}")
return {}
@staticmethod
def save_tone_presets(app_instance, tone_presets):
"""Save tone presets to JSON file."""
config_dir = Path("config")
config_dir.mkdir(parents=True, exist_ok=True)
tone_presets_file = config_dir / "tone_presets.json"
data = {"tone_presets": tone_presets}
try:
with open(tone_presets_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
return True
except Exception as e:
print(f"Error saving tone presets: {e}")
return False