diff --git a/utils/__pycache__/hotkey_manager.cpython-312.pyc b/utils/__pycache__/hotkey_manager.cpython-312.pyc index 9af06f5..2fe0ce5 100644 Binary files a/utils/__pycache__/hotkey_manager.cpython-312.pyc and b/utils/__pycache__/hotkey_manager.cpython-312.pyc differ diff --git a/utils/hotkey_manager.py b/utils/hotkey_manager.py index 23abbd3..357bdc5 100644 --- a/utils/hotkey_manager.py +++ b/utils/hotkey_manager.py @@ -1,30 +1,160 @@ import tkinter as tk from tkinter import ttk, messagebox import keyboard +import platform class HotkeyManager: """Class to handle hotkey operations.""" def __init__(self, app): self.app = app + self.hotkeys = [] # Track registered hotkeys + self.is_mac = platform.system() == 'Darwin' self.setup_hotkeys() def setup_hotkeys(self): """Set up hotkeys based on settings.""" - try: - # Attempt to clear existing hotkeys - keyboard.unhook_all() # This should clear all hotkeys in some versions of the library. - except AttributeError: - pass # Ignore if the method isn't supported - + # First, clear all existing hotkeys + self.clear_hotkeys() + settings = self.app.load_settings() - def parse_hotkey(combo): - return '+'.join(filter(None, combo)) - - keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["record_start_stop"]), lambda: self.hotkey_record_trigger()) - keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["stop_recording"]), lambda: self.hotkey_stop_trigger()) - keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["play_last_audio"]), lambda: self.hotkey_play_last_audio_trigger()) + # Register the hotkeys and track them + self.register_hotkeys(settings["hotkeys"]) + + def register_hotkeys(self, hotkey_settings): + """Register hotkeys from settings and track them.""" + try: + self.hotkeys.append(keyboard.add_hotkey( + self.format_shortcut(hotkey_settings["record_start_stop"]), + lambda: self.hotkey_record_trigger() + )) + + self.hotkeys.append(keyboard.add_hotkey( + self.format_shortcut(hotkey_settings["stop_recording"]), + lambda: self.hotkey_stop_trigger() + )) + + self.hotkeys.append(keyboard.add_hotkey( + self.format_shortcut(hotkey_settings["play_last_audio"]), + lambda: self.hotkey_play_last_audio_trigger() + )) + + return True + except Exception as e: + print(f"Error registering hotkeys: {e}") + return False + + def clear_hotkeys(self): + """Clear all registered hotkeys.""" + try: + # Clear tracked hotkeys + for hotkey in self.hotkeys: + try: + keyboard.remove_hotkey(hotkey) + except: + pass + self.hotkeys.clear() + + # Also attempt to use the more aggressive approach + try: + keyboard.unhook_all() + except AttributeError: + pass # Ignore if the method isn't supported + + # Try to reset internal keyboard state + try: + keyboard._recording = False + keyboard._pressed_events.clear() + keyboard._physically_pressed_keys.clear() + keyboard._logically_pressed_keys.clear() + except Exception as e: + print(f"Warning: Error while resetting keyboard state: {e}") + + except Exception as e: + print(f"Error clearing hotkeys: {e}") + + def force_hotkey_refresh(self, callback=None): + """Force a complete refresh of all hotkeys.""" + print("Forcing hotkey refresh") + + try: + # Clear all existing keyboard hooks + self.clear_hotkeys() + + # Get current settings + settings = self.app.load_settings() + + # Re-register hotkeys + success = self.register_hotkeys(settings["hotkeys"]) + + if success and self.hotkeys: + print(f"Hotkey refresh completed successfully with {len(self.hotkeys)} hotkeys") + if callback: + callback(True) + return True + else: + print("Failed to register hotkeys") + if callback: + callback(False) + messagebox.showerror("Hotkey Error", "Failed to re-register hotkeys.") + return False + + except Exception as e: + print(f"Error during hotkey refresh: {e}") + if callback: + callback(False) + return False + + def verify_hotkeys(self): + """Verify that hotkeys are working.""" + try: + # Simple check if hotkeys are registered + return len(self.hotkeys) > 0 + except: + return False + + def format_shortcut(self, key_combo): + """Format key combination consistently.""" + if isinstance(key_combo, (list, set)): + # Convert to list if it's a set + if isinstance(key_combo, set): + key_combo = list(key_combo) + + # Filter out empty values first + filtered_combo = list(filter(None, key_combo)) + + # Define modifier order + modifier_order = ['ctrl', 'alt', 'shift', 'win', 'command'] + + # Split into modifiers and regular keys + modifiers = [k for k in filtered_combo if k.lower() in modifier_order] + regular_keys = [k for k in filtered_combo if k.lower() not in modifier_order] + + # Sort modifiers according to preferred order + sorted_modifiers = sorted(modifiers, key=lambda x: modifier_order.index(x.lower()) if x.lower() in modifier_order else 999) + + # For regular keys, we don't want to sort them as they might be multiple characters + # We only need one non-modifier key anyway + if len(regular_keys) > 1: + # Keep only the first non-modifier key to avoid confusion + print(f"Warning: Multiple non-modifier keys detected: {regular_keys}. Using only: {regular_keys[0]}") + regular_keys = [regular_keys[0]] + + # Combine sorted modifiers and regular keys + combined_keys = sorted_modifiers + regular_keys + + # Make sure we have at least one key + if not combined_keys: + return "" + + # Join with plus signs + return "+".join(combined_keys) + elif isinstance(key_combo, str): + # If it's already a string, split it and re-format it + return self.format_shortcut(key_combo.split('+')) + else: + return "" def hotkey_play_last_audio_trigger(self): """Trigger playing the last audio.""" @@ -53,73 +183,586 @@ class HotkeyManager: @staticmethod def hotkey_settings_dialog(app): - """Show the hotkey settings dialog.""" + """Show the hotkey settings dialog with interactive key capture.""" settings = app.load_settings() hotkey_window = tk.Toplevel(app) hotkey_window.title("Hotkey Settings") hotkey_window.grab_set() # Grab the focus on this toplevel window + + # Temporarily suspend global hotkeys + old_hotkeys = [] + if hasattr(app, 'hotkey_manager'): + # Store current hotkeys + old_hotkeys = app.hotkey_manager.hotkeys.copy() + # Clear them while dialog is open + app.hotkey_manager.clear_hotkeys() + + # Set size and center the window + window_width = 500 + window_height = 400 + position_x = app.winfo_x() + (app.winfo_width() - window_width) // 2 + position_y = app.winfo_y() + (app.winfo_height() - window_height) // 2 + hotkey_window.geometry(f"{window_width}x{window_height}+{position_x}+{position_y}") - main_frame = ttk.Frame(hotkey_window, padding="10") - main_frame.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + main_frame = ttk.Frame(hotkey_window, padding="20") + main_frame.pack(fill=tk.BOTH, expand=True) - # Create dropdowns for each hotkey - keys = ["", "ctrl", "shift", "alt", "tab", "altgr"] - main_keys = list("abcdefghijklmnopqrstuvwxyz1234567890[];'#,./`") + \ - [f"f{i}" for i in range(1, 13)] # Add function keys F1 to F12 + # Add title + title_label = ttk.Label( + main_frame, + text="Keyboard Shortcuts", + font=("Arial", 12, "bold") + ) + title_label.pack(pady=(0, 15)) - def create_hotkey_row(label_text, key_combo): - ttk.Label(main_frame, text=label_text).grid(row=create_hotkey_row.row, column=0, sticky=tk.W, pady=2) + # Create frame for shortcuts + shortcuts_frame = ttk.Frame(main_frame) + shortcuts_frame.pack(fill=tk.BOTH, expand=True) - var1 = tk.StringVar(value=key_combo[0] if len(key_combo) > 0 else "") - var2 = tk.StringVar(value=key_combo[1] if len(key_combo) > 1 else "") - var3 = tk.StringVar(value=key_combo[2] if len(key_combo) > 2 else "") + hotkey_manager = app.hotkey_manager if hasattr(app, 'hotkey_manager') else HotkeyManager(app) + + # Dictionary to store labels for each shortcut for easy updating + shortcut_labels = {} - option_menu1 = ttk.OptionMenu(main_frame, var1, key_combo[0], *keys) - option_menu1.grid(row=create_hotkey_row.row, column=1, sticky=tk.W, pady=2) + # Function to handle shortcut editing + def start_shortcut_edit(shortcut_name, button, label): + # Add check for valid button + if not button or not button.winfo_exists(): + print("Warning: Button no longer exists") + return + + button.config(text="Press new shortcut...") + + # Track pressed keys and modifiers + pressed_keys = set() + currently_pressed = set() # Track keys that are currently held down + last_state = 0 # Track the last event state + + def on_key_press(event): + nonlocal last_state + last_state = event.state + + # Convert key to lowercase + key = event.keysym.lower() + + # Debug print + print(f"Key press - key: {key}") + print(f"State bits: {format(event.state, '016b')}") + print(f"State value: {event.state}") + print(f"Currently pressed keys before: {currently_pressed}") + + # Map left/right modifier variants to their base form + modifier_map = { + 'control_l': 'ctrl', 'control_r': 'ctrl', + 'alt_l': 'alt', 'alt_r': 'alt', + 'shift_l': 'shift', 'shift_r': 'shift', + 'super_l': 'win', 'super_r': 'win', + 'win_l': 'win', 'win_r': 'win' + } + + # Map for shift+number keys to their actual number + shift_number_map = { + # Standard US layout shift+number keys + 'exclam': '1', # Shift+1 + 'at': '2', # Shift+2 + 'numbersign': '3', # Shift+3 + 'dollar': '4', # Shift+4 + 'percent': '5', # Shift+5 + 'asciicircum': '6', # Shift+6 + 'ampersand': '7', # Shift+7 + 'asterisk': '8', # Shift+8 + 'parenleft': '9', # Shift+9 + 'parenright': '0', # Shift+0 + + # Additional keys for different keyboard layouts + 'quotedbl': '2', # Shift+2 on some layouts + 'sterling': '3', # Shift+3 on UK layout + 'eacute': '2', # French keyboards + 'quoteleft': '`', # Backtick + 'asciitilde': '`', # Shift+` + 'equal': '=', # Equal sign + 'plus': '=', # Shift+= + 'minus': '-', # Minus sign + 'underscore': '-', # Shift+- + 'bracketleft': '[', # Left bracket + 'braceleft': '[', # Shift+[ + 'bracketright': ']', # Right bracket + 'braceright': ']', # Shift+] + 'backslash': '\\', # Backslash + 'bar': '\\', # Shift+\ + 'semicolon': ';', # Semicolon + 'colon': ';', # Shift+; + 'apostrophe': "'", # Apostrophe + 'comma': ',', # Comma + 'less': ',', # Shift+, + 'period': '.', # Period + 'greater': '.', # Shift+. + 'slash': '/', # Slash + 'question': '/' # Shift+/ + } + + # Additional direct mappings for numeric keys - handle different representations + numeric_keys_map = { + 'kp_0': '0', 'kp_1': '1', 'kp_2': '2', 'kp_3': '3', 'kp_4': '4', + 'kp_5': '5', 'kp_6': '6', 'kp_7': '7', 'kp_8': '8', 'kp_9': '9', + 'zero': '0', 'one': '1', 'two': '2', 'three': '3', 'four': '4', + 'five': '5', 'six': '6', 'seven': '7', 'eight': '8', 'nine': '9', + } + + # Check if this is a shift+number key + if key in shift_number_map: + key = shift_number_map[key] + print(f"Mapped shift+number key to: {key}") + # Check if it's a numeric key with a different representation + elif key in numeric_keys_map: + key = numeric_keys_map[key] + print(f"Mapped numeric key to: {key}") + + # Special handling for numpad keys which might come as "kp_1", "kp_2" + elif key.startswith('kp_') and len(key) > 3: + mapped_key = key[3:] # Strip the 'kp_' prefix + print(f"Mapped keypad key from {key} to {mapped_key}") + key = mapped_key + + # Handle direct number key presses - these might be reported with character codes + if event.char and event.char.isdigit() and not key.isdigit(): + print(f"Detected digit character: {event.char}") + key = event.char + + # Add to currently pressed keys + if key in modifier_map: + mod_key = modifier_map[key] + currently_pressed.add(mod_key) + else: + # Only add non-modifier keys if they're actual keys (not just state changes) + if len(key) == 1 or key in ('left', 'right', 'up', 'down', 'space', 'tab', 'return', + 'backspace', 'delete', 'escape', 'home', 'end', 'pageup', + 'pagedown', 'insert', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', + 'f7', 'f8', 'f9', 'f10', 'f11', 'f12'): + currently_pressed.add(key) + elif key: # If it's any other non-empty key, add it anyway + print(f"Adding unrecognized key: {key}") + currently_pressed.add(key) + + # Store the character directly if it exists and isn't already captured + if event.char and event.char not in currently_pressed and event.char not in ('', ' '): + currently_pressed.add(event.char) + + # Update pressed_keys with all current keys + pressed_keys.clear() + pressed_keys.update(currently_pressed) + + # Add modifiers based on state + if event.state & 0x4: + pressed_keys.add('ctrl') + if event.state & 0x1: + pressed_keys.add('shift') + if event.state & 0x20000: + pressed_keys.add('alt') + if event.state & 0x40000 or 'win' in currently_pressed: + pressed_keys.add('win') + + print(f"Currently pressed keys after: {pressed_keys}") + + # Update button text to show current combination + current_combo = "+".join(sorted(pressed_keys)) + button.config(text=f"Press: {current_combo}") + + return "break" + + def on_key_release(event): + nonlocal currently_pressed + key = event.keysym.lower() + + print(f"Key release - key: {key}") + print(f"Key event char: {repr(event.char)}") + print(f"Currently pressed before release: {currently_pressed}") + + # Map for shift+number keys to their actual number (same as in on_key_press) + shift_number_map = { + # Standard US layout shift+number keys + 'exclam': '1', # Shift+1 + 'at': '2', # Shift+2 + 'numbersign': '3', # Shift+3 + 'dollar': '4', # Shift+4 + 'percent': '5', # Shift+5 + 'asciicircum': '6', # Shift+6 + 'ampersand': '7', # Shift+7 + 'asterisk': '8', # Shift+8 + 'parenleft': '9', # Shift+9 + 'parenright': '0', # Shift+0 + + # Additional keys for different keyboard layouts + 'quotedbl': '2', # Shift+2 on some layouts + 'sterling': '3', # Shift+3 on UK layout + 'eacute': '2', # French keyboards + 'quoteleft': '`', # Backtick + 'asciitilde': '`', # Shift+` + 'equal': '=', # Equal sign + 'plus': '=', # Shift+= + 'minus': '-', # Minus sign + 'underscore': '-', # Shift+- + 'bracketleft': '[', # Left bracket + 'braceleft': '[', # Shift+[ + 'bracketright': ']', # Right bracket + 'braceright': ']', # Shift+] + 'backslash': '\\', # Backslash + 'bar': '\\', # Shift+\ + 'semicolon': ';', # Semicolon + 'colon': ';', # Shift+; + 'apostrophe': "'", # Apostrophe + 'comma': ',', # Comma + 'less': ',', # Shift+, + 'period': '.', # Period + 'greater': '.', # Shift+. + 'slash': '/', # Slash + 'question': '/' # Shift+/ + } + + # Additional direct mappings for numeric keys - handle different representations + numeric_keys_map = { + 'kp_0': '0', 'kp_1': '1', 'kp_2': '2', 'kp_3': '3', 'kp_4': '4', + 'kp_5': '5', 'kp_6': '6', 'kp_7': '7', 'kp_8': '8', 'kp_9': '9', + 'zero': '0', 'one': '1', 'two': '2', 'three': '3', 'four': '4', + 'five': '5', 'six': '6', 'seven': '7', 'eight': '8', 'nine': '9', + } + + # Check if this is a shift+number key + if key in shift_number_map: + key = shift_number_map[key] + print(f"Mapped shift+number key release to: {key}") + # Check if it's a numeric key with a different representation + elif key in numeric_keys_map: + key = numeric_keys_map[key] + print(f"Mapped numeric key release to: {key}") + # Special handling for numpad keys + elif key.startswith('kp_') and len(key) > 3: + mapped_key = key[3:] # Strip the 'kp_' prefix + print(f"Mapped keypad key release from {key} to {mapped_key}") + key = mapped_key + + # Handle direct number key presses - these might be reported with character codes + if event.char and event.char.isdigit() and not key.isdigit(): + print(f"Detected digit character release: {event.char}") + key = event.char + + # Remove released key from currently pressed set + if key in currently_pressed: + currently_pressed.remove(key) + else: + # Try to find the key in currently_pressed by removing case, digit comparison, etc. + # This helps catch edge cases where the key representation changes between press and release + for pressed_key in list(currently_pressed): + # Check if it's the same key but with different case + if pressed_key.lower() == key.lower(): + print(f"Found case-insensitive match: {pressed_key} for released key: {key}") + currently_pressed.remove(pressed_key) + break + # Check if both are digits but with different representations + elif (pressed_key.isdigit() and key.isdigit()) or \ + (pressed_key == '0' and key == 'parenright') or \ + (pressed_key in '0123456789' and key in numeric_keys_map.values()): + print(f"Found digit key match: {pressed_key} for released key: {key}") + currently_pressed.remove(pressed_key) + break + + # If we're releasing a character key, remove that character too + if event.char and event.char in currently_pressed: + currently_pressed.remove(event.char) + + # Handle modifier key releases + modifier_map = { + 'control_l': 'ctrl', 'control_r': 'ctrl', + 'alt_l': 'alt', 'alt_r': 'alt', + 'shift_l': 'shift', 'shift_r': 'shift', + 'super_l': 'win', 'super_r': 'win', + 'win_l': 'win', 'win_r': 'win' + } + if key in modifier_map: + mod_key = modifier_map[key] + if mod_key in currently_pressed: + currently_pressed.remove(mod_key) + + print(f"Currently pressed after release: {currently_pressed}") + + # Process hotkey when all keys are released + # Or when we have a complete valid hotkey (modifiers + key) + has_complete_hotkey = ( + len(pressed_keys) >= 2 and # At least 2 keys + any(mod in pressed_keys for mod in ('ctrl', 'alt', 'shift', 'win', 'command')) and # Has modifier + any(k not in ('ctrl', 'alt', 'shift', 'win', 'command') for k in pressed_keys) # Has non-modifier + ) + + # Either all keys are released, or we have a complete valid hotkey on key release + if (not currently_pressed) or (has_complete_hotkey and key not in modifier_map): + try: + print(f"Processing hotkey: {pressed_keys}") + + # Check for empty pressed_keys + if not pressed_keys: + print("Warning: No keys in pressed_keys, aborting shortcut update") + button.config(text="Edit") + return + + # Create the shortcut string with consistent ordering + new_shortcut = hotkey_manager.format_shortcut(pressed_keys) + print(f"Formatted shortcut: '{new_shortcut}'") + + # Check if the formatting succeeded + if not new_shortcut: + print("Warning: format_shortcut returned empty string") + messagebox.showerror("Error", "Failed to format shortcut keys") + button.config(text="Edit") + return + + # Check if there's at least one modifier + has_modifier = any(mod in pressed_keys for mod in ('ctrl', 'alt', 'shift', 'win', 'command')) + + # Validate the shortcut + if not has_modifier: + messagebox.showerror("Error", + "Please include at least one modifier key (Ctrl, Alt, Shift, or Win)") + button.config(text="Edit") + return + + # Check if there's a non-modifier key + has_key = any(k not in ('ctrl', 'alt', 'shift', 'win', 'command') for k in pressed_keys) + if not has_key: + # Keep waiting for a full key combo + return + + # Convert to list format for settings + # Get a sorted list of modifiers first + modifier_order = ['ctrl', 'alt', 'shift', 'win', 'command'] + modifiers = sorted([k for k in pressed_keys if k in modifier_order], + key=lambda x: modifier_order.index(x)) + + # Get the first non-modifier key + main_key = next((k for k in pressed_keys if k not in modifier_order), "") + + # Create the 3-element format: [modifier1, modifier2, main_key] + shortcut_list = [ + modifiers[0] if len(modifiers) > 0 else "", + modifiers[1] if len(modifiers) > 1 else "", + main_key + ] + + print(f"Shortcut list: {shortcut_list}") + + # Update settings + settings = app.load_settings() + settings["hotkeys"][shortcut_name] = shortcut_list + app.save_settings_to_JSON(settings) + + # Update the label immediately with the newly formatted shortcut + label.config(text=new_shortcut) + # Ensure the update takes effect by forcing a UI update + label.update_idletasks() + + # Update button text + button.config(text="Edit") + button.update_idletasks() + + # Force a focus change to ensure the key events are processed + hotkey_window.focus_set() + + # Show a confirmation to the user + print(f"Shortcut updated to: '{new_shortcut}'") + + except Exception as e: + print(f"Error saving shortcut: {e}") + import traceback + traceback.print_exc() + messagebox.showerror("Error", f"Failed to update shortcut: {e}") + button.config(text="Edit") + + finally: + # Clear the sets + pressed_keys.clear() + currently_pressed.clear() + + # Remove key bindings + hotkey_window.unbind('') + hotkey_window.unbind('') + + # Remove any existing bindings first + hotkey_window.unbind('') + hotkey_window.unbind('') + + # Bind both key press and release events + hotkey_window.bind('', on_key_press) + hotkey_window.bind('', on_key_release) - option_menu2 = ttk.OptionMenu(main_frame, var2, key_combo[1] if len(key_combo) > 1 else "", *keys) - option_menu2.grid(row=create_hotkey_row.row, column=2, sticky=tk.W, pady=2) + # Add each shortcut with its edit button + row = 0 + for name, key_combo in settings["hotkeys"].items(): + # Format readable name + display_name = name.replace('_', ' ').title() + ":" + + # Format the current shortcut for display + current_shortcut = hotkey_manager.format_shortcut(key_combo) + + # Create frame for this shortcut + frame = ttk.Frame(shortcuts_frame) + frame.pack(fill=tk.X, pady=5) + + # Add shortcut name + name_label = ttk.Label(frame, text=display_name, width=20, anchor="w") + name_label.pack(side=tk.LEFT, padx=(0, 10)) + + # Add current shortcut + shortcut_label = ttk.Label(frame, text=current_shortcut, width=15, anchor="w") + shortcut_label.pack(side=tk.LEFT, padx=(0, 10)) + + # Store label reference for easy access + shortcut_labels[name] = shortcut_label + + # Add edit button + edit_button = ttk.Button( + frame, + text="Edit", + width=10 + ) + edit_button.pack(side=tk.RIGHT) + + # Configure button command after creation to avoid stale references + edit_button.configure(command=lambda n=name, b=edit_button, l=shortcut_label: + start_shortcut_edit(n, b, l)) + + row += 1 - option_menu3 = ttk.OptionMenu(main_frame, var3, key_combo[2] if len(key_combo) > 2 else "", *main_keys) - option_menu3.grid(row=create_hotkey_row.row, column=3, sticky=tk.W, pady=2) + # Create a frame for the buttons + button_frame = ttk.Frame(main_frame) + button_frame.pack(pady=20) - create_hotkey_row.row += 1 - return [var1, var2, var3] + # Add refresh button + refresh_button = ttk.Button( + button_frame, + text="Save & Close", + width=15, + command=lambda: close_and_save() + ) + refresh_button.pack(side=tk.LEFT, padx=5) - create_hotkey_row.row = 0 + # Add reset to defaults button + reset_button = ttk.Button( + button_frame, + text="Reset to Defaults", + width=15, + command=lambda: HotkeyManager.reset_shortcuts_to_default(app, hotkey_window) + ) + reset_button.pack(side=tk.LEFT, padx=5) - record_start_stop_vars = create_hotkey_row("Record Start/Stop:", settings["hotkeys"]["record_start_stop"]) - stop_recording_vars = create_hotkey_row("Stop Recording:", settings["hotkeys"]["stop_recording"]) - play_last_audio_vars = create_hotkey_row("Play Last Audio:", settings["hotkeys"]["play_last_audio"]) + # Add note about Windows lock + note_text = ("Note: If shortcuts stop working after unlocking Windows,\n" + "use this dialog to refresh them.") + + ttk.Label( + main_frame, + text=note_text, + justify=tk.CENTER, + font=("Arial", 9), + foreground="#666666" + ).pack(pady=10) - # Save Button - save_btn = ttk.Button(main_frame, text="Save", command=lambda: HotkeyManager.save_hotkey_settings(app, { - "record_start_stop": [record_start_stop_vars[0].get(), record_start_stop_vars[1].get(), record_start_stop_vars[2].get()], - "stop_recording": [stop_recording_vars[0].get(), stop_recording_vars[1].get(), stop_recording_vars[2].get()], - "play_last_audio": [play_last_audio_vars[0].get(), play_last_audio_vars[1].get(), play_last_audio_vars[2].get()] - })) - save_btn.grid(row=create_hotkey_row.row, column=1, sticky=tk.W + tk.E, pady=10) + # Function to properly close the dialog and restore hotkeys + def close_and_save(): + # Re-register global hotkeys before closing + if hasattr(app, 'hotkey_manager'): + app.hotkey_manager.force_hotkey_refresh() + messagebox.showinfo("Settings Updated", "Your hotkey settings have been saved successfully.") + hotkey_window.destroy() + + # Close button + close_button = ttk.Button( + main_frame, + text="Cancel", + command=lambda: cancel_and_close() + ) + close_button.pack(pady=(5, 0)) + + # Function to cancel without saving + def cancel_and_close(): + # Re-register the original hotkeys + if hasattr(app, 'hotkey_manager'): + # Force reload from settings + app.hotkey_manager.setup_hotkeys() + hotkey_window.destroy() + + # Make sure hotkeys are re-registered when window is closed + hotkey_window.protocol("WM_DELETE_WINDOW", cancel_and_close) @staticmethod - def save_hotkey_settings(app, hotkeys): - """Save hotkey settings.""" - settings = app.load_settings() - settings["hotkeys"] = hotkeys - app.save_settings_to_JSON(settings) - app.hotkey_manager.setup_hotkeys() # Re-register the hotkeys with the new settings - messagebox.showinfo("Settings Updated", "Your hotkey settings have been saved successfully.") + def reset_shortcuts_to_default(app, parent_window=None): + """Reset all keyboard shortcuts to their default values.""" + if parent_window is None: + parent_window = app + + # Create custom confirmation dialog + confirm = messagebox.askyesno( + "Reset Shortcuts", + "Are you sure you want to reset all keyboard shortcuts to their default values?", + parent=parent_window + ) + + if confirm: + try: + # Default shortcuts + is_mac = platform.system() == 'Darwin' + default_shortcuts = { + "record_start_stop": ["ctrl", "shift", "0"], + "stop_recording": ["ctrl", "shift", "9"], + "play_last_audio": ["ctrl", "shift", "8"] + } + + # Update settings + settings = app.load_settings() + settings["hotkeys"] = default_shortcuts + app.save_settings_to_JSON(settings) + + # Refresh hotkeys + def on_refresh_complete(success): + if success: + messagebox.showinfo( + "Success", + "Shortcuts have been reset to defaults", + parent=parent_window + ) + + # If this is a settings dialog, close and reopen it to refresh the UI + if parent_window != app and hasattr(parent_window, 'destroy'): + parent_window.destroy() + app.after(100, lambda: HotkeyManager.hotkey_settings_dialog(app)) + else: + messagebox.showerror( + "Error", + "Failed to register default shortcuts.", + parent=parent_window + ) + + app.hotkey_manager.force_hotkey_refresh(callback=on_refresh_complete) + + except Exception as e: + messagebox.showerror( + "Error", + f"Failed to reset shortcuts: {e}", + parent=parent_window + ) @staticmethod def show_hotkey_instructions(parent): """Show instructions for using hotkeys.""" instruction_window = tk.Toplevel(parent) instruction_window.title("Hotkey Instructions") - instruction_window.geometry("600x400") # Width x Height + instruction_window.geometry("600x500") # Width x Height settings = parent.load_settings() - record_shortcut = "+".join(filter(None, settings["hotkeys"]["record_start_stop"])) - play_shortcut = "+".join(filter(None, settings["hotkeys"]["play_last_audio"])) - stop_shortcut = "+".join(filter(None, settings["hotkeys"]["stop_recording"])) + + # Format hotkeys consistently for display + hotkey_manager = parent.hotkey_manager if hasattr(parent, 'hotkey_manager') else HotkeyManager(parent) + record_shortcut = hotkey_manager.format_shortcut(settings["hotkeys"]["record_start_stop"]) + play_shortcut = hotkey_manager.format_shortcut(settings["hotkeys"]["play_last_audio"]) + stop_shortcut = hotkey_manager.format_shortcut(settings["hotkeys"]["stop_recording"]) instructions = f"""Available Hotkeys: @@ -135,10 +778,36 @@ class HotkeyManager: These hotkeys work globally across your system, even when the app is minimized. You can customize these hotkeys in Settings → Hotkey Settings. +If hotkeys stop working after your computer wakes from sleep or after unlocking your screen, +use Settings → Hotkey Settings → Refresh Hotkeys to restore functionality. + You can also access tone presets from Settings → Manage Tone Presets to modify how your text is spoken. """ - tk.Label(instruction_window, text=instructions, justify=tk.LEFT, wraplength=580).pack(padx=10, pady=10) + main_frame = ttk.Frame(instruction_window, padding="20") + main_frame.pack(fill=tk.BOTH, expand=True) - # Add a button to close the window - ttk.Button(instruction_window, text="Close", command=instruction_window.destroy).pack(pady=(10, 0)) \ No newline at end of file + # Add scrolling text + text_frame = ttk.Frame(main_frame) + text_frame.pack(fill=tk.BOTH, expand=True, pady=10) + + scrollbar = ttk.Scrollbar(text_frame) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + text_widget = tk.Text(text_frame, wrap=tk.WORD, yscrollcommand=scrollbar.set) + text_widget.insert(tk.END, instructions) + text_widget.config(state=tk.DISABLED) # Make read-only + text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + scrollbar.config(command=text_widget.yview) + + # Add refresh button + refresh_btn = ttk.Button(main_frame, text="Refresh Hotkeys", + command=lambda: parent.hotkey_manager.force_hotkey_refresh( + callback=lambda success: messagebox.showinfo("Hotkey Refresh", + "Hotkeys refreshed successfully" if success else "Failed to refresh hotkeys") + )) + refresh_btn.pack(pady=(5, 10)) + + # Add a close button + ttk.Button(main_frame, text="Close", command=instruction_window.destroy).pack(pady=5) \ No newline at end of file