Skip to content

Commit

Permalink
feat(gui): add global shortcuts management
Browse files Browse the repository at this point in the history
  • Loading branch information
AAClause committed Oct 27, 2024
1 parent f716ce0 commit 0cdd444
Show file tree
Hide file tree
Showing 2 changed files with 223 additions and 0 deletions.
208 changes: 208 additions & 0 deletions basilisk/gui/global_shortcuts_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import logging
from typing import Dict, Optional, Tuple

import win32api
import win32con
import wx
import wx.adv

log = logging.getLogger(__name__)


class HotkeyAction:
TOGGLE_VISIBILITY = 1
CAPTURE_FULL = 2
CAPTURE_WINDOW = 3


MODIFIER_MAP: Dict[int, str] = {
# Translators: Modifier key names
win32con.MOD_ALT: "Alt",
# Translators: Modifier key names
win32con.MOD_CONTROL: _("Ctrl"),
# Translators: Modifier key names
win32con.MOD_WIN: _("Win"),
# Translators: Modifier key names
win32con.MOD_SHIFT: _("Shift"),
}

KEY_MAP: Dict[int, str] = {win32con.VK_SNAPSHOT: "PrintScreen"}


def get_key_name_from_win32con(key_code: int) -> Optional[str]:
"""Retrieve the key name from win32con module."""
for attr_name in dir(win32con):
if attr_name.startswith("VK_"):
if getattr(win32con, attr_name) == key_code:
log.debug(f"Found key code: {attr_name}")
return attr_name[3:]
log.debug(f"Key code not found: {key_code}")
return None


def shortcut_to_string(shortcut: Tuple[int, int]) -> str:
"""Converts a shortcut tuple to a human-readable string."""
modifiers, raw_key_code = shortcut
mod_parts = [name for mod, name in MODIFIER_MAP.items() if modifiers & mod]
key_name = get_key_name_from_win32con(raw_key_code) or chr(raw_key_code)
return '+'.join(mod_parts + [key_name])


class GlobalShortcutsDialog(wx.Dialog):
def __init__(self, parent: Optional[wx.Window] = None):
super().__init__(
parent,
# Translators: Title of the global shortcuts dialog
title=_("Global Shortcuts"),
size=(350, 250),
)
self.current_shortcuts = {
# Translators: Action name
_("Minimize/Restore"): (
win32con.MOD_WIN | win32con.MOD_ALT,
win32con.VK_SPACE,
),
# Translators: Action name
_("Capture Fullscreen"): (
win32con.MOD_WIN | win32con.MOD_CONTROL,
ord('U'),
),
# Translators: Action name
_("Capture Active Window"): (
win32con.MOD_WIN | win32con.MOD_CONTROL,
ord('W'),
),
}
self.init_ui()

def init_ui(self) -> None:
panel = wx.Panel(self)
main_sizer = wx.BoxSizer(wx.VERTICAL)

self.action_list = wx.ListCtrl(panel, style=wx.LC_REPORT)
self.action_list.InsertColumn(
0,
# Translators: Column header for the action name
_("Action"),
width=180,
)
self.action_list.InsertColumn(
1,
# Translators: Column header for the shortcut key
_("Shortcut"),
width=140,
)
self.populate_shortcut_list()

assign_btn = wx.Button(panel, label="Assign")
assign_btn.Bind(wx.EVT_BUTTON, self.on_set_shortcut)

close_button = wx.Button(panel, wx.ID_CLOSE)
close_button.Bind(wx.EVT_BUTTON, self.on_close)

main_sizer.Add(
self.action_list, proportion=1, flag=wx.EXPAND | wx.ALL, border=5
)
main_sizer.Add(assign_btn, flag=wx.ALIGN_CENTER | wx.ALL, border=5)
main_sizer.Add(close_button, flag=wx.ALIGN_CENTER | wx.ALL, border=5)

panel.SetSizer(main_sizer)

def populate_shortcut_list(self) -> None:
self.action_list.DeleteAllItems()
for idx, (action, shortcut) in enumerate(
self.current_shortcuts.items()
):
self.action_list.InsertItem(idx, action)
self.action_list.SetItem(idx, 1, shortcut_to_string(shortcut))

def on_set_shortcut(self, event: wx.Event) -> None:
selected_index = self.action_list.GetFirstSelected()
if selected_index == -1:
wx.MessageBox(
# Translators: Error message when no action is selected
_("Please select an action to assign a new shortcut."),
# Translators: Error message title
_("Error"),
wx.ICON_ERROR | wx.OK,
)
return

