diff --git a/mininterface/_lib/shortcuts.py b/mininterface/_lib/shortcuts.py new file mode 100644 index 0000000..2f86848 --- /dev/null +++ b/mininterface/_lib/shortcuts.py @@ -0,0 +1,59 @@ +def convert_to_tkinter_shortcut(shortcut: str) -> str: + """Convert a textual shortcut format to tkinter format. + + Args: + shortcut: Shortcut in textual format (e.g., "ctrl+t", "f4") + + Returns: + Shortcut in tkinter format (e.g., "", "") + """ + # Handle function keys + if shortcut.startswith("f") and shortcut[1:].isdigit(): + return f"<{shortcut.upper()}>" + + # Handle modifier keys + mods = { + "ctrl": "Control", + "alt": "Alt", + "shift": "Shift", + "cmd": "Command", # For macOS + "meta": "Meta", + } + + parts = shortcut.lower().split("+") + keys = [mods.get(p, p) for p in parts] + modifiers = keys[:-1] + key = keys[-1] + + return f"<{'-'.join(modifiers + [key])}>" + + +def convert_to_textual_shortcut(shortcut: str) -> str: + """Convert a tkinter shortcut format to textual format. + + Args: + shortcut: Shortcut in tkinter format (e.g., "", "") + + Returns: + Shortcut in textual format (e.g., "ctrl+t", "f4") + """ + shortcut = shortcut.strip("<>") + + # Handle function keys + if shortcut.startswith("F") and shortcut[1:].isdigit(): + return shortcut.lower() + + # Handle modifier keys + mods = { + "Control": "ctrl", + "Alt": "alt", + "Shift": "shift", + "Command": "cmd", # For macOS + "Meta": "meta", + } + + parts = shortcut.split("-") + # Convert each part to its proper form + keys = [mods.get(part.title(), part.lower()) for part in parts] + + return "+".join(keys) diff --git a/mininterface/_tk_interface/date_entry.py b/mininterface/_tk_interface/date_entry.py index bcf78a0..975030c 100644 --- a/mininterface/_tk_interface/date_entry.py +++ b/mininterface/_tk_interface/date_entry.py @@ -9,6 +9,7 @@ Calendar = None from ..tag.datetime_tag import DatetimeTag +from .._lib.shortcuts import convert_to_tkinter_shortcut if TYPE_CHECKING: from mininterface._tk_interface.adaptor import TkAdaptor @@ -76,8 +77,9 @@ def create_spinbox(self, variable: tk.Variable): # Bind key release event to update calendar when user changes the input field spinbox.bind("", self.on_spinbox_change) - # Toggle calendar widget with ctrl+shift+c - spinbox.bind("", self.toggle_calendar) + # Toggle calendar widget with the shortcut from settings + tk_shortcut = convert_to_tkinter_shortcut(self.tk_app.options.toggle_widget) + spinbox.bind(tk_shortcut, self.toggle_calendar) # Select all in the spinbox with ctrl+a spinbox.bind("", self.select_all) diff --git a/mininterface/_tk_interface/secret_entry.py b/mininterface/_tk_interface/secret_entry.py index b8beeac..85b4675 100644 --- a/mininterface/_tk_interface/secret_entry.py +++ b/mininterface/_tk_interface/secret_entry.py @@ -1,29 +1,31 @@ import tkinter as tk from tkinter import Button, Entry from ..tag.secret_tag import SecretTag +from .._lib.shortcuts import convert_to_tkinter_shortcut class SecretEntryWrapper: - def __init__(self, master, tag: SecretTag, variable: tk.Variable, grid_info): + def __init__(self, master, tag: SecretTag, variable: tk.Variable, grid_info, adaptor): self.tag = tag self.entry = Entry(master, text=variable, show="•") # Add more hints here as needed self.entry._shortcut_hints = [ - "Ctrl+T: Toggle visibility of password field" + f"{adaptor.options.toggle_widget}: Toggle visibility of password field" ] row = grid_info['row'] col = grid_info['column'] self.entry.grid(row=row, column=col, sticky="we") - # Add Ctrl+T binding to the entry widget - self.entry.bind('', self._on_toggle) + # Add binding using the shortcut from settings + tk_shortcut = convert_to_tkinter_shortcut(adaptor.options.toggle_widget) + self.entry.bind(tk_shortcut, self._on_toggle) if tag.show_toggle: self.button = Button(master, text='👁', command=self.toggle_show) self.button.grid(row=row, column=col + 1) def _on_toggle(self, event=None): - """Handle Ctrl+T key event""" + """Handle toggle key event""" self.toggle_show() return "break" # Prevent event propagation diff --git a/mininterface/_tk_interface/utils.py b/mininterface/_tk_interface/utils.py index f6bc1b1..0f0ff99 100644 --- a/mininterface/_tk_interface/utils.py +++ b/mininterface/_tk_interface/utils.py @@ -226,7 +226,7 @@ def handle_return(event): grid_info = widget.grid_info() widget.grid_forget() # Create wrapper and store it in the widget list - wrapper = SecretEntryWrapper(master, tag, variable, grid_info) + wrapper = SecretEntryWrapper(master, tag, variable, grid_info, adaptor) widget = wrapper.entry # Add shortcut to the central shortcuts set adaptor.shortcuts.add("Ctrl+T: Toggle visibility of password field") diff --git a/mininterface/settings.py b/mininterface/settings.py index b2ccaa3..2826cae 100644 --- a/mininterface/settings.py +++ b/mininterface/settings.py @@ -9,22 +9,18 @@ @dataclass class UiSettings: toggle_widget: str = "f4" - """ Shortcuts to toggle ex. calendar or file picker. """ - - # NOTE should be used in tkinter - # But we have to convert textual shortcut to tkinter shortcut with something like this - # mods = { - # "ctrl": "Control", - # "alt": "Alt", - # "shift": "Shift", - # } - - # parts = shortcut.lower().split("+") - # keys = [mods.get(p, p) for p in parts] - # modifiers = keys[:-1] - # key = keys[-1] - - # return f"<{'-'.join(modifiers + [key])}>" + """ Shortcut to toggle widgets like secret fields, file picker, and datetime dialog. + + The shortcut should be in Textual format (e.g., "ctrl+t", "f4"). + This format will be automatically converted to the appropriate format + for each interface (e.g., "" for Tkinter). + + Examples: + - "ctrl+t" for Control+T + - "alt+f" for Alt+F + - "f4" for F4 key + - "cmd+s" for Command+S (macOS) + """ mnemonic: Optional[bool] = True """ Allow users to access fields with the `Alt+char` shortcut. diff --git a/tests/test_shortcuts.py b/tests/test_shortcuts.py new file mode 100644 index 0000000..91e06e7 --- /dev/null +++ b/tests/test_shortcuts.py @@ -0,0 +1,87 @@ +from mininterface._lib.shortcuts import convert_to_tkinter_shortcut, convert_to_textual_shortcut +from shared import TestAbstract + + +class TestShortcuts(TestAbstract): + def test_basic_modifiers(self): + """Test basic modifier key conversions.""" + # Textual to Tkinter + self.assertEqual(convert_to_tkinter_shortcut("ctrl+t"), "") + self.assertEqual(convert_to_tkinter_shortcut("alt+f"), "") + self.assertEqual(convert_to_tkinter_shortcut("shift+s"), "") + + # Tkinter to Textual + self.assertEqual(convert_to_textual_shortcut(""), "ctrl+t") + self.assertEqual(convert_to_textual_shortcut(""), "alt+f") + self.assertEqual(convert_to_textual_shortcut(""), "shift+s") + + def test_function_keys(self): + """Test function key conversions.""" + # Textual to Tkinter + self.assertEqual(convert_to_tkinter_shortcut("f4"), "") + self.assertEqual(convert_to_tkinter_shortcut("f12"), "") + + # Tkinter to Textual + self.assertEqual(convert_to_textual_shortcut(""), "f4") + self.assertEqual(convert_to_textual_shortcut(""), "f12") + + def test_multiple_modifiers(self): + """Test multiple modifier key combinations.""" + # Textual to Tkinter + self.assertEqual(convert_to_tkinter_shortcut("ctrl+alt+t"), "") + self.assertEqual(convert_to_tkinter_shortcut("ctrl+shift+s"), "") + + # Tkinter to Textual + self.assertEqual(convert_to_textual_shortcut(""), "ctrl+alt+t") + self.assertEqual(convert_to_textual_shortcut(""), "ctrl+shift+s") + + def test_macos_keys(self): + """Test macOS specific key conversions.""" + # Textual to Tkinter + self.assertEqual(convert_to_tkinter_shortcut("cmd+s"), "") + self.assertEqual(convert_to_tkinter_shortcut("meta+t"), "") + + # Tkinter to Textual + self.assertEqual(convert_to_textual_shortcut(""), "cmd+s") + self.assertEqual(convert_to_textual_shortcut(""), "meta+t") + + def test_case_insensitivity(self): + """Test case insensitivity in conversions.""" + # Textual to Tkinter + self.assertEqual(convert_to_tkinter_shortcut("CTRL+T"), "") + self.assertEqual(convert_to_tkinter_shortcut("Ctrl+t"), "") + self.assertEqual(convert_to_tkinter_shortcut("cTrL+t"), "") + + # Tkinter to Textual + self.assertEqual(convert_to_textual_shortcut(""), "ctrl+t") + self.assertEqual(convert_to_textual_shortcut(""), "ctrl+t") + self.assertEqual(convert_to_textual_shortcut(""), "ctrl+t") + + def test_roundtrip_conversion(self): + """Test that converting back and forth gives the same result.""" + test_shortcuts = [ + "ctrl+t", + "alt+f", + "shift+s", + "f4", + "ctrl+alt+t", + "cmd+s", + "meta+t" + ] + + for shortcut in test_shortcuts: + tk_shortcut = convert_to_tkinter_shortcut(shortcut) + back_to_textual = convert_to_textual_shortcut(tk_shortcut) + self.assertEqual(back_to_textual, shortcut) + + def test_invalid_input(self): + """Test handling of invalid inputs.""" + with self.assertRaises(AttributeError): + convert_to_tkinter_shortcut(None) + + with self.assertRaises(AttributeError): + convert_to_textual_shortcut(None) + + # Empty string should be handled gracefully + self.assertEqual(convert_to_tkinter_shortcut(""), "<>") + self.assertEqual(convert_to_textual_shortcut("<>"), "")