add presets area, working but some refinement needed

This commit is contained in:
Andrew Ward
2024-10-28 16:02:28 +00:00
parent 6670c216d2
commit 3429e51c08
2 changed files with 268 additions and 3 deletions

View File

@@ -11,7 +11,7 @@ import sys
from pystray import Icon as icon, MenuItem as item, Menu as menu
from PIL import Image, ImageDraw
from tkinter import ttk, messagebox, simpledialog, Menu
from tkinter import ttk, messagebox, simpledialog, Menu, Frame, Canvas, Scrollbar
from openai import OpenAI
from dotenv import load_dotenv
from pathlib import Path
@@ -40,6 +40,10 @@ class TextToMic(tk.Tk):
self.style.configure('Recording.TButton', background='red', foreground='white')
self.style.configure("Green.TButton", background="green", foreground="white")
self.presets = self.load_presets()
self.current_category = "All"
self.presets_collapsed = True
# Ensure that the config directory exists
self.ensure_config_directory()
@@ -65,6 +69,9 @@ class TextToMic(tk.Tk):
self.create_menu()
self.initialize_gui()
self.setup_hotkeys()
def ensure_config_directory(self):
"""Ensure the config directory exists."""
@@ -116,6 +123,7 @@ class TextToMic(tk.Tk):
help_menu.add_command(label="Hotkey Instructions", command=self.show_hotkey_instructions)
def hotkey_settings(self):
settings = self.load_settings()
hotkey_window = tk.Toplevel(self)
@@ -287,10 +295,19 @@ class TextToMic(tk.Tk):
secondary_device_menu = ttk.OptionMenu(main_frame, self.device_index_2, "None", *self.available_devices.keys())
secondary_device_menu.grid(column=1, row=3, sticky=tk.W)
# Preset selection and save button
self.category_var = tk.StringVar(value="Select Category")
categories = [cat["category"] for cat in self.presets]
ttk.OptionMenu(main_frame, self.category_var, *categories).grid(column=1, row=4, sticky=tk.W)
save_button = ttk.Button(main_frame, text="Save", command=self.save_current_text_as_preset)
save_button.grid(column=2, row=4, sticky=tk.E)
# Specify the text to read
ttk.Label(main_frame, text="Text to Read:").grid(column=0, row=4, sticky=tk.W, pady=(10, 0))
self.text_input = tk.Text(main_frame, height=10, width=50)
self.text_input.grid(column=0, row=5, columnspan=2, pady=(0, 20)) # Padding added before submit button
# Button configuration
@@ -304,16 +321,69 @@ class TextToMic(tk.Tk):
self.create_presets_section()
#Credits
info_label = tk.Label(main_frame, text="Created by Scorchsoft.com App Development", fg="blue", cursor="hand2")
info_label.grid(column=0, row=7, columnspan=2, pady=(0, 0))
info_label.grid(column=0, row=9, columnspan=2, pady=(0, 0))
info_label.bind("<Button-1>", lambda e: self.open_scorchsoft())
def create_presets_section(self):
# Accordion frame to show/hide presets section
self.presets_frame = ttk.Frame(self)
self.presets_button = ttk.Button(self, text="▶ Presets", command=self.toggle_presets)
self.presets_button.grid(column=0, row=7, columnspan=2, sticky=tk.W)
self.presets_frame.grid(column=0, row=8, columnspan=2, sticky=(tk.W, tk.E))
# Tabs for categories with scrolling arrows
self.tab_frame = ttk.Frame(self.presets_frame)
self.tab_frame.pack(fill=tk.X)
# Thinner left arrow for tabs
self.left_arrow = ttk.Button(self.tab_frame, text="", command=self.scroll_left, width=2, style="Flat.TButton")
self.left_arrow.pack(side=tk.LEFT, padx=1) # Reduced padding
# Canvas for scrolling tabs horizontally, removing the horizontal scrollbar
self.tabs_canvas = Canvas(self.tab_frame, height=30)
self.tabs_canvas.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.tabs_frame_inner = ttk.Frame(self.tabs_canvas)
self.tabs_canvas.create_window((0, 0), window=self.tabs_frame_inner, anchor="nw")
# Thinner right arrow for tabs
self.right_arrow = ttk.Button(self.tab_frame, text="", command=self.scroll_right, width=2, style="Flat.TButton")
self.right_arrow.pack(side=tk.RIGHT, padx=1) # Reduced padding
# Presets display area with a fixed height and vertical scrollbar
self.presets_canvas = Canvas(self.presets_frame, height=200, width=self.presets_frame.winfo_width())
self.presets_scrollbar = Scrollbar(self.presets_frame, orient="vertical", command=self.presets_canvas.yview)
self.presets_canvas.configure(yscrollcommand=self.presets_scrollbar.set)
# Frame inside the canvas to hold presets
self.presets_scrollable_frame = ttk.Frame(self.presets_canvas)
self.presets_canvas.create_window((0, 0), window=self.presets_scrollable_frame, anchor="nw")
# Pack the canvas and scrollbar
self.presets_canvas.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.presets_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Configure the scroll region to update when the frame changes
self.presets_scrollable_frame.bind("<Configure>", lambda e: self.presets_canvas.configure(scrollregion=self.presets_canvas.bbox("all")))
# Populate tabs and presets
self.populate_tabs()
self.refresh_presets_display()
def scroll_left(self):
self.tabs_canvas.xview_scroll(-5, "units")
def scroll_right(self):
self.tabs_canvas.xview_scroll(5, "units")
@@ -996,3 +1066,198 @@ Please also make sure you read the Terms of use and licence statement before usi
return return_text
def populate_tabs(self):
# Clear current tabs
for widget in self.tabs_frame_inner.winfo_children():
widget.destroy()
# Add "All" and "Favourites" tabs along with dynamic categories
for category in ["All", "Favourites"] + [cat["category"] for cat in self.presets if cat["category"] not in ["All", "Favourites"]]:
btn = ttk.Button(self.tabs_frame_inner, text=category, command=lambda c=category: self.switch_category(c))
btn.pack(side=tk.LEFT, padx=2)
# Style selected category
if category == self.current_category:
btn.state(['pressed']) # Visual style for selected tab
else:
btn.state(['!pressed'])
def switch_category(self, category):
"""Switch displayed category."""
self.current_category = category
self.populate_tabs() # Refresh tabs to show selection
self.refresh_presets_display()
def refresh_presets_display(self):
"""Refresh displayed presets based on selected category."""
# Clear existing items in the scrollable frame
for widget in self.presets_scrollable_frame.winfo_children():
widget.destroy()
# Debounce - cancel any previous refresh call if pending
if hasattr(self, 'refresh_handle'):
self.after_cancel(self.refresh_handle)
self.refresh_handle = self.after(100, self._populate_presets)
def _populate_presets(self):
"""Populate presets into grid layout with responsive columns."""
# Filter presets based on current category
display_phrases = []
if self.current_category == "All":
for cat in self.presets:
display_phrases.extend(cat["phrases"])
elif self.current_category == "Favourites":
for cat in self.presets:
display_phrases.extend([p for p in cat["phrases"] if p["isFavourite"]])
else:
display_phrases = next((cat["phrases"] for cat in self.presets if cat["category"] == self.current_category), [])
# Update canvas width to calculate dynamic column width
self.presets_canvas.update_idletasks()
preset_width = max(self.presets_canvas.winfo_width() // 3, 100) # Minimum width of 100
preset_height = 100
# Configure columns to fill available space
for col in range(3):
self.presets_scrollable_frame.columnconfigure(col, weight=1)
# Populate filtered presets in grid layout
for i, phrase in enumerate(display_phrases):
frame = ttk.Frame(self.presets_scrollable_frame, width=preset_width, height=preset_height, relief="solid", borderwidth=1)
frame.grid(row=i // 3, column=i % 3, padx=2, pady=2, sticky="nsew")
frame.grid_propagate(False)
# Text label with truncation for long text
wrapped_text = self.wrap_text(phrase["text"], max_lines=3, max_chars_per_line=20)
label = ttk.Label(frame, text=wrapped_text, anchor="center", justify="center", width=20)
label.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
label.bind("<Button-1>", lambda e, t=phrase["text"]: self.insert_text(t))
label.bind("<Double-Button-1>", lambda e, t=phrase["text"]: self.play_preset(t))
# Bottom frame for icons
bottom_frame = ttk.Frame(frame)
bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=2)
# Favourite button
fav_icon = "❤️" if phrase["isFavourite"] else ""
fav_btn = ttk.Button(bottom_frame, text=fav_icon, command=lambda p=phrase: self.toggle_favourite(p), width=3, style="Flat.TButton")
fav_btn.pack(side=tk.RIGHT, padx=2)
# Delete button
del_btn = ttk.Button(bottom_frame, text="🗑️", command=lambda t=phrase["text"]: self.delete_preset(self.current_category, t), width=3, style="Flat.TButton")
del_btn.pack(side=tk.RIGHT, padx=2)
# Update scroll region after populating all items
self.presets_canvas.configure(scrollregion=self.presets_canvas.bbox("all"))
def wrap_text(self, text, max_lines=3, max_chars_per_line=20):
"""Wrap text to fit within a limited number of lines and characters."""
words = text.split()
wrapped_text = ""
line = ""
line_count = 0
for word in words:
if len(line + word) <= max_chars_per_line:
line += word + " "
else:
wrapped_text += line.strip() + "\n"
line = word + " "
line_count += 1
if line_count >= max_lines - 1: # Leave space for ellipsis
break
# Add final line and handle overflow with ellipsis
wrapped_text += line.strip()
if line_count >= max_lines - 1 and len(wrapped_text.splitlines()) >= max_lines:
wrapped_text = "\n".join(wrapped_text.splitlines()[:max_lines - 1]) + "\n..."
return wrapped_text
def insert_text(self, text):
"""Insert preset text into the text area."""
self.text_input.delete("1.0", tk.END)
self.text_input.insert("1.0", text)
def play_preset(self, text):
"""Insert text and play audio immediately."""
self.insert_text(text)
self.submit_text()
def toggle_favourite(self, phrase):
"""Toggle the favourite status of a preset."""
phrase["isFavourite"] = not phrase["isFavourite"]
self.save_presets()
self.refresh_presets_display()
def toggle_presets(self):
if self.presets_collapsed:
self.presets_frame.grid()
self.presets_button.config(text="▼ Presets")
else:
self.presets_frame.grid_remove()
self.presets_button.config(text="▶ Presets")
self.presets_collapsed = not self.presets_collapsed
def save_current_text_as_preset(self):
"""Save current text to the selected category as a preset."""
text = self.text_input.get("1.0", tk.END).strip()
category = self.category_var.get()
if text and category != "Select Category":
self.add_preset(category, text, is_favourite=False)
def load_presets(self):
"""Load presets from the JSON file."""
try:
with open("config/presets.json", "r") as f:
data = json.load(f)
return data.get("presets", [])
except (FileNotFoundError, json.JSONDecodeError):
# Create a default structure if the file doesn't exist or is corrupted
return []
def save_presets(self):
"""Save presets to the JSON file."""
data = {"presets": self.presets}
with open("config/presets.json", "w") as f:
json.dump(data, f, indent=2)
def add_preset(self, category, text, is_favourite=False):
"""Add a new preset and save it."""
for cat in self.presets:
if cat["category"] == category:
cat["phrases"].append({"text": text, "isFavourite": is_favourite})
break
else:
# Add a new category if not found
self.presets.append({"category": category, "phrases": [{"text": text, "isFavourite": is_favourite}]})
self.save_presets()
self.refresh_presets_display()
def delete_preset(self, category, text):
"""Delete a preset by category and text."""
for cat in self.presets:
if cat["category"] == category:
cat["phrases"] = [p for p in cat["phrases"] if p["text"] != text]
break
self.save_presets()
self.refresh_presets_display()