From 170199c604283658d083b0fe54ee5f0d5459c08f Mon Sep 17 00:00:00 2001 From: Andrew Ward Date: Fri, 21 Mar 2025 18:18:34 +0000 Subject: [PATCH] update presets performance and refactor class, plus change more icons --- assets/icons/chevron-down-black.png | Bin 0 -> 467 bytes assets/icons/chevron-left-black.png | Bin 0 -> 446 bytes assets/icons/chevron-right-black.png | Bin 0 -> 425 bytes assets/icons/chevron-up-black.png | Bin 0 -> 465 bytes assets/icons/delete-black.png | Bin 0 -> 528 bytes assets/icons/delete-white.png | Bin 0 -> 553 bytes assets/icons/heart-black.png | Bin 0 -> 942 bytes assets/icons/heart-fill-black.png | Bin 0 -> 726 bytes assets/icons/heart-fill-white.png | Bin 0 -> 772 bytes assets/icons/heart-white.png | Bin 0 -> 1003 bytes check_image_size.py | 9 + .../presets_manager.cpython-312.pyc | Bin 0 -> 29658 bytes utils/__pycache__/text_to_mic.cpython-312.pyc | Bin 79996 -> 58642 bytes utils/presets_manager.py | 545 ++++++++++++++++++ utils/text_to_mic.py | 391 +------------ 15 files changed, 570 insertions(+), 375 deletions(-) create mode 100644 assets/icons/chevron-down-black.png create mode 100644 assets/icons/chevron-left-black.png create mode 100644 assets/icons/chevron-right-black.png create mode 100644 assets/icons/chevron-up-black.png create mode 100644 assets/icons/delete-black.png create mode 100644 assets/icons/delete-white.png create mode 100644 assets/icons/heart-black.png create mode 100644 assets/icons/heart-fill-black.png create mode 100644 assets/icons/heart-fill-white.png create mode 100644 assets/icons/heart-white.png create mode 100644 check_image_size.py create mode 100644 utils/__pycache__/presets_manager.cpython-312.pyc create mode 100644 utils/presets_manager.py diff --git a/assets/icons/chevron-down-black.png b/assets/icons/chevron-down-black.png new file mode 100644 index 0000000000000000000000000000000000000000..e1c3f718398ebe407881297d60f55d49232a990a GIT binary patch literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%zdp%toLn`9l z&baH#?8wtH*KdKaSEJVg;Y*xrC-@!mx;b5R`rYXMdKLdT;ky-f{cnTyg8me1zKiE* z;MDMIP!6zRh-s2x>~kn&l4*Rz_(1dlM*&Nh@BhpJdHhl`r_yAya|z zPsfSd%nWM|9L%`>Lv&&|3yVlYT*Ug7OBr|*uQWI~Fi3E3ZYWr?ux$ZrL5PIVAs=T} zN9Keu38q7?dH;26NXtEL|Mc?pKTCpU}Z7?tij;H zvF_PK1_6OPXBRVOD74Jj&ai>`;nR;?dy4|LZU1}mu%P#c{ps^}@@XFnUH)!ngJ{EM z&l!#l<@>m!m>e7pjPtiLAIr-+cJRs`@rJu9GiHBhDG+PwuFPjxEpX?IJHrge0#kX` z2OLeie|j(|w+MW`$Y9lS``vuW&D)nxeZMxoB`k~MFE3-uBY%Z+EGijJ8Js>28vj@x n`8YJGwM;v}f>$EWjpj_CkF>nTpA!iQLVgs&-ER~SZsjHv8@qSdtzVdrgyUN^@59v}i`&Asm z4m@DG$|? z&JfW0ooxa4drk+*d)y6Xd)OIDD@+(_D^whW9|kg(DO1=+G*_>d-8}>Ch;^ zyFq;6ZhqybbCPbFy>9g6_p54H_01rpft686u2ja($U1u}1NV834KczC9!4^99eVry z<-54u&!;~^=mTP+PFBcxtVxk9pC6_ R-vLGlgQu&X%Q~loCICmZt5N^} literal 0 HcmV?d00001 diff --git a/assets/icons/chevron-right-black.png b/assets/icons/chevron-right-black.png new file mode 100644 index 0000000000000000000000000000000000000000..d2683d90d0710ee6cf0f7ed4f06e1a3e3bc7bc1c GIT binary patch literal 425 zcmV;a0apHrP)Px$V@X6oRCr$P+0hY#Fc1XLr4#K$JCVvPMLW?_Bp)~qqY@oiCTmcS0c4Zt42-L2x-0CV^`w;ci# zU>V*Lfd%j>yd44qU@g2A0t29Icm<*63f~hL03~=c2rPhR5SRc>Ag}@K5flK{2r2+u z1SNnaf*QaMAp*b(Aqv0-Arim>AsQeY!2#d`;864C1mHNey;ioZY8DL;8omfc0)&T; z1h5Dn1z;0C0>CP~8o(~R62LON3cxnJ0>C;v8^At16QGIkEP(TL3Vf=15GOp_SN8jC z#&{Gir$VIy)=q~uf3f*BJgWlAeb*2y!ZQKBA=rdx1H4193aPx$i%CR5RCr$P*x^mXKnw-o*GXU$m;^?llax(hG_cf2QI$ry*!OJ7FB1Py?f0GU zF3JVXpG))n5P&A2xq!An^8wHys4bu^&|E-Up!opk7SI;Z7HBS@Ezo=bbPHr}flmN0 z0Ipe+m+s9sryyhiY=uulU<2%hPeEV;90^ZHU;&JTry?)_&V;8S z%m(m;ry$G(hzLI&VHQAS_^Ajp07k<90j;9YhGzn-$2|mBW2Vz4R z4xAGV;CeHMLCA6G0XNNp)U@qO*7FL;0bNNVL~Ni;mTf3h>z^v}C378m-C`ZDOWy~5cY#+~-FOZV`JvVyt$1kFc3T%Je9W(jW*wsfV;$L%I4Z^&edQ75D!`I^&<$zo*=le;}2;@6@+n?d%J# zo8H{8yMf_FOM`@gmuy4!dQ+(ulY03JC5P{()I5xzW!j`)E3AKLUG$u<3=FFe*)?X* zEY9k_uDBz4O`a_SgQ}vRrRi$*trs>_eto!{o#6*x6T7h2`&O}I&b-cyty7oFxlXhB z!j$l@?CjzUtxuX6uMD54vhQU~Xy%mHXE-!np@J#N;R934ZgFNL&VNQ7?YoXGdqoZa PqnN?d)z4*}Q$iB}`xo8l literal 0 HcmV?d00001 diff --git a/assets/icons/delete-white.png b/assets/icons/delete-white.png new file mode 100644 index 0000000000000000000000000000000000000000..b9431f0dc492f9c8471620e7ae35a3c356188d39 GIT binary patch literal 553 zcmV+^0@nSBP)Px$<4Ht8RCr$PTI*TEFbtG7fk~i~Kqmnvp-e*Aq?-stT|UR9*cYdXO)fvNgst=G z;Yuni|8(W{3jrVlR*nE&VC4j05Tpyt;R0rMBO>?UUVBwNfI6`FEDSKSM-h1iPq$Uo z9jF6~695=85iv8A&Uat?JcE0={0FFWK-~srKrsFtJlI^~8&%ySn-Ks6Ete$&j{d)T z7kG%6<0&rvu9dzVe6K0uFW&3r>OnBI)XXgTn-?$})U`_F&CHoM$Uv$J$FX>?D&-Ip z2ml!{M1U@k#(?}uW2wx@!UFHfG%)E zfH8nEpwqWNF39E983Y*w83dUHmnEFjaBifu-mc9t~bSefZqx_0#|e9Sa+wY+nRqi1prP-XF)o;Qgf~n-Rj$)gaM(D zapRo{zs(Dk7=y}q0p;E0>RIAy2E->HITVc=?>7$(L;khFqyV6nv>;CaTDXNJ&p-w& riGB-vlL0NPx&Xh}ptRCr$PT8m8_F%UJKB$bd<0;vS3B%uN82E4P~~kG~uQ916H{296ZCaseDgFe3%-ODVUdlpkj`eJiE>JAd%6r|!E) zbQK$z_0wWMpXPmS=JH^Fj*Sw4U>};GEfd4hUj}D^eRmp%;H_rG{(=`F;KdnuKk1)F z75%l}&cOToA4@4v1yo`(4*`%M9IR3|O~4F-gFhQbf&F&`u({Wz1`h$&m9Ok0D9I`b z7zb>NA-Gx#EZC|NtOQ8G!zrp5K`FI3Mpc7!c{>G-zpYg~sScUa!9oBg0T^YFF$mBM z1r0cwDq5TjZ1{~`h&7D>E;sue)(F7$Ds_znR}P;imaxA|MNuPwTYc;5P{c@;9>D`U zVjKyg3N!*xnfnz}Wo7W5!W{{|PDw48h$|65$sf{(q(-e@tt?#lMUl&>7mWa_5MJ_DnA!<~bgB+qstULVH*x3E6u`BXk*7hUio?UG;-}Uu3jtJP0WU8p z6~a%g76K4#c!KbH+Hd62)U;(=3E)y;>v|v^*qEMLJp^zn*s2~#C&}iP-9vzs3ad$w zj_6(Ox|aZuz#VBt&hqS;{{Kksl-iW{EmVX6sUEB#feSu#q!uMWI)$A{LTd2ZiI67G z)g*AitIduSNK^q8a}^0wf*)0Yg@n=FL8|scD_@--RlrOVrU##;fEgr6O-{J5c)`7Z;qL7{j;&BY^+$Tkg!6gHiBnb|U?JW^g6omiGC4P9B?0gOBp~0V z*JR9x86$zu>H{o3q1vM^P$Vo3eq{~liRno|cL&*9-Z-<6Y#y^Z1zN}27bG_ceoFdpqL;wH) literal 0 HcmV?d00001 diff --git a/assets/icons/heart-fill-black.png b/assets/icons/heart-fill-black.png new file mode 100644 index 0000000000000000000000000000000000000000..b7e97d9cecd6b9a8220752e281fcde317156392c GIT binary patch literal 726 zcmV;{0xA88P)Px%kV!;ARCr$PTG3HNAq>oQqLrwXsFkRdsFi3ZT8g*MF~e{VNJxMO4`%Me69T(z z!t(B&=-};iaQw6gSPD2e11kj%E`U`8EmGi#i0+8!{-UNEBKp02@YP%Jw?uR@3%LXB=&#bQUU<>#RJWm7z=%7EEcgJ&;0;*}8aB?KN$`r)Y}{NK9} zcsTx*h@c9ngw-SjK!RAHlR9gHNGB}t!CNR||7ZewV?}C^5TM=p5T7Ff;1S!hg#<@q za|y5pZwWRQi0#NJ_Y{Z6j;}j}ZfMss>BJeg<6mtp~dG{URNfqP>KxKvsMs7G2 zaN=yX6ah2H-+_*%L5cu4F`WA|G&oQgu&b6LV21p6fDcaqb{`~&6rlCxdrF??|bTmA(TeAz&mGWTjt1z$mE@z4?_m z!L2cS_6DrHFRcN4uLnuPQ|bg`6S)_yI%hV$u8h~D!`HC-Yn;m zX5Ozy4KN3x3AhN*us6flrGOcBQG;PAK*Qb)W0wME*hLM7Ke|YDK@oRXkpKVy07*qo IM6N<$g8JVxng9R* literal 0 HcmV?d00001 diff --git a/assets/icons/heart-fill-white.png b/assets/icons/heart-fill-white.png new file mode 100644 index 0000000000000000000000000000000000000000..4246d670ffe5cbe83fa49cc91ed6b82ecf95e7d1 GIT binary patch literal 772 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 B^n(BZ literal 0 HcmV?d00001 diff --git a/utils/__pycache__/text_to_mic.cpython-312.pyc b/utils/__pycache__/text_to_mic.cpython-312.pyc index 0f95fef40dc182a7aa9345ff96d06ab861203430..306f4a5e7bc873577669bd46b691bd6c422206e7 100644 GIT binary patch delta 9964 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 delta 25872 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 diff --git a/utils/presets_manager.py b/utils/presets_manager.py new file mode 100644 index 0000000..daa3e0a --- /dev/null +++ b/utils/presets_manager.py @@ -0,0 +1,545 @@ +import tkinter as tk +from tkinter import ttk, messagebox, Frame, Canvas, Scrollbar +import json +from pathlib import Path +from PIL import Image, ImageTk +import threading +import time + +class PresetsManager: + """ + A class to manage the presets functionality in the Text to Mic application. + This handles the display, navigation, and interaction with text presets. + """ + + def __init__(self, parent): + """ + Initialize the PresetsManager. + + Args: + parent: The parent application instance (TextToMic) + """ + self.parent = parent + self.presets = self.load_presets() + self.current_category = "All" + self.presets_collapsed = True + self.icon_cache = {} + + # Add save debounce variables + self.save_pending = False + self.save_timer = None + self.preset_cards = {} # Dictionary to store references to preset cards for efficient updates + + # Load navigation icons + self.chevron_right = self.get_icon("assets/icons/chevron-right-black.png", 16) + self.chevron_down = self.get_icon("assets/icons/chevron-down-black.png", 16) + self.chevron_left = self.get_icon("assets/icons/chevron-left-black.png", 16) + self.chevron_right_small = self.get_icon("assets/icons/chevron-right-black.png", 16) + + # Initialize the UI components + self.create_presets_section() + + def create_presets_section(self): + """Create the presets section UI with accordion behavior.""" + # Accordion frame to show/hide presets section + self.presets_frame = ttk.Frame(self.parent) + + # Create toggle button with icon instead of text + toggle_frame = ttk.Frame(self.parent) + toggle_frame.grid(column=0, row=6, columnspan=2, sticky=tk.W) + + # Use an icon for the toggle button + self.presets_button = ttk.Button( + toggle_frame, + image=self.chevron_right, + compound=tk.LEFT, + text=" Presets", + command=self.toggle_presets, + style="Flat.TButton" + ) + self.presets_button.pack(side=tk.LEFT, padx=0, pady=2) + + self.presets_frame.grid(column=0, row=7, 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) + + # Style for flat buttons + bg_color = self.parent.style.lookup('TFrame', 'background') + accent_color = "#e0e0e4" # Slightly darker grey for accents + + self.parent.style.configure("Flat.TButton", + borderwidth=0, + highlightthickness=0, + font=("Arial", 12), + anchor="center", + background=bg_color) + + # Create compact styles for arrow buttons + self.parent.style.configure("Arrow.TButton", + borderwidth=0, + highlightthickness=0, + padding=2, + background=bg_color) + + # Create common styles for preset cards + self.setup_preset_card_styles(bg_color, accent_color) + + # Left arrow with icon + self.left_arrow = ttk.Button( + self.tab_frame, + image=self.chevron_left, + command=self.scroll_left, + style="Arrow.TButton" + ) + self.left_arrow.pack(side=tk.LEFT, padx=1) + + # Canvas for scrolling tabs horizontally, removing the horizontal scrollbar + self.tabs_canvas = Canvas(self.tab_frame, height=30, bg=bg_color, highlightthickness=0) + 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") + + # Right arrow with icon + self.right_arrow = ttk.Button( + self.tab_frame, + image=self.chevron_right_small, + command=self.scroll_right, + style="Arrow.TButton" + ) + self.right_arrow.pack(side=tk.RIGHT, padx=1) + + # Presets display area with a fixed height and vertical scrollbar + self.presets_canvas = Canvas(self.presets_frame, height=250, width=self.presets_frame.winfo_width(), + bg=bg_color, highlightthickness=0) + 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() # Refresh tabs to show selection + self.refresh_presets_display() + # Initially toggle to show/hide based on the default state + self.toggle_presets() + self.toggle_presets() + self.enable_mouse_wheel_scrolling() + + def setup_preset_card_styles(self, bg_color, accent_color): + """Set up common styles for preset cards once to avoid recreating them repeatedly.""" + preset_bg_color = "#f0f4f8" # Light blue-gray that contrasts with the main background + text_color = self.parent.style.lookup('TLabel', 'foreground') + + # Common styles for cards, buttons, and labels + self.parent.style.configure('PresetCard.TFrame', + background=preset_bg_color, + borderwidth=0, + relief="flat") + + self.parent.style.configure('PresetBottom.TFrame', + background=preset_bg_color) + + self.parent.style.configure('PresetLabel.TLabel', + background=preset_bg_color, + foreground=text_color) + + self.parent.style.configure('PresetButton.TButton', + borderwidth=0, + highlightthickness=0, + background=preset_bg_color) + + # Cache common icons + self.heart_icon = self.get_icon("assets/icons/heart-black.png", 24) + self.heart_filled_icon = self.get_icon("assets/icons/heart-fill-black.png", 24) + self.delete_icon = self.get_icon("assets/icons/delete-black.png", 24) + + def scroll_left(self): + """Scroll the tabs canvas to the left.""" + self.tabs_canvas.xview_scroll(-5, "units") + + def scroll_right(self): + """Scroll the tabs canvas to the right.""" + self.tabs_canvas.xview_scroll(5, "units") + + def enable_mouse_wheel_scrolling(self): + """Enable conditional mouse wheel scrolling for the presets canvas and category tabs canvas.""" + + def on_vertical_scroll(event): + # Scroll the presets_canvas vertically + if event.num == 4: # macOS scroll up + self.presets_canvas.yview_scroll(-1, "units") + elif event.num == 5: # macOS scroll down + self.presets_canvas.yview_scroll(1, "units") + else: # Windows and Linux + self.presets_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + + def on_horizontal_scroll(event): + # Scroll the tabs_canvas horizontally + if event.num == 4: # macOS scroll left + self.tabs_canvas.xview_scroll(-1, "units") + elif event.num == 5: # macOS scroll right + self.tabs_canvas.xview_scroll(1, "units") + else: # Windows and Linux + self.tabs_canvas.xview_scroll(int(-1 * (event.delta / 120)), "units") + + # Bind scroll events when mouse enters or leaves the presets canvas area + self.presets_canvas.bind("", lambda e: self.presets_canvas.bind_all("", on_vertical_scroll)) + self.presets_canvas.bind("", lambda e: self.presets_canvas.unbind_all("")) + + # Bind scroll events when mouse enters or leaves the tabs canvas area + self.tabs_canvas.bind("", lambda e: self.tabs_canvas.bind_all("", on_horizontal_scroll)) + self.tabs_canvas.bind("", lambda e: self.tabs_canvas.unbind_all("")) + + def populate_tabs(self): + """Populate the tabs for different preset categories.""" + # 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() + + # Clear the preset cards tracking dictionary + self.preset_cards = {} + + # Debounce - cancel any previous refresh call if pending + if hasattr(self, 'refresh_handle'): + self.parent.after_cancel(self.refresh_handle) + self.refresh_handle = self.parent.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, 150) # Minimum width of 150 + 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): + # Create a unique identifier for the card + card_id = f"{phrase['text']}" + + # Create a frame with no border for cleaner look + frame = ttk.Frame(self.presets_scrollable_frame, width=preset_width, height=preset_height) + frame.grid(row=i // 4, column=i % 4, padx=3, pady=3, sticky="nsew") + frame.grid_propagate(False) + + # Create inner frame with distinct background and no border - use common style + inner_frame = ttk.Frame(frame, style='PresetCard.TFrame') + inner_frame.pack(fill=tk.BOTH, expand=True, padx=1, pady=1) + + self.presets_scrollable_frame.grid_columnconfigure(i % 4, weight=1) # Make columns expandable + self.presets_scrollable_frame.grid_rowconfigure(i // 4, weight=1) # Make rows expandable + + # Text label with truncation for long text - use common style + wrapped_text = self.wrap_text(phrase["text"], max_lines=3, max_chars_per_line=20) + label = ttk.Label(inner_frame, text=wrapped_text, anchor="center", justify="center", + width=20, style='PresetLabel.TLabel') + 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 - use common style + bottom_frame = ttk.Frame(inner_frame, style='PresetBottom.TFrame') + bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=2) + + # Choose correct heart icon based on favorite status + fav_img = self.heart_filled_icon if phrase["isFavourite"] else self.heart_icon + + # Favourite button with image - use common style and command that includes card_id + fav_btn = ttk.Button(bottom_frame, image=fav_img, + command=lambda p=phrase, c_id=card_id: self.toggle_favourite(p, c_id), + style='PresetButton.TButton') + fav_btn.pack(side=tk.RIGHT, padx=2) + + # Delete button with image - use common style + del_btn = ttk.Button(bottom_frame, image=self.delete_icon, + command=lambda t=phrase["text"]: self.delete_preset(self.current_category, t), + style='PresetButton.TButton') + del_btn.pack(side=tk.RIGHT, padx=2) + + # Store references to the card components for efficient updates + self.preset_cards[card_id] = { + 'frame': frame, + 'inner_frame': inner_frame, + 'label': label, + 'bottom_frame': bottom_frame, + 'fav_btn': fav_btn, + 'del_btn': del_btn, + 'phrase': phrase + } + + # Update scroll region after populating all items + self.presets_canvas.configure(scrollregion=self.presets_canvas.bbox("all")) + + def update_preset_card(self, card_id): + """Update a single preset card without refreshing the entire display.""" + if card_id not in self.preset_cards: + return + + card = self.preset_cards[card_id] + phrase = card['phrase'] + + # Update the favorite button image based on current state + fav_img = self.heart_filled_icon if phrase["isFavourite"] else self.heart_icon + card['fav_btn'].configure(image=fav_img) + + def get_icon(self, icon_path, size=24): + """ + Load and resize an icon, caching the result for later use. + + Args: + icon_path: Path to the icon file + size: Size to resize the icon to (square) + + Returns: + A PhotoImage object with the icon + """ + # Create a cache key based on path and size + cache_key = f"{icon_path}_{size}" + + # Check if icon is already in cache + if cache_key in self.icon_cache: + return self.icon_cache[cache_key] + + # Load and resize the icon + try: + # Use resource_path to get the correct path + full_path = self.parent.resource_path(icon_path) + + # Use PIL to open and resize the image + img = Image.open(full_path) + img = img.resize((size, size), Image.LANCZOS) + + # Convert to PhotoImage + photo_img = ImageTk.PhotoImage(img) + + # Store in cache + self.icon_cache[cache_key] = photo_img + + return photo_img + except Exception as e: + print(f"Error loading icon {icon_path}: {e}") + # Return a default empty image + return tk.PhotoImage(width=size, height=size) + + 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. + + Args: + text: The text to wrap + max_lines: Maximum number of lines to display + max_chars_per_line: Maximum characters per line + + Returns: + The wrapped text with ellipsis if truncated + """ + 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.parent.text_input.delete("1.0", tk.END) + self.parent.text_input.insert("1.0", text) + + def play_preset(self, text): + """Insert text and play audio immediately.""" + self.insert_text(text) + self.parent.submit_text() + + def toggle_favourite(self, phrase, card_id=None): + """Toggle the favourite status of a preset.""" + phrase["isFavourite"] = not phrase["isFavourite"] + + # If we have the card_id, update just that card - much faster than refreshing everything + if card_id and card_id in self.preset_cards: + self.update_preset_card(card_id) + else: + # Otherwise, refresh the entire display (fallback, should rarely happen) + self.refresh_presets_display() + + # Debounce the save operation to avoid writing to disk on every toggle + self.debounced_save() + + def debounced_save(self): + """Save presets with debouncing to avoid frequent disk writes.""" + # Cancel any pending save + if self.save_timer: + self.parent.after_cancel(self.save_timer) + + # Schedule a new save operation + self.save_timer = self.parent.after(1000, self._perform_save) + + def _perform_save(self): + """Actually perform the save operation after debounce.""" + # Use threading to avoid blocking the UI + threading.Thread(target=self.save_presets, daemon=True).start() + self.save_timer = None + + def toggle_presets(self): + """Toggle the visibility of the presets panel.""" + if self.presets_collapsed: + self.presets_frame.grid() + # Update button icon to down chevron while preserving text + self.presets_button.configure(image=self.chevron_down, text=" Presets") + self.parent.geometry(self.parent.default_geometry) + else: + self.presets_frame.grid_remove() + # Update button icon to right chevron while preserving text + self.presets_button.configure(image=self.chevron_right, text=" Presets") + self.parent.geometry(self.parent.untoggled_geometry) + 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.parent.text_input.get("1.0", tk.END).strip() + category = self.parent.category_var.get() + if text and category != "Select Category": + self.add_preset(category, text, is_favourite=False) + # Show success message with category information + messagebox.showinfo("Save Successful", f"The text has been successfully saved to the category: '{category}'.") + else: + messagebox.showinfo("Error", "Please enter text and select a category before saving.") + + def load_presets(self): + """ + Load presets from the JSON file, copying from example if necessary. + + Returns: + List of preset categories with their phrases + """ + presets_path = Path("config/presets.json") + example_path = self.parent.resource_path("assets/presets.example.json") # Path for the example file + + # Check if presets.json exists, and if not, copy presets.example.json to config + if not presets_path.exists(): + try: + # Ensure config directory exists + presets_path.parent.mkdir(parents=True, exist_ok=True) + + # Copy example presets to config directory + with open(example_path, "r", encoding="utf-8") as example_file: + with open(presets_path, "w", encoding="utf-8") as config_file: + config_file.write(example_file.read()) + except Exception as e: + messagebox.showerror("Error", f"Failed to copy example presets: {e}") + return [] # Return empty if unable to load or copy presets + + # Load presets.json as usual + try: + with open(presets_path, "r", encoding="utf-8") as f: + data = json.load(f) + return data.get("presets", []) + except (FileNotFoundError, json.JSONDecodeError) as e: + messagebox.showerror("Error", f"Error loading presets: {e}") + return [] # Default to empty if load fails + + 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. + + Args: + category: The category to add the preset to + text: The text of the preset + is_favourite: Whether the preset is a favorite + """ + 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}]}) + + # Save and refresh + self.debounced_save() + self.refresh_presets_display() + + def delete_preset(self, category, text): + """ + Delete a preset by category and text. + + Args: + category: The category of the preset + text: The text of the preset to delete + """ + for cat in self.presets: + if cat["category"] == category: + cat["phrases"] = [p for p in cat["phrases"] if p["text"] != text] + break + + # Save and refresh + self.debounced_save() + self.refresh_presets_display() \ No newline at end of file diff --git a/utils/text_to_mic.py b/utils/text_to_mic.py index 5d7d1d7..5b47954 100644 --- a/utils/text_to_mic.py +++ b/utils/text_to_mic.py @@ -9,7 +9,7 @@ import json import sys from pystray import Icon as icon, MenuItem as item, Menu as menu -from PIL import Image, ImageDraw +from PIL import Image, ImageDraw, ImageTk from tkinter import ttk, messagebox, simpledialog, Menu, Frame, Canvas, Scrollbar import customtkinter as ctk from openai import OpenAI @@ -22,6 +22,7 @@ from utils.api_key_manager import APIKeyManager from utils.hotkey_manager import HotkeyManager from utils.resource_utils import ResourceUtils from utils.tone_presets_manager import TonePresetsManager +from utils.presets_manager import PresetsManager # Modify the load environment variables to load from config/.env def load_env_file(): @@ -41,6 +42,9 @@ class TextToMic(tk.Tk): self.available_models = ["gpt-4o-mini", "gpt-4o", "gpt-4-turbo"] self.default_model = "gpt-4o-mini" + # Cache for icons - will store loaded and resized icon images + self.icon_cache = {} + self.style = ttk.Style(self) if self.tk.call('tk', 'windowingsystem') == 'aqua': self.style.theme_use('aqua') @@ -71,11 +75,6 @@ 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() load_env_file() @@ -100,7 +99,14 @@ class TextToMic(tk.Tk): # Load tone presets self.tone_presets = TonePresetsManager.load_tone_presets(self) self.current_tone_name = self.load_current_tone_from_settings() + + # Create the category variable for the dropdown + self.category_var = tk.StringVar(value="Select Category") + # Create the presets manager before initializing the GUI + self.presets_manager = PresetsManager(self) + + # Create menu and initialize GUI after presets manager is created self.create_menu() self.initialize_gui() @@ -301,8 +307,7 @@ class TextToMic(tk.Tk): save_frame.grid(column=1, row=0, sticky=tk.E) # Preset Category dropdown - self.category_var = tk.StringVar(value="Select Category") - categories = [cat["category"] for cat in self.presets] + categories = [cat["category"] for cat in self.presets_manager.presets] category_menu = ttk.OptionMenu(save_frame, self.category_var, *categories) category_menu.grid(column=0, row=0, sticky=tk.E, padx=(0, 5)) category_menu.config(style='Compact.TMenubutton') @@ -365,8 +370,6 @@ class TextToMic(tk.Tk): ) self.submit_button.grid(row=0, column=1, sticky="ew", padx=(10, 0)) - self.create_presets_section() - #Credits # Banner image that links to Scorchsoft banner_path = self.resource_path("assets/ss-banner-550.png") @@ -384,110 +387,12 @@ class TextToMic(tk.Tk): info_label.grid(column=0, row=7, columnspan=2, pady=(10, 10)) 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=6, columnspan=2, sticky=tk.W) - self.presets_frame.grid(column=0, row=7, 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) - - # Style for flat buttons - bg_color = self.style.lookup('TFrame', 'background') - accent_color = "#e0e0e4" # Slightly darker grey for accents - - self.style.configure("Flat.TButton", - borderwidth=0, - highlightthickness=0, - font=("Arial", 12), - anchor="center", - background=bg_color) - - # 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, bg=bg_color, highlightthickness=0) - 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=250, width=self.presets_frame.winfo_width(), bg=bg_color, highlightthickness=0) - 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() # Refresh tabs to show selection - self.refresh_presets_display() - self.toggle_presets() - self.toggle_presets() - self.enable_mouse_wheel_scrolling() - - - - def scroll_left(self): - self.tabs_canvas.xview_scroll(-5, "units") - - def scroll_right(self): - self.tabs_canvas.xview_scroll(5, "units") - - - def enable_mouse_wheel_scrolling(self): - """Enable conditional mouse wheel scrolling for the presets canvas and category tabs canvas.""" - - def on_vertical_scroll(event): - # Scroll the presets_canvas vertically - if event.num == 4: # macOS scroll up - self.presets_canvas.yview_scroll(-1, "units") - elif event.num == 5: # macOS scroll down - self.presets_canvas.yview_scroll(1, "units") - else: # Windows and Linux - self.presets_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") - - def on_horizontal_scroll(event): - # Scroll the tabs_canvas horizontally - if event.num == 4: # macOS scroll left - self.tabs_canvas.xview_scroll(-1, "units") - elif event.num == 5: # macOS scroll right - self.tabs_canvas.xview_scroll(1, "units") - else: # Windows and Linux - self.tabs_canvas.xview_scroll(int(-1 * (event.delta / 120)), "units") - - # Bind scroll events when mouse enters or leaves the presets canvas area - self.presets_canvas.bind("", lambda e: self.presets_canvas.bind_all("", on_vertical_scroll)) - self.presets_canvas.bind("", lambda e: self.presets_canvas.unbind_all("")) - - # Bind scroll events when mouse enters or leaves the tabs canvas area - self.tabs_canvas.bind("", lambda e: self.tabs_canvas.bind_all("", on_horizontal_scroll)) - self.tabs_canvas.bind("", lambda e: self.tabs_canvas.unbind_all("")) - - - def open_scorchsoft(self, event=None): webbrowser.open('https://www.scorchsoft.com') - - + def save_current_text_as_preset(self): + """Forward the save request to the presets manager.""" + self.presets_manager.save_current_text_as_preset() def show_instructions(self): instruction_window = tk.Toplevel(self) @@ -534,7 +439,6 @@ Please also make sure you read the Terms of use and licence statement before usi # Add a button to close the window ttk.Button(instruction_window, text="Close", command=instruction_window.destroy).pack(pady=(10, 0)) - def show_terms_of_use(self): # Get the path to the LICENSE.md file using the resource_path method license_path = self.resource_path("LICENSE.md") @@ -578,13 +482,6 @@ Please also make sure you read the Terms of use and licence statement before usi # Add a button to close the window ttk.Button(instruction_window, text="Close", command=instruction_window.destroy).pack(pady=(10, 0)) - - - - - - - def get_app_support_path_mac(self): home = Path.home() app_support_path = home / 'Library' / 'Application Support' / 'scorchsoft-text-to-mic' @@ -966,262 +863,6 @@ 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, 150) # Minimum width of 100 - preset_height = 100 - - # Get our custom style colors - bg_color = self.style.lookup('TFrame', 'background') - text_color = self.style.lookup('TLabel', 'foreground') - - # Use a light gray with subtle blue tint for preset buttons - distinct from white but still clean - preset_bg_color = "#f0f4f8" # Light blue-gray that contrasts with the main background - - # 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): - # Create a frame with no border for cleaner look - frame = ttk.Frame(self.presets_scrollable_frame, width=preset_width, height=preset_height) - frame.grid(row=i // 4, column=i % 4, padx=3, pady=3, sticky="nsew") - frame.grid_propagate(False) - - # Create a unique style name for each card to avoid affecting other widgets - card_style_name = f'Card{i}.TFrame' - self.style.configure(card_style_name, - background=preset_bg_color, - borderwidth=0, - relief="flat") - - # Create inner frame with distinct background and no border - inner_frame = ttk.Frame(frame, style=card_style_name) - inner_frame.pack(fill=tk.BOTH, expand=True, padx=1, pady=1) - - self.presets_scrollable_frame.grid_columnconfigure(i % 4, weight=1) # Make columns expandable - self.presets_scrollable_frame.grid_rowconfigure(i // 4, weight=1) # Make rows expandable - - # Create a unique style for each label - label_style_name = f'CardLabel{i}.TLabel' - self.style.configure(label_style_name, - background=preset_bg_color, - foreground=text_color) - - # 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(inner_frame, text=wrapped_text, anchor="center", justify="center", - width=20, style=label_style_name) - 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)) - - # Create a unique style for each bottom frame - bottom_frame_style = f'BottomFrame{i}.TFrame' - self.style.configure(bottom_frame_style, background=preset_bg_color) - - # Bottom frame for icons - bottom_frame = ttk.Frame(inner_frame, style=bottom_frame_style) - bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=2) - - # Create a unique style for each button - button_style_name = f'FlatButton{i}.TButton' - self.style.configure(button_style_name, - borderwidth=0, - highlightthickness=0, - font=("Arial", 12), - anchor="center", - background=preset_bg_color) - - # 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=2, style=button_style_name) - 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=2, style=button_style_name) - 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") - self.geometry(self.default_geometry) - else: - self.presets_frame.grid_remove() - self.presets_button.config(text="▶ Presets") - self.geometry(self.untoggled_geometry) - 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) - # Show success message with category information - messagebox.showinfo("Save Successful", f"The text has been successfully saved to the category: '{category}'.") - else: - messagebox.showinfo("Error", "Please enter text and select a category before saving.") - - - def load_presets(self): - """Load presets from the JSON file, copying from example if necessary.""" - presets_path = Path("config/presets.json") - example_path = self.resource_path("assets/presets.example.json") # Path for the example file - - # Check if presets.json exists, and if not, copy presets.example.json to config - if not presets_path.exists(): - try: - # Ensure config directory exists - presets_path.parent.mkdir(parents=True, exist_ok=True) - - # Copy example presets to config directory - with open(example_path, "r", encoding="utf-8") as example_file: - with open(presets_path, "w", encoding="utf-8") as config_file: - config_file.write(example_file.read()) - except Exception as e: - messagebox.showerror("Error", f"Failed to copy example presets: {e}") - return [] # Return empty if unable to load or copy presets - - # Load presets.json as usual - try: - with open(presets_path, "r", encoding="utf-8") as f: - data = json.load(f) - return data.get("presets", []) - except (FileNotFoundError, json.JSONDecodeError) as e: - messagebox.showerror("Error", f"Error loading presets: {e}") - return [] # Default to empty if load fails - - 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() - def get_device_info(self, device_index): p = pyaudio.PyAudio() try: