Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
52 changes: 15 additions & 37 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 @@ -251,6 +244,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 @@ -259,38 +258,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 @@ -45,7 +46,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 @@ -60,6 +61,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 @@ -75,7 +94,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 @@ -99,7 +118,7 @@ def test_in_dict():
kbs[new_a]


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

Expand All @@ -113,7 +132,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