add presets area, working but some refinement needed
This commit is contained in:
Binary file not shown.
@@ -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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user