diff --git a/utils/__pycache__/text_to_mic.cpython-312.pyc b/utils/__pycache__/text_to_mic.cpython-312.pyc index 4e95b71..aa7ca5f 100644 Binary files a/utils/__pycache__/text_to_mic.cpython-312.pyc and b/utils/__pycache__/text_to_mic.cpython-312.pyc differ diff --git a/utils/text_to_mic.py b/utils/text_to_mic.py index f8cf4e1..3f09f71 100644 --- a/utils/text_to_mic.py +++ b/utils/text_to_mic.py @@ -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("", 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("", 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("", lambda e, t=phrase["text"]: self.insert_text(t)) + label.bind("", 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() + +