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
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('<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)
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))
# 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)