From ece9d883cfeee12ab22126c581863a02e773d4c2 Mon Sep 17 00:00:00 2001 From: Andrew Ward Date: Fri, 21 Mar 2025 12:37:14 +0000 Subject: [PATCH] hotkey manager working --- .../hotkey_manager.cpython-312.pyc | Bin 10778 -> 35651 bytes utils/hotkey_manager.py | 783 ++++++++++++++++-- 2 files changed, 726 insertions(+), 57 deletions(-) diff --git a/utils/__pycache__/hotkey_manager.cpython-312.pyc b/utils/__pycache__/hotkey_manager.cpython-312.pyc index 9af06f5544022d746e423f59536d0ea9fdff2e79..2fe0ce515838b6e81cec67365b33a83a6b880adc 100644 GIT binary patch literal 35651 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 literal 10778 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

#deoqQXJc&h$(1h&T?K$F7Fdh-ffD znIA^@%d4fgkhqtBf(9Hy1IWDw*=vw}1|110s$rM;7~BaUjb}LaBvchH=2+7qUybT2 zO@rJ&5sC?*NXQVxcoCfpt1+x1d`#-nT)Wk?`k-2xF{j(gKg=KadzJkSzF zl4^e$QrLpv_JUmbUkDRBh`J>?PrL4E&v;g)nFkHaKB%}}aicrauqItO%hX#zrS)0H zhl|ZR)0}0vuj?DG4qO?yHg!kH*6&EWXPMedRnt}1&fMTXs{Kvfuj@XNf200d{l}yK z)c@Q5&o+L(`IF6B=l*n6mO1d)K~#68=?8TU+S2`*x&sf|R?m<#8~J&rCFxw-&;3ooA!OtpdCG-9~jb3#`Obd zGOe=4G=J6Hp>^*5c-t2T^zL`G<3YW9G}9c?n8vSKI<*bmAHVxWNZ(G~V+$BG2`@H>= z_AhJyQ1`pKPu6Pv@1!g5GspkuaW_fSbSg*){Bte4x684>r9m8v44fx-Aj45KIQr6L ziD(EpkPs9gyPyNas?fNSG+7jMh8_rdNh%kzu;4>LV&3&A=lD=ULHL1_iDIDjNI6;8 zYh|2N1)e2=|llg~D8c06Y#DNkk$^9|~Y0 zel+SaV^~x8A$t1bzPEdgMR{V+-(ur?5E<R-fm_~rz-#x%vy zjeV+ZA#dQhO4ulfM)pGXG-J#*uS_$Sm%=5 zpDYTQ0tyIMShe&2DstCx5@?`Md>Jk{kXp8sWnq<|)L$Bcc@>V_1_a64Cp-apX$VZ& zHP{6g3N%5uiH5vl(hfQMOXrlM$gM4em(1Bw4&3KZ3&nDdrcil%krf&UBNMG82~|SX z?P}{xfZxzX9!}vh;WY>lKxitAuDVOo?OH3)RCX56?KW*Co5h`4G5T@M6Y< zplo-$H~6^JDQjKJfSf<42H$WoL`B5?r^VZ;DP4=xmo zCsa-l$3p?p=XP%d_6C7(?%P0F-%?=5}xLaS3#P zftOiqk|QVwqEQiiFja)1$l6jkK9=NoI6jb9g_Xie`bR~@5`qdhA<1K0Fd_}ZO)u&$ zOYo$~+(}YV#ppT?42l>xF$x)4BRv1%;3T5(@+KNeiGt4!Qj!bsF)omRX;B^OS2RN? zRtVH_|M8bU&!2gr4pwAAfVUG8ECGpAc-0gWxws5~RK+~rf_z3m%r{V6Kk9Wj-wPf> zMI|9Lk_0?Wn0r9pJX)?2m&U}H&mBTS3M6a~5QpIm!-}E1;hvCrFq~pRbyOaK7W6Iz z-2AihKGeRr9svR{YKzD}Z*j@1QQs;PnSOjj7f7f^B{s$9 zi`dpK6xkv;ih`sy5DQAC`PBd&CoD_9xNB-viIRCnKeT?O4XE-Q0TxIz_rZzlCuSCB znmRRR*(}l}oX&7s>y|9D6&F|N%!(Tazyy2fS(@{#);+5;9&ehNUDou$n(J$RwPmLL z4s&;@-r2479M?PF&8!&6EE`N$%{DZCz+7j3wRnb}8NHL#eY>?ieY&qd(|Rn^0LDwX z_M^J5FVoteX*iayoTI7b^h4UC+3My9TRjcdqP_OqF8KD$e>*+oTuoI2oo2snBU}~e zCQ_?eF|+P7>T~BOPOblVmU%bFoYa|h*+c|=vwr`X1Ru$_IQ@P>3i$mpQdXv+(2BlL zXkSKp!eGJ6f}l3g+NdPRNYu;7I*_L!yGh{5Oq&5&bJ(v#&jl1@ke?^MqSrm9DSOjn zrpmsXdfaBWcRgNYx37M@$YtL)x0rC$ena`}?~+e!@HvM+=T4Kj+F>8Ds?q0MHLh*3 zN6gx9DD-Jgk7kCDyHgzt#o&RJ$tuWNz($YUhQ9FXE}<`^#PEvY4U%~(Z^&1m4yjUQ gIb`tCf+YV))cqT=;*Uhtcl1vvGD1Ek@E;!if0o~fo&W#< 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