Skip to content

Commit

Permalink
feat: initial exploration for keybinding source addition and inverse …
Browse files Browse the repository at this point in the history
…map for keybinding registry
  • Loading branch information
dalthviz committed Nov 29, 2024
1 parent 56d0d4f commit e206dbb
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 4 deletions.
64 changes: 61 additions & 3 deletions src/app_model/registries/_keybindings_reg.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from __future__ import annotations

from bisect import insort_left
from typing import TYPE_CHECKING, Callable, NamedTuple

from psygnal import Signal

from app_model.types import KeyBinding

if TYPE_CHECKING:
from collections.abc import Iterator
from collections.abc import Iterator, Mapping
from typing import TypeVar

from app_model import expressions
from app_model.types import Action, DisposeCallable, KeyBindingRule
from app_model.types._constants import KeyBindingSource

CommandDecorator = Callable[[Callable], Callable]
CommandCallable = TypeVar("CommandCallable", bound=Callable)
Expand All @@ -23,8 +25,28 @@ class _RegisteredKeyBinding(NamedTuple):
keybinding: KeyBinding # the keycode to bind to
command_id: str # the command to run
weight: int # the weight of the binding, for prioritization
source: KeyBindingSource # who defined the binding, for prioritization
when: expressions.Expr | None = None # condition to enable keybinding

def __gt__(self, other: object) -> bool:
if not isinstance(other, _RegisteredKeyBinding):

Check warning on line 32 in src/app_model/registries/_keybindings_reg.py

View check run for this annotation

Codecov / codecov/patch

src/app_model/registries/_keybindings_reg.py#L32

Added line #L32 was not covered by tests
return NotImplemented
return self.source.value > other.source.value or (

Check warning on line 34 in src/app_model/registries/_keybindings_reg.py

View check run for this annotation

Codecov / codecov/patch

src/app_model/registries/_keybindings_reg.py#L34

Added line #L34 was not covered by tests
self.source == other.source and self.weight > other.weight
)

def __lt__(self, other: object) -> bool:
if not isinstance(other, _RegisteredKeyBinding):

Check warning on line 39 in src/app_model/registries/_keybindings_reg.py

View check run for this annotation

Codecov / codecov/patch

src/app_model/registries/_keybindings_reg.py#L39

Added line #L39 was not covered by tests
return NotImplemented
return self.source.value < other.source.value or (

Check warning on line 41 in src/app_model/registries/_keybindings_reg.py

View check run for this annotation

Codecov / codecov/patch

src/app_model/registries/_keybindings_reg.py#L41

Added line #L41 was not covered by tests
self.source == other.source and self.weight < other.weight
)

def __eq__(self, other: object) -> bool:
if not isinstance(other, _RegisteredKeyBinding):
return NotImplemented
return self.source.value == other.source.value and self.weight == other.weight


class KeyBindingsRegistry:
"""Registry for keybindings.
Expand All @@ -38,9 +60,11 @@ class KeyBindingsRegistry:
"""

registered = Signal()
unregistered = Signal()

def __init__(self) -> None:
self._keybindings: list[_RegisteredKeyBinding] = []
self._keymap: dict[int, list[_RegisteredKeyBinding]] = {}
self._filter_keybinding: Callable[[KeyBinding], str] | None = None

@property
Expand Down Expand Up @@ -123,6 +147,7 @@ def register_keybinding_rule(
A callable that can be used to unregister the keybinding
"""
if plat_keybinding := rule._bind_to_current_platform():
# list registry
keybinding = KeyBinding.validate(plat_keybinding)
if self._filter_keybinding:
msg = self._filter_keybinding(keybinding)
Expand All @@ -133,13 +158,31 @@ def register_keybinding_rule(
command_id=id,
weight=rule.weight,
when=rule.when,
source=rule.source,
)
self._keybindings.append(entry)

# inverse map registry
kb = keybinding.to_int()
if kb not in self._keymap:
entries: list[_RegisteredKeyBinding] = []
self._keymap[kb] = entries
else:
entries = self._keymap[kb]

Check warning on line 171 in src/app_model/registries/_keybindings_reg.py

View check run for this annotation

Codecov / codecov/patch

src/app_model/registries/_keybindings_reg.py#L171

Added line #L171 was not covered by tests
insort_left(entries, entry)

self.registered.emit()

def _dispose() -> None:
# list registry remove
self._keybindings.remove(entry)

# inverse map registry remove
entries.remove(entry)
self.unregistered.emit()
if len(entries) == 0:
del self._keymap[kb]

return _dispose
return None # pragma: no cover

Expand All @@ -150,9 +193,24 @@ def __repr__(self) -> str:
name = self.__class__.__name__
return f"<{name} at {hex(id(self))} ({len(self._keybindings)} bindings)>"

def get_keybinding(self, key: str) -> _RegisteredKeyBinding | None:
def get_keybinding(self, command_id: str) -> _RegisteredKeyBinding | None:
"""Return the first keybinding that matches the given command ID."""
# TODO: improve me.
return next(
(entry for entry in self._keybindings if entry.command_id == key), None
(entry for entry in self._keybindings if entry.command_id == command_id),
None,
)

def get_context_prioritized_keybinding(
self, key: int, context: Mapping[str, object]
) -> _RegisteredKeyBinding | None:
if key not in self._keymap:
return None
return next(

Check warning on line 209 in src/app_model/registries/_keybindings_reg.py

View check run for this annotation

Codecov / codecov/patch

src/app_model/registries/_keybindings_reg.py#L207-L209

Added lines #L207 - L209 were not covered by tests
(
entry
for entry in self._keymap[key]
if entry.when is None or entry.when.eval(context)
),
None,
)
8 changes: 8 additions & 0 deletions src/app_model/types/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
from enum import Enum


class KeyBindingSource(Enum):
"""Keybinding source enum."""

SYSTEM = 0
PLUGIN = 1
USER = 2


class OperatingSystem(Enum):
"""Operating system enum."""

Expand Down
6 changes: 5 additions & 1 deletion src/app_model/types/_keybinding_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from app_model import expressions

from ._base import _BaseModel
from ._constants import OperatingSystem
from ._constants import KeyBindingSource, OperatingSystem
from ._keys import StandardKeyBinding

KeyEncoding = Union[int, str]
Expand Down Expand Up @@ -49,6 +49,10 @@ class KeyBindingRule(_BaseModel):
description="Internal weight used to sort keybindings. "
"This is not part of the plugin schema",
)
source: KeyBindingSource = Field(
KeyBindingSource.SYSTEM,
description="Who registered the keybinding. Used to sort keybindings.",
)

def _bind_to_current_platform(self) -> Optional[KeyEncoding]:
if _WIN and self.win:
Expand Down

0 comments on commit e206dbb

Please sign in to comment.