Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve support for overriding default menu items #2636

Merged
merged 15 commits into from
Jun 12, 2024
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
4 changes: 3 additions & 1 deletion android/src/toga_android/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from org.beeware.android import IPythonApp, MainActivity

from toga.command import Command, Group, Separator
from toga.handlers import simple_handler

from .libs import events
from .screens import Screen as ScreenImpl
Expand Down Expand Up @@ -207,9 +208,10 @@ def create_app_commands(self):
self.interface.commands.add(
# About should be the last item in the menu, in a section on its own.
Command(
lambda _: self.interface.about(),
simple_handler(self.interface.about),
f"About {self.interface.formal_name}",
section=sys.maxsize,
id=Command.ABOUT,
),
)

Expand Down
1 change: 1 addition & 0 deletions changes/2636.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Commands can now be retrieved by ID. System-installed commands (such as "About" and "Visit Homepage") are installed using a known ID that can be used at runtime to manipulate those commands.
1 change: 1 addition & 0 deletions changes/90.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
An app can now define a ``preferences()`` method to hook into the default menu item generated by Toga.
71 changes: 36 additions & 35 deletions cocoa/src/toga_cocoa/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
from rubicon.objc.eventloop import CocoaLifecycle, EventLoopPolicy

import toga
from toga.command import Separator
from toga.handlers import NativeHandler
from toga.app import overridden
from toga.command import Command, Separator
from toga.handlers import NativeHandler, simple_handler

