Skip to content

Commit

Permalink
Merge branch 'issue_89' of https://github.com/dalthviz/app-model into…
Browse files Browse the repository at this point in the history
… issue_89
  • Loading branch information
dalthviz committed Jul 16, 2024
2 parents eb96af5 + 5160031 commit 58b9a23
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 91 deletions.
2 changes: 2 additions & 0 deletions src/app_model/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from ._action import Action
from ._command_rule import CommandRule, ToggleRule
from ._constants import OperatingSystem
from ._icon import Icon
from ._keybinding_rule import KeyBindingRule
from ._keys import (
Expand Down Expand Up @@ -41,6 +42,7 @@
"KeyCode",
"KeyCombo",
"KeyMod",
"OperatingSystem",
"MenuItem",
"MenuItemBase",
"MenuRule",
Expand Down
84 changes: 38 additions & 46 deletions src/app_model/types/_keys/_key_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,47 +604,43 @@ class _KM(NamedTuple):
'cmd': KeyCode.Meta,
}

# key symbols on all platforms
KEY_SYMBOLS: dict[KeyCode, str] = {
KeyCode.Shift: "⇧",
KeyCode.LeftArrow: "←",
KeyCode.RightArrow: "→",
KeyCode.UpArrow: "↑",
KeyCode.DownArrow: "↓",
KeyCode.Backspace: "⌫",
KeyCode.Delete: "⌦",
KeyCode.Tab: "⇥",
KeyCode.Escape: "⎋",
KeyCode.Enter: "↵",
KeyCode.Space: "␣",
KeyCode.CapsLock: "⇪",
}
# key symbols mappings per platform
WIN_KEY_SYMBOLS: dict[str, str] = {
"Ctrl": "Ctrl",
"Shift": "⇧",
"Alt": "Alt",
"Meta": "⊞",
"Left": "←",
"Right": "→",
"Up": "↑",
"Down": "↓",
"Backspace": "⌫",
"Delete": "⌦",
"Tab": "↹",
"Escape": "Esc",
"Return": "⏎",
"Enter": "↵",
"Space": "␣",
OS_KEY_SYMBOLS: dict[OperatingSystem, dict[KeyCode, str]] = {
OperatingSystem.WINDOWS: {**KEY_SYMBOLS, KeyCode.Meta: "⊞"},
OperatingSystem.LINUX: {**KEY_SYMBOLS, KeyCode.Meta: "Super"},
OperatingSystem.MACOS: {
**KEY_SYMBOLS,
KeyCode.Ctrl: "⌃",
KeyCode.Alt: "⌥",
KeyCode.Meta: "⌘",
},
}
MACOS_KEY_SYMBOLS: dict[str, str] = {**WIN_KEY_SYMBOLS, "Ctrl": "⌃", "Alt": "⌥", "Meta": "⌘"}
LINUX_KEY_SYMBOLS: dict[str, str] = {**WIN_KEY_SYMBOLS, "Meta": "Super"}

# key names mappings per platform
WIN_KEY_NAMES: dict[str, str] = {
"Ctrl": "Ctrl",
"Shift": "Shift",
"Alt": "Alt",
"Meta": "Win",
"Left": "Left",
"Right": "Right",
"Up": "Up",
"Down": "Down",
"Backspace": "Backspace",
"Delete": "Supr",
"Tab": "Tab",
"Escape": "Esc",
"Return": "Return",
"Enter": "Enter",
"Space": "Space",
OS_KEY_NAMES: dict[OperatingSystem, dict[KeyCode, str]] = {
OperatingSystem.WINDOWS: {KeyCode.Meta: "Win"},
OperatingSystem.LINUX: {KeyCode.Meta: "Super"},
OperatingSystem.MACOS: {
KeyCode.Ctrl: "Control",
KeyCode.Alt: "Option",
KeyCode.Meta: "Cmd",
},
}
MACOS_KEY_NAMES: dict[str, str] = {**WIN_KEY_NAMES, "Ctrl": "Control", "Alt": "Option", "Meta": "Cmd"}
LINUX_KEY_NAMES: dict[str, str] = {**WIN_KEY_NAMES, "Meta": "Super"}

seen_scancodes: Set[ScanCode] = set()
seen_keycodes: Set[KeyCode] = set()
Expand Down Expand Up @@ -679,19 +675,15 @@ def _keycode_from_string(keystr: str) -> KeyCode:

def _keycode_to_os_symbol(keycode: KeyCode, os: OperatingSystem) -> str:
"""Return key symbol for an OS for a given KeyCode."""
if os == OperatingSystem.MACOS:
return MACOS_KEY_SYMBOLS.get(str(keycode), str(keycode))
elif os == OperatingSystem.LINUX:
return LINUX_KEY_SYMBOLS.get(str(keycode), str(keycode))
return WIN_KEY_SYMBOLS.get(str(keycode), str(keycode))
if keycode in (symbols := OS_KEY_SYMBOLS.get(os, {})):
return symbols[keycode]
return str(keycode)

def _keycode_to_os_name(keycode: KeyCode, os: OperatingSystem) -> str:
"""Return key name for an OS for a given KeyCode."""
if os == OperatingSystem.MACOS:
return MACOS_KEY_NAMES.get(str(keycode), str(keycode))
elif os == OperatingSystem.LINUX:
return LINUX_KEY_NAMES.get(str(keycode), str(keycode))
return WIN_KEY_NAMES.get(str(keycode), str(keycode))
if keycode in (names := OS_KEY_NAMES.get(os, {})):
return names[keycode]
return str(keycode)

def _scancode_to_string(scancode: ScanCode) -> str:
"""Return the string representation of a ScanCode."""
Expand Down
60 changes: 19 additions & 41 deletions src/app_model/types/_keys/_keybindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,6 @@
from pydantic.annotated_handlers import GetCoreSchemaHandler
from pydantic_core import core_schema

_re_ctrl = re.compile(r"ctrl[\+|\-]")
_re_shift = re.compile(r"shift[\+|\-]")
_re_alt = re.compile(r"alt[\+|\-]")
_re_meta = re.compile(r"meta[\+|\-]")
_re_win = re.compile(r"win[\+|\-]")
_re_cmd = re.compile(r"cmd[\+|\-]")


class SimpleKeyBinding(BaseModel):
"""Represent a simple combination modifier(s) and a key, e.g. Ctrl+A."""
Expand Down Expand Up @@ -122,13 +115,13 @@ def _mods2keycodes(self) -> List[KeyCode]:
"""Create KeyCode instances list of modifiers from this SimpleKeyBinding."""
mods = []
if self.ctrl:
mods.append(KeyCode.from_string("Ctrl"))
mods.append(KeyCode.Ctrl)
if self.shift:
mods.append(KeyCode.from_string("Shift"))
mods.append(KeyCode.Shift)
if self.alt:
mods.append(KeyCode.from_string("Alt"))
mods.append(KeyCode.Alt)
if self.meta:
mods.append(KeyCode.from_string("Meta"))
mods.append(KeyCode.Meta)
return mods

def to_text(
Expand Down Expand Up @@ -311,6 +304,12 @@ def validate(cls, v: Any) -> "KeyBinding":
raise TypeError("invalid keybinding") # pragma: no cover


_re_ctrl = re.compile(r"(ctrl|control|ctl|⌃|\^)[\+|\-]")
_re_shift = re.compile(r"(shift|⇧)[\+|\-]")
_re_alt = re.compile(r"(alt|opt|option|⌥)[\+|\-]")
_re_meta = re.compile(r"(meta|super|win|windows|⊞|cmd|command|⌘)[\+|\-]")


def _parse_modifiers(input: str) -> Tuple[Dict[str, bool], str]:
"""Parse modifiers from a string (case insensitive).
Expand All @@ -319,38 +318,17 @@ def _parse_modifiers(input: str) -> Tuple[Dict[str, bool], str]:
"""
remainder = input.lower()

ctrl = False
shift = False
alt = False
meta = False

patterns = {"ctrl": _re_ctrl, "shift": _re_shift, "alt": _re_alt, "meta": _re_meta}
mods = dict.fromkeys(patterns, False)
while True:
saw_modifier = False
if _re_ctrl.match(remainder):
remainder = remainder[5:]
ctrl = True
saw_modifier = True
if _re_shift.match(remainder):
remainder = remainder[6:]
shift = True
saw_modifier = True
if _re_alt.match(remainder):
remainder = remainder[4:]
alt = True
saw_modifier = True
if _re_meta.match(remainder):
remainder = remainder[5:]
meta = True
saw_modifier = True
if _re_win.match(remainder):
remainder = remainder[4:]
meta = True
saw_modifier = True
if _re_cmd.match(remainder):
remainder = remainder[4:]
meta = True
saw_modifier = True
for key, ptrn in patterns.items():
if m := ptrn.match(remainder):
remainder = remainder[m.span()[1] :]
mods[key] = True
saw_modifier = True
break
if not saw_modifier:
break

return {"ctrl": ctrl, "shift": shift, "alt": alt, "meta": meta}, remainder
return mods, remainder
27 changes: 23 additions & 4 deletions tests/test_keybindings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import itertools
import sys
from typing import ClassVar

Expand Down Expand Up @@ -102,7 +103,7 @@ def test_simple_keybinding_single_mod(mod: str, key: str) -> None:
assert int(as_full_kb) == int(kb)


def test_simple_keybinding_multi_mod():
def test_simple_keybinding_multi_mod() -> None:
# here we're also testing that cmd and win get cast to 'KeyMod.CtrlCmd'

kb = SimpleKeyBinding.from_str("cmd+shift+A")
Expand All @@ -117,6 +118,24 @@ def test_simple_keybinding_multi_mod():
assert kb.is_modifier_key()


controls = ["ctrl", "control", "ctl", "⌃", "^"]
shifts = ["shift", "⇧"]
alts = ["alt", "opt", "option", "⌥"]
metas = ["meta", "super", "cmd", "command", "⌘", "win", "windows", "⊞"]
delimiters = ["+", "-"]
key = ["A"]
combos = [
delim.join(x)
for delim, *x in itertools.product(delimiters, controls, shifts, alts, metas, key)
]


@pytest.mark.parametrize("key", combos)
def test_keybinding_parser(key: str) -> None:
# Test all the different ways to write the modifiers
assert str(KeyBinding.from_str(key)) == "Ctrl+Shift+Alt+Meta+A"


def test_chord_keybinding() -> None:
kb = KeyBinding.from_str("Shift+A Cmd+9")
assert len(kb) == 2
Expand All @@ -132,7 +151,7 @@ def test_chord_keybinding() -> None:
assert KeyBinding.validate(kb) == kb


def test_in_dict():
def test_in_dict() -> None:
a = SimpleKeyBinding.from_str("Shift+A")
b = KeyBinding.from_str("Shift+B")

Expand All @@ -156,7 +175,7 @@ def test_in_dict():
kbs[new_a]


def test_in_model():
def test_in_model() -> None:
class M(BaseModel):
key: KeyBinding

Expand All @@ -170,7 +189,7 @@ class Config:
assert m.model_dump_json().replace('": "', '":"') == '{"key":"Shift+A B"}'


def test_standard_keybindings():
def test_standard_keybindings() -> None:
class M(BaseModel):
key: KeyBindingRule

Expand Down

0 comments on commit 58b9a23

Please sign in to comment.