Skip to content

Commit fd7d350

Browse files
Czakitlambert03
andauthored
feat: Update of shortcuts on menu rebuild (#258)
* add update of shortcuts on menu rebuild * pytest.mark.usefixtures * test draft * Fix code to deturn proper tooltip and sort keybinding * fix using text as tooltip * fix pyright * improve tests * Update src/app_model/registries/_keybindings_reg.py Co-authored-by: Talley Lambert <[email protected]> * fix code * Update src/app_model/backends/qt/_qaction.py Co-authored-by: Talley Lambert <[email protected]> --------- Co-authored-by: Talley Lambert <[email protected]>
1 parent 302b04b commit fd7d350

File tree

3 files changed

+67
-13
lines changed

3 files changed

+67
-13
lines changed

src/app_model/backends/qt/_qaction.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from typing import TYPE_CHECKING, ClassVar
55
from weakref import WeakValueDictionary
66

7+
from qtpy.QtGui import QKeySequence
8+
79
from app_model import Application
810
from app_model.expressions import Expr
911
from app_model.types import ToggleRule
@@ -52,6 +54,15 @@ def __init__(
5254
self._keybinding_tooltip = f"({kb.keybinding.to_text()})"
5355
self.triggered.connect(self._on_triggered)
5456

57+
def _update_keybinding(self) -> None:
58+
shortcut = self.shortcut()
59+
if kb := self._app.keybindings.get_keybinding(self._command_id):
60+
self.setShortcut(QKeyBindingSequence(kb.keybinding))
61+
self._keybinding_tooltip = f"({kb.keybinding.to_text()})"
62+
elif not shortcut.isEmpty():
63+
self.setShortcut(QKeySequence())
64+
self._keybinding_tooltip = ""
65+
5566
def _on_triggered(self, checked: bool) -> None:
5667
# execute_command returns a Future, for the sake of eventually being
5768
# asynchronous without breaking the API. For now, we call result()
@@ -84,23 +95,29 @@ def __init__(
8495
) -> None:
8596
super().__init__(command_rule.id, app, parent)
8697
self._cmd_rule = command_rule
98+
self._tooltip = command_rule.tooltip or ""
8799
if use_short_title and command_rule.short_title:
88100
self.setText(command_rule.short_title) # pragma: no cover
89101
else:
90102
self.setText(command_rule.title)
91103
if command_rule.icon:
92104
self.setIcon(to_qicon(command_rule.icon))
93105
self.setIconVisibleInMenu(command_rule.icon_visible_in_menu)
94-
if command_rule.tooltip:
95-
self.setToolTip(command_rule.tooltip)
96106
if command_rule.status_tip:
97107
self.setStatusTip(command_rule.status_tip)
98108
if command_rule.toggled is not None:
99109
self.setCheckable(True)
100110
self._refresh()
101-
tooltip_with_keybinding = (
102-
f"{self.toolTip()} {self._keybinding_tooltip}".rstrip()
103-
)
111+
tooltip_with_keybinding = f"{self._tooltip} {self._keybinding_tooltip}".rstrip()
112+
self.setToolTip(tooltip_with_keybinding)
113+
114+
def setText(self, text: str | None) -> None:
115+
super().setText(text)
116+
self._tooltip = self._tooltip or text or ""
117+
118+
def _update_keybinding(self) -> None:
119+
super()._update_keybinding()
120+
tooltip_with_keybinding = f"{self._tooltip} {self._keybinding_tooltip}".rstrip()
104121
self.setToolTip(tooltip_with_keybinding)
105122

106123
def update_from_context(self, ctx: Mapping[str, object]) -> None:
@@ -176,6 +193,7 @@ def create(
176193
cache_key = QMenuItemAction._cache_key(app, menu_item)
177194
if cache_key in cls._cache:
178195
res = cls._cache[cache_key]
196+
res._update_keybinding()
179197
res.setParent(parent)
180198
return res
181199

src/app_model/registries/_keybindings_reg.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,9 @@ def __repr__(self) -> str:
194194
def get_keybinding(self, command_id: str) -> _RegisteredKeyBinding | None:
195195
"""Return the first keybinding that matches the given command ID."""
196196
# TODO: improve me.
197-
return next(
198-
(entry for entry in self._keybindings if entry.command_id == command_id),
199-
None,
200-
)
197+
matches = (kb for kb in self._keybindings if kb.command_id == command_id)
198+
sorted_matches = sorted(matches, key=lambda x: x.source, reverse=True)
199+
return next(iter(sorted_matches), None)
201200

202201
def get_context_prioritized_keybinding(
203202
self, key: int, context: Mapping[str, object]

tests/test_qt/test_qactions.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
Action,
99
CommandRule,
1010
KeyBindingRule,
11+
KeyBindingSource,
1112
KeyCode,
1213
MenuItem,
1314
ToggleRule,
@@ -18,7 +19,8 @@
1819
from conftest import FullApp
1920

2021

21-
def test_cache_qaction(qapp, full_app: "FullApp") -> None:
22+
@pytest.mark.usefixtures("qapp")
23+
def test_cache_qaction(full_app: "FullApp") -> None:
2224
action = next(
2325
i for k, items in full_app.menus for i in items if isinstance(i, MenuItem)
2426
)
@@ -28,7 +30,8 @@ def test_cache_qaction(qapp, full_app: "FullApp") -> None:
2830
assert repr(a1).startswith("QMenuItemAction")
2931

3032

31-
def test_toggle_qaction(qapp, simple_app: "Application") -> None:
33+
@pytest.mark.usefixtures("qapp")
34+
def test_toggle_qaction(simple_app: "Application") -> None:
3235
mock = Mock()
3336
x = False
3437

@@ -75,6 +78,7 @@ def test_icon_visible_in_menu(qapp, simple_app: "Application") -> None:
7578
assert not q_action.isIconVisibleInMenu()
7679

7780

81+
@pytest.mark.usefixtures("qapp")
7882
@pytest.mark.parametrize(
7983
("tooltip", "expected_tooltip"),
8084
[
@@ -83,7 +87,7 @@ def test_icon_visible_in_menu(qapp, simple_app: "Application") -> None:
8387
],
8488
)
8589
def test_tooltip(
86-
qapp, simple_app: "Application", tooltip: str, expected_tooltip: str
90+
simple_app: "Application", tooltip: str, expected_tooltip: str
8791
) -> None:
8892
action = Action(
8993
id="test.tooltip", title="Test tooltip", tooltip=tooltip, callback=lambda: None
@@ -93,6 +97,7 @@ def test_tooltip(
9397
assert q_action.toolTip() == expected_tooltip
9498

9599

100+
@pytest.mark.usefixtures("qapp")
96101
@pytest.mark.parametrize(
97102
("tooltip", "tooltip_with_keybinding", "tooltip_without_keybinding"),
98103
[
@@ -105,7 +110,6 @@ def test_tooltip(
105110
],
106111
)
107112
def test_keybinding_in_tooltip(
108-
qapp,
109113
simple_app: "Application",
110114
tooltip: str,
111115
tooltip_with_keybinding: str,
@@ -127,3 +131,36 @@ def test_keybinding_in_tooltip(
127131
# check setting tooltip manually removes keybinding info
128132
q_action.setToolTip(tooltip)
129133
assert q_action.toolTip() == tooltip_without_keybinding
134+
135+
136+
@pytest.mark.usefixtures("qapp")
137+
def test_update_keybinding_in_tooltip(
138+
simple_app: "Application",
139+
) -> None:
140+
action = Action(
141+
id="test.update.keybinding.tooltip",
142+
title="Test update keybinding tooltip",
143+
callback=lambda: None,
144+
tooltip="Initial tooltip",
145+
keybindings=[KeyBindingRule(primary=KeyCode.KeyK)],
146+
)
147+
dispose1 = simple_app.register_action(action)
148+
149+
q_action = QCommandRuleAction(action, simple_app)
150+
assert q_action.toolTip() == "Initial tooltip (K)"
151+
152+
# Update the keybinding
153+
dispose2 = simple_app.keybindings.register_keybinding_rule(
154+
"test.update.keybinding.tooltip",
155+
KeyBindingRule(primary=KeyCode.KeyL, source=KeyBindingSource.USER),
156+
)
157+
q_action._update_keybinding()
158+
assert q_action.toolTip() == "Initial tooltip (L)"
159+
160+
dispose2()
161+
q_action._update_keybinding()
162+
assert q_action.toolTip() == "Initial tooltip (K)"
163+
164+
dispose1()
165+
q_action._update_keybinding()
166+
assert q_action.toolTip() == "Initial tooltip"

0 commit comments

Comments
 (0)