from .keys import cocoa_key
from .libs import (
Expand Down Expand Up @@ -162,9 +163,6 @@ def create(self):
# Commands and menus
######################################################################

def _menu_about(self, command, **kwargs):
self.interface.about()

def _menu_close_all_windows(self, command, **kwargs):
# Convert to a list to so that we're not altering a set while iterating
for window in list(self.interface.windows):
Expand All @@ -178,74 +176,76 @@ def _menu_minimize(self, command, **kwargs):
if self.interface.current_window:
self.interface.current_window._impl.native.miniaturize(None)

def _menu_quit(self, command, **kwargs):
self.interface.on_exit()

def _menu_visit_homepage(self, command, **kwargs):
self.interface.visit_homepage()

def create_app_commands(self):
formal_name = self.interface.formal_name
self.interface.commands.add(
# ---- App menu -----------------------------------
toga.Command(
self._menu_about,
Command(
simple_handler(self.interface.about),
"About " + formal_name,
group=toga.Group.APP,
id=Command.ABOUT,
),
toga.Command(
None,
# Include a preferences menu item; but only enable it if the user has
# overridden it in their App class.
Command(
simple_handler(self.interface.preferences),
"Settings\u2026",
shortcut=toga.Key.MOD_1 + ",",
group=toga.Group.APP,
section=20,
enabled=overridden(self.interface.preferences),
id=Command.PREFERENCES,
),
toga.Command(
Command(
NativeHandler(SEL("hide:")),
"Hide " + formal_name,
shortcut=toga.Key.MOD_1 + "h",
group=toga.Group.APP,
order=0,
section=sys.maxsize - 1,
),
toga.Command(
Command(
NativeHandler(SEL("hideOtherApplications:")),
"Hide Others",
shortcut=toga.Key.MOD_1 + toga.Key.MOD_2 + "h",
group=toga.Group.APP,
order=1,
section=sys.maxsize - 1,
),
toga.Command(
Command(
NativeHandler(SEL("unhideAllApplications:")),
"Show All",
group=toga.Group.APP,
order=2,
section=sys.maxsize - 1,
),
# Quit should always be the last item, in a section on its own
toga.Command(
self._menu_quit,
"Quit " + formal_name,
# Quit should always be the last item, in a section on its own. Invoke
# `on_exit` rather than `exit`, because we want to trigger the "OK to exit?"
# logic. It's already a bound handler, so we can use it directly.
Command(
self.interface.on_exit,
f"Quit {formal_name}",
shortcut=toga.Key.MOD_1 + "q",
group=toga.Group.APP,
section=sys.maxsize,
id=Command.EXIT,
),
# ---- File menu ----------------------------------
# This is a bit of an oddity. Apple HIG apps that don't have tabs as
# part of their interface (so, Preview and Numbers, but not Safari)
# have a "Close" item that becomes "Close All" when you press Option
# (MOD_2). That behavior isn't something we're currently set up to
# implement, so we live with a separate menu item for now.
toga.Command(
Command(
self._menu_close_window,
"Close",
shortcut=toga.Key.MOD_1 + "w",
group=toga.Group.FILE,
order=1,
section=50,
),
toga.Command(
Command(
self._menu_close_all_windows,
"Close All",
shortcut=toga.Key.MOD_2 + toga.Key.MOD_1 + "w",
Expand All @@ -254,60 +254,60 @@ def create_app_commands(self):
section=50,
),
# ---- Edit menu ----------------------------------
toga.Command(
Command(
NativeHandler(SEL("undo:")),
"Undo",
shortcut=toga.Key.MOD_1 + "z",
group=toga.Group.EDIT,
order=10,
),
toga.Command(
Command(
NativeHandler(SEL("redo:")),
"Redo",
shortcut=toga.Key.SHIFT + toga.Key.MOD_1 + "z",
group=toga.Group.EDIT,
order=20,
),
toga.Command(
Command(
NativeHandler(SEL("cut:")),
"Cut",
shortcut=toga.Key.MOD_1 + "x",
group=toga.Group.EDIT,
section=10,
order=10,
),
toga.Command(
Command(
NativeHandler(SEL("copy:")),
"Copy",
shortcut=toga.Key.MOD_1 + "c",
group=toga.Group.EDIT,
section=10,
order=20,
),
toga.Command(
Command(
NativeHandler(SEL("paste:")),
"Paste",
shortcut=toga.Key.MOD_1 + "v",
group=toga.Group.EDIT,
section=10,
order=30,
),
toga.Command(
Command(
NativeHandler(SEL("pasteAsPlainText:")),
"Paste and Match Style",
shortcut=toga.Key.MOD_2 + toga.Key.SHIFT + toga.Key.MOD_1 + "v",
group=toga.Group.EDIT,
section=10,
order=40,
),
toga.Command(
Command(
NativeHandler(SEL("delete:")),
"Delete",
group=toga.Group.EDIT,
section=10,
order=50,
),
toga.Command(
Command(
NativeHandler(SEL("selectAll:")),
"Select All",
shortcut=toga.Key.MOD_1 + "a",
Expand All @@ -316,18 +316,19 @@ def create_app_commands(self):
order=60,
),
# ---- Window menu ----------------------------------
toga.Command(
Command(
self._menu_minimize,
"Minimize",
shortcut=toga.Key.MOD_1 + "m",
group=toga.Group.WINDOW,
),
# ---- Help menu ----------------------------------
toga.Command(
self._menu_visit_homepage,
Command(
simple_handler(self.interface.visit_homepage),
"Visit homepage",
enabled=self.interface.home_page is not None,
group=toga.Group.HELP,
id=Command.VISIT_HOMEPAGE,
),
)

Expand Down
32 changes: 31 additions & 1 deletion core/src/toga/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,21 @@ def _default_title(self) -> str:
return self.doc.path.name


def overridable(method):
"""Decorate the method as being user-overridable"""
method._overridden = True
return method


def overridden(coroutine_or_method):
"""Has the user overridden this method?

This is based on the method *not* having a ``_overridden`` attribute. Overridable
default methods have this attribute; user-defined method will not.
"""
return not hasattr(coroutine_or_method, "_overridden")


class App:
#: The currently running :class:`~toga.App`. Since there can only be one running
#: Toga app in a process, this is available as a class property via ``toga.App.app``.
Expand Down Expand Up @@ -746,10 +761,25 @@ def beep(self) -> None:
"""Play the default system notification sound."""
self._impl.beep()

@overridable
def preferences(self) -> None:
"""Open a preferences panel for the app.

By default, this will do nothing, and the Preferences/Settings menu item
will be disabled. However, if you override this method in your App class,
the menu item will be enabled, and this method will be invoked when the
menu item is selected.
"""
# Default implementation won't ever be invoked, because the menu item
# isn't enabled unless it's overridden.
pass # pragma: no cover

def visit_homepage(self) -> None:
"""Open the application's :any:`home_page` in the default browser.

If the :any:`home_page` is ``None``, this is a no-op.
This method is invoked as a handler by the "Visit homepage" default menu item.
If the :any:`home_page` is ``None``, this is a no-op, and the default menu item
will be disabled.
"""
if self.home_page is not None:
webbrowser.open(self.home_page)
Expand Down
Loading
Loading