diff --git a/src/app_model/backends/qt/_qaction.py b/src/app_model/backends/qt/_qaction.py index 748e77f..49cb5bc 100644 --- a/src/app_model/backends/qt/_qaction.py +++ b/src/app_model/backends/qt/_qaction.py @@ -44,7 +44,10 @@ def __init__( self.triggered.connect(self._on_triggered) def _on_triggered(self, checked: bool) -> None: - self._app.commands.execute_command(self._command_id) + # execute_command returns a Future, for the sake of eventually being + # asynchronous without breaking the API. For now, we call result() + # to raise any exceptions. + self._app.commands.execute_command(self._command_id).result() class QCommandRuleAction(QCommandAction): @@ -80,10 +83,13 @@ def __init__( self.setToolTip(command_rule.tooltip) if command_rule.status_tip: self.setStatusTip(command_rule.status_tip) + self.setCheckable(command_rule.toggled is not None) def update_from_context(self, ctx: Mapping[str, object]) -> None: """Update the enabled state of this menu item from `ctx`.""" self.setEnabled(expr.eval(ctx) if (expr := self._cmd_rule.enablement) else True) + if expr := self._cmd_rule.toggled: + self.setChecked(expr.eval(ctx)) class QMenuItemAction(QCommandRuleAction): diff --git a/src/app_model/types/_command_rule.py b/src/app_model/types/_command_rule.py index 2b7aa42..dabf556 100644 --- a/src/app_model/types/_command_rule.py +++ b/src/app_model/types/_command_rule.py @@ -52,6 +52,11 @@ class CommandRule(_StrictModel): "the UI. Menus pick either `title` or `short_title` depending on the context " "in which they show commands.", ) + toggled: Optional[expressions.Expr] = Field( + None, + description="(Optional) Condition under which the command should appear " + "in any GUI representation (like a menu).", + ) def _as_command_rule(self) -> "CommandRule": """Simplify (subclasses) to a plain CommandRule.""" diff --git a/tests/conftest.py b/tests/conftest.py index a814580..ed15e21 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,6 +32,7 @@ class Commands: REDO = "redo" COPY = "copy" PASTE = "paste" + TOGGLE_THING = "toggle_thing" OPEN_FROM_A = "open.from_a" OPEN_FROM_B = "open.from_b" UNIMPORTABLE = "unimportable" @@ -195,6 +196,13 @@ def build_app(name: str = "complete_test_app") -> FullApp: title="Will raise an error", callback=_raise_an_error, ), + Action( + id=Commands.TOGGLE_THING, + title="Toggle Thing", + callback=lambda: None, + menus=[{"id": Menus.HELP}], + toggled="thing_toggled", + ), ] for action in actions: app.register_action(action) diff --git a/tests/test_qt/test_qmenu.py b/tests/test_qt/test_qmenu.py index d41a72a..b3996a2 100644 --- a/tests/test_qt/test_qmenu.py +++ b/tests/test_qt/test_qmenu.py @@ -127,3 +127,16 @@ def test_cache_action(full_app: "FullApp") -> None: a1 = QMenuItemAction(action, full_app) a2 = QMenuItemAction(action, full_app) assert a1 is a2 + + +def test_toggled_menu_item(qtbot: "QtBot", full_app: "FullApp") -> None: + app = full_app + menu = QModelMenu(app.Menus.HELP, app) + qtbot.addWidget(menu) + + menu.update_from_context({"thing_toggled": True}) + action = menu.findAction(app.Commands.TOGGLE_THING) + assert action.isChecked() + + menu.update_from_context({"thing_toggled": False}) + assert not action.isChecked()