action = self.action_list.GetItemText(selected_index)
dlg = ShortcutCaptureDialog(self)
if dlg.ShowModal() == wx.ID_OK:
new_shortcut = dlg.get_shortcut()
if new_shortcut:
self.current_shortcuts[action] = new_shortcut
wx.MessageBox(
# Translators: Message when a new shortcut is assigned
_("Captured shortcut for {action}: {new_shortcut}").format(
action=action,
new_shortcut=shortcut_to_string(new_shortcut),
),
# Translators: Message title when a new shortcut is assigned
_("Shortcut Captured"),
wx.OK | wx.ICON_INFORMATION,
)
self.populate_shortcut_list()

def on_close(self, event: wx.Event) -> None:
self.EndModal(wx.ID_CLOSE)


class ShortcutCaptureDialog(wx.Dialog):
def __init__(self, parent: Optional[wx.Window] = None):
super().__init__(
parent,
# Translators: Title of the shortcut capture dialog
title=_("Press the Shortcut"),
size=(250, 100),
)
self.shortcut: Optional[Tuple[int, int]] = None
self.init_ui()

def init_ui(self) -> None:
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)

instruction = wx.StaticText(
panel, label="Press the desired shortcut key combination."
)
sizer.Add(instruction, flag=wx.EXPAND | wx.ALL, border=5)

panel.SetSizer(sizer)
panel.Bind(wx.EVT_KEY_UP, self.on_key_up)

def on_key_up(self, event: wx.KeyEvent) -> None:
raw_key_code = event.GetRawKeyCode()

mods = 0
if win32api.GetAsyncKeyState(
win32con.VK_LWIN
) or win32api.GetAsyncKeyState(win32con.VK_RWIN):
mods |= win32con.MOD_WIN
if win32api.GetAsyncKeyState(win32con.VK_MENU): # Alt key
mods |= win32con.MOD_ALT
if win32api.GetAsyncKeyState(win32con.VK_CONTROL):
mods |= win32con.MOD_CONTROL
if win32api.GetAsyncKeyState(win32con.VK_SHIFT):
mods |= win32con.MOD_SHIFT

log.debug(f"Key down: {mods=}, key_code={raw_key_code=}")
if raw_key_code and mods:
self.shortcut = (mods, raw_key_code)
self.EndModal(wx.ID_OK)
else:
log.debug("Invalid shortcut key combination.")
wx.MessageBox(
# Translators: Error message when an invalid shortcut key is pressed
_(
"Please press a valid key with at least one modifier ({modifiers})."
).format(modifiers=', '.join(MODIFIER_MAP.values())),
# Translators: Error message title
_("Invalid Shortcut"),
wx.OK | wx.ICON_WARNING,
)

def get_shortcut(self) -> Optional[Tuple[int, int]]:
return self.shortcut
15 changes: 15 additions & 0 deletions basilisk/gui/main_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ def update_item_label_suffix(item: wx.MenuItem, suffix: str = "..."):
preferences_item = tool_menu.Append(wx.ID_PREFERENCES)
self.Bind(wx.EVT_MENU, self.on_preferences, preferences_item)
update_item_label_suffix(preferences_item, "...\tCtrl+,")
global_shortcuts_item = tool_menu.Append(
wx.ID_ANY,
# Translators: A label for a menu item to manage global shortcuts
_("Manage global shortcuts") + "...\tCtrl+Shift+K",
)
self.Bind(
wx.EVT_MENU, self.on_manage_global_shortcuts, global_shortcuts_item
)
tool_menu.AppendSeparator()
install_nvda_addon = tool_menu.Append(
wx.ID_ANY, _("Install NVDA addon")
Expand Down Expand Up @@ -503,6 +511,13 @@ def on_preferences(self, event):
self.refresh_tabs()
preferences_dialog.Destroy()

def on_manage_global_shortcuts(self, event):
from .global_shortcuts_dialog import GlobalShortcutsDialog

global_shortcuts_dialog = GlobalShortcutsDialog(self)
global_shortcuts_dialog.ShowModal()
global_shortcuts_dialog.Destroy()

def on_manage_conversation_profiles(self, event):
from .conversation_profile_dialog import ConversationProfileDialog

Expand Down

0 comments on commit 0cdd444

Please sign in to comment.