hotkey manager working

This commit is contained in:
Andrew Ward
2025-03-21 12:37:14 +00:00
parent 1f476f2c60
commit ece9d883cf
2 changed files with 726 additions and 57 deletions

View File

@@ -1,30 +1,160 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox from tkinter import ttk, messagebox
import keyboard import keyboard
import platform
class HotkeyManager: class HotkeyManager:
"""Class to handle hotkey operations.""" """Class to handle hotkey operations."""
def __init__(self, app): def __init__(self, app):
self.app = app self.app = app
self.hotkeys = [] # Track registered hotkeys
self.is_mac = platform.system() == 'Darwin'
self.setup_hotkeys() self.setup_hotkeys()
def setup_hotkeys(self): def setup_hotkeys(self):
"""Set up hotkeys based on settings.""" """Set up hotkeys based on settings."""
try: # First, clear all existing hotkeys
# Attempt to clear existing hotkeys self.clear_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
settings = self.app.load_settings() settings = self.app.load_settings()
def parse_hotkey(combo): # Register the hotkeys and track them
return '+'.join(filter(None, combo)) self.register_hotkeys(settings["hotkeys"])
keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["record_start_stop"]), lambda: self.hotkey_record_trigger()) def register_hotkeys(self, hotkey_settings):
keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["stop_recording"]), lambda: self.hotkey_stop_trigger()) """Register hotkeys from settings and track them."""
keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["play_last_audio"]), lambda: self.hotkey_play_last_audio_trigger()) 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): def hotkey_play_last_audio_trigger(self):
"""Trigger playing the last audio.""" """Trigger playing the last audio."""
@@ -53,73 +183,586 @@ class HotkeyManager:
@staticmethod @staticmethod
def hotkey_settings_dialog(app): def hotkey_settings_dialog(app):
"""Show the hotkey settings dialog.""" """Show the hotkey settings dialog with interactive key capture."""
settings = app.load_settings() settings = app.load_settings()
hotkey_window = tk.Toplevel(app) hotkey_window = tk.Toplevel(app)
hotkey_window.title("Hotkey Settings") hotkey_window.title("Hotkey Settings")
hotkey_window.grab_set() # Grab the focus on this toplevel window 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 = ttk.Frame(hotkey_window, padding="20")
main_frame.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S)) main_frame.pack(fill=tk.BOTH, expand=True)
# Create dropdowns for each hotkey # Add title
keys = ["", "ctrl", "shift", "alt", "tab", "altgr"] title_label = ttk.Label(
main_keys = list("abcdefghijklmnopqrstuvwxyz1234567890[];'#,./`") + \ main_frame,
[f"f{i}" for i in range(1, 13)] # Add function keys F1 to F12 text="Keyboard Shortcuts",
font=("Arial", 12, "bold")
)
title_label.pack(pady=(0, 15))
def create_hotkey_row(label_text, key_combo): # Create frame for shortcuts
ttk.Label(main_frame, text=label_text).grid(row=create_hotkey_row.row, column=0, sticky=tk.W, pady=2) 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 "") hotkey_manager = app.hotkey_manager if hasattr(app, 'hotkey_manager') else HotkeyManager(app)
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 "") # Dictionary to store labels for each shortcut for easy updating
shortcut_labels = {}
option_menu1 = ttk.OptionMenu(main_frame, var1, key_combo[0], *keys) # Function to handle shortcut editing
option_menu1.grid(row=create_hotkey_row.row, column=1, sticky=tk.W, pady=2) 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('<KeyPress>')
hotkey_window.unbind('<KeyRelease>')
# Remove any existing bindings first
hotkey_window.unbind('<KeyPress>')
hotkey_window.unbind('<KeyRelease>')
# Bind both key press and release events
hotkey_window.bind('<KeyPress>', on_key_press)
hotkey_window.bind('<KeyRelease>', on_key_release)
option_menu2 = ttk.OptionMenu(main_frame, var2, key_combo[1] if len(key_combo) > 1 else "", *keys) # Add each shortcut with its edit button
option_menu2.grid(row=create_hotkey_row.row, column=2, sticky=tk.W, pady=2) 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) # Create a frame for the buttons
option_menu3.grid(row=create_hotkey_row.row, column=3, sticky=tk.W, pady=2) button_frame = ttk.Frame(main_frame)
button_frame.pack(pady=20)
create_hotkey_row.row += 1 # Add refresh button
return [var1, var2, var3] 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"]) # Add note about Windows lock
stop_recording_vars = create_hotkey_row("Stop Recording:", settings["hotkeys"]["stop_recording"]) note_text = ("Note: If shortcuts stop working after unlocking Windows,\n"
play_last_audio_vars = create_hotkey_row("Play Last Audio:", settings["hotkeys"]["play_last_audio"]) "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 # Function to properly close the dialog and restore hotkeys
save_btn = ttk.Button(main_frame, text="Save", command=lambda: HotkeyManager.save_hotkey_settings(app, { def close_and_save():
"record_start_stop": [record_start_stop_vars[0].get(), record_start_stop_vars[1].get(), record_start_stop_vars[2].get()], # Re-register global hotkeys before closing
"stop_recording": [stop_recording_vars[0].get(), stop_recording_vars[1].get(), stop_recording_vars[2].get()], if hasattr(app, 'hotkey_manager'):
"play_last_audio": [play_last_audio_vars[0].get(), play_last_audio_vars[1].get(), play_last_audio_vars[2].get()] app.hotkey_manager.force_hotkey_refresh()
})) messagebox.showinfo("Settings Updated", "Your hotkey settings have been saved successfully.")
save_btn.grid(row=create_hotkey_row.row, column=1, sticky=tk.W + tk.E, pady=10) 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 @staticmethod
def save_hotkey_settings(app, hotkeys): def reset_shortcuts_to_default(app, parent_window=None):
"""Save hotkey settings.""" """Reset all keyboard shortcuts to their default values."""
settings = app.load_settings() if parent_window is None:
settings["hotkeys"] = hotkeys parent_window = app
app.save_settings_to_JSON(settings)
app.hotkey_manager.setup_hotkeys() # Re-register the hotkeys with the new settings # Create custom confirmation dialog
messagebox.showinfo("Settings Updated", "Your hotkey settings have been saved successfully.") 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 @staticmethod
def show_hotkey_instructions(parent): def show_hotkey_instructions(parent):
"""Show instructions for using hotkeys.""" """Show instructions for using hotkeys."""
instruction_window = tk.Toplevel(parent) instruction_window = tk.Toplevel(parent)
instruction_window.title("Hotkey Instructions") instruction_window.title("Hotkey Instructions")
instruction_window.geometry("600x400") # Width x Height instruction_window.geometry("600x500") # Width x Height
settings = parent.load_settings() settings = parent.load_settings()
record_shortcut = "+".join(filter(None, settings["hotkeys"]["record_start_stop"]))
play_shortcut = "+".join(filter(None, settings["hotkeys"]["play_last_audio"])) # Format hotkeys consistently for display
stop_shortcut = "+".join(filter(None, settings["hotkeys"]["stop_recording"])) 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: instructions = f"""Available Hotkeys:
@@ -135,10 +778,36 @@ class HotkeyManager:
These hotkeys work globally across your system, even when the app is minimized. These hotkeys work globally across your system, even when the app is minimized.
You can customize these hotkeys in Settings → Hotkey Settings. 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. 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 # Add scrolling text
ttk.Button(instruction_window, text="Close", command=instruction_window.destroy).pack(pady=(10, 0)) 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)