455 lines
20 KiB
Python
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 |