From daf3cf390f704b1bb76fb703fb7d653dee994e4a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 6 Nov 2023 07:42:41 +0800 Subject: [PATCH 01/19] Correct the creation of menubars in the web backend. --- changes/2194.bugfix.rst | 1 + web/src/toga_web/app.py | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 changes/2194.bugfix.rst diff --git a/changes/2194.bugfix.rst b/changes/2194.bugfix.rst new file mode 100644 index 0000000000..5e064641e3 --- /dev/null +++ b/changes/2194.bugfix.rst @@ -0,0 +1 @@ +The web backend no longer generates a duplicate titlebar. diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index 839892acc1..4706ea3e35 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -23,7 +23,7 @@ def create(self): self.interface.commands.add( # ---- Help menu ---------------------------------- toga.Command( - lambda _: self.interface.about(), + self._menu_about, "About " + formal_name, group=toga.Group.HELP, ), @@ -34,7 +34,9 @@ def create(self): ), ) - # Create the menus. + # Create the menus. This is done before main window content to ensure + # the
for the menubar is inserted before the
for the + # main window. self.create_menus() # Call user code to populate the main window @@ -103,8 +105,10 @@ def create_menus(self): else: help_menu_container.appendChild(submenu) + menubar_id = f"{self.interface.app_id}-header" self.menubar = create_element( "header", + id=menubar_id, classes=["toga"], children=[ create_element( @@ -124,8 +128,15 @@ def create_menus(self): ], ) - # Menubar exists at the app level. - self.native.appendChild(self.menubar) + # If there's an existing menubar, replace it. + old_menubar = js.document.getElementById(menubar_id) + if old_menubar: + old_menubar.replaceWith(self.menubar) + else: + self.native.append(self.menubar) + + def _menu_about(self, event, widget, **kwargs): + self.interface.about() def main_loop(self): self.create() From 14b860ec65d8dc9cb0334281f946a524ed766969 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 6 Nov 2023 07:46:42 +0800 Subject: [PATCH 02/19] Add changenote for about dialog fix. --- changes/2103.docs.rst | 1 - changes/2195.bugfix.rst | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 changes/2103.docs.rst create mode 100644 changes/2195.bugfix.rst diff --git a/changes/2103.docs.rst b/changes/2103.docs.rst deleted file mode 100644 index b57a159b25..0000000000 --- a/changes/2103.docs.rst +++ /dev/null @@ -1 +0,0 @@ -The widget screenshots were updated to provide examples of widgets on every platform. diff --git a/changes/2195.bugfix.rst b/changes/2195.bugfix.rst new file mode 100644 index 0000000000..4fb4695357 --- /dev/null +++ b/changes/2195.bugfix.rst @@ -0,0 +1 @@ +An issue with the display of the About dialog on the web backend was corrected. From f5733670a46f0c46fd17e970ce1bd98ff9089306 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Mon, 6 Nov 2023 10:58:57 +0000 Subject: [PATCH 03/19] Enable test_keys on WinForms --- android/tests_backend/app.py | 5 ++- cocoa/src/toga_cocoa/keys.py | 4 --- cocoa/tests_backend/app.py | 3 ++ core/src/toga/keys.py | 38 ++++++++++---------- gtk/src/toga_gtk/keys.py | 3 -- gtk/tests_backend/app.py | 6 ++-- iOS/tests_backend/app.py | 5 ++- testbed/tests/test_keys.py | 11 ++++-- winforms/src/toga_winforms/keys.py | 56 +++++++++++++++++++++++++++--- winforms/tests_backend/app.py | 7 +++- 10 files changed, 95 insertions(+), 43 deletions(-) diff --git a/android/tests_backend/app.py b/android/tests_backend/app.py index 436b114d29..76365fcc4d 100644 --- a/android/tests_backend/app.py +++ b/android/tests_backend/app.py @@ -11,6 +11,8 @@ class AppProbe(BaseProbe): + supports_key = False + def __init__(self, app): super().__init__(app) self.native = self.app._impl.native @@ -88,9 +90,6 @@ def activate_menu_close_all_windows(self): def activate_menu_minimize(self): xfail("This backend doesn't have a window management menu") - def keystroke(self, combination): - xfail("This backend doesn't use keyboard shortcuts") - def enter_background(self): xfail( "This is possible (https://stackoverflow.com/a/7071289), but there's no " diff --git a/cocoa/src/toga_cocoa/keys.py b/cocoa/src/toga_cocoa/keys.py index e386d9ca30..9648bba62b 100644 --- a/cocoa/src/toga_cocoa/keys.py +++ b/cocoa/src/toga_cocoa/keys.py @@ -2,7 +2,6 @@ from toga import Key from toga_cocoa.libs import ( - NSEventModifierFlagCapsLock, NSEventModifierFlagCommand, NSEventModifierFlagControl, NSEventModifierFlagOption, @@ -151,8 +150,6 @@ def toga_key(event): modifiers.add(Key.MOD_2) if event.modifierFlags & NSEventModifierFlagControl: modifiers.add(Key.MOD_3) - if event.modifierFlags & NSEventModifierFlagCapsLock: - modifiers.add(Key.CAPSLOCK) return {"key": key, "modifiers": modifiers} @@ -213,7 +210,6 @@ def toga_key(event): COCOA_MODIFIERS = { Key.SHIFT: NSEventModifierFlagShift, - Key.CAPSLOCK: NSEventModifierFlagCapsLock, Key.MOD_1: NSEventModifierFlagCommand, Key.MOD_2: NSEventModifierFlagOption, Key.MOD_3: NSEventModifierFlagControl, diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index 25f0032787..91f85c0aee 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -16,6 +16,9 @@ class AppProbe(BaseProbe): + supports_key = True + supports_key_mod3 = True + def __init__(self, app): super().__init__() self.app = app diff --git a/core/src/toga/keys.py b/core/src/toga/keys.py index d6d02f6fd8..17009c42d4 100644 --- a/core/src/toga/keys.py +++ b/core/src/toga/keys.py @@ -92,25 +92,25 @@ class Key(Enum): MOD_2 = "" # OPT on macOS, ALT on Linux/Windows MOD_3 = "" # CTRL on macOS, Flag on Windows, Tux on Linux - F1 = "" - F2 = "" - F3 = "" - F4 = "" - F5 = "" - F6 = "" - F7 = "" - F8 = "" - F9 = "" - F10 = "" - F11 = "" - F12 = "" - F13 = "" - F14 = "" - F15 = "" - F16 = "" - F17 = "" - F18 = "" - F19 = "" + F1 = "" + F2 = "" + F3 = "" + F4 = "" + F5 = "" + F6 = "" + F7 = "" + F8 = "" + F9 = "" + F10 = "" + F11 = "" + F12 = "" + F13 = "" + F14 = "" + F15 = "" + F16 = "" + F17 = "" + F18 = "" + F19 = "" EJECT = "" diff --git a/gtk/src/toga_gtk/keys.py b/gtk/src/toga_gtk/keys.py index 9d48e4da9b..7c92fab4c4 100644 --- a/gtk/src/toga_gtk/keys.py +++ b/gtk/src/toga_gtk/keys.py @@ -215,7 +215,6 @@ } GTK_MODIFIER_CODES = { - Key.CAPSLOCK: "", Key.SHIFT: "", Key.MOD_1: "", Key.MOD_2: "", @@ -229,8 +228,6 @@ def toga_key(event): modifiers = set() - if event.state & Gdk.ModifierType.LOCK_MASK: - modifiers.add(Key.CAPSLOCK) if event.state & Gdk.ModifierType.SHIFT_MASK: modifiers.add(Key.SHIFT) if event.state & Gdk.ModifierType.CONTROL_MASK: diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index e08bd657d0..bc69ba1089 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -9,6 +9,9 @@ class AppProbe(BaseProbe): + supports_key = True + supports_key_mod3 = True + def __init__(self, app): super().__init__() self.app = app @@ -130,9 +133,6 @@ def keystroke(self, combination): if "" in accel: state |= Gdk.ModifierType.HYPER_MASK accel = accel.replace("", "") - if "" in accel: - state |= Gdk.ModifierType.LOCK_MASK - accel = accel.replace("", "") if "" in accel: state |= Gdk.ModifierType.SHIFT_MASK accel = accel.replace("", "") diff --git a/iOS/tests_backend/app.py b/iOS/tests_backend/app.py index a62fd46cb5..98a4ba0369 100644 --- a/iOS/tests_backend/app.py +++ b/iOS/tests_backend/app.py @@ -13,6 +13,8 @@ class AppProbe(BaseProbe): + supports_key = False + def __init__(self, app): super().__init__() self.app = app @@ -54,9 +56,6 @@ def activate_menu_visit_homepage(self): def assert_menu_item(self, path, enabled): pytest.skip("Menus not implemented on iOS") - def keystroke(self, combination): - pytest.skip("iOS doesn't use keyboard shortcuts") - def enter_background(self): self.native.delegate.applicationWillResignActive(self.native) self.native.delegate.applicationDidEnterBackground(self.native) diff --git a/testbed/tests/test_keys.py b/testbed/tests/test_keys.py index a453c78ca3..76c662f76e 100644 --- a/testbed/tests/test_keys.py +++ b/testbed/tests/test_keys.py @@ -14,7 +14,6 @@ (Key.MOD_1 + "a", {"key": Key.A, "modifiers": {Key.MOD_1}}), (Key.MOD_2 + "a", {"key": Key.A, "modifiers": {Key.MOD_2}}), (Key.MOD_3 + "a", {"key": Key.A, "modifiers": {Key.MOD_3}}), - (Key.CAPSLOCK + "a", {"key": Key.A, "modifiers": {Key.CAPSLOCK}}), # modifier combinations ( Key.MOD_1 + Key.MOD_2 + "a", @@ -39,4 +38,12 @@ ) def test_key_combinations(app_probe, key_combo, key_data): """Key combinations can be round tripped""" - assert app_probe.keystroke(key_combo) == key_data + + if not app_probe.supports_key: + pytest.xfail("This backend doesn't use keyboard shortcuts") + + if (Key.MOD_3 in key_data["modifiers"]) and not app_probe.supports_key_mod3: + with pytest.raises(ValueError): + app_probe.keystroke(key_combo) + else: + assert app_probe.keystroke(key_combo) == key_data diff --git a/winforms/src/toga_winforms/keys.py b/winforms/src/toga_winforms/keys.py index 01df70a0e1..33208d0657 100644 --- a/winforms/src/toga_winforms/keys.py +++ b/winforms/src/toga_winforms/keys.py @@ -1,6 +1,7 @@ import operator import re from functools import reduce +from string import ascii_lowercase import System.Windows.Forms as WinForms @@ -12,24 +13,39 @@ Key.SHIFT: WinForms.Keys.Shift, } -WINFORMS_KEYS_MAP = { - Key.PLUS.value: WinForms.Keys.Oemplus, - Key.MINUS.value: WinForms.Keys.OemMinus, +WINFORMS_KEYS = { + "+": WinForms.Keys.Oemplus, + "-": WinForms.Keys.OemMinus, } -WINFORMS_KEYS_MAP.update( +WINFORMS_KEYS.update( {str(digit): getattr(WinForms.Keys, f"D{digit}") for digit in range(10)} ) +SHIFTED_KEYS = {symbol: number for symbol, number in zip("!@#$%^&*()", "1234567890")} +SHIFTED_KEYS.update( + {lower.upper(): lower for lower in ascii_lowercase}, +) + def toga_to_winforms_key(key): + # Convert a Key object into string form. + try: + key = key.value + except AttributeError: + pass + codes = [] for modifier, modifier_code in WINFORMS_MODIFIERS.items(): if modifier.value in key: codes.append(modifier_code) key = key.replace(modifier.value, "") + if lower := SHIFTED_KEYS.get(key): + key = lower + codes.append(WinForms.Keys.Shift) + try: - codes.append(WINFORMS_KEYS_MAP[key]) + codes.append(WINFORMS_KEYS[key]) except KeyError: if match := re.fullmatch(r"<(.+)>", key): key = match[1] @@ -39,3 +55,33 @@ def toga_to_winforms_key(key): raise ValueError(f"unknown key: {key!r}") from None return reduce(operator.or_, codes) + + +def winforms_to_toga_key(code): + modifiers = set() + + code_names = str(code).split(", ") + for toga_mod, code in WINFORMS_MODIFIERS.items(): + try: + code_names.remove(str(code)) + except ValueError: + pass + else: + modifiers.add(toga_mod) + + assert len(code_names) == 1 + for toga_value, code in WINFORMS_KEYS.items(): + if str(code) == code_names[0]: + break + else: + toga_value = code_names[0].lower() + if len(toga_value) > 1: + toga_value = f"<{toga_value}>" + + if (Key.SHIFT in modifiers) and (toga_value not in ascii_lowercase): + for symbol, number in SHIFTED_KEYS.items(): + if toga_value == number: + toga_value = symbol + modifiers.remove(Key.SHIFT) + + return {"key": Key(toga_value), "modifiers": modifiers} diff --git a/winforms/tests_backend/app.py b/winforms/tests_backend/app.py index 986fe574f7..6813613105 100644 --- a/winforms/tests_backend/app.py +++ b/winforms/tests_backend/app.py @@ -7,11 +7,16 @@ from System.Drawing import Point from System.Windows.Forms import Application, Cursor +from toga_winforms.keys import toga_to_winforms_key, winforms_to_toga_key + from .probe import BaseProbe from .window import WindowProbe class AppProbe(BaseProbe): + supports_key = True + supports_key_mod3 = False + def __init__(self, app): super().__init__() self.app = app @@ -152,4 +157,4 @@ def activate_menu_minimize(self): pytest.xfail("This platform doesn't have a window management menu") def keystroke(self, combination): - pytest.xfail("Not applicable to this backend") + return winforms_to_toga_key(toga_to_winforms_key(combination)) From 2b22efdbb9bcdf58ad6faac807dd7c4cc3d30777 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Mon, 6 Nov 2023 11:15:32 +0000 Subject: [PATCH 04/19] Fix documentation error --- changes/2198.feature.rst | 1 + docs/reference/api/resources/command.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changes/2198.feature.rst diff --git a/changes/2198.feature.rst b/changes/2198.feature.rst new file mode 100644 index 0000000000..4b2d73689a --- /dev/null +++ b/changes/2198.feature.rst @@ -0,0 +1 @@ +A wider range of command shortcut keys are now supported on WinForms. diff --git a/docs/reference/api/resources/command.rst b/docs/reference/api/resources/command.rst index ca4fb73d1f..9eb805f59e 100644 --- a/docs/reference/api/resources/command.rst +++ b/docs/reference/api/resources/command.rst @@ -46,7 +46,7 @@ For example: callback, label='Example command', tooltip='Tells you when it has been activated', - shortcut='k', + shortcut=toga.Key.MOD_1 + 'k', icon='icons/pretty.png', group=stuff_group, section=0 From 9e179a602d21e9c5a64b9756ed60c972adf004d8 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Mon, 6 Nov 2023 18:55:06 +0000 Subject: [PATCH 05/19] Android and WinForms rehint updates --- android/src/toga_android/widgets/progressbar.py | 4 ---- android/src/toga_android/widgets/selection.py | 4 ---- android/src/toga_android/widgets/slider.py | 4 ---- core/src/toga/widgets/scrollcontainer.py | 3 --- winforms/src/toga_winforms/widgets/selection.py | 4 ---- winforms/src/toga_winforms/widgets/slider.py | 4 ---- 6 files changed, 23 deletions(-) diff --git a/android/src/toga_android/widgets/progressbar.py b/android/src/toga_android/widgets/progressbar.py index 9267217284..2567537be3 100644 --- a/android/src/toga_android/widgets/progressbar.py +++ b/android/src/toga_android/widgets/progressbar.py @@ -3,7 +3,6 @@ from android import R from android.view import View from android.widget import ProgressBar as A_ProgressBar -from travertino.size import at_least from .base import Widget @@ -94,9 +93,6 @@ def rehint(self): View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED, ) - self.interface.intrinsic.width = self.scale_out( - at_least(self.native.getMeasuredWidth()), ROUND_UP - ) self.interface.intrinsic.height = self.scale_out( self.native.getMeasuredHeight(), ROUND_UP ) diff --git a/android/src/toga_android/widgets/selection.py b/android/src/toga_android/widgets/selection.py index 716b4eb4c7..cddd649f5f 100644 --- a/android/src/toga_android/widgets/selection.py +++ b/android/src/toga_android/widgets/selection.py @@ -4,7 +4,6 @@ from android.view import View from android.widget import AdapterView, ArrayAdapter, Spinner from java import dynamic_proxy -from travertino.size import at_least from .base import Widget @@ -86,9 +85,6 @@ def clear(self): def rehint(self): self.native.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) - self.interface.intrinsic.width = self.scale_out( - at_least(self.native.getMeasuredWidth()), ROUND_UP - ) self.interface.intrinsic.height = self.scale_out( self.native.getMeasuredHeight(), ROUND_UP ) diff --git a/android/src/toga_android/widgets/slider.py b/android/src/toga_android/widgets/slider.py index 8621e2bf27..12a263b152 100644 --- a/android/src/toga_android/widgets/slider.py +++ b/android/src/toga_android/widgets/slider.py @@ -4,7 +4,6 @@ from android.view import View from android.widget import SeekBar from java import dynamic_proxy -from travertino.size import at_least import toga @@ -69,9 +68,6 @@ def _load_tick_drawable(self): def rehint(self): self.native.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) - self.interface.intrinsic.width = self.scale_out( - at_least(self.native.getMeasuredWidth()), ROUND_UP - ) self.interface.intrinsic.height = self.scale_out( self.native.getMeasuredHeight(), ROUND_UP ) diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index 43a1ea77c3..4ef05900df 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -6,9 +6,6 @@ class ScrollContainer(Widget): - _MIN_WIDTH = 0 - _MIN_HEIGHT = 0 - def __init__( self, id=None, diff --git a/winforms/src/toga_winforms/widgets/selection.py b/winforms/src/toga_winforms/widgets/selection.py index afe19aad1d..432cabbc2a 100644 --- a/winforms/src/toga_winforms/widgets/selection.py +++ b/winforms/src/toga_winforms/widgets/selection.py @@ -2,7 +2,6 @@ from decimal import ROUND_UP import System.Windows.Forms as WinForms -from travertino.size import at_least from ..libs.wrapper import WeakrefCallable from .base import Widget @@ -78,9 +77,6 @@ def get_selected_index(self): return None if index == -1 else index def rehint(self): - self.interface.intrinsic.width = self.scale_out( - at_least(self.native.PreferredSize.Width), ROUND_UP - ) self.interface.intrinsic.height = self.scale_out( self.native.PreferredSize.Height, ROUND_UP ) diff --git a/winforms/src/toga_winforms/widgets/slider.py b/winforms/src/toga_winforms/widgets/slider.py index 132588a7d4..7d6c3481f1 100644 --- a/winforms/src/toga_winforms/widgets/slider.py +++ b/winforms/src/toga_winforms/widgets/slider.py @@ -1,7 +1,6 @@ from decimal import ROUND_UP import System.Windows.Forms as WinForms -from travertino.size import at_least from toga.widgets.slider import IntSliderImpl @@ -57,9 +56,6 @@ def set_ticks_visible(self, visible): self.native.TickStyle = BOTTOM_RIGHT_TICK_STYLE if visible else NONE_TICK_STYLE def rehint(self): - self.interface.intrinsic.width = self.scale_out( - at_least(self.native.PreferredSize.Width), ROUND_UP - ) self.interface.intrinsic.height = self.scale_out( self.native.PreferredSize.Height, ROUND_UP ) From 9d2af8a838394fbd197585732925604f41eeed10 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Mon, 6 Nov 2023 19:07:13 +0000 Subject: [PATCH 06/19] Add change note --- changes/2200.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2200.feature.rst diff --git a/changes/2200.feature.rst b/changes/2200.feature.rst new file mode 100644 index 0000000000..701f35111b --- /dev/null +++ b/changes/2200.feature.rst @@ -0,0 +1 @@ +Most widgets with flexible sizes now default to a minimum size of 100 CSS pixels. An explicit size will still override this value. From f8fa58c68a5df639cd3e21bccd58e827cab362b1 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 7 Nov 2023 07:41:37 +0800 Subject: [PATCH 07/19] Correct handling of DOM click events on menu items. --- web/src/toga_web/app.py | 4 ++-- web/src/toga_web/command.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index 4706ea3e35..4d8090c05d 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -81,7 +81,7 @@ def create_menus(self): content=cmd.text, disabled=not cmd.enabled, ) - menu_item.onclick = cmd.action + menu_item.onclick = cmd._impl.dom_click submenu.append(menu_item) @@ -135,7 +135,7 @@ def create_menus(self): else: self.native.append(self.menubar) - def _menu_about(self, event, widget, **kwargs): + def _menu_about(self, widget, **kwargs): self.interface.about() def main_loop(self): diff --git a/web/src/toga_web/command.py b/web/src/toga_web/command.py index 43dc630c02..3378a4e760 100644 --- a/web/src/toga_web/command.py +++ b/web/src/toga_web/command.py @@ -6,6 +6,9 @@ def __init__(self, interface): self.interface = interface self.native = [] + def dom_click(self, event): + self.interface.action() + def set_enabled(self, value): pass # enabled = self.interface.enabled From cf9eee3d229efd9ea794a47b1a04073a20210383 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 7 Nov 2023 08:04:29 +0800 Subject: [PATCH 08/19] Use a consistent pattern on backends that use the same menu activation approach. --- gtk/src/toga_gtk/app.py | 11 +---------- gtk/src/toga_gtk/command.py | 6 ++++++ gtk/src/toga_gtk/window.py | 11 +---------- winforms/src/toga_winforms/app.py | 2 +- winforms/src/toga_winforms/command.py | 6 +++--- winforms/src/toga_winforms/window.py | 2 +- 6 files changed, 13 insertions(+), 25 deletions(-) diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 80cf53d819..6fdf4adc02 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -14,15 +14,6 @@ from .window import Window -def gtk_menu_item_activate(cmd): - """Convert a GTK menu item activation into a command invocation.""" - - def _handler(action, data): - cmd.action() - - return _handler - - class MainWindow(Window): def create(self): self.native = Gtk.ApplicationWindow() @@ -151,7 +142,7 @@ def create_menus(self): cmd_id = "command-%s" % id(cmd) action = Gio.SimpleAction.new(cmd_id, None) - action.connect("activate", gtk_menu_item_activate(cmd)) + action.connect("activate", cmd._impl.gtk_activate) cmd._impl.native.append(action) cmd._impl.set_enabled(cmd.enabled) diff --git a/gtk/src/toga_gtk/command.py b/gtk/src/toga_gtk/command.py index c8cec0cca7..95d4e0e3e4 100644 --- a/gtk/src/toga_gtk/command.py +++ b/gtk/src/toga_gtk/command.py @@ -9,6 +9,12 @@ def __init__(self, interface): self.interface = interface self.native = [] + def gtk_activate(self, action, data): + self.interface.action() + + def gtk_clicked(self, action): + self.interface.action() + def set_enabled(self, value): enabled = self.interface.enabled for widget in self.native: diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 32589bebfd..0bcea2d8a0 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -4,15 +4,6 @@ from .libs import Gdk, Gtk -def gtk_toolbar_item_clicked(cmd): - """Convert a GTK toolbar item click into a command invocation.""" - - def _handler(widget): - cmd.action() - - return _handler - - class Window: def __init__(self, interface, title, position, size): self.interface = interface @@ -100,7 +91,7 @@ def create_toolbar(self): item_impl.set_label(cmd.text) if cmd.tooltip: item_impl.set_tooltip_text(cmd.tooltip) - item_impl.connect("clicked", gtk_toolbar_item_clicked(cmd)) + item_impl.connect("clicked", cmd._impl.gtk_clicked) cmd._impl.native.append(item_impl) self.toolbar_items[cmd] = item_impl self.native_toolbar.insert(item_impl, -1) diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 94486b38fc..9bca588af9 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -146,7 +146,7 @@ def create_menus(self): else: submenu = self._submenu(cmd.group, menubar) item = WinForms.ToolStripMenuItem(cmd.text) - item.Click += WeakrefCallable(cmd._impl.winforms_handler) + item.Click += WeakrefCallable(cmd._impl.winforms_Click) if cmd.shortcut is not None: try: item.ShortcutKeys = toga_to_winforms_key(cmd.shortcut) diff --git a/winforms/src/toga_winforms/command.py b/winforms/src/toga_winforms/command.py index 30b2ac712b..562eb7fb2e 100644 --- a/winforms/src/toga_winforms/command.py +++ b/winforms/src/toga_winforms/command.py @@ -3,10 +3,10 @@ def __init__(self, interface): self.interface = interface self.native = [] + def winforms_Click(self, sender, event): + return self.interface.action() + def set_enabled(self, value): if self.native: for widget in self.native: widget.Enabled = self.interface.enabled - - def winforms_handler(self, sender, event): - return self.interface.action() diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 2eec14647e..b76b36a66a 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -63,7 +63,7 @@ def create_toolbar(self): if cmd.icon is not None: item.Image = cmd.icon._impl.native.ToBitmap() item.Enabled = cmd.enabled - item.Click += WeakrefCallable(cmd._impl.winforms_handler) + item.Click += WeakrefCallable(cmd._impl.winforms_Click) cmd._impl.native.append(item) self.toolbar_native.Items.Add(item) From 730ff97d2c68816f65942221dfa6e257f017e83b Mon Sep 17 00:00:00 2001 From: HalfWhitt <50283671+HalfWhitt@users.noreply.github.com> Date: Mon, 6 Nov 2023 19:34:01 -0500 Subject: [PATCH 09/19] Removed stray repeated word in Paths usage --- docs/reference/api/resources/app_paths.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/api/resources/app_paths.rst b/docs/reference/api/resources/app_paths.rst index e4b174a59d..12fa3e48d0 100644 --- a/docs/reference/api/resources/app_paths.rst +++ b/docs/reference/api/resources/app_paths.rst @@ -25,7 +25,7 @@ cases, hard restrictions) over where certain file types should be stored. For example, macOS provides the ``~/Library/Application Support`` folder; Linux encourages use of the ``~/.config`` folder (amongst others), and Windows provides the ``AppData/Local`` folder in the user's home directory. Application -sandbox and security policies will prevent sometimes prevent reading or +sandbox and security policies will sometimes prevent reading or writing files in any location other than these pre-approved locations. To assist with finding an appropriate location to store application files, every From ee0d572164db9dbcd5f539b383e1421d6620e31d Mon Sep 17 00:00:00 2001 From: HalfWhitt <50283671+HalfWhitt@users.noreply.github.com> Date: Mon, 6 Nov 2023 19:46:22 -0500 Subject: [PATCH 10/19] Added change note --- changes/2201.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2201.misc.rst diff --git a/changes/2201.misc.rst b/changes/2201.misc.rst new file mode 100644 index 0000000000..4d73995780 --- /dev/null +++ b/changes/2201.misc.rst @@ -0,0 +1 @@ +Minor fix: removed a repeated word in the docs for App Paths From fa452d8f71d21c328809185759b2caf428406c88 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 7 Nov 2023 09:02:27 +0800 Subject: [PATCH 11/19] Add removal note about CAPSLOCK. --- changes/2198.removal.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2198.removal.rst diff --git a/changes/2198.removal.rst b/changes/2198.removal.rst new file mode 100644 index 0000000000..02e332e010 --- /dev/null +++ b/changes/2198.removal.rst @@ -0,0 +1 @@ +The use of Caps Lock as a keyboard modifier for commands was removed. From dcaf9396984862d6daac6527c242d30570fa4110 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 7 Nov 2023 09:04:32 +0800 Subject: [PATCH 12/19] Add documentation for toga.Key. --- changes/2199.doc.rst | 1 + core/src/toga/keys.py | 6 +++++- docs/reference/api/index.rst | 2 ++ docs/reference/api/keys.rst | 41 ++++++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 changes/2199.doc.rst create mode 100644 docs/reference/api/keys.rst diff --git a/changes/2199.doc.rst b/changes/2199.doc.rst new file mode 100644 index 0000000000..4ee080ce2d --- /dev/null +++ b/changes/2199.doc.rst @@ -0,0 +1 @@ +Documentation for ``toga.Key`` was added. diff --git a/core/src/toga/keys.py b/core/src/toga/keys.py index 17009c42d4..6e78728eb8 100644 --- a/core/src/toga/keys.py +++ b/core/src/toga/keys.py @@ -2,6 +2,9 @@ class Key(Enum): + """An enumeration providing a symbolic representation for the characters on + a keyboard.""" + A = "a" B = "b" C = "c" @@ -144,7 +147,8 @@ class Key(Enum): NUMPAD_MULTIPLY = "numpad:*" NUMPAD_PLUS = "numpad:+" - def is_printable(self): + def is_printable(self) -> bool: + """Does pressing the key result in a printable character?""" return not (self.value.startswith("<") and self.value.endswith(">")) def __add__(self, other): diff --git a/docs/reference/api/index.rst b/docs/reference/api/index.rst index 0165206842..3b46b6e653 100644 --- a/docs/reference/api/index.rst +++ b/docs/reference/api/index.rst @@ -96,6 +96,7 @@ Other Component Description ============================================== ======================================================================== :doc:`Constants ` Symbolic constants used by various APIs. + :doc:`Keys ` Symbolic representation of keys used for keyboard shortcuts. ============================================== ======================================================================== .. toctree:: @@ -109,3 +110,4 @@ Other resources/index widgets/index constants + keys diff --git a/docs/reference/api/keys.rst b/docs/reference/api/keys.rst new file mode 100644 index 0000000000..31e1f175a4 --- /dev/null +++ b/docs/reference/api/keys.rst @@ -0,0 +1,41 @@ +Keys +==== + +A symbolic representation of keys used for keyboard shortcuts. + +Most keys have a constant that matches the text on the key, or the name of the +key if the text on the key isn't a legal Python identifier. + +However, due to differences between platforms, there's no representation of +"modifier" keys like Control, Command, Option, or the Windows Key. Instead, Toga +provides three generic modifier constants, and maps those to the modifier keys, +matching the precedence with which they are used on the underlying platforms: + +========== ============== ============== ================== + Platform :any:`MOD_1` :any:`MOD_2` :any:`MOD_3` +========== ============== ============== ================== + Linux Control Alt Tux/Windows/Meta + macOS Command (⌘) Option Control (^) + Windows Control Alt Not supported +========== ============== ============== ================== + +Key combinations can be expressed by combining multiple ``Key`` values with the +``+`` operator. + +.. code-block:: python + + from toga import Key + + just_an_a = Key.A + shift_a = Key.SHIFT + Key.A + # Windows/Linux - Control-Shift-A: + # macOS - Command-Shift-A: + modified_shift_a = Key.MOD_1 + Key.SHIFT + Key.A + +The order of addition is not significant. ``Key.SHIFT + Key.A`` and ``Key.A + +Key.SHIFT`` will produce the same key representation. + +Reference +--------- + +.. autoclass:: toga.Key From f0897b386a03e7d20d16e5ba752f64cf8746e0b2 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Tue, 7 Nov 2023 08:03:08 +0000 Subject: [PATCH 13/19] Add link from Command to Key --- core/src/toga/command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/toga/command.py b/core/src/toga/command.py index 8fa139a47c..8dd95ea554 100644 --- a/core/src/toga/command.py +++ b/core/src/toga/command.py @@ -4,6 +4,7 @@ from toga.handlers import wrapped_handler from toga.icons import Icon +from toga.keys import Key from toga.platform import get_platform_factory if TYPE_CHECKING: @@ -163,7 +164,7 @@ def __init__( action: ActionHandler | None, text: str, *, - shortcut: str | None = None, + shortcut: str | Key | None = None, tooltip: str | None = None, icon: str | Icon | None = None, group: Group = Group.COMMANDS, From 1e834317a92e3894e89344e1621fd5eb274e4809 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 8 Nov 2023 12:15:54 +0800 Subject: [PATCH 14/19] Correct some discrepancies and inconsistencies in widget support docs. --- docs/reference/api/documentapp.rst | 2 +- docs/reference/api/mainwindow.rst | 16 ++++++++++++++++ .../reference/api/widgets/multilinetextinput.rst | 7 ------- docs/reference/api/widgets/selection.rst | 12 ++---------- docs/reference/api/window.rst | 8 ++++++++ docs/reference/data/widgets_by_platform.csv | 2 +- 6 files changed, 28 insertions(+), 19 deletions(-) diff --git a/docs/reference/api/documentapp.rst b/docs/reference/api/documentapp.rst index 966fc17bb0..88a96fe724 100644 --- a/docs/reference/api/documentapp.rst +++ b/docs/reference/api/documentapp.rst @@ -7,7 +7,7 @@ The top-level representation of an application that manages documents. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(DocumentApp|Component))'} diff --git a/docs/reference/api/mainwindow.rst b/docs/reference/api/mainwindow.rst index f7d7ac864a..7dc17936d7 100644 --- a/docs/reference/api/mainwindow.rst +++ b/docs/reference/api/mainwindow.rst @@ -35,6 +35,22 @@ The main window of the application. :align: center :width: 450px + .. group-tab:: Web |beta| + + .. .. figure:: /reference/images/mainwindow-web.png + .. :align: center + .. :width: 300px + + Screenshot not available + + .. group-tab:: Textual |beta| + + .. .. figure:: /reference/images/mainwindow-textual.png + .. :align: center + .. :width: 300px + + Screenshot not available + Usage ----- diff --git a/docs/reference/api/widgets/multilinetextinput.rst b/docs/reference/api/widgets/multilinetextinput.rst index 46848bc953..35fbe4f176 100644 --- a/docs/reference/api/widgets/multilinetextinput.rst +++ b/docs/reference/api/widgets/multilinetextinput.rst @@ -43,13 +43,6 @@ A scrollable panel that allows for the display and editing of multiple lines of Not supported -.. rst-class:: widget-support -.. csv-filter:: Availability (:ref:`Key `) - :header-rows: 1 - :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9,10 - :exclude: {0: '(?!^(MultilineTextInput|Component)$)'} - Usage ----- diff --git a/docs/reference/api/widgets/selection.rst b/docs/reference/api/widgets/selection.rst index e543411385..d0ba61a19a 100644 --- a/docs/reference/api/widgets/selection.rst +++ b/docs/reference/api/widgets/selection.rst @@ -35,22 +35,14 @@ A widget to select a single option from a list of alternatives. :align: center :width: 300px - .. group-tab:: Web + .. group-tab:: Web |no| Not supported - .. group-tab:: Textual + .. group-tab:: Textual |no| Not supported -.. rst-class:: widget-support -.. csv-filter:: Availability (:ref:`Key `) - :header-rows: 1 - :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9,10 - :exclude: {0: '(?!^(Selection|Component)$)'} - - Usage ----- diff --git a/docs/reference/api/window.rst b/docs/reference/api/window.rst index 1c154f0fc9..f0730f0968 100644 --- a/docs/reference/api/window.rst +++ b/docs/reference/api/window.rst @@ -31,6 +31,14 @@ An operating system-managed container of widgets. Not supported + .. group-tab:: Web |no| + + Not supported + + .. group-tab:: Textual |no| + + Not supported + Usage ----- diff --git a/docs/reference/data/widgets_by_platform.csv b/docs/reference/data/widgets_by_platform.csv index 7560f4172c..0040fe654e 100644 --- a/docs/reference/data/widgets_by_platform.csv +++ b/docs/reference/data/widgets_by_platform.csv @@ -1,7 +1,7 @@ Component,Type,Component,Description,macOS,GTK,Windows,iOS,Android,Web,Terminal Application,Core Component,:class:`~toga.App`,The application itself,|y|,|y|,|y|,|y|,|y|,|b|,|b| DocumentApp,Core Component,:class:`~toga.DocumentApp`,An application that manages documents.,|b|,|b|,,,,, -Window,Core Component,:class:`~toga.Window`,An operating system-managed container of widgets.,|y|,|y|,|y|,|y|,|y|,|b|,|b| +Window,Core Component,:class:`~toga.Window`,An operating system-managed container of widgets.,|y|,|y|,|y|,,,, MainWindow,Core Component,:class:`~toga.MainWindow`,The main window of the application.,|y|,|y|,|y|,|y|,|y|,|b|,|b| ActivityIndicator,General Widget,:class:`~toga.ActivityIndicator`,A spinning activity animation,|y|,|y|,,,,|b|, Button,General Widget,:class:`~toga.Button`,Basic clickable Button,|y|,|y|,|y|,|y|,|y|,|b|,|b| From 37146093d8891e426f4fee4814485dea89ffa944 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 8 Nov 2023 12:18:58 +0800 Subject: [PATCH 15/19] Add changenote. --- changes/2204.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2204.misc.rst diff --git a/changes/2204.misc.rst b/changes/2204.misc.rst new file mode 100644 index 0000000000..1524aa29c5 --- /dev/null +++ b/changes/2204.misc.rst @@ -0,0 +1 @@ +Some inconsistencies in widget support documentation were corrected. From 549483e73b04847a6416897b03d511a707fdd2f1 Mon Sep 17 00:00:00 2001 From: Tom Arn Date: Wed, 8 Nov 2023 20:42:55 +0100 Subject: [PATCH 16/19] moved app command creation to method _create_app_commands() --- android/src/toga_android/app.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 0769930344..2c572a642a 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -177,7 +177,12 @@ def create(self): self._listener = TogaApp(self) # Call user code to populate the main window self.interface._startup() + self._create_app_commands() + def create_menus(self): + self.native.invalidateOptionsMenu() # Triggers onPrepareOptionsMenu + + 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( @@ -187,9 +192,6 @@ def create(self): ), ) - def create_menus(self): - self.native.invalidateOptionsMenu() # Triggers onPrepareOptionsMenu - def main_loop(self): # In order to support user asyncio code, start the Python/Android cooperative event loop. self.loop.run_forever_cooperatively() From cdc8cb6ed1134c1198aad6b654c3cb59a5f03180 Mon Sep 17 00:00:00 2001 From: Tom Arn Date: Wed, 8 Nov 2023 20:49:33 +0100 Subject: [PATCH 17/19] Added news fragment --- changes/2215.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2215.misc.rst diff --git a/changes/2215.misc.rst b/changes/2215.misc.rst new file mode 100644 index 0000000000..918e180a5b --- /dev/null +++ b/changes/2215.misc.rst @@ -0,0 +1 @@ +On Android the creation of the app commands has been moved to the method _create_app_commands() From dce7dfd22069d8def361c151554e6bded495a7a3 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Thu, 9 Nov 2023 07:59:15 +0000 Subject: [PATCH 18/19] Add more tests for sliders with empty ranges --- core/tests/widgets/test_slider.py | 66 +++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/core/tests/widgets/test_slider.py b/core/tests/widgets/test_slider.py index 7b558a41ef..8f53e12f84 100644 --- a/core/tests/widgets/test_slider.py +++ b/core/tests/widgets/test_slider.py @@ -229,36 +229,44 @@ def test_range(slider, on_change, min, max, value): @pytest.mark.parametrize( - "new_min, new_max", + "new_min, new_value, new_max", [ - [-5, 10], # less than old min - [5, 10], # more than old min, less than max - [15, 15], # more than max + [-5, 5, 10], # less than old min + [5, 5, 10], # more than old min + [6, 6, 10], # more than old min and value + [15, 15, 15], # more than old min, value and max ], ) -def test_min_clipping(slider, new_min, new_max): +def test_min_clipping(slider, new_min, new_value, new_max): + slider.tick_count = None slider.min = 0 + slider.value = 5 slider.max = 10 slider.min = new_min assert slider.min == new_min + assert slider.value == new_value assert slider.max == new_max @pytest.mark.parametrize( - "new_max, new_min", + "new_min, new_value, new_max", [ - [15, 0], # less than old max - [5, 0], # less than old max, more than min - [-5, -5], # less than min + [0, 5, 15], # more than old max + [0, 5, 5], # less than old max + [0, 4, 4], # less than old max and value + [-5, -5, -5], # less than old max, value and min ], ) -def test_max_clipping(slider, new_max, new_min): +def test_max_clipping(slider, new_min, new_value, new_max): + slider.tick_count = None slider.min = 0 + slider.value = 5 slider.max = 10 slider.max = new_max assert slider.min == new_min + assert slider.value == new_value assert slider.max == new_max @@ -429,7 +437,7 @@ def test_int_impl_continuous(): assert impl.int_value == int_value assert impl.get_value() == value - # Check a range that doesn't start at zero. + # Range that doesn't start at zero impl.set_min(-0.4) assert impl.get_min() == pytest.approx(-0.4) impl.set_max(0.6) @@ -437,6 +445,23 @@ def test_int_impl_continuous(): impl.set_value(0.5) assert impl.get_value() == 0.5 assert impl.int_value == 9000 + assert impl.int_max == 10000 + + # Empty range + impl.set_min(0) + impl.set_max(0) + impl.set_value(0) + assert impl.get_value() == 0 + assert impl.int_value == 0 + assert impl.int_max == 10000 + + # Empty range that doesn't start at zero + impl.set_min(1) + impl.set_max(1) + impl.set_value(1) + assert impl.get_value() == 1 + assert impl.int_value == 0 + assert impl.int_max == 10000 def test_int_impl_discrete(): @@ -467,7 +492,7 @@ def test_int_impl_discrete(): assert impl.get_value() == value assert impl.int_value == int_value - # Check a range that doesn't start at zero. + # Range that doesn't start at zero impl.set_min(-0.4) assert impl.get_min() == pytest.approx(-0.4) impl.set_max(0.6) @@ -475,6 +500,23 @@ def test_int_impl_discrete(): impl.set_value(0.5) assert impl.get_value() == 0.5 assert impl.int_value == 7 + assert impl.int_max == 8 + + # Empty range + impl.set_min(0) + impl.set_max(0) + impl.set_value(0) + assert impl.get_value() == 0 + assert impl.int_value == 0 + assert impl.int_max == 8 + + # Empty range that doesn't start at zero + impl.set_min(1) + impl.set_max(1) + impl.set_value(1) + assert impl.get_value() == 1 + assert impl.int_value == 0 + assert impl.int_max == 8 @pytest.mark.parametrize( From 0a7ed9c8f7c433d86bf89b68b55f85cb96bf366b Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Thu, 9 Nov 2023 08:02:26 +0000 Subject: [PATCH 19/19] Add change note --- changes/2216.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2216.misc.rst diff --git a/changes/2216.misc.rst b/changes/2216.misc.rst new file mode 100644 index 0000000000..dd3c07c506 --- /dev/null +++ b/changes/2216.misc.rst @@ -0,0 +1 @@ +Add more tests for sliders with empty ranges