From 43dd2fe8cf2f6042316062fb63ca4700331d2675 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 2 Aug 2023 01:22:03 -0700 Subject: [PATCH 001/102] Updated codebase to base on the latest main branch. --- cocoa/src/toga_cocoa/app.py | 4 ++++ cocoa/src/toga_cocoa/screen.py | 18 ++++++++++++++++++ cocoa/src/toga_cocoa/window.py | 9 +++++++++ core/src/toga/app.py | 4 ++++ core/src/toga/screen.py | 7 +++++++ core/src/toga/window.py | 8 ++++++++ dummy/src/toga_dummy/app.py | 4 ++++ dummy/src/toga_dummy/screen.py | 19 +++++++++++++++++++ dummy/src/toga_dummy/window.py | 8 ++++++++ examples/window/window/app.py | 15 +++++++++++++++ gtk/src/toga_gtk/app.py | 6 ++++++ gtk/src/toga_gtk/screen.py | 19 +++++++++++++++++++ gtk/src/toga_gtk/window.py | 13 ++++++++++++- winforms/src/toga_winforms/app.py | 4 ++++ winforms/src/toga_winforms/screen.py | 18 ++++++++++++++++++ winforms/src/toga_winforms/window.py | 10 ++++++++++ 16 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 cocoa/src/toga_cocoa/screen.py create mode 100644 core/src/toga/screen.py create mode 100644 dummy/src/toga_dummy/screen.py create mode 100644 gtk/src/toga_gtk/screen.py create mode 100644 winforms/src/toga_winforms/screen.py diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index c983720d4d..355b8e4160 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -34,6 +34,7 @@ objc_method, objc_property, ) +from .screen import Screen as ScreenImpl from .window import Window @@ -339,6 +340,9 @@ def main_loop(self): self.loop.run_forever(lifecycle=CocoaLifecycle(self.native)) + def get_screens(self): + return [ScreenImpl(native=screen) for screen in NSScreen.screens] + def set_main_window(self, window): pass diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py new file mode 100644 index 0000000000..fdf5e1ad6b --- /dev/null +++ b/cocoa/src/toga_cocoa/screen.py @@ -0,0 +1,18 @@ +from toga.screen import Screen as ScreenInterface + + +class Screen: + _instances = {} + + def __new__(cls, native): + if native in cls._instances: + return cls._instances[native] + else: + instance = super().__new__(cls) + instance.interface = ScreenInterface(_impl=instance) + instance.native = native + cls._instances[native] = instance + return instance + + def get_name(self): + return self.native.localizedName diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 5e7e80a7f5..15016e7644 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -266,3 +266,12 @@ def cocoa_windowShouldClose(self): def close(self): self.native.close() + + def get_current_screen(self): + frame_native = self.native.frame() + for screen in self.interface._app.screens: + if frame_native.origin in screen._impl.native.frame: + return screen._impl + + def set_current_screen(self, app_screen): + self.native.setFrameOrigin(app_screen._impl.native.visibleFrame.origin) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 90ea3a24e8..35cba1604b 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -387,6 +387,10 @@ class is defined, and look for a ``.dist-info`` file matching that name. def _create_impl(self): return self.factory.App(interface=self) + @property + def screens(self): + return [screen.interface for screen in self._impl.get_screens()] + @property def paths(self) -> Paths: """Paths for platform appropriate locations on the user's file system. diff --git a/core/src/toga/screen.py b/core/src/toga/screen.py new file mode 100644 index 0000000000..05017ed471 --- /dev/null +++ b/core/src/toga/screen.py @@ -0,0 +1,7 @@ +class Screen: + def __init__(self, _impl): + self._impl = _impl + + @property + def name(self): + return self._impl.get_name() diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 12f1e02f23..24750c231e 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -123,6 +123,14 @@ def __init__( self.on_close = on_close + @property + def screen(self): + return self._impl.get_current_screen().interface + + @screen.setter + def screen(self, app_screen): + self._impl.set_current_screen(app_screen) + @property def id(self) -> str: """The DOM identifier for the window. diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index 9a5fd67fe4..eeb0331f37 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -47,6 +47,10 @@ def beep(self): def exit(self): self._action("exit") + @not_required_on("mobile", "web") + def get_screens(self): + self._get_value("screens") + @not_required_on("mobile") def get_current_window(self): self._action("get_current_window") diff --git a/dummy/src/toga_dummy/screen.py b/dummy/src/toga_dummy/screen.py new file mode 100644 index 0000000000..b50438e0b2 --- /dev/null +++ b/dummy/src/toga_dummy/screen.py @@ -0,0 +1,19 @@ +from .utils import LoggedObject, not_required, not_required_on # noqa + + +@not_required_on("mobile", "web") +class Screen(LoggedObject): + _instances = {} + + def __new__(cls, native): + if native in cls._instances: + return cls._instances[native] + else: + instance = super().__new__(cls) + instance.interface = None + instance.native = native + cls._instances[native] = instance + return instance + + def get_name(self): + self._get_value("ScreenName") diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index b9f51cc3ac..86ae2b247f 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -109,3 +109,11 @@ def set_full_screen(self, is_full_screen): @not_required def toga_on_close(self): self._action("handle Window on_close") + + @not_required_on("mobile", "web") + def get_current_screen(self): + self._get_value("screen") + + @not_required_on("mobile", "web") + def set_current_screen(self, app_screen): + self._set_value("screen", app_screen) diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 2e770f561d..9ef3e350a2 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -167,6 +167,20 @@ def startup(self): "Change content", on_press=self.do_next_content, style=btn_style ) btn_hide = toga.Button("Hide", on_press=self.do_hide, style=btn_style) + screen_change_btns_box = toga.Box( + children=[toga.Label(text="Move current window to:")] + ) + + def do_screen_change(screen): + self.current_window.screen = screen + + for screen in self.screens: + screen_change_btns_box.add( + toga.Button( + text=screen.name, + on_press=lambda x, screen=screen: do_screen_change(screen), + ) + ) self.main_box = toga.Box( children=[ self.label, @@ -183,6 +197,7 @@ def startup(self): btn_do_report, btn_change_content, btn_hide, + screen_change_btns_box, ], style=Pack(direction=COLUMN), ) diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 5f7dee07c6..34f0a1f59a 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -13,6 +13,7 @@ from .keys import gtk_accel from .libs import TOGA_DEFAULT_STYLES, Gdk, Gio, GLib, Gtk +from .screen import Screen as ScreenImpl from .window import Window @@ -195,6 +196,11 @@ def main_loop(self): self.loop.run_forever(application=self.native) + def get_screens(self): + display = Gdk.Display.get_default() + n_monitors = display.get_n_monitors() + return [ScreenImpl(native=display.get_monitor(i)) for i in range(n_monitors)] + def set_main_window(self, window): pass diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py new file mode 100644 index 0000000000..6799170e97 --- /dev/null +++ b/gtk/src/toga_gtk/screen.py @@ -0,0 +1,19 @@ +from toga.screen import Screen as ScreenInterface + + +class Screen: + _instances = {} + + def __new__(cls, native): + if native in cls._instances: + return cls._instances[native] + else: + instance = super().__new__(cls) + instance.interface = ScreenInterface(_impl=instance) + instance.native = native + cls._instances[native] = instance + return instance + + def get_name(self): + display = self.native.get_display() + return display.get_name() diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index e20f151007..948a4e8644 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -2,7 +2,7 @@ from toga.handlers import wrapped_handler from .container import TogaContainer -from .libs import Gtk +from .libs import Gdk, Gtk class Window: @@ -141,3 +141,14 @@ def set_full_screen(self, is_full_screen): self.native.fullscreen() else: self.native.unfullscreen() + + def get_current_screen(self): + display = Gdk.Display.get_default() + monitor_native = display.get_monitor_at_window(self.native.get_window()) + for screen in self.interface._app.screens: + if monitor_native == screen._impl.native: + return screen._impl + + def set_current_screen(self, app_screen): + geometry = app_screen._impl.native.get_geometry() + self.native.move(geometry.x, geometry.y) diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 7a50715fc0..379b7665e1 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -18,6 +18,7 @@ win_version, ) from .libs.proactor import WinformsProactorEventLoop +from .screen import Screen as ScreenImpl from .window import Window @@ -297,6 +298,9 @@ def exit(self): self._is_exiting = True self.native.Exit() + def get_screens(self): + return [ScreenImpl(native=screen) for screen in WinForms.Screen.AllScreens] + def set_main_window(self, window): self.app_context.MainForm = window._impl.native diff --git a/winforms/src/toga_winforms/screen.py b/winforms/src/toga_winforms/screen.py new file mode 100644 index 0000000000..db9825805a --- /dev/null +++ b/winforms/src/toga_winforms/screen.py @@ -0,0 +1,18 @@ +from toga.screen import Screen as ScreenInterface + + +class Screen: + _instances = {} + + def __new__(cls, native): + if native in cls._instances: + return cls._instances[native] + else: + instance = super().__new__(cls) + instance.interface = ScreenInterface(_impl=instance) + instance.native = native + cls._instances[native] = instance + return instance + + def get_name(self): + return self.native.DeviceName diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 422a1a4df8..604b2247f1 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -166,3 +166,13 @@ def resize_content(self): self.native.ClientSize.Width, self.native.ClientSize.Height - vertical_shift, ) + + def get_current_screen(self): + screen_native = WinForms.Screen.FromControl(self.native) + for screen in self.interface._app.screens: + if screen_native == screen._impl.native: + return screen._impl + + def set_current_screen(self, app_screen): + self.native.StartPosition = WinForms.FormStartPosition.Manual + self.native.Location = app_screen._impl.native.WorkingArea.Location From 5ea20217ea1250ce74c129b764c04d574b5cec99 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 2 Aug 2023 01:30:03 -0700 Subject: [PATCH 002/102] Added the changelog file. --- changes/1930.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/1930.feature.rst diff --git a/changes/1930.feature.rst b/changes/1930.feature.rst new file mode 100644 index 0000000000..87814368e4 --- /dev/null +++ b/changes/1930.feature.rst @@ -0,0 +1 @@ +APIs for detecting multiple displays or screens and setting windows on them were added. From 775ed13431b9b7caccf8b58ccdd39f36f87389ac Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 4 Aug 2023 11:46:46 -0700 Subject: [PATCH 003/102] Modified code to align to desired API behaviour. --- cocoa/src/toga_cocoa/screen.py | 7 ++++ cocoa/src/toga_cocoa/window.py | 58 +++++++++++++++++++--------- core/src/toga/screen.py | 4 ++ core/src/toga/window.py | 55 +++++++++++++++++++++++--- dummy/src/toga_dummy/screen.py | 3 ++ dummy/src/toga_dummy/window.py | 8 +++- gtk/src/toga_gtk/screen.py | 4 ++ gtk/src/toga_gtk/window.py | 14 +++++-- winforms/src/toga_winforms/screen.py | 3 ++ winforms/src/toga_winforms/window.py | 13 +++++-- 10 files changed, 137 insertions(+), 32 deletions(-) diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index fdf5e1ad6b..1166f776ac 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -16,3 +16,10 @@ def __new__(cls, native): def get_name(self): return self.native.localizedName + + def get_origin(self): + frame_native = self.native.frame() + return ( + frame_native.origin.x, + frame_native.origin.y + frame_native.size.height, + ) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 15016e7644..76909d9c04 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -190,32 +190,46 @@ def get_position(self): if len(NSScreen.screens) == 0: return 0, 0 - # The "primary" screen has index 0 and origin (0, 0). - primary_screen = NSScreen.screens[0].frame window_frame = self.native.frame - - # macOS origin is bottom left of screen, and the screen might be - # offset relative to other screens. Adjust for this. return ( window_frame.origin.x, - primary_screen.size.height - - (window_frame.origin.y + window_frame.size.height), + window_frame.origin.y + window_frame.size.height, ) def set_position(self, position): - # If there is no active screen, we can't set a position - if len(NSScreen.screens) == 0: - return + self.native.setFrameTopLeftPoint(NSPoint(*position)) + + # def get_position(self): + # # If there is no active screen, we can't get a position + # if len(NSScreen.screens) == 0: + # return 0, 0 + + # # The "primary" screen has index 0 and origin (0, 0). + # primary_screen = NSScreen.screens[0].frame + # window_frame = self.native.frame + + # # macOS origin is bottom left of screen, and the screen might be + # # offset relative to other screens. Adjust for this. + # return ( + # window_frame.origin.x, + # primary_screen.size.height + # - (window_frame.origin.y + window_frame.size.height), + # ) - # The "primary" screen has index 0 and origin (0, 0). - primary_screen = NSScreen.screens[0].frame + # def set_position(self, position): + # # If there is no active screen, we can't set a position + # if len(NSScreen.screens) == 0: + # return - # macOS origin is bottom left of screen, and the screen might be - # offset relative to other screens. Adjust for this. - x = position[0] - y = primary_screen.size.height - position[1] + # # The "primary" screen has index 0 and origin (0, 0). + # primary_screen = NSScreen.screens[0].frame - self.native.setFrameTopLeftPoint(NSPoint(x, y)) + # # macOS origin is bottom left of screen, and the screen might be + # # offset relative to other screens. Adjust for this. + # x = position[0] + # y = primary_screen.size.height - position[1] + + # self.native.setFrameTopLeftPoint(NSPoint(x, y)) def get_size(self): frame = self.native.frame @@ -273,5 +287,11 @@ def get_current_screen(self): if frame_native.origin in screen._impl.native.frame: return screen._impl - def set_current_screen(self, app_screen): - self.native.setFrameOrigin(app_screen._impl.native.visibleFrame.origin) + # def set_current_screen(self, app_screen): + # self.native.setFrameOrigin(app_screen._impl.native.visibleFrame.origin) + + def get_primary_screen(self): + screen_native = NSScreen.screens[0] + for screen in self.interface._app.screens: + if screen_native == screen._impl.native: + return screen._impl diff --git a/core/src/toga/screen.py b/core/src/toga/screen.py index 05017ed471..6989adefb1 100644 --- a/core/src/toga/screen.py +++ b/core/src/toga/screen.py @@ -5,3 +5,7 @@ def __init__(self, _impl): @property def name(self): return self._impl.get_name() + + @property + def origin(self): + return self._impl.get_origin() diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 24750c231e..a7140fc0c9 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -12,6 +12,7 @@ if TYPE_CHECKING: from toga.app import App + from toga.screen import Screen from toga.widgets.base import Widget @@ -124,12 +125,29 @@ def __init__( self.on_close = on_close @property - def screen(self): + def screen(self) -> Screen: + """Instance of the :class:`toga.Screen` on which this window is present. + + Returns: + The screen of :class:`toga.Screen` on which this window is present. + + """ return self._impl.get_current_screen().interface @screen.setter - def screen(self, app_screen): - self._impl.set_current_screen(app_screen) + def screen(self, app_screen: Screen) -> None: + OriginalWindowLocation = self.position + OriginalOrigin = self.screen.origin + NewOrigin = app_screen.origin + x = OriginalWindowLocation[0] - OriginalOrigin[0] + NewOrigin[0] + y = OriginalWindowLocation[1] - OriginalOrigin[1] + NewOrigin[1] + + self._impl.set_position( + ( + x, + y, + ) + ) @property def id(self) -> str: @@ -223,11 +241,38 @@ def size(self, size: tuple[int, int]) -> None: @property def position(self) -> tuple[int, int]: """Position of the window, as an ``(x, y)`` tuple.""" - return self._impl.get_position() + assumed_absolute_origin = self._impl.get_primary_screen().get_origin() + absolute_window_position = self._impl.get_position() + assumed_absolute_window_position = ( + absolute_window_position[0] - assumed_absolute_origin[0], + absolute_window_position[1] - assumed_absolute_origin[1], + ) + return assumed_absolute_window_position @position.setter def position(self, position: tuple[int, int]) -> None: - self._impl.set_position(position) + assumed_absolute_origin = self._impl.get_primary_screen().get_origin() + assumed_absolute_new_position = ( + position[0] + assumed_absolute_origin[0], + position[1] + assumed_absolute_origin[1], + ) + self._impl.set_position(assumed_absolute_new_position) + + @property + def screen_position(self) -> tuple[int, int]: + current_relative_position = ( + self.position[0] - self.screen.origin[0], + self.position[1] - self.screen.origin[1], + ) + return current_relative_position + + @screen_position.setter + def screen_position(self, position: tuple[int, int]) -> None: + new_relative_position = ( + position[0] + self.screen.origin[0], + position[1] + self.screen.origin[1], + ) + self._impl.set_position(new_relative_position) def show(self) -> None: """Show window, if hidden.""" diff --git a/dummy/src/toga_dummy/screen.py b/dummy/src/toga_dummy/screen.py index b50438e0b2..6f4019458a 100644 --- a/dummy/src/toga_dummy/screen.py +++ b/dummy/src/toga_dummy/screen.py @@ -17,3 +17,6 @@ def __new__(cls, native): def get_name(self): self._get_value("ScreenName") + + def get_origin(self): + self._get_value("origin", (0, 0)) diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 86ae2b247f..f323c16b4d 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -114,6 +114,10 @@ def toga_on_close(self): def get_current_screen(self): self._get_value("screen") + # @not_required_on("mobile", "web") + # def set_current_screen(self, app_screen): + # self._set_value("screen", app_screen) + @not_required_on("mobile", "web") - def set_current_screen(self, app_screen): - self._set_value("screen", app_screen) + def get_primary_screen(self): + self._get_value("screen") diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index 6799170e97..ac1e1be740 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -17,3 +17,7 @@ def __new__(cls, native): def get_name(self): display = self.native.get_display() return display.get_name() + + def get_origin(self): + geometry = self.native.get_geometry() + return geometry.x, geometry.y diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 948a4e8644..d45f59b956 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -149,6 +149,14 @@ def get_current_screen(self): if monitor_native == screen._impl.native: return screen._impl - def set_current_screen(self, app_screen): - geometry = app_screen._impl.native.get_geometry() - self.native.move(geometry.x, geometry.y) + # def set_current_screen(self, app_screen): + # geometry = app_screen._impl.native.get_geometry() + # self.native.move(geometry.x, geometry.y) + + def get_primary_screen(self): + display = Gdk.Display.get_default() + primary_monitor = display.get_primary_monitor() + monitor_native = display.get_monitor(primary_monitor) + for screen in self.interface._app.screens: + if monitor_native == screen._impl.native: + return screen._impl diff --git a/winforms/src/toga_winforms/screen.py b/winforms/src/toga_winforms/screen.py index db9825805a..5ad47d446f 100644 --- a/winforms/src/toga_winforms/screen.py +++ b/winforms/src/toga_winforms/screen.py @@ -16,3 +16,6 @@ def __new__(cls, native): def get_name(self): return self.native.DeviceName + + def get_origin(self): + return self.native.Bounds.X, self.native.Bounds.Y diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 604b2247f1..12f637738d 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -76,6 +76,7 @@ def get_position(self): return self.native.Location.X, self.native.Location.Y def set_position(self, position): + self.native.StartPosition = WinForms.FormStartPosition.Manual self.native.Location = Point(*position) def get_size(self): @@ -173,6 +174,12 @@ def get_current_screen(self): if screen_native == screen._impl.native: return screen._impl - def set_current_screen(self, app_screen): - self.native.StartPosition = WinForms.FormStartPosition.Manual - self.native.Location = app_screen._impl.native.WorkingArea.Location + # def set_current_screen(self, app_screen): + # self.native.StartPosition = WinForms.FormStartPosition.Manual + # self.native.Location = app_screen._impl.native.WorkingArea.Location + + def get_primary_screen(self): + screen_native = WinForms.Screen.PrimaryScreen + for screen in self.interface._app.screens: + if screen_native == screen._impl.native: + return screen._impl From b54fc356197c14b08c182ca0f1f2811dcb1d2f8a Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 5 Aug 2023 10:03:58 -0700 Subject: [PATCH 004/102] Modified dummy core code. --- dummy/src/toga_dummy/app.py | 12 ++++++++---- dummy/src/toga_dummy/screen.py | 11 ++++++++--- dummy/src/toga_dummy/window.py | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index eeb0331f37..54a6d63d80 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -1,3 +1,4 @@ +from .screen import Screen as ScreenImpl from .utils import LoggedObject, not_required, not_required_on from .window import Window @@ -47,10 +48,6 @@ def beep(self): def exit(self): self._action("exit") - @not_required_on("mobile", "web") - def get_screens(self): - self._get_value("screens") - @not_required_on("mobile") def get_current_window(self): self._action("get_current_window") @@ -75,6 +72,13 @@ def show_cursor(self): def hide_cursor(self): self._action("hide_cursor") + @not_required_on("mobile", "web") + def get_screens(self): + return [ + ScreenImpl(native="primary_screen"), + ScreenImpl(native="secondary_screen"), + ] + @not_required_on("mobile", "web") class DocumentApp(App): diff --git a/dummy/src/toga_dummy/screen.py b/dummy/src/toga_dummy/screen.py index 6f4019458a..6500103a6d 100644 --- a/dummy/src/toga_dummy/screen.py +++ b/dummy/src/toga_dummy/screen.py @@ -1,3 +1,5 @@ +from toga.screen import Screen as ScreenInterface + from .utils import LoggedObject, not_required, not_required_on # noqa @@ -10,13 +12,16 @@ def __new__(cls, native): return cls._instances[native] else: instance = super().__new__(cls) - instance.interface = None + instance.interface = ScreenInterface(_impl=instance) instance.native = native cls._instances[native] = instance return instance def get_name(self): - self._get_value("ScreenName") + return self.native def get_origin(self): - self._get_value("origin", (0, 0)) + if self.native == "primary_screen": + return (0, 0) + else: + return (-1920, 0) diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index f323c16b4d..7584c85184 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -120,4 +120,4 @@ def get_current_screen(self): @not_required_on("mobile", "web") def get_primary_screen(self): - self._get_value("screen") + return self.interface._app.screens[0] From ba232fdff5ca3cd3a4aa7798d9fa7149d072e700 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Mon, 7 Aug 2023 04:12:29 -0700 Subject: [PATCH 005/102] Modified GTK backend to address reported error. --- gtk/src/toga_gtk/window.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index d45f59b956..90259f2ffd 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -155,8 +155,7 @@ def get_current_screen(self): def get_primary_screen(self): display = Gdk.Display.get_default() - primary_monitor = display.get_primary_monitor() - monitor_native = display.get_monitor(primary_monitor) + monitor_native = display.get_primary_monitor() for screen in self.interface._app.screens: if monitor_native == screen._impl.native: return screen._impl From 1cf94128ee47b1e6044b89143923a79d7dfb792e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 8 Aug 2023 11:19:41 -0700 Subject: [PATCH 006/102] Modified cocoa backend to address reported errors. --- cocoa/src/toga_cocoa/window.py | 52 +++++++++++++--------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 76909d9c04..e8abeee254 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -190,46 +190,32 @@ def get_position(self): if len(NSScreen.screens) == 0: return 0, 0 + # The "primary" screen has index 0 and origin (0, 0). + primary_screen = NSScreen.screens[0].frame window_frame = self.native.frame + + # macOS origin is bottom left of screen, and the screen might be + # offset relative to other screens. Adjust for this. return ( window_frame.origin.x, - window_frame.origin.y + window_frame.size.height, + primary_screen.size.height + - (window_frame.origin.y + window_frame.size.height), ) def set_position(self, position): - self.native.setFrameTopLeftPoint(NSPoint(*position)) - - # def get_position(self): - # # If there is no active screen, we can't get a position - # if len(NSScreen.screens) == 0: - # return 0, 0 - - # # The "primary" screen has index 0 and origin (0, 0). - # primary_screen = NSScreen.screens[0].frame - # window_frame = self.native.frame - - # # macOS origin is bottom left of screen, and the screen might be - # # offset relative to other screens. Adjust for this. - # return ( - # window_frame.origin.x, - # primary_screen.size.height - # - (window_frame.origin.y + window_frame.size.height), - # ) - - # def set_position(self, position): - # # If there is no active screen, we can't set a position - # if len(NSScreen.screens) == 0: - # return + # If there is no active screen, we can't set a position + if len(NSScreen.screens) == 0: + return - # # The "primary" screen has index 0 and origin (0, 0). - # primary_screen = NSScreen.screens[0].frame + # The "primary" screen has index 0 and origin (0, 0). + primary_screen = NSScreen.screens[0].frame - # # macOS origin is bottom left of screen, and the screen might be - # # offset relative to other screens. Adjust for this. - # x = position[0] - # y = primary_screen.size.height - position[1] + # macOS origin is bottom left of screen, and the screen might be + # offset relative to other screens. Adjust for this. + x = position[0] + y = primary_screen.size.height - position[1] - # self.native.setFrameTopLeftPoint(NSPoint(x, y)) + self.native.setFrameTopLeftPoint(NSPoint(x, y)) def get_size(self): frame = self.native.frame @@ -282,9 +268,9 @@ def close(self): self.native.close() def get_current_screen(self): - frame_native = self.native.frame() + screen_native = self.native.screen for screen in self.interface._app.screens: - if frame_native.origin in screen._impl.native.frame: + if screen_native == screen._impl.native: return screen._impl # def set_current_screen(self, app_screen): From 26f6f1cbb35a307f2030d492eb895c5653a28d57 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 9 Aug 2023 07:53:20 +0800 Subject: [PATCH 007/102] Correct cocoa implementation. --- cocoa/src/toga_cocoa/screen.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index 1166f776ac..5e78c0f184 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -18,8 +18,5 @@ def get_name(self): return self.native.localizedName def get_origin(self): - frame_native = self.native.frame() - return ( - frame_native.origin.x, - frame_native.origin.y + frame_native.size.height, - ) + frame_native = self.native.frame + return (frame_native.origin.x, frame_native.origin.y) From 56aabe1adb078852b92f4be7d93662418415c8a6 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 9 Aug 2023 04:12:51 -0700 Subject: [PATCH 008/102] Modified codebase to address reported issues. --- cocoa/src/toga_cocoa/screen.py | 4 ++++ cocoa/src/toga_cocoa/window.py | 16 +++---------- core/src/toga/screen.py | 4 ++++ core/src/toga/window.py | 35 ++++++++++++---------------- gtk/src/toga_gtk/app.py | 9 +++++-- gtk/src/toga_gtk/screen.py | 7 ++++-- gtk/src/toga_gtk/window.py | 16 ++----------- winforms/src/toga_winforms/app.py | 8 ++++++- winforms/src/toga_winforms/screen.py | 3 +++ winforms/src/toga_winforms/window.py | 16 ++----------- 10 files changed, 52 insertions(+), 66 deletions(-) diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index 5e78c0f184..9bad1c8588 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -20,3 +20,7 @@ def get_name(self): def get_origin(self): frame_native = self.native.frame return (frame_native.origin.x, frame_native.origin.y) + + def get_size(self): + frame_native = self.native.frame + return (frame_native.size.width, frame_native.size.height) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index e8abeee254..cbe1d477e2 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -20,6 +20,8 @@ objc_property, ) +from .screen import Screen as ScreenImpl + def toolbar_identifier(cmd): return "ToolbarItem-%s" % id(cmd) @@ -268,16 +270,4 @@ def close(self): self.native.close() def get_current_screen(self): - screen_native = self.native.screen - for screen in self.interface._app.screens: - if screen_native == screen._impl.native: - return screen._impl - - # def set_current_screen(self, app_screen): - # self.native.setFrameOrigin(app_screen._impl.native.visibleFrame.origin) - - def get_primary_screen(self): - screen_native = NSScreen.screens[0] - for screen in self.interface._app.screens: - if screen_native == screen._impl.native: - return screen._impl + return ScreenImpl(self.native.screen) diff --git a/core/src/toga/screen.py b/core/src/toga/screen.py index 6989adefb1..13b1bcc969 100644 --- a/core/src/toga/screen.py +++ b/core/src/toga/screen.py @@ -9,3 +9,7 @@ def name(self): @property def origin(self): return self._impl.get_origin() + + @property + def size(self): + return self._impl.get_size() diff --git a/core/src/toga/window.py b/core/src/toga/window.py index a7140fc0c9..4e94c90c28 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -136,18 +136,13 @@ def screen(self) -> Screen: @screen.setter def screen(self, app_screen: Screen) -> None: - OriginalWindowLocation = self.position - OriginalOrigin = self.screen.origin - NewOrigin = app_screen.origin - x = OriginalWindowLocation[0] - OriginalOrigin[0] + NewOrigin[0] - y = OriginalWindowLocation[1] - OriginalOrigin[1] + NewOrigin[1] - - self._impl.set_position( - ( - x, - y, - ) - ) + original_window_location = self.position + original_origin = self.screen.origin + new_origin = app_screen.origin + x = original_window_location[0] - original_origin[0] + new_origin[0] + y = original_window_location[1] - original_origin[1] + new_origin[1] + + self._impl.set_position((x, y)) @property def id(self) -> str: @@ -241,22 +236,22 @@ def size(self, size: tuple[int, int]) -> None: @property def position(self) -> tuple[int, int]: """Position of the window, as an ``(x, y)`` tuple.""" - assumed_absolute_origin = self._impl.get_primary_screen().get_origin() + absolute_origin = self._app.screens[0].origin absolute_window_position = self._impl.get_position() assumed_absolute_window_position = ( - absolute_window_position[0] - assumed_absolute_origin[0], - absolute_window_position[1] - assumed_absolute_origin[1], + absolute_window_position[0] - absolute_origin[0], + absolute_window_position[1] - absolute_origin[1], ) return assumed_absolute_window_position @position.setter def position(self, position: tuple[int, int]) -> None: - assumed_absolute_origin = self._impl.get_primary_screen().get_origin() - assumed_absolute_new_position = ( - position[0] + assumed_absolute_origin[0], - position[1] + assumed_absolute_origin[1], + absolute_origin = self._app.screens[0].origin + absolute_new_position = ( + position[0] + absolute_origin[0], + position[1] + absolute_origin[1], ) - self._impl.set_position(assumed_absolute_new_position) + self._impl.set_position(absolute_new_position) @property def screen_position(self) -> tuple[int, int]: diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 34f0a1f59a..77383c43d8 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -198,8 +198,13 @@ def main_loop(self): def get_screens(self): display = Gdk.Display.get_default() - n_monitors = display.get_n_monitors() - return [ScreenImpl(native=display.get_monitor(i)) for i in range(n_monitors)] + primary_screen = ScreenImpl(display.get_primary_monitor()) + screen_list = [primary_screen] + [ + ScreenImpl(native=display.get_monitor(i)) + for i in range(display.get_n_monitors()) + if display.get_monitor(i) != primary_screen.native + ] + return screen_list def set_main_window(self, window): pass diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index ac1e1be740..1544eafe04 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -15,9 +15,12 @@ def __new__(cls, native): return instance def get_name(self): - display = self.native.get_display() - return display.get_name() + return self.native.get_model() def get_origin(self): geometry = self.native.get_geometry() return geometry.x, geometry.y + + def get_size(self): + geometry = self.native.get_geometry() + return geometry.width, geometry.height diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 90259f2ffd..2f5b4c391f 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -3,6 +3,7 @@ from .container import TogaContainer from .libs import Gdk, Gtk +from .screen import Screen as ScreenImpl class Window: @@ -145,17 +146,4 @@ def set_full_screen(self, is_full_screen): def get_current_screen(self): display = Gdk.Display.get_default() monitor_native = display.get_monitor_at_window(self.native.get_window()) - for screen in self.interface._app.screens: - if monitor_native == screen._impl.native: - return screen._impl - - # def set_current_screen(self, app_screen): - # geometry = app_screen._impl.native.get_geometry() - # self.native.move(geometry.x, geometry.y) - - def get_primary_screen(self): - display = Gdk.Display.get_default() - monitor_native = display.get_primary_monitor() - for screen in self.interface._app.screens: - if monitor_native == screen._impl.native: - return screen._impl + return ScreenImpl(monitor_native) diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 379b7665e1..ca34779176 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -299,7 +299,13 @@ def exit(self): self.native.Exit() def get_screens(self): - return [ScreenImpl(native=screen) for screen in WinForms.Screen.AllScreens] + primary_screen = ScreenImpl(WinForms.Screen.PrimaryScreen) + screen_list = [primary_screen] + [ + ScreenImpl(native=screen) + for screen in WinForms.Screen.AllScreens + if screen != primary_screen.native + ] + return screen_list def set_main_window(self, window): self.app_context.MainForm = window._impl.native diff --git a/winforms/src/toga_winforms/screen.py b/winforms/src/toga_winforms/screen.py index 5ad47d446f..b0d1379f11 100644 --- a/winforms/src/toga_winforms/screen.py +++ b/winforms/src/toga_winforms/screen.py @@ -19,3 +19,6 @@ def get_name(self): def get_origin(self): return self.native.Bounds.X, self.native.Bounds.Y + + def get_size(self): + return self.native.Bounds.Width, self.native.Bounds.Height diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 12f637738d..e4ffc80d4b 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -2,6 +2,7 @@ from .container import Container, MinimumContainer from .libs import Point, Size, WinForms +from .screen import Screen as ScreenImpl class Window(Container): @@ -169,17 +170,4 @@ def resize_content(self): ) def get_current_screen(self): - screen_native = WinForms.Screen.FromControl(self.native) - for screen in self.interface._app.screens: - if screen_native == screen._impl.native: - return screen._impl - - # def set_current_screen(self, app_screen): - # self.native.StartPosition = WinForms.FormStartPosition.Manual - # self.native.Location = app_screen._impl.native.WorkingArea.Location - - def get_primary_screen(self): - screen_native = WinForms.Screen.PrimaryScreen - for screen in self.interface._app.screens: - if screen_native == screen._impl.native: - return screen._impl + return ScreenImpl(WinForms.Screen.FromControl(self.native)) From 8e219b1102a723705ff0f9e2e1b07143e1e11d05 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Wed, 9 Aug 2023 04:18:14 -0700 Subject: [PATCH 009/102] Update examples/window/window/app.py Co-authored-by: Russell Keith-Magee --- examples/window/window/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 9ef3e350a2..115b75a16e 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -174,10 +174,10 @@ def startup(self): def do_screen_change(screen): self.current_window.screen = screen - for screen in self.screens: + for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): screen_change_btns_box.add( toga.Button( - text=screen.name, + text=f"{index}: {screen.name}", on_press=lambda x, screen=screen: do_screen_change(screen), ) ) From a18a9d5acee6317d47ef6fb937ad60e1901e2cf1 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 9 Aug 2023 05:27:08 -0700 Subject: [PATCH 010/102] Added `screen_position` tests in window example app. --- dummy/src/toga_dummy/screen.py | 3 +++ dummy/src/toga_dummy/window.py | 8 -------- examples/window/window/app.py | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/dummy/src/toga_dummy/screen.py b/dummy/src/toga_dummy/screen.py index 6500103a6d..c7f2cc9a09 100644 --- a/dummy/src/toga_dummy/screen.py +++ b/dummy/src/toga_dummy/screen.py @@ -25,3 +25,6 @@ def get_origin(self): return (0, 0) else: return (-1920, 0) + + def get_size(self): + return (1920, 1080) diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 7584c85184..b3ebfed660 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -113,11 +113,3 @@ def toga_on_close(self): @not_required_on("mobile", "web") def get_current_screen(self): self._get_value("screen") - - # @not_required_on("mobile", "web") - # def set_current_screen(self, app_screen): - # self._set_value("screen", app_screen) - - @not_required_on("mobile", "web") - def get_primary_screen(self): - return self.interface._app.screens[0] diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 115b75a16e..2d02e2a077 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -17,6 +17,12 @@ def do_left(self, widget, **kwargs): def do_right(self, widget, **kwargs): self.main_window.position = (2000, 500) + def do_left_current_screen(self, widget, **kwargs): + self.main_window.screen_position = (0, 100) + + def do_right_current_screen(self, widget, **kwargs): + self.main_window.screen_position = (1080, 100) + def do_small(self, widget, **kwargs): self.main_window.size = (400, 300) @@ -137,6 +143,16 @@ def startup(self): ) btn_do_left = toga.Button("Go left", on_press=self.do_left, style=btn_style) btn_do_right = toga.Button("Go right", on_press=self.do_right, style=btn_style) + btn_do_left_current_screen = toga.Button( + "Go left on current screen", + on_press=self.do_left_current_screen, + style=btn_style, + ) + btn_do_right_current_screen = toga.Button( + "Go right on current screen", + on_press=self.do_right_current_screen, + style=btn_style, + ) btn_do_small = toga.Button( "Become small", on_press=self.do_small, style=btn_style ) @@ -187,6 +203,8 @@ def do_screen_change(screen): btn_do_origin, btn_do_left, btn_do_right, + btn_do_left_current_screen, + btn_do_right_current_screen, btn_do_small, btn_do_large, btn_do_full_screen, From cefe1905e476a6a1c2779e6a64d7f102bb4d37b9 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 9 Aug 2023 05:48:26 -0700 Subject: [PATCH 011/102] Modified DocString --- core/src/toga/window.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 4e94c90c28..d3387c560e 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -126,12 +126,7 @@ def __init__( @property def screen(self) -> Screen: - """Instance of the :class:`toga.Screen` on which this window is present. - - Returns: - The screen of :class:`toga.Screen` on which this window is present. - - """ + """Instance of the :class:`toga.Screen` on which this window is present.""" return self._impl.get_current_screen().interface @screen.setter From 56a915f522488f66ad9539abdab812af087a339a Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 9 Aug 2023 06:28:37 -0700 Subject: [PATCH 012/102] Added screen code to web backend. --- core/src/toga/app.py | 1 + core/src/toga/screen.py | 3 +++ core/src/toga/window.py | 3 ++- web/src/toga_web/app.py | 5 +++++ web/src/toga_web/screen.py | 24 ++++++++++++++++++++++++ web/src/toga_web/window.py | 5 +++++ 6 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 web/src/toga_web/screen.py diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 35cba1604b..e8ca0166ef 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -389,6 +389,7 @@ def _create_impl(self): @property def screens(self): + """Returns a list of available screens.""" return [screen.interface for screen in self._impl.get_screens()] @property diff --git a/core/src/toga/screen.py b/core/src/toga/screen.py index 13b1bcc969..11aa19a7f5 100644 --- a/core/src/toga/screen.py +++ b/core/src/toga/screen.py @@ -4,12 +4,15 @@ def __init__(self, _impl): @property def name(self): + """Unique name of the screen.""" return self._impl.get_name() @property def origin(self): + """The absolute coordinates of the screen's origin, as a ``(x, y)`` tuple.""" return self._impl.get_origin() @property def size(self): + """The size of the screen, as a ``(width, height)`` tuple.""" return self._impl.get_size() diff --git a/core/src/toga/window.py b/core/src/toga/window.py index d3387c560e..86c3f1be37 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -230,7 +230,7 @@ def size(self, size: tuple[int, int]) -> None: @property def position(self) -> tuple[int, int]: - """Position of the window, as an ``(x, y)`` tuple.""" + """Absolute position of the window, as a ``(x, y)`` tuple.""" absolute_origin = self._app.screens[0].origin absolute_window_position = self._impl.get_position() assumed_absolute_window_position = ( @@ -250,6 +250,7 @@ def position(self, position: tuple[int, int]) -> None: @property def screen_position(self) -> tuple[int, int]: + """Position of the window with respect to current screen, as a ``(x, y)`` tuple.""" current_relative_position = ( self.position[0] - self.screen.origin[0], self.position[1] - self.screen.origin[1], diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index fdb71cba10..c340428453 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -2,6 +2,8 @@ from toga_web.libs import create_element, js from toga_web.window import Window +from .screen import Screen as ScreenImpl + class MainWindow(Window): def on_close(self, *args): @@ -199,3 +201,6 @@ def show_cursor(self): def hide_cursor(self): self.interface.factory.not_implemented("App.hide_cursor()") + + def get_screens(self): + return [ScreenImpl(js.document.documentElement)] diff --git a/web/src/toga_web/screen.py b/web/src/toga_web/screen.py new file mode 100644 index 0000000000..b71804ddac --- /dev/null +++ b/web/src/toga_web/screen.py @@ -0,0 +1,24 @@ +from toga.screen import Screen as ScreenInterface + + +class Screen: + _instances = {} + + def __new__(cls, native): + if native in cls._instances: + return cls._instances[native] + else: + instance = super().__new__(cls) + instance.interface = ScreenInterface(_impl=instance) + instance.native = native + cls._instances[native] = instance + return instance + + def get_name(self): + return "Web Screen" + + def get_origin(self): + return (0, 0) + + def get_size(self): + return self.native.clientWidth, self.native.clientHeight diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index 6e437cda4e..9595636404 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -1,5 +1,7 @@ from toga_web.libs import create_element, js +from .screen import Screen as ScreenImpl + class Window: def __init__(self, interface, title, position, size): @@ -77,3 +79,6 @@ def set_size(self, size): def set_full_screen(self, is_full_screen): self.interface.factory.not_implemented("Window.set_full_screen()") + + def get_current_screen(self): + return ScreenImpl(js.document.documentElement) From 264a9af7a64c5f90e8d8086e0ed925997fb54f84 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 9 Aug 2023 06:48:31 -0700 Subject: [PATCH 013/102] Added screen code to Android backend. --- android/src/toga_android/app.py | 4 ++++ android/src/toga_android/screen.py | 24 ++++++++++++++++++++++++ android/src/toga_android/window.py | 4 ++++ 3 files changed, 32 insertions(+) create mode 100644 android/src/toga_android/screen.py diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 269bf49171..74d543017d 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -8,6 +8,7 @@ from .libs.activity import IPythonApp, MainActivity from .libs.android.graphics import Drawable from .libs.android.view import Menu, MenuItem +from .screen import Screen as ScreenImpl from .window import Window # `MainWindow` is defined here in `app.py`, not `window.py`, to mollify the test suite. @@ -239,3 +240,6 @@ def hide_cursor(self): def show_cursor(self): pass + + def get_screens(self): + return [ScreenImpl(self.interface.main_window._impl)] diff --git a/android/src/toga_android/screen.py b/android/src/toga_android/screen.py new file mode 100644 index 0000000000..be435d4444 --- /dev/null +++ b/android/src/toga_android/screen.py @@ -0,0 +1,24 @@ +from toga.screen import Screen as ScreenInterface + + +class Screen: + _instances = {} + + def __new__(cls, native): + if native in cls._instances: + return cls._instances[native] + else: + instance = super().__new__(cls) + instance.interface = ScreenInterface(_impl=instance) + instance.native = native + cls._instances[native] = instance + return instance + + def get_name(self): + return "Android Screen" + + def get_origin(self): + return (0, 0) + + def get_size(self): + return self.native.width, self.native.height diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 4e1a3b4046..de042cb358 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -1,6 +1,7 @@ from .container import Container from .libs.android import R__id from .libs.android.view import ViewTreeObserver__OnGlobalLayoutListener +from .screen import Screen as ScreenImpl class LayoutListener(ViewTreeObserver__OnGlobalLayoutListener): @@ -71,3 +72,6 @@ def close(self): def set_full_screen(self, is_full_screen): self.interface.factory.not_implemented("Window.set_full_screen()") + + def get_current_screen(self): + return ScreenImpl(self) From b28a492dcad5709187d99dc44f1201f18134af7c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 9 Aug 2023 06:58:27 -0700 Subject: [PATCH 014/102] Added screen code to ios backend. --- iOS/src/toga_iOS/app.py | 7 ++++++- iOS/src/toga_iOS/screen.py | 24 ++++++++++++++++++++++++ iOS/src/toga_iOS/window.py | 5 +++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 iOS/src/toga_iOS/screen.py diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index c137e2e07c..7f85e6bb84 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -3,9 +3,11 @@ from rubicon.objc import objc_method from rubicon.objc.eventloop import EventLoopPolicy, iOSLifecycle -from toga_iOS.libs import UIResponder +from toga_iOS.libs import UIResponder, UIScreen from toga_iOS.window import Window +from .screen import Screen as ScreenImpl + class MainWindow(Window): pass @@ -95,3 +97,6 @@ def hide_cursor(self): def show_cursor(self): pass + + def get_screens(self): + return [ScreenImpl(UIScreen.mainScreen)] diff --git a/iOS/src/toga_iOS/screen.py b/iOS/src/toga_iOS/screen.py new file mode 100644 index 0000000000..2416359b8f --- /dev/null +++ b/iOS/src/toga_iOS/screen.py @@ -0,0 +1,24 @@ +from toga.screen import Screen as ScreenInterface + + +class Screen: + _instances = {} + + def __new__(cls, native): + if native in cls._instances: + return cls._instances[native] + else: + instance = super().__new__(cls) + instance.interface = ScreenInterface(_impl=instance) + instance.native = native + cls._instances[native] = instance + return instance + + def get_name(self): + return "IOS Screen" + + def get_origin(self): + return (0, 0) + + def get_size(self): + return self.native.bounds.size.width, self.native.bounds.size.height diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index 4158cc00d5..9f1cb6c117 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -5,6 +5,8 @@ UIWindow, ) +from .screen import Screen as ScreenImpl + class Window: def __init__(self, interface, title, position, size): @@ -84,3 +86,6 @@ def get_visible(self): def close(self): pass + + def get_current_screen(self): + return ScreenImpl(UIScreen.mainScreen) From aff7fc04411718b2ce33ba4ca8054a0f78e3d26b Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 9 Aug 2023 12:10:28 -0700 Subject: [PATCH 015/102] Added screen capture API in core and in winforms. --- android/src/toga_android/screen.py | 3 +++ cocoa/src/toga_cocoa/screen.py | 3 +++ core/src/toga/screen.py | 6 ++++++ dummy/src/toga_dummy/screen.py | 3 +++ gtk/src/toga_gtk/screen.py | 3 +++ iOS/src/toga_iOS/screen.py | 3 +++ web/src/toga_web/screen.py | 3 +++ winforms/src/toga_winforms/screen.py | 19 +++++++++++++++++++ 8 files changed, 43 insertions(+) diff --git a/android/src/toga_android/screen.py b/android/src/toga_android/screen.py index be435d4444..9345baeae4 100644 --- a/android/src/toga_android/screen.py +++ b/android/src/toga_android/screen.py @@ -22,3 +22,6 @@ def get_origin(self): def get_size(self): return self.native.width, self.native.height + + def get_image_data(self): + pass diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index 9bad1c8588..8700053787 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -24,3 +24,6 @@ def get_origin(self): def get_size(self): frame_native = self.native.frame return (frame_native.size.width, frame_native.size.height) + + def get_image_data(self): + pass diff --git a/core/src/toga/screen.py b/core/src/toga/screen.py index 11aa19a7f5..66d64bd9a2 100644 --- a/core/src/toga/screen.py +++ b/core/src/toga/screen.py @@ -1,3 +1,6 @@ +from .images import Image + + class Screen: def __init__(self, _impl): self._impl = _impl @@ -16,3 +19,6 @@ def origin(self): def size(self): """The size of the screen, as a ``(width, height)`` tuple.""" return self._impl.get_size() + + def as_image(self): + return Image(data=self._impl.get_image_data()) diff --git a/dummy/src/toga_dummy/screen.py b/dummy/src/toga_dummy/screen.py index c7f2cc9a09..4d61ea4362 100644 --- a/dummy/src/toga_dummy/screen.py +++ b/dummy/src/toga_dummy/screen.py @@ -28,3 +28,6 @@ def get_origin(self): def get_size(self): return (1920, 1080) + + def get_image_data(self): + pass diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index 1544eafe04..337dc6ec38 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -24,3 +24,6 @@ def get_origin(self): def get_size(self): geometry = self.native.get_geometry() return geometry.width, geometry.height + + def get_image_data(self): + pass diff --git a/iOS/src/toga_iOS/screen.py b/iOS/src/toga_iOS/screen.py index 2416359b8f..d20aed4647 100644 --- a/iOS/src/toga_iOS/screen.py +++ b/iOS/src/toga_iOS/screen.py @@ -22,3 +22,6 @@ def get_origin(self): def get_size(self): return self.native.bounds.size.width, self.native.bounds.size.height + + def get_image_data(self): + pass diff --git a/web/src/toga_web/screen.py b/web/src/toga_web/screen.py index b71804ddac..1faf8dd535 100644 --- a/web/src/toga_web/screen.py +++ b/web/src/toga_web/screen.py @@ -22,3 +22,6 @@ def get_origin(self): def get_size(self): return self.native.clientWidth, self.native.clientHeight + + def get_image_data(self): + pass diff --git a/winforms/src/toga_winforms/screen.py b/winforms/src/toga_winforms/screen.py index b0d1379f11..35a9d2272c 100644 --- a/winforms/src/toga_winforms/screen.py +++ b/winforms/src/toga_winforms/screen.py @@ -1,4 +1,12 @@ from toga.screen import Screen as ScreenInterface +from toga_winforms.libs import ( + Bitmap, + Graphics, + ImageFormat, + MemoryStream, + Point, + Size, +) class Screen: @@ -22,3 +30,14 @@ def get_origin(self): def get_size(self): return self.native.Bounds.Width, self.native.Bounds.Height + + def get_image_data(self): + bitmap = Bitmap(*self.get_size()) + graphics = Graphics.FromImage(bitmap) + source_point = Point(*self.get_origin()) + destination_point = Point(0, 0) + copy_size = Size(*self.get_size()) + graphics.CopyFromScreen(source_point, destination_point, copy_size) + stream = MemoryStream() + bitmap.Save(stream, ImageFormat.Png) + return stream.ToArray() From 397e064cfc18ff74b169ac1a394c87b8f912d75e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 9 Aug 2023 13:16:38 -0700 Subject: [PATCH 016/102] Added test of `Screen.as_image()` in WindowExample --- examples/window/window/app.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 2d02e2a077..5c8feb2c16 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -197,6 +197,34 @@ def do_screen_change(screen): on_press=lambda x, screen=screen: do_screen_change(screen), ) ) + + screen_as_image_btns_box = toga.Box( + children=[toga.Label(text="Take screenshot of screen:")] + ) + + async def do_screen_as_image(screen): + path = await self.main_window.save_file_dialog( + "Screenshot save path", + suggested_filename=f"Screenshot_{screen.name}.png", + file_types=["jpg", "png"], + ) + if path is None: + return + screen.as_image().save(path) + self.main_window.info_dialog( + "Screenshot saved", f"Screenshot of {screen.name} was saved properly!" + ) + + for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): + screen_as_image_btns_box.add( + toga.Button( + text=f"{index}: {screen.name}", + on_press=lambda _, screen=screen: asyncio.create_task( + do_screen_as_image(screen) + ), + ) + ) + self.main_box = toga.Box( children=[ self.label, @@ -216,6 +244,7 @@ def do_screen_change(screen): btn_change_content, btn_hide, screen_change_btns_box, + screen_as_image_btns_box, ], style=Pack(direction=COLUMN), ) From b64d361178431d5d8e83cfbea1c3f06ce8ad749c Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Thu, 10 Aug 2023 06:45:06 +0530 Subject: [PATCH 017/102] Update iOS/src/toga_iOS/screen.py Co-authored-by: Russell Keith-Magee --- iOS/src/toga_iOS/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOS/src/toga_iOS/screen.py b/iOS/src/toga_iOS/screen.py index d20aed4647..2239223100 100644 --- a/iOS/src/toga_iOS/screen.py +++ b/iOS/src/toga_iOS/screen.py @@ -15,7 +15,7 @@ def __new__(cls, native): return instance def get_name(self): - return "IOS Screen" + return "iOS Screen" def get_origin(self): return (0, 0) From c32a0ce32e65ebfff540eff306067a98c33afdd0 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 10 Aug 2023 23:08:56 -0700 Subject: [PATCH 018/102] Modified codebase to address some reported issues. --- android/src/toga_android/screen.py | 2 +- cocoa/src/toga_cocoa/screen.py | 2 +- core/src/toga/screen.py | 3 +++ dummy/src/toga_dummy/screen.py | 2 +- examples/window/window/app.py | 43 +++++++++++++++++------------- gtk/src/toga_gtk/screen.py | 2 +- iOS/src/toga_iOS/screen.py | 2 +- web/src/toga_web/screen.py | 2 +- 8 files changed, 33 insertions(+), 25 deletions(-) diff --git a/android/src/toga_android/screen.py b/android/src/toga_android/screen.py index 9345baeae4..b826315efa 100644 --- a/android/src/toga_android/screen.py +++ b/android/src/toga_android/screen.py @@ -24,4 +24,4 @@ def get_size(self): return self.native.width, self.native.height def get_image_data(self): - pass + self.interface.factory.not_implemented("Screen.get_image_data()") diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index 8700053787..7c67f75866 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -26,4 +26,4 @@ def get_size(self): return (frame_native.size.width, frame_native.size.height) def get_image_data(self): - pass + self.interface.factory.not_implemented("Screen.get_image_data()") diff --git a/core/src/toga/screen.py b/core/src/toga/screen.py index 66d64bd9a2..0dde3b9717 100644 --- a/core/src/toga/screen.py +++ b/core/src/toga/screen.py @@ -1,9 +1,12 @@ +from toga.platform import get_platform_factory + from .images import Image class Screen: def __init__(self, _impl): self._impl = _impl + self.factory = get_platform_factory() @property def name(self): diff --git a/dummy/src/toga_dummy/screen.py b/dummy/src/toga_dummy/screen.py index 4d61ea4362..2686406d4a 100644 --- a/dummy/src/toga_dummy/screen.py +++ b/dummy/src/toga_dummy/screen.py @@ -30,4 +30,4 @@ def get_size(self): return (1920, 1080) def get_image_data(self): - pass + self._action("get_image_data") diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 5c8feb2c16..7f158b2727 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -75,6 +75,24 @@ def do_new_windows(self, widget, **kwargs): self.app.windows += no_close_handler_window no_close_handler_window.show() + def do_screen_change(self, widget, **kwargs): + screen = kwargs["screen"] + self.current_window.screen = screen + + async def do_screen_as_image(self, widget, **kwargs): + screen = kwargs["screen"] + path = await self.main_window.save_file_dialog( + "Screenshot save path", + suggested_filename=f"Screenshot_{screen.name}.png", + file_types=["jpg", "png"], + ) + if path is None: + return + screen.as_image().save(path) + self.main_window.info_dialog( + "Screenshot saved", f"Screenshot of {screen.name} was saved properly!" + ) + async def do_current_window_cycling(self, widget, **kwargs): for window in self.windows: self.current_window = window @@ -183,18 +201,18 @@ def startup(self): "Change content", on_press=self.do_next_content, style=btn_style ) btn_hide = toga.Button("Hide", on_press=self.do_hide, style=btn_style) + screen_change_btns_box = toga.Box( children=[toga.Label(text="Move current window to:")] ) - def do_screen_change(screen): - self.current_window.screen = screen - for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): screen_change_btns_box.add( toga.Button( text=f"{index}: {screen.name}", - on_press=lambda x, screen=screen: do_screen_change(screen), + on_press=lambda widget, screen=screen: self.do_screen_change( + widget, screen=screen + ), ) ) @@ -202,25 +220,12 @@ def do_screen_change(screen): children=[toga.Label(text="Take screenshot of screen:")] ) - async def do_screen_as_image(screen): - path = await self.main_window.save_file_dialog( - "Screenshot save path", - suggested_filename=f"Screenshot_{screen.name}.png", - file_types=["jpg", "png"], - ) - if path is None: - return - screen.as_image().save(path) - self.main_window.info_dialog( - "Screenshot saved", f"Screenshot of {screen.name} was saved properly!" - ) - for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): screen_as_image_btns_box.add( toga.Button( text=f"{index}: {screen.name}", - on_press=lambda _, screen=screen: asyncio.create_task( - do_screen_as_image(screen) + on_press=lambda widget, screen=screen: asyncio.create_task( + self.do_screen_as_image(widget, screen=screen) ), ) ) diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index 337dc6ec38..0c22046cfd 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -26,4 +26,4 @@ def get_size(self): return geometry.width, geometry.height def get_image_data(self): - pass + self.interface.factory.not_implemented("Screen.get_image_data()") diff --git a/iOS/src/toga_iOS/screen.py b/iOS/src/toga_iOS/screen.py index 2239223100..b0dee923f0 100644 --- a/iOS/src/toga_iOS/screen.py +++ b/iOS/src/toga_iOS/screen.py @@ -24,4 +24,4 @@ def get_size(self): return self.native.bounds.size.width, self.native.bounds.size.height def get_image_data(self): - pass + self.interface.factory.not_implemented("Screen.get_image_data()") diff --git a/web/src/toga_web/screen.py b/web/src/toga_web/screen.py index 1faf8dd535..dd876bbae4 100644 --- a/web/src/toga_web/screen.py +++ b/web/src/toga_web/screen.py @@ -24,4 +24,4 @@ def get_size(self): return self.native.clientWidth, self.native.clientHeight def get_image_data(self): - pass + self.interface.factory.not_implemented("Screen.get_image_data()") From 5f4ef8c7df4c51a1d059fe6b4de6bcbb80a39073 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 11 Aug 2023 03:53:33 -0700 Subject: [PATCH 019/102] Modified android codebase as per recommendation. --- android/src/toga_android/app.py | 4 +++- android/src/toga_android/libs/android/hardware.py | 3 +++ android/src/toga_android/libs/android/view.py | 2 ++ android/src/toga_android/screen.py | 14 ++++++++++++-- android/src/toga_android/window.py | 2 +- 5 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 android/src/toga_android/libs/android/hardware.py diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 74d543017d..f8979be591 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -7,6 +7,7 @@ from .libs.activity import IPythonApp, MainActivity from .libs.android.graphics import Drawable +from .libs.android.hardware import DisplayManager from .libs.android.view import Menu, MenuItem from .screen import Screen as ScreenImpl from .window import Window @@ -242,4 +243,5 @@ def show_cursor(self): pass def get_screens(self): - return [ScreenImpl(self.interface.main_window._impl)] + screen_list = DisplayManager.getDisplays() + return [ScreenImpl(screen) for screen in screen_list] diff --git a/android/src/toga_android/libs/android/hardware.py b/android/src/toga_android/libs/android/hardware.py new file mode 100644 index 0000000000..53ace7a72e --- /dev/null +++ b/android/src/toga_android/libs/android/hardware.py @@ -0,0 +1,3 @@ +from rubicon.java import JavaClass + +DisplayManager = JavaClass("android/hardware/DisplayManager") diff --git a/android/src/toga_android/libs/android/view.py b/android/src/toga_android/libs/android/view.py index f86ed0d17c..f1d8a92b45 100644 --- a/android/src/toga_android/libs/android/view.py +++ b/android/src/toga_android/libs/android/view.py @@ -17,3 +17,5 @@ ) OnKeyListener = JavaInterface("android/view/View$OnKeyListener") KeyEvent = JavaClass("android/view/KeyEvent") +WindowInsets = JavaClass("android/view/WindowInsets") +WindowManager = JavaClass("android/view/WindowManager") diff --git a/android/src/toga_android/screen.py b/android/src/toga_android/screen.py index b826315efa..facf7a3b92 100644 --- a/android/src/toga_android/screen.py +++ b/android/src/toga_android/screen.py @@ -1,5 +1,7 @@ from toga.screen import Screen as ScreenInterface +from .android.view import WindowInsets, WindowManager + class Screen: _instances = {} @@ -15,13 +17,21 @@ def __new__(cls, native): return instance def get_name(self): - return "Android Screen" + return self.native.getName() def get_origin(self): return (0, 0) def get_size(self): - return self.native.width, self.native.height + metrics = WindowManager.getCurrentWindowMetrics() + window_insets = metrics.getWindowInsets() + insets = window_insets.getInsetsIgnoringVisibility( + WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout() + ) + insets_width = insets.right + insets.left + insets_height = insets.top + insets.bottom + bounds = metrics.getBounds() + return (bounds.width() - insets_width, bounds.height() - insets_height) def get_image_data(self): self.interface.factory.not_implemented("Screen.get_image_data()") diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index de042cb358..1f716cf658 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -74,4 +74,4 @@ def set_full_screen(self, is_full_screen): self.interface.factory.not_implemented("Window.set_full_screen()") def get_current_screen(self): - return ScreenImpl(self) + return ScreenImpl(self.app.native.getContext().getResources().getDisplay()) From bede871ceb415cb6490f6e13808e1464602386c9 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 11 Aug 2023 04:04:00 -0700 Subject: [PATCH 020/102] Resolving file conflict --- winforms/src/toga_winforms/window.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index e4ffc80d4b..be1cdc9361 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -2,6 +2,8 @@ from .container import Container, MinimumContainer from .libs import Point, Size, WinForms +from .widgets.base import Scalable + from .screen import Screen as ScreenImpl @@ -74,17 +76,18 @@ def create_toolbar(self): self.resize_content() def get_position(self): - return self.native.Location.X, self.native.Location.Y + location = self.native.Location + return tuple(map(self.scale_out, (location.X, location.Y))) def set_position(self, position): - self.native.StartPosition = WinForms.FormStartPosition.Manual - self.native.Location = Point(*position) + self.native.Location = Point(*map(self.scale_in, position)) def get_size(self): - return self.native.ClientSize.Width, self.native.ClientSize.Height + size = self.native.ClientSize + return tuple(map(self.scale_out, (size.Width, size.Height))) def set_size(self, size): - self.native.ClientSize = Size(*size) + self.native.ClientSize = Size(*map(self.scale_in, size)) def set_app(self, app): if app is None: From 3a3c82d18642396724a0ef519066fe50ce97f708 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 11 Aug 2023 04:06:52 -0700 Subject: [PATCH 021/102] Re: Resolving file conflict --- winforms/src/toga_winforms/window.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index be1cdc9361..5e0dbb7b81 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -4,8 +4,6 @@ from .libs import Point, Size, WinForms from .widgets.base import Scalable -from .screen import Screen as ScreenImpl - class Window(Container): def __init__(self, interface, title, position, size): From 2b453f64c51a7a0f398afe3e7a3dbbeebd53a6fe Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 11 Aug 2023 04:09:08 -0700 Subject: [PATCH 022/102] Re: Conflict resolve and updating to main codebase --- winforms/src/toga_winforms/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index ce5531697d..9023e6a618 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -2,6 +2,7 @@ from .container import Container, MinimumContainer from .libs import Point, Size, WinForms +from .screen import Screen as ScreenImpl from .widgets.base import Scalable From 17258743cc7ed26201a259182a957926417fc5df Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 14 Aug 2023 06:00:25 -0700 Subject: [PATCH 023/102] Added `as_image()` support for gtk screen backend. --- gtk/src/toga_gtk/app.py | 20 +++++++++++++------- gtk/src/toga_gtk/screen.py | 25 ++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 77383c43d8..7f485561c4 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -198,13 +198,19 @@ def main_loop(self): def get_screens(self): display = Gdk.Display.get_default() - primary_screen = ScreenImpl(display.get_primary_monitor()) - screen_list = [primary_screen] + [ - ScreenImpl(native=display.get_monitor(i)) - for i in range(display.get_n_monitors()) - if display.get_monitor(i) != primary_screen.native - ] - return screen_list + if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": + primary_screen = ScreenImpl(display.get_primary_monitor()) + screen_list = [primary_screen] + [ + ScreenImpl(native=display.get_monitor(i)) + for i in range(display.get_n_monitors()) + if display.get_monitor(i) != primary_screen.native + ] + return screen_list + else: + return [ + ScreenImpl(native=display.get_monitor(i)) + for i in range(display.get_n_monitors()) + ] def set_main_window(self, window): pass diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index 0c22046cfd..611a7bd292 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -1,5 +1,9 @@ +import os + from toga.screen import Screen as ScreenInterface +from .libs import Gdk + class Screen: _instances = {} @@ -26,4 +30,23 @@ def get_size(self): return geometry.width, geometry.height def get_image_data(self): - self.interface.factory.not_implemented("Screen.get_image_data()") + def get_image_data(self): + if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": + # Only works for x11 + display = self.native.get_display() + screen = display.get_default_screen() + window = screen.get_root_window() + geometry = self.native.get_geometry() + screenshot = Gdk.pixbuf_get_from_window( + window, geometry.x, geometry.y, geometry.width, geometry.height + ) + success, buffer = screenshot.save_to_bufferv("png", [], []) + if success: + return bytes(buffer) + else: + print("Failed to save screenshot to buffer.") + return None + else: + # Not implemented for wayland + self.interface.factory.not_implemented("Screen.get_image_data()") + return None From 98bb5ef1e40d1c46ac250ec3954bbbc491118b8f Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 15 Aug 2023 05:52:03 -0700 Subject: [PATCH 024/102] Added `as_image()` support for cocoa screen. --- cocoa/src/toga_cocoa/libs/__init__.py | 2 ++ cocoa/src/toga_cocoa/screen.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/libs/__init__.py b/cocoa/src/toga_cocoa/libs/__init__.py index 310642942a..5997248f73 100644 --- a/cocoa/src/toga_cocoa/libs/__init__.py +++ b/cocoa/src/toga_cocoa/libs/__init__.py @@ -2,7 +2,9 @@ from rubicon.objc import NSArray # noqa: F401 from rubicon.objc import ( # noqa: F401 SEL, + CGDisplayCreateImage, CGFloat, + CGMainDisplayID, CGRect, NSMakePoint, NSMakeRect, diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index 7c67f75866..b5ce978ec5 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -1,4 +1,10 @@ from toga.screen import Screen as ScreenInterface +from toga_cocoa.libs import ( + CGDisplayCreateImage, + CGMainDisplayID, + NSBitmapImageFileType, + NSBitmapImageRep, +) class Screen: @@ -26,4 +32,11 @@ def get_size(self): return (frame_native.size.width, frame_native.size.height) def get_image_data(self): - self.interface.factory.not_implemented("Screen.get_image_data()") + screenshot_frame = self.native.frame() + cg_image = CGDisplayCreateImage(CGMainDisplayID(), screenshot_frame) + bitmap_rep = NSBitmapImageRep.alloc().initWithCGImage(cg_image) + data = bitmap_rep.representationUsingType( + NSBitmapImageFileType.PNG, + properties=None, + ) + return data From 63af787e825e6cc03439c7d8752c59fcd0893227 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 16 Aug 2023 19:52:11 -0700 Subject: [PATCH 025/102] Corrected reported gtk `as_image()` behaviour. --- gtk/src/toga_gtk/screen.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index 611a7bd292..c5248c2600 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -30,23 +30,22 @@ def get_size(self): return geometry.width, geometry.height def get_image_data(self): - def get_image_data(self): - if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": - # Only works for x11 - display = self.native.get_display() - screen = display.get_default_screen() - window = screen.get_root_window() - geometry = self.native.get_geometry() - screenshot = Gdk.pixbuf_get_from_window( - window, geometry.x, geometry.y, geometry.width, geometry.height - ) - success, buffer = screenshot.save_to_bufferv("png", [], []) - if success: - return bytes(buffer) - else: - print("Failed to save screenshot to buffer.") - return None + if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": + # Only works for x11 + display = self.native.get_display() + screen = display.get_default_screen() + window = screen.get_root_window() + geometry = self.native.get_geometry() + screenshot = Gdk.pixbuf_get_from_window( + window, geometry.x, geometry.y, geometry.width, geometry.height + ) + success, buffer = screenshot.save_to_bufferv("png", [], []) + if success: + return bytes(buffer) else: - # Not implemented for wayland - self.interface.factory.not_implemented("Screen.get_image_data()") + print("Failed to save screenshot to buffer.") return None + else: + # Not implemented for wayland + self.interface.factory.not_implemented("Screen.get_image_data()") + return None From 7508b5c3fd3e03c12e978ba122594f85711be49a Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 17 Aug 2023 01:19:07 -0700 Subject: [PATCH 026/102] Corrected example app as per recommendation. --- examples/window/window/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 7f158b2727..10654c958b 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -81,6 +81,7 @@ def do_screen_change(self, widget, **kwargs): async def do_screen_as_image(self, widget, **kwargs): screen = kwargs["screen"] + screenshot = screen.as_image() path = await self.main_window.save_file_dialog( "Screenshot save path", suggested_filename=f"Screenshot_{screen.name}.png", @@ -88,7 +89,7 @@ async def do_screen_as_image(self, widget, **kwargs): ) if path is None: return - screen.as_image().save(path) + screenshot.save(path) self.main_window.info_dialog( "Screenshot saved", f"Screenshot of {screen.name} was saved properly!" ) From 03f55af20d6e96e77ffe6324a64c928deb1ea6f0 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 17 Aug 2023 17:42:32 -0700 Subject: [PATCH 027/102] Corrected cocoa quartz API importing. --- cocoa/src/toga_cocoa/libs/__init__.py | 2 -- cocoa/src/toga_cocoa/libs/core_graphics.py | 12 ++++++++++++ cocoa/src/toga_cocoa/screen.py | 3 ++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/cocoa/src/toga_cocoa/libs/__init__.py b/cocoa/src/toga_cocoa/libs/__init__.py index 5997248f73..310642942a 100644 --- a/cocoa/src/toga_cocoa/libs/__init__.py +++ b/cocoa/src/toga_cocoa/libs/__init__.py @@ -2,9 +2,7 @@ from rubicon.objc import NSArray # noqa: F401 from rubicon.objc import ( # noqa: F401 SEL, - CGDisplayCreateImage, CGFloat, - CGMainDisplayID, CGRect, NSMakePoint, NSMakeRect, diff --git a/cocoa/src/toga_cocoa/libs/core_graphics.py b/cocoa/src/toga_cocoa/libs/core_graphics.py index b9b514c4a8..b4679f29db 100644 --- a/cocoa/src/toga_cocoa/libs/core_graphics.py +++ b/cocoa/src/toga_cocoa/libs/core_graphics.py @@ -18,6 +18,7 @@ ###################################################################### core_graphics = load_library("CoreGraphics") +quartz = load_library("Quartz") ###################################################################### ###################################################################### @@ -217,3 +218,14 @@ class CGEventRef(c_void_p): kCGBitmapByteOrder32Little = 2 << 12 kCGBitmapByteOrder16Big = 3 << 12 kCGBitmapByteOrder32Big = 4 << 12 + +###################################################################### +# Quartz functions + +# CGDirectDisplayID CGMainDisplayID(void); +quartz.CGMainDisplayID.restype = c_uint32 # CGDirectDisplayID is a UInt32 +quartz.CGMainDisplayID.argtypes = None + +# CGImageRef CGDisplayCreateImage(CGDirectDisplayID displayID, CGRect rect); +quartz.CGDisplayCreateImage.restype = c_void_p # CGImageRef is a void pointer +quartz.CGDisplayCreateImage.argtypes = [c_uint32, CGRect] diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index b5ce978ec5..a27880b199 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -2,6 +2,7 @@ from toga_cocoa.libs import ( CGDisplayCreateImage, CGMainDisplayID, + CGRect, NSBitmapImageFileType, NSBitmapImageRep, ) @@ -32,7 +33,7 @@ def get_size(self): return (frame_native.size.width, frame_native.size.height) def get_image_data(self): - screenshot_frame = self.native.frame() + screenshot_frame = CGRect(self.native.frame()) cg_image = CGDisplayCreateImage(CGMainDisplayID(), screenshot_frame) bitmap_rep = NSBitmapImageRep.alloc().initWithCGImage(cg_image) data = bitmap_rep.representationUsingType( From 4da8cd6aaf48bd739c97a23a5f7ff6a9db16eaa3 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 21 Aug 2023 13:43:45 +0930 Subject: [PATCH 028/102] Correct core_graphics usage. --- cocoa/src/toga_cocoa/libs/core_graphics.py | 17 +++++++++++++---- cocoa/src/toga_cocoa/screen.py | 21 ++++++++++++++------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/cocoa/src/toga_cocoa/libs/core_graphics.py b/cocoa/src/toga_cocoa/libs/core_graphics.py index b4679f29db..f4cb1058c1 100644 --- a/cocoa/src/toga_cocoa/libs/core_graphics.py +++ b/cocoa/src/toga_cocoa/libs/core_graphics.py @@ -222,10 +222,19 @@ class CGEventRef(c_void_p): ###################################################################### # Quartz functions +CGDirectDisplayID = c_uint32 + # CGDirectDisplayID CGMainDisplayID(void); -quartz.CGMainDisplayID.restype = c_uint32 # CGDirectDisplayID is a UInt32 -quartz.CGMainDisplayID.argtypes = None +core_graphics.CGMainDisplayID.restype = CGDirectDisplayID +core_graphics.CGMainDisplayID.argtypes = None + + +class CGImageRef(c_void_p): + pass + + +register_preferred_encoding(b"^{CGImage=}", CGImageRef) # CGImageRef CGDisplayCreateImage(CGDirectDisplayID displayID, CGRect rect); -quartz.CGDisplayCreateImage.restype = c_void_p # CGImageRef is a void pointer -quartz.CGDisplayCreateImage.argtypes = [c_uint32, CGRect] +core_graphics.CGDisplayCreateImage.restype = CGImageRef +core_graphics.CGDisplayCreateImage.argtypes = [CGDirectDisplayID, CGRect] diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index a27880b199..1debca7a65 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -1,10 +1,10 @@ +from ctypes import POINTER, c_char, cast + from toga.screen import Screen as ScreenInterface from toga_cocoa.libs import ( - CGDisplayCreateImage, - CGMainDisplayID, - CGRect, NSBitmapImageFileType, NSBitmapImageRep, + core_graphics, ) @@ -33,11 +33,18 @@ def get_size(self): return (frame_native.size.width, frame_native.size.height) def get_image_data(self): - screenshot_frame = CGRect(self.native.frame()) - cg_image = CGDisplayCreateImage(CGMainDisplayID(), screenshot_frame) - bitmap_rep = NSBitmapImageRep.alloc().initWithCGImage(cg_image) + image = core_graphics.CGDisplayCreateImage( + core_graphics.CGMainDisplayID(), + self.native.frame, + ) + bitmap_rep = NSBitmapImageRep.alloc().initWithCGImage(image) data = bitmap_rep.representationUsingType( NSBitmapImageFileType.PNG, properties=None, ) - return data + + # data is an NSData object that has .bytes as a c_void_p, and a .length. Cast to + # POINTER(c_char) to get an addressable array of bytes, and slice that array to + # the known length. We don't use c_char_p because it has handling of NUL + # termination, and POINTER(c_char) allows array subscripting. + return cast(data.bytes, POINTER(c_char))[: data.length] From 7d0ba46807747f5cc3e0f1ca85a7e1e66baf8f2d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 21 Aug 2023 13:44:23 +0930 Subject: [PATCH 029/102] Tweaks to screen aspects of window demo. --- examples/window/window/app.py | 42 ++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 10654c958b..dc4cbe267e 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -1,8 +1,9 @@ import asyncio from datetime import datetime +from functools import partial import toga -from toga.constants import COLUMN +from toga.constants import COLUMN, RIGHT from toga.style import Pack @@ -75,24 +76,19 @@ def do_new_windows(self, widget, **kwargs): self.app.windows += no_close_handler_window no_close_handler_window.show() - def do_screen_change(self, widget, **kwargs): - screen = kwargs["screen"] + def do_screen_change(self, screen, widget, **kwargs): self.current_window.screen = screen - async def do_screen_as_image(self, widget, **kwargs): - screen = kwargs["screen"] + async def do_save_screenshot(self, screen, window, **kwargs): screenshot = screen.as_image() path = await self.main_window.save_file_dialog( - "Screenshot save path", + "Save screenshot", suggested_filename=f"Screenshot_{screen.name}.png", - file_types=["jpg", "png"], + file_types=["png"], ) if path is None: return screenshot.save(path) - self.main_window.info_dialog( - "Screenshot saved", f"Screenshot of {screen.name} was saved properly!" - ) async def do_current_window_cycling(self, widget, **kwargs): for window in self.windows: @@ -204,30 +200,40 @@ def startup(self): btn_hide = toga.Button("Hide", on_press=self.do_hide, style=btn_style) screen_change_btns_box = toga.Box( - children=[toga.Label(text="Move current window to:")] + children=[ + toga.Label( + text="Move current window to:", + style=Pack(width=200, text_align=RIGHT), + ) + ], + style=Pack(padding=5), ) for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): screen_change_btns_box.add( toga.Button( text=f"{index}: {screen.name}", - on_press=lambda widget, screen=screen: self.do_screen_change( - widget, screen=screen - ), + on_press=partial(self.do_screen_change, screen), + style=Pack(padding_left=5), ) ) screen_as_image_btns_box = toga.Box( - children=[toga.Label(text="Take screenshot of screen:")] + children=[ + toga.Label( + text="Take screenshot of screen:", + style=Pack(width=200, text_align=RIGHT), + ) + ], + style=Pack(padding=5), ) for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): screen_as_image_btns_box.add( toga.Button( text=f"{index}: {screen.name}", - on_press=lambda widget, screen=screen: asyncio.create_task( - self.do_screen_as_image(widget, screen=screen) - ), + on_press=partial(self.do_save_screenshot, screen), + style=Pack(padding_left=5), ) ) From 0f23940b095bca2c5dee657905b01a820ef1fb15 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 21 Aug 2023 14:07:42 +0930 Subject: [PATCH 030/102] Remove the explicit quartz wrapper. --- cocoa/src/toga_cocoa/libs/core_graphics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/libs/core_graphics.py b/cocoa/src/toga_cocoa/libs/core_graphics.py index f4cb1058c1..8277b73243 100644 --- a/cocoa/src/toga_cocoa/libs/core_graphics.py +++ b/cocoa/src/toga_cocoa/libs/core_graphics.py @@ -18,7 +18,6 @@ ###################################################################### core_graphics = load_library("CoreGraphics") -quartz = load_library("Quartz") ###################################################################### ###################################################################### From 983dd7433c4f12af096f26955cdf213603271ca3 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 22 Aug 2023 22:47:10 -0700 Subject: [PATCH 031/102] Resolving file conflicts --- winforms/src/toga_winforms/window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 005977d895..d033ba2bd4 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -1,8 +1,9 @@ +import System.Windows.Forms as WinForms +from System.Drawing import Point, Size + from toga import GROUP_BREAK, SECTION_BREAK from .container import Container -from .libs import Point, Size, WinForms -from .screen import Screen as ScreenImpl from .widgets.base import Scalable From 8e0aa0c3935a4d9b7cfa61f85e76d6c347694532 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 22 Aug 2023 22:54:39 -0700 Subject: [PATCH 032/102] Re: Resolving file conflict. --- winforms/src/toga_winforms/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index d033ba2bd4..de3cca5324 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -4,6 +4,7 @@ from toga import GROUP_BREAK, SECTION_BREAK from .container import Container +from .screen import Screen as ScreenImpl from .widgets.base import Scalable From 5af21b73bb0b91591456a1cdd5289998024cdf4d Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 30 Aug 2023 01:57:14 -0700 Subject: [PATCH 033/102] Resolving file conflicts. --- examples/window/window/app.py | 92 ++++++----------------------------- 1 file changed, 15 insertions(+), 77 deletions(-) diff --git a/examples/window/window/app.py b/examples/window/window/app.py index dc4cbe267e..184ad0cc76 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -1,9 +1,8 @@ import asyncio from datetime import datetime -from functools import partial import toga -from toga.constants import COLUMN, RIGHT +from toga.constants import COLUMN from toga.style import Pack @@ -18,12 +17,6 @@ def do_left(self, widget, **kwargs): def do_right(self, widget, **kwargs): self.main_window.position = (2000, 500) - def do_left_current_screen(self, widget, **kwargs): - self.main_window.screen_position = (0, 100) - - def do_right_current_screen(self, widget, **kwargs): - self.main_window.screen_position = (1080, 100) - def do_small(self, widget, **kwargs): self.main_window.size = (400, 300) @@ -76,20 +69,6 @@ def do_new_windows(self, widget, **kwargs): self.app.windows += no_close_handler_window no_close_handler_window.show() - def do_screen_change(self, screen, widget, **kwargs): - self.current_window.screen = screen - - async def do_save_screenshot(self, screen, window, **kwargs): - screenshot = screen.as_image() - path = await self.main_window.save_file_dialog( - "Save screenshot", - suggested_filename=f"Screenshot_{screen.name}.png", - file_types=["png"], - ) - if path is None: - return - screenshot.save(path) - async def do_current_window_cycling(self, widget, **kwargs): for window in self.windows: self.current_window = window @@ -116,7 +95,7 @@ def do_next_content(self, widget): self.main_window.content = self.next_box def do_prev_content(self, widget): - self.main_window.content = self.main_box + self.main_window.content = self.main_scroller def do_hide(self, widget): self.main_window.visible = False @@ -126,6 +105,9 @@ def do_hide(self, widget): self.main_window.visible = True self.main_window.info_dialog("Here we go again", "I'm back!") + def do_beep(self, widget): + self.app.beep() + def exit_handler(self, app, **kwargs): self.close_count += 1 if self.close_count % 2 == 1: @@ -158,16 +140,6 @@ def startup(self): ) btn_do_left = toga.Button("Go left", on_press=self.do_left, style=btn_style) btn_do_right = toga.Button("Go right", on_press=self.do_right, style=btn_style) - btn_do_left_current_screen = toga.Button( - "Go left on current screen", - on_press=self.do_left_current_screen, - style=btn_style, - ) - btn_do_right_current_screen = toga.Button( - "Go right on current screen", - on_press=self.do_right_current_screen, - style=btn_style, - ) btn_do_small = toga.Button( "Become small", on_press=self.do_small, style=btn_style ) @@ -198,53 +170,14 @@ def startup(self): "Change content", on_press=self.do_next_content, style=btn_style ) btn_hide = toga.Button("Hide", on_press=self.do_hide, style=btn_style) + btn_beep = toga.Button("Beep", on_press=self.do_beep, style=btn_style) - screen_change_btns_box = toga.Box( - children=[ - toga.Label( - text="Move current window to:", - style=Pack(width=200, text_align=RIGHT), - ) - ], - style=Pack(padding=5), - ) - - for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): - screen_change_btns_box.add( - toga.Button( - text=f"{index}: {screen.name}", - on_press=partial(self.do_screen_change, screen), - style=Pack(padding_left=5), - ) - ) - - screen_as_image_btns_box = toga.Box( - children=[ - toga.Label( - text="Take screenshot of screen:", - style=Pack(width=200, text_align=RIGHT), - ) - ], - style=Pack(padding=5), - ) - - for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): - screen_as_image_btns_box.add( - toga.Button( - text=f"{index}: {screen.name}", - on_press=partial(self.do_save_screenshot, screen), - style=Pack(padding_left=5), - ) - ) - - self.main_box = toga.Box( + self.inner_box = toga.Box( children=[ self.label, btn_do_origin, btn_do_left, btn_do_right, - btn_do_left_current_screen, - btn_do_right_current_screen, btn_do_small, btn_do_large, btn_do_full_screen, @@ -255,11 +188,16 @@ def startup(self): btn_do_report, btn_change_content, btn_hide, - screen_change_btns_box, - screen_as_image_btns_box, + btn_beep, ], style=Pack(direction=COLUMN), ) + self.main_scroller = toga.ScrollContainer( + horizontal=False, + vertical=True, + style=Pack(flex=1), + ) + self.main_scroller.content = self.inner_box btn_change_back = toga.Button( "Go back", on_press=self.do_prev_content, style=btn_style @@ -278,7 +216,7 @@ def startup(self): self.main_window.toolbar.add(restore_command) # Add the content on the main window - self.main_window.content = self.main_box + self.main_window.content = self.main_scroller # Show the main window self.main_window.show() From 5a12f3b31cb80ace7ce5dd6b91e4cd2c58bf54ee Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 30 Aug 2023 02:28:04 -0700 Subject: [PATCH 034/102] Re: Resolving file conflicts --- examples/window/window/app.py | 72 ++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 184ad0cc76..36d5770bc7 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -1,8 +1,9 @@ import asyncio from datetime import datetime +from functools import partial import toga -from toga.constants import COLUMN +from toga.constants import COLUMN, RIGHT from toga.style import Pack @@ -17,6 +18,12 @@ def do_left(self, widget, **kwargs): def do_right(self, widget, **kwargs): self.main_window.position = (2000, 500) + def do_left_current_screen(self, widget, **kwargs): + self.main_window.screen_position = (0, 100) + + def do_right_current_screen(self, widget, **kwargs): + self.main_window.screen_position = (1080, 100) + def do_small(self, widget, **kwargs): self.main_window.size = (400, 300) @@ -69,6 +76,20 @@ def do_new_windows(self, widget, **kwargs): self.app.windows += no_close_handler_window no_close_handler_window.show() + def do_screen_change(self, screen, widget, **kwargs): + self.current_window.screen = screen + + async def do_save_screenshot(self, screen, window, **kwargs): + screenshot = screen.as_image() + path = await self.main_window.save_file_dialog( + "Save screenshot", + suggested_filename=f"Screenshot_{screen.name}.png", + file_types=["png"], + ) + if path is None: + return + screenshot.save(path) + async def do_current_window_cycling(self, widget, **kwargs): for window in self.windows: self.current_window = window @@ -140,6 +161,16 @@ def startup(self): ) btn_do_left = toga.Button("Go left", on_press=self.do_left, style=btn_style) btn_do_right = toga.Button("Go right", on_press=self.do_right, style=btn_style) + btn_do_left_current_screen = toga.Button( + "Go left on current screen", + on_press=self.do_left_current_screen, + style=btn_style, + ) + btn_do_right_current_screen = toga.Button( + "Go right on current screen", + on_press=self.do_right_current_screen, + style=btn_style, + ) btn_do_small = toga.Button( "Become small", on_press=self.do_small, style=btn_style ) @@ -172,12 +203,49 @@ def startup(self): btn_hide = toga.Button("Hide", on_press=self.do_hide, style=btn_style) btn_beep = toga.Button("Beep", on_press=self.do_beep, style=btn_style) + screen_change_btns_box = toga.Box( + children=[ + toga.Label( + text="Move current window to:", + style=Pack(width=200, text_align=RIGHT), + ) + ], + style=Pack(padding=5), + ) + + for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): + screen_change_btns_box.add( + toga.Button( + text=f"{index}: {screen.name}", + on_press=partial(self.do_screen_change, screen), + style=Pack(padding_left=5), + ) + ) + screen_as_image_btns_box = toga.Box( + children=[ + toga.Label( + text="Take screenshot of screen:", + style=Pack(width=200, text_align=RIGHT), + ) + ], + style=Pack(padding=5), + ) + for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): + screen_as_image_btns_box.add( + toga.Button( + text=f"{index}: {screen.name}", + on_press=partial(self.do_save_screenshot, screen), + style=Pack(padding_left=5), + ) + ) self.inner_box = toga.Box( children=[ self.label, btn_do_origin, btn_do_left, btn_do_right, + btn_do_left_current_screen, + btn_do_right_current_screen, btn_do_small, btn_do_large, btn_do_full_screen, @@ -189,6 +257,8 @@ def startup(self): btn_change_content, btn_hide, btn_beep, + screen_change_btns_box, + screen_as_image_btns_box, ], style=Pack(direction=COLUMN), ) From 57d9cfb9100dffabac46206b0b4a78e9c9ef4516 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 30 Aug 2023 03:32:56 -0700 Subject: [PATCH 035/102] Re: Resolving file conflicts. --- examples/window/window/app.py | 72 +--------------------------- winforms/src/toga_winforms/screen.py | 11 +++-- 2 files changed, 7 insertions(+), 76 deletions(-) diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 36d5770bc7..184ad0cc76 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -1,9 +1,8 @@ import asyncio from datetime import datetime -from functools import partial import toga -from toga.constants import COLUMN, RIGHT +from toga.constants import COLUMN from toga.style import Pack @@ -18,12 +17,6 @@ def do_left(self, widget, **kwargs): def do_right(self, widget, **kwargs): self.main_window.position = (2000, 500) - def do_left_current_screen(self, widget, **kwargs): - self.main_window.screen_position = (0, 100) - - def do_right_current_screen(self, widget, **kwargs): - self.main_window.screen_position = (1080, 100) - def do_small(self, widget, **kwargs): self.main_window.size = (400, 300) @@ -76,20 +69,6 @@ def do_new_windows(self, widget, **kwargs): self.app.windows += no_close_handler_window no_close_handler_window.show() - def do_screen_change(self, screen, widget, **kwargs): - self.current_window.screen = screen - - async def do_save_screenshot(self, screen, window, **kwargs): - screenshot = screen.as_image() - path = await self.main_window.save_file_dialog( - "Save screenshot", - suggested_filename=f"Screenshot_{screen.name}.png", - file_types=["png"], - ) - if path is None: - return - screenshot.save(path) - async def do_current_window_cycling(self, widget, **kwargs): for window in self.windows: self.current_window = window @@ -161,16 +140,6 @@ def startup(self): ) btn_do_left = toga.Button("Go left", on_press=self.do_left, style=btn_style) btn_do_right = toga.Button("Go right", on_press=self.do_right, style=btn_style) - btn_do_left_current_screen = toga.Button( - "Go left on current screen", - on_press=self.do_left_current_screen, - style=btn_style, - ) - btn_do_right_current_screen = toga.Button( - "Go right on current screen", - on_press=self.do_right_current_screen, - style=btn_style, - ) btn_do_small = toga.Button( "Become small", on_press=self.do_small, style=btn_style ) @@ -203,49 +172,12 @@ def startup(self): btn_hide = toga.Button("Hide", on_press=self.do_hide, style=btn_style) btn_beep = toga.Button("Beep", on_press=self.do_beep, style=btn_style) - screen_change_btns_box = toga.Box( - children=[ - toga.Label( - text="Move current window to:", - style=Pack(width=200, text_align=RIGHT), - ) - ], - style=Pack(padding=5), - ) - - for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): - screen_change_btns_box.add( - toga.Button( - text=f"{index}: {screen.name}", - on_press=partial(self.do_screen_change, screen), - style=Pack(padding_left=5), - ) - ) - screen_as_image_btns_box = toga.Box( - children=[ - toga.Label( - text="Take screenshot of screen:", - style=Pack(width=200, text_align=RIGHT), - ) - ], - style=Pack(padding=5), - ) - for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): - screen_as_image_btns_box.add( - toga.Button( - text=f"{index}: {screen.name}", - on_press=partial(self.do_save_screenshot, screen), - style=Pack(padding_left=5), - ) - ) self.inner_box = toga.Box( children=[ self.label, btn_do_origin, btn_do_left, btn_do_right, - btn_do_left_current_screen, - btn_do_right_current_screen, btn_do_small, btn_do_large, btn_do_full_screen, @@ -257,8 +189,6 @@ def startup(self): btn_change_content, btn_hide, btn_beep, - screen_change_btns_box, - screen_as_image_btns_box, ], style=Pack(direction=COLUMN), ) diff --git a/winforms/src/toga_winforms/screen.py b/winforms/src/toga_winforms/screen.py index 35a9d2272c..eaebf36763 100644 --- a/winforms/src/toga_winforms/screen.py +++ b/winforms/src/toga_winforms/screen.py @@ -1,12 +1,13 @@ -from toga.screen import Screen as ScreenInterface -from toga_winforms.libs import ( +from System.Drawing import ( Bitmap, Graphics, - ImageFormat, - MemoryStream, + Imaging, Point, Size, ) +from System.IO import MemoryStream + +from toga.screen import Screen as ScreenInterface class Screen: @@ -39,5 +40,5 @@ def get_image_data(self): copy_size = Size(*self.get_size()) graphics.CopyFromScreen(source_point, destination_point, copy_size) stream = MemoryStream() - bitmap.Save(stream, ImageFormat.Png) + bitmap.Save(stream, Imaging.ImageFormat.Png) return stream.ToArray() From 72384d30e6a0585b746b7a8d09afe42316b282e8 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 30 Aug 2023 03:40:31 -0700 Subject: [PATCH 036/102] Re: Resolving file conflicts. --- examples/window/window/app.py | 72 ++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 184ad0cc76..f326038c4d 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -1,8 +1,9 @@ import asyncio from datetime import datetime +from functools import partial import toga -from toga.constants import COLUMN +from toga.constants import COLUMN, RIGHT from toga.style import Pack @@ -17,6 +18,12 @@ def do_left(self, widget, **kwargs): def do_right(self, widget, **kwargs): self.main_window.position = (2000, 500) + def do_left_current_screen(self, widget, **kwargs): + self.main_window.screen_position = (0, 100) + + def do_right_current_screen(self, widget, **kwargs): + self.main_window.screen_position = (1080, 100) + def do_small(self, widget, **kwargs): self.main_window.size = (400, 300) @@ -69,6 +76,20 @@ def do_new_windows(self, widget, **kwargs): self.app.windows += no_close_handler_window no_close_handler_window.show() + def do_screen_change(self, screen, widget, **kwargs): + self.current_window.screen = screen + + async def do_save_screenshot(self, screen, window, **kwargs): + screenshot = screen.as_image() + path = await self.main_window.save_file_dialog( + "Save screenshot", + suggested_filename=f"Screenshot_{screen.name}.png", + file_types=["png"], + ) + if path is None: + return + screenshot.save(path) + async def do_current_window_cycling(self, widget, **kwargs): for window in self.windows: self.current_window = window @@ -140,6 +161,16 @@ def startup(self): ) btn_do_left = toga.Button("Go left", on_press=self.do_left, style=btn_style) btn_do_right = toga.Button("Go right", on_press=self.do_right, style=btn_style) + btn_do_left_current_screen = toga.Button( + "Go left on current screen", + on_press=self.do_left_current_screen, + style=btn_style, + ) + btn_do_right_current_screen = toga.Button( + "Go right on current screen", + on_press=self.do_right_current_screen, + style=btn_style, + ) btn_do_small = toga.Button( "Become small", on_press=self.do_small, style=btn_style ) @@ -172,12 +203,49 @@ def startup(self): btn_hide = toga.Button("Hide", on_press=self.do_hide, style=btn_style) btn_beep = toga.Button("Beep", on_press=self.do_beep, style=btn_style) + screen_change_btns_box = toga.Box( + children=[ + toga.Label( + text="Move current window to:", + style=Pack(width=200, text_align=RIGHT), + ) + ], + style=Pack(padding=5), + ) + for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): + screen_change_btns_box.add( + toga.Button( + text=f"{index}: {screen.name}", + on_press=partial(self.do_screen_change, screen), + style=Pack(padding_left=5), + ) + ) + screen_as_image_btns_box = toga.Box( + children=[ + toga.Label( + text="Take screenshot of screen:", + style=Pack(width=200, text_align=RIGHT), + ) + ], + style=Pack(padding=5), + ) + for index, screen in sorted(enumerate(self.screens), key=lambda s: s[1].origin): + screen_as_image_btns_box.add( + toga.Button( + text=f"{index}: {screen.name}", + on_press=partial(self.do_save_screenshot, screen), + style=Pack(padding_left=5), + ) + ) + self.inner_box = toga.Box( children=[ self.label, btn_do_origin, btn_do_left, btn_do_right, + btn_do_left_current_screen, + btn_do_right_current_screen, btn_do_small, btn_do_large, btn_do_full_screen, @@ -189,6 +257,8 @@ def startup(self): btn_change_content, btn_hide, btn_beep, + screen_change_btns_box, + screen_as_image_btns_box, ], style=Pack(direction=COLUMN), ) From 6cf2b2c2105a6af4c08e253cea3c563b757b9fe4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 13 Oct 2023 23:57:35 -0700 Subject: [PATCH 037/102] Modifying to base on the latest main branch. --- android/setup.py | 1 - android/src/toga_android/app.py | 15 +- android/src/toga_android/colors.py | 2 +- android/src/toga_android/container.py | 5 +- android/src/toga_android/dialogs.py | 14 +- android/src/toga_android/fonts.py | 164 +++---- android/src/toga_android/images.py | 2 +- android/src/toga_android/keys.py | 3 +- android/src/toga_android/libs/activity.py | 20 - .../src/toga_android/libs/android/__init__.py | 8 - android/src/toga_android/libs/android/app.py | 3 - .../src/toga_android/libs/android/content.py | 6 - .../libs/android/graphics/__init__.py | 16 - .../libs/android/graphics/drawable.py | 4 - .../src/toga_android/libs/android/hardware.py | 3 - android/src/toga_android/libs/android/os.py | 3 - android/src/toga_android/libs/android/text.py | 5 - android/src/toga_android/libs/android/util.py | 5 - android/src/toga_android/libs/android/view.py | 22 - .../src/toga_android/libs/android/webkit.py | 5 - .../src/toga_android/libs/android/widget.py | 45 -- .../toga_android/libs/androidx/__init__.py | 0 .../libs/androidx/swiperefreshlayout.py | 6 - android/src/toga_android/libs/events.py | 433 ++++++++++++++++++ android/src/toga_android/screen.py | 3 +- android/src/toga_android/widgets/base.py | 26 +- android/src/toga_android/widgets/box.py | 3 +- android/src/toga_android/widgets/button.py | 12 +- android/src/toga_android/widgets/canvas.py | 329 +++++++------ android/src/toga_android/widgets/dateinput.py | 14 +- .../src/toga_android/widgets/detailedlist.py | 76 ++- android/src/toga_android/widgets/imageview.py | 2 +- .../toga_android/widgets/internal/pickers.py | 12 +- android/src/toga_android/widgets/label.py | 26 +- .../widgets/multilinetextinput.py | 5 +- .../src/toga_android/widgets/numberinput.py | 2 +- .../src/toga_android/widgets/passwordinput.py | 3 +- .../src/toga_android/widgets/progressbar.py | 16 +- .../toga_android/widgets/scrollcontainer.py | 30 +- android/src/toga_android/widgets/selection.py | 25 +- android/src/toga_android/widgets/slider.py | 15 +- android/src/toga_android/widgets/switch.py | 15 +- android/src/toga_android/widgets/table.py | 66 +-- android/src/toga_android/widgets/textinput.py | 32 +- android/src/toga_android/widgets/timeinput.py | 14 +- android/src/toga_android/widgets/webview.py | 5 +- android/src/toga_android/window.py | 11 +- android/tests_backend/app.py | 2 +- android/tests_backend/fonts.py | 107 +++++ android/tests_backend/probe.py | 9 - android/tests_backend/widgets/base.py | 14 +- android/tests_backend/widgets/button.py | 9 +- android/tests_backend/widgets/canvas.py | 50 ++ android/tests_backend/widgets/imageview.py | 5 + android/tests_backend/widgets/label.py | 14 +- android/tests_backend/widgets/numberinput.py | 9 +- .../tests_backend/widgets/passwordinput.py | 10 +- android/tests_backend/widgets/properties.py | 48 +- android/tests_backend/widgets/selection.py | 9 +- android/tests_backend/widgets/table.py | 10 +- android/tests_backend/widgets/textinput.py | 21 + 61 files changed, 1157 insertions(+), 692 deletions(-) delete mode 100644 android/src/toga_android/libs/activity.py delete mode 100644 android/src/toga_android/libs/android/__init__.py delete mode 100644 android/src/toga_android/libs/android/app.py delete mode 100644 android/src/toga_android/libs/android/content.py delete mode 100644 android/src/toga_android/libs/android/graphics/__init__.py delete mode 100644 android/src/toga_android/libs/android/graphics/drawable.py delete mode 100644 android/src/toga_android/libs/android/hardware.py delete mode 100644 android/src/toga_android/libs/android/os.py delete mode 100644 android/src/toga_android/libs/android/text.py delete mode 100644 android/src/toga_android/libs/android/util.py delete mode 100644 android/src/toga_android/libs/android/view.py delete mode 100644 android/src/toga_android/libs/android/webkit.py delete mode 100644 android/src/toga_android/libs/android/widget.py delete mode 100644 android/src/toga_android/libs/androidx/__init__.py delete mode 100644 android/src/toga_android/libs/androidx/swiperefreshlayout.py create mode 100644 android/src/toga_android/libs/events.py create mode 100644 android/tests_backend/fonts.py create mode 100644 android/tests_backend/widgets/canvas.py diff --git a/android/setup.py b/android/setup.py index 20ad56ced1..117b407133 100644 --- a/android/setup.py +++ b/android/setup.py @@ -6,7 +6,6 @@ setup( version=version, install_requires=[ - "rubicon-java>=0.2.6", "toga-core==%s" % version, ], ) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 335c4c1d31..837c94b313 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -1,15 +1,16 @@ import asyncio -from rubicon.java import android_events +from java import dynamic_proxy +from org.beeware.android import IPythonApp, MainActivity import toga +from android.graphics.drawable import Drawable +from android.hardware import DisplayManager from android.media import RingtoneManager +from android.view import Menu, MenuItem from toga.command import Group -from .libs.activity import IPythonApp, MainActivity -from .libs.android.graphics import Drawable -from .libs.android.hardware import DisplayManager -from .libs.android.view import Menu, MenuItem +from .libs import events from .screen import Screen as ScreenImpl from .window import Window @@ -17,7 +18,7 @@ MainWindow = Window -class TogaApp(IPythonApp): +class TogaApp(dynamic_proxy(IPythonApp)): last_intent_requestcode = ( -1 ) # always increment before using it for invoking new Intents @@ -172,7 +173,7 @@ def __init__(self, interface): self.interface._impl = self self._listener = None - self.loop = android_events.AndroidEventLoop() + self.loop = events.AndroidEventLoop() @property def native(self): diff --git a/android/src/toga_android/colors.py b/android/src/toga_android/colors.py index b4078ef5ee..971ef9ea1b 100644 --- a/android/src/toga_android/colors.py +++ b/android/src/toga_android/colors.py @@ -1,6 +1,6 @@ from travertino.colors import NAMED_COLOR, TRANSPARENT -from .libs.android.graphics import Color +from android.graphics import Color CACHE = {TRANSPARENT: Color.TRANSPARENT} diff --git a/android/src/toga_android/container.py b/android/src/toga_android/container.py index 763b74df74..ae129953b9 100644 --- a/android/src/toga_android/container.py +++ b/android/src/toga_android/container.py @@ -1,4 +1,5 @@ -from .libs.android.widget import RelativeLayout, RelativeLayout__LayoutParams +from android.widget import RelativeLayout + from .widgets.base import Scalable @@ -56,7 +57,7 @@ def remove_content(self, widget): self.native_content.removeView(widget.native) def set_content_bounds(self, widget, x, y, width, height): - lp = RelativeLayout__LayoutParams(width, height) + lp = RelativeLayout.LayoutParams(width, height) lp.topMargin = y lp.leftMargin = x widget.native.setLayoutParams(lp) diff --git a/android/src/toga_android/dialogs.py b/android/src/toga_android/dialogs.py index 8c8be3066f..c887727353 100644 --- a/android/src/toga_android/dialogs.py +++ b/android/src/toga_android/dialogs.py @@ -1,11 +1,13 @@ from abc import ABC -from .libs.android import R__drawable -from .libs.android.app import AlertDialog__Builder -from .libs.android.content import DialogInterface__OnClickListener +from java import dynamic_proxy +from android import R +from android.app import AlertDialog +from android.content import DialogInterface -class OnClickListener(DialogInterface__OnClickListener): + +class OnClickListener(dynamic_proxy(DialogInterface.OnClickListener)): def __init__(self, fn=None, value=None): super().__init__() self._fn = fn @@ -44,7 +46,7 @@ def __init__( super().__init__(interface=interface) self.on_result = on_result - self.native = AlertDialog__Builder(interface.window._impl.app.native) + self.native = AlertDialog.Builder(interface.window._impl.app.native) self.native.setCancelable(False) self.native.setTitle(title) self.native.setMessage(message) @@ -108,7 +110,7 @@ def __init__(self, interface, title, message, on_result=None): title=title, message=message, positive_text="OK", - icon=R__drawable.ic_dialog_alert, + icon=R.drawable.ic_dialog_alert, on_result=on_result, ) diff --git a/android/src/toga_android/fonts.py b/android/src/toga_android/fonts.py index 86e521648e..677343115f 100644 --- a/android/src/toga_android/fonts.py +++ b/android/src/toga_android/fonts.py @@ -1,20 +1,25 @@ -import os +from pathlib import Path -import toga +from org.beeware.android import MainActivity + +from android import R +from android.graphics import Typeface +from android.util import TypedValue from toga.fonts import ( _REGISTERED_FONT_CACHE, BOLD, CURSIVE, FANTASY, ITALIC, + MESSAGE, MONOSPACE, + OBLIQUE, SANS_SERIF, SERIF, SYSTEM, SYSTEM_DEFAULT_FONT_SIZE, + SYSTEM_DEFAULT_FONTS, ) -from toga_android.libs.android.graphics import Typeface -from toga_android.libs.android.util import TypedValue _FONT_CACHE = {} @@ -23,89 +28,88 @@ class Font: def __init__(self, interface): self.interface = interface - def apply(self, tv, default_size, default_typeface): - """Apply the font to the given native widget. - - :param tv: A native instance of TextView, or one of its subclasses. - :param default_size: The default font size of this widget, in pixels. - :param default_typeface: The default Typeface of this widget. - """ - if self.interface.size == SYSTEM_DEFAULT_FONT_SIZE: - tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, default_size) - else: - # The default size for most widgets is 14sp, so mapping 1 Toga "point" to 1sp - # will give relative sizes that are consistent with desktop platforms. Using - # SP means font sizes will all change proportionately if the user adjusts the - # text size in the system settings. - tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, self.interface.size) + def typeface(self, *, default=Typeface.DEFAULT): + cache_key = (self.interface, default) + if typeface := _FONT_CACHE.get(cache_key): + return typeface - cache_key = (self.interface, default_typeface) + font_key = self.interface._registered_font_key( + self.interface.family, + weight=self.interface.weight, + style=self.interface.style, + variant=self.interface.variant, + ) try: - typeface = _FONT_CACHE[cache_key] + font_path = _REGISTERED_FONT_CACHE[font_key] except KeyError: - typeface = None - font_key = self.interface.registered_font_key( - self.interface.family, - weight=self.interface.weight, - style=self.interface.style, - variant=self.interface.variant, - ) - if font_key in _REGISTERED_FONT_CACHE: - font_path = str( - toga.App.app.paths.app / _REGISTERED_FONT_CACHE[font_key] + # Not a pre-registered font + if self.interface.family not in SYSTEM_DEFAULT_FONTS: + print( + f"Unknown font '{self.interface}'; " + "using system font as a fallback" ) - if os.path.isfile(font_path): - typeface = Typeface.createFromFile(font_path) - # If the typeface cannot be created, following Exception is thrown: - # E/Minikin: addFont failed to create font, invalid request - # It does not kill the app, but there is currently no way to - # catch this Exception on Android - else: - print(f"Registered font path {font_path!r} could not be found") + else: + if Path(font_path).is_file(): + typeface = Typeface.createFromFile(font_path) + if typeface is Typeface.DEFAULT: + raise ValueError(f"Unable to load font file {font_path}") + else: + raise ValueError(f"Font file {font_path} could not be found") - if typeface is None: - if self.interface.family is SYSTEM: - # The default button font is not marked as bold, but it has a weight - # of "medium" (500), which is in between "normal" (400), and "bold" - # (600 or 700). To preserve this, we use the widget's original - # typeface as a starting point rather than Typeface.DEFAULT. - typeface = default_typeface - elif self.interface.family is SERIF: - typeface = Typeface.SERIF - elif self.interface.family is SANS_SERIF: - typeface = Typeface.SANS_SERIF - elif self.interface.family is MONOSPACE: - typeface = Typeface.MONOSPACE - elif self.interface.family is CURSIVE: - typeface = Typeface.create("cursive", Typeface.NORMAL) - elif self.interface.family is FANTASY: - # Android appears to not have a fantasy font available by default, - # but if it ever does, we'll start using it. Android seems to choose - # a serif font when asked for a fantasy font. - typeface = Typeface.create("fantasy", Typeface.NORMAL) - else: - typeface = Typeface.create(self.interface.family, Typeface.NORMAL) + if typeface is None: + if self.interface.family is SYSTEM: + # The default button font is not marked as bold, but it has a weight + # of "medium" (500), which is in between "normal" (400), and "bold" + # (600 or 700). To preserve this, we use the widget's original + # typeface as a starting point rather than Typeface.DEFAULT. + typeface = default + elif self.interface.family is MESSAGE: + typeface = Typeface.DEFAULT + elif self.interface.family is SERIF: + typeface = Typeface.SERIF + elif self.interface.family is SANS_SERIF: + typeface = Typeface.SANS_SERIF + elif self.interface.family is MONOSPACE: + typeface = Typeface.MONOSPACE + elif self.interface.family is CURSIVE: + typeface = Typeface.create("cursive", Typeface.NORMAL) + elif self.interface.family is FANTASY: + # Android appears to not have a fantasy font available by default, + # but if it ever does, we'll start using it. Android seems to choose + # a serif font when asked for a fantasy font. + typeface = Typeface.create("fantasy", Typeface.NORMAL) + else: + typeface = Typeface.create(self.interface.family, Typeface.NORMAL) - native_style = typeface.getStyle() - if self.interface.weight is not None: - native_style = set_bits( - native_style, Typeface.BOLD, self.interface.weight == BOLD - ) - if self.interface.style is not None: - native_style = set_bits( - native_style, Typeface.ITALIC, self.interface.style == ITALIC - ) - if native_style != typeface.getStyle(): - typeface = Typeface.create(typeface, native_style) + native_style = typeface.getStyle() + if self.interface.weight == BOLD: + native_style |= Typeface.BOLD + if self.interface.style in {ITALIC, OBLIQUE}: + native_style |= Typeface.ITALIC - _FONT_CACHE[cache_key] = typeface + if native_style != typeface.getStyle(): + typeface = Typeface.create(typeface, native_style) - tv.setTypeface(typeface) + _FONT_CACHE[cache_key] = typeface + return typeface + def size(self, *, default=None): + """Return the font size in physical pixels.""" + context = MainActivity.singletonThis + if self.interface.size == SYSTEM_DEFAULT_FONT_SIZE: + if default is None: + typed_array = context.obtainStyledAttributes( + R.style.TextAppearance_Small, [R.attr.textSize] + ) + default = typed_array.getDimension(0, 0) + typed_array.recycle() + return default -def set_bits(input, mask, enable=True): - if enable: - output = input | mask - else: - output = input & ~mask - return output + else: + # Using SP means we follow the standard proportion between CSS pixels and + # points by default, but respect the system text scaling setting. + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + self.interface.size * (96 / 72), + context.getResources().getDisplayMetrics(), + ) diff --git a/android/src/toga_android/images.py b/android/src/toga_android/images.py index 8eab9249d6..e3a357e57f 100644 --- a/android/src/toga_android/images.py +++ b/android/src/toga_android/images.py @@ -2,7 +2,7 @@ from java.io import FileOutputStream -from .libs.android.graphics import Bitmap, BitmapFactory +from android.graphics import Bitmap, BitmapFactory class Image: diff --git a/android/src/toga_android/keys.py b/android/src/toga_android/keys.py index 6dec5b9d5e..db1407f57b 100644 --- a/android/src/toga_android/keys.py +++ b/android/src/toga_android/keys.py @@ -1,7 +1,6 @@ +from android.view import KeyEvent from toga.keys import Key -from .libs.android.view import KeyEvent - KEYEVENT_KEYS = { KeyEvent.KEYCODE_ESCAPE: Key.ESCAPE, KeyEvent.KEYCODE_F1: Key.F1, diff --git a/android/src/toga_android/libs/activity.py b/android/src/toga_android/libs/activity.py deleted file mode 100644 index 52e682909a..0000000000 --- a/android/src/toga_android/libs/activity.py +++ /dev/null @@ -1,20 +0,0 @@ -from rubicon.java import JavaClass, JavaInterface - -# The Android cookiecutter template creates an app whose main Activity is -# called `MainActivity`. The activity assumes that we will store a reference -# to an implementation/subclass of `IPythonApp` in it. -MainActivity = JavaClass("org/beeware/android/MainActivity") - -# The `IPythonApp` interface in Java allows Python code to -# run on Android activity lifecycle hooks such as `onCreate()`. -IPythonApp = JavaInterface("org/beeware/android/IPythonApp") - -# The `DrawHandlerView` Java class is an `android.view.View`. It allows user -# code to draw on its canvas. -# -# After a `DrawHandlerView` is constructed, you must provide a draw handler -# with `setDrawHandler()`. Whenever Android calls the `DrawHandlerView`'s `onDraw()`, -# the draw handler view will call the draw handler's `handleDraw()` with the -# `android.graphics.Canvas`. -DrawHandlerView = JavaClass("org/beeware/android/DrawHandlerView") -IDrawHandler = JavaInterface("org/beeware/android/IDrawHandler") diff --git a/android/src/toga_android/libs/android/__init__.py b/android/src/toga_android/libs/android/__init__.py deleted file mode 100644 index 535697d8b7..0000000000 --- a/android/src/toga_android/libs/android/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from rubicon.java import JavaClass - -R__attr = JavaClass("android/R$attr") -R__color = JavaClass("android/R$color") -R__drawable = JavaClass("android/R$drawable") -R__id = JavaClass("android/R$id") -R__layout = JavaClass("android/R$layout") -R__style = JavaClass("android/R$style") diff --git a/android/src/toga_android/libs/android/app.py b/android/src/toga_android/libs/android/app.py deleted file mode 100644 index bb327bcca1..0000000000 --- a/android/src/toga_android/libs/android/app.py +++ /dev/null @@ -1,3 +0,0 @@ -from rubicon.java import JavaClass - -AlertDialog__Builder = JavaClass("android/app/AlertDialog$Builder") diff --git a/android/src/toga_android/libs/android/content.py b/android/src/toga_android/libs/android/content.py deleted file mode 100644 index 6493c36965..0000000000 --- a/android/src/toga_android/libs/android/content.py +++ /dev/null @@ -1,6 +0,0 @@ -from rubicon.java import JavaClass, JavaInterface - -DialogInterface__OnClickListener = JavaInterface( - "android/content/DialogInterface$OnClickListener" -) -Intent = JavaClass("android/content/Intent") diff --git a/android/src/toga_android/libs/android/graphics/__init__.py b/android/src/toga_android/libs/android/graphics/__init__.py deleted file mode 100644 index 010d44d0d1..0000000000 --- a/android/src/toga_android/libs/android/graphics/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from rubicon.java import JavaClass - -Bitmap = JavaClass("android/graphics/Bitmap") -BitmapFactory = JavaClass("android/graphics/BitmapFactory") -Color = JavaClass("android/graphics/Color") -DashPathEffect = JavaClass("android/graphics/DashPathEffect") -Drawable = JavaClass("android/graphics/drawable/Drawable") -Matrix = JavaClass("android/graphics/Matrix") -Paint = JavaClass("android/graphics/Paint") -Path = JavaClass("android/graphics/Path") -Path__Direction = JavaClass("android/graphics/Path$Direction") -Paint__Style = JavaClass("android/graphics/Paint$Style") -PorterDuff__Mode = JavaClass("android/graphics/PorterDuff$Mode") -PorterDuffColorFilter = JavaClass("android/graphics/PorterDuffColorFilter") -Rect = JavaClass("android/graphics/Rect") -Typeface = JavaClass("android/graphics/Typeface") diff --git a/android/src/toga_android/libs/android/graphics/drawable.py b/android/src/toga_android/libs/android/graphics/drawable.py deleted file mode 100644 index 1e907cb19d..0000000000 --- a/android/src/toga_android/libs/android/graphics/drawable.py +++ /dev/null @@ -1,4 +0,0 @@ -from rubicon.java import JavaClass - -ColorDrawable = JavaClass("android/graphics/drawable/ColorDrawable") -InsetDrawable = JavaClass("android/graphics/drawable/InsetDrawable") diff --git a/android/src/toga_android/libs/android/hardware.py b/android/src/toga_android/libs/android/hardware.py deleted file mode 100644 index 53ace7a72e..0000000000 --- a/android/src/toga_android/libs/android/hardware.py +++ /dev/null @@ -1,3 +0,0 @@ -from rubicon.java import JavaClass - -DisplayManager = JavaClass("android/hardware/DisplayManager") diff --git a/android/src/toga_android/libs/android/os.py b/android/src/toga_android/libs/android/os.py deleted file mode 100644 index 25f4a762b5..0000000000 --- a/android/src/toga_android/libs/android/os.py +++ /dev/null @@ -1,3 +0,0 @@ -from rubicon.java import JavaClass - -Build = JavaClass("android/os/Build") diff --git a/android/src/toga_android/libs/android/text.py b/android/src/toga_android/libs/android/text.py deleted file mode 100644 index bf66e31adb..0000000000 --- a/android/src/toga_android/libs/android/text.py +++ /dev/null @@ -1,5 +0,0 @@ -from rubicon.java import JavaClass, JavaInterface - -InputType = JavaClass("android/text/InputType") -Layout = JavaClass("android/text/Layout") -TextWatcher = JavaInterface("android/text/TextWatcher") diff --git a/android/src/toga_android/libs/android/util.py b/android/src/toga_android/libs/android/util.py deleted file mode 100644 index b380d5cf7b..0000000000 --- a/android/src/toga_android/libs/android/util.py +++ /dev/null @@ -1,5 +0,0 @@ -from rubicon.java import JavaClass, JavaInterface - -AttributeSet = JavaInterface("android/util/AttributeSet") -TypedValue = JavaClass("android/util/TypedValue") -Xml = JavaClass("android/util/Xml") diff --git a/android/src/toga_android/libs/android/view.py b/android/src/toga_android/libs/android/view.py deleted file mode 100644 index 4387e179b1..0000000000 --- a/android/src/toga_android/libs/android/view.py +++ /dev/null @@ -1,22 +0,0 @@ -from rubicon.java import JavaClass, JavaInterface - -Gravity = JavaClass("android/view/Gravity") -OnClickListener = JavaInterface("android/view/View$OnClickListener") -OnLongClickListener = JavaInterface("android/view/View$OnLongClickListener") -Menu = JavaClass("android/view/Menu") -MenuItem = JavaClass("android/view/MenuItem") -MotionEvent = JavaClass("android/view/MotionEvent") -SubMenu = JavaClass("android/view/SubMenu") -View = JavaClass("android/view/View") -ViewGroup__LayoutParams = JavaClass("android/view/ViewGroup$LayoutParams") -View__MeasureSpec = JavaClass("android/view/View$MeasureSpec") -View__OnFocusChangeListener = JavaInterface("android/view/View$OnFocusChangeListener") -View__OnScrollChangeListener = JavaInterface("android/view/View$OnScrollChangeListener") -View__OnTouchListener = JavaInterface("android/view/View$OnTouchListener") -ViewTreeObserver__OnGlobalLayoutListener = JavaInterface( - "android/view/ViewTreeObserver$OnGlobalLayoutListener" -) -OnKeyListener = JavaInterface("android/view/View$OnKeyListener") -KeyEvent = JavaClass("android/view/KeyEvent") -WindowInsets = JavaClass("android/view/WindowInsets") -WindowManager = JavaClass("android/view/WindowManager") diff --git a/android/src/toga_android/libs/android/webkit.py b/android/src/toga_android/libs/android/webkit.py deleted file mode 100644 index 0dad687b9d..0000000000 --- a/android/src/toga_android/libs/android/webkit.py +++ /dev/null @@ -1,5 +0,0 @@ -from rubicon.java import JavaClass, JavaInterface - -ValueCallback = JavaInterface("android/webkit/ValueCallback") -WebView = JavaClass("android/webkit/WebView") -WebViewClient = JavaClass("android/webkit/WebViewClient") diff --git a/android/src/toga_android/libs/android/widget.py b/android/src/toga_android/libs/android/widget.py deleted file mode 100644 index d57ad32cc9..0000000000 --- a/android/src/toga_android/libs/android/widget.py +++ /dev/null @@ -1,45 +0,0 @@ -from rubicon.java import JavaClass, JavaInterface - -ArrayAdapter = JavaClass("android/widget/ArrayAdapter") -# `ArrayAdapter` can also be typecast into a `SpinnerAdapter`. -# This is required until `rubicon-java` explores the interfaces -# implemented by a class's subclasses. -ArrayAdapter._alternates.append(b"Landroid/widget/SpinnerAdapter;") - -Button = JavaClass("android/widget/Button") -CompoundButton__OnCheckedChangeListener = JavaInterface( - "android/widget/CompoundButton$OnCheckedChangeListener" -) -DatePickerDialog = JavaClass("android/app/DatePickerDialog") -DatePickerDialog__OnDateSetListener = JavaInterface( - "android/app/DatePickerDialog$OnDateSetListener" -) -EditText = JavaClass("android/widget/EditText") -HorizontalScrollView = JavaClass("android/widget/HorizontalScrollView") -ImageView = JavaClass("android/widget/ImageView") -ImageView__ScaleType = JavaClass("android/widget/ImageView$ScaleType") -LinearLayout = JavaClass("android/widget/LinearLayout") -LinearLayout__LayoutParams = JavaClass("android/widget/LinearLayout$LayoutParams") -NumberPicker = JavaClass("android/widget/NumberPicker") -OnItemSelectedListener = JavaInterface( - "android/widget/AdapterView$OnItemSelectedListener" -) -RelativeLayout = JavaClass("android/widget/RelativeLayout") -RelativeLayout__LayoutParams = JavaClass("android/widget/RelativeLayout$LayoutParams") -ProgressBar = JavaClass("android/widget/ProgressBar") -ScrollView = JavaClass("android/widget/ScrollView") -SeekBar = JavaClass("android/widget/SeekBar") -SeekBar__OnSeekBarChangeListener = JavaInterface( - "android/widget/SeekBar$OnSeekBarChangeListener" -) -Switch = JavaClass("android/widget/Switch") -Spinner = JavaClass("android/widget/Spinner") -TableLayout = JavaClass("android/widget/TableLayout") -TableLayout__Layoutparams = JavaClass("android/widget/TableLayout$LayoutParams") -TableRow = JavaClass("android/widget/TableRow") -TableRow__Layoutparams = JavaClass("android/widget/TableRow$LayoutParams") -TextView = JavaClass("android/widget/TextView") -TimePickerDialog = JavaClass("android/app/TimePickerDialog") -TimePickerDialog__OnTimeSetListener = JavaInterface( - "android/app/TimePickerDialog$OnTimeSetListener" -) diff --git a/android/src/toga_android/libs/androidx/__init__.py b/android/src/toga_android/libs/androidx/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/android/src/toga_android/libs/androidx/swiperefreshlayout.py b/android/src/toga_android/libs/androidx/swiperefreshlayout.py deleted file mode 100644 index 0e06ddf021..0000000000 --- a/android/src/toga_android/libs/androidx/swiperefreshlayout.py +++ /dev/null @@ -1,6 +0,0 @@ -from rubicon.java import JavaClass, JavaInterface - -SwipeRefreshLayout = JavaClass("androidx/swiperefreshlayout/widget/SwipeRefreshLayout") -SwipeRefreshLayout__OnRefreshListener = JavaInterface( - "androidx/swiperefreshlayout/widget/SwipeRefreshLayout$OnRefreshListener" -) diff --git a/android/src/toga_android/libs/events.py b/android/src/toga_android/libs/events.py new file mode 100644 index 0000000000..2e1a3c16f9 --- /dev/null +++ b/android/src/toga_android/libs/events.py @@ -0,0 +1,433 @@ +import asyncio +import asyncio.base_events +import asyncio.events +import asyncio.log +import heapq +import selectors +import sys +import threading + +from java import dynamic_proxy +from java.io import FileDescriptor +from java.lang import Runnable + +from android.os import Handler, Looper, MessageQueue + +# Some methods in this file are based on CPython's implementation. +# Per https://github.com/python/cpython/blob/master/LICENSE , re-use is permitted +# via the Python Software Foundation License Version 2, which includes inclusion +# into this project under its BSD license terms so long as we retain this copyright notice: +# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, +# 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; All Rights Reserved. + + +class AndroidEventLoop(asyncio.SelectorEventLoop): + # `AndroidEventLoop` exists to support starting the Python event loop cooperatively with + # the built-in Android event loop. Since it's cooperative, it has a `run_forever_cooperatively()` + # method which returns immediately. This is is different from the parent class's `run_forever()`, + # which blocks. + # + # In some cases, for simplicity of implementation, this class reaches into the internals of the + # parent and grandparent classes. + # + # A Python event loop handles two kinds of tasks. It needs to run delayed tasks after waiting + # the right amount of time, and it needs to do I/O when file descriptors are ready for I/O. + # + # `SelectorEventLoop` uses an approach we **cannot** use: it calls the `select()` method + # to block waiting for specific file descriptors to be come ready for I/O, or a timeout + # corresponding to the soonest delayed task, whichever occurs sooner. + # + # To handle delayed tasks, `AndroidEventLoop` asks the Android event loop to wake it up when + # its soonest delayed task is ready. To accomplish this, it relies on a `SelectorEventLoop` + # implementation detail: `_scheduled` is a collection of tasks sorted by soonest wakeup time. + # + # To handle waking up when it's possible to do I/O, `AndroidEventLoop` will register file descriptors + # with the Android event loop so the platform can wake it up accordingly. It does not do this yet. + def __init__(self): + # Tell the parent constructor to use our custom Selector. + selector = AndroidSelector(self) + super().__init__(selector) + # Create placeholders for lazily-created objects. + self.android_interop = AndroidInterop() + + # Override parent `run_in_executor()` to run all code synchronously. This disables the + # `executor` thread that typically exists in event loops. The event loop itself relies + # on `run_in_executor()` for DNS lookups. In the future, we can restore `run_in_executor()`. + async def run_in_executor(self, executor, func, *args): + return func(*args) + + # Override parent `_call_soon()` to ensure Android wakes us up to do the delayed task. + def _call_soon(self, callback, args, context): + ret = super()._call_soon(callback, args, context) + self.enqueue_android_wakeup_for_delayed_tasks() + return ret + + # Override parent `_add_callback()` to ensure Android wakes us up to do the delayed task. + def _add_callback(self, handle): + ret = super()._add_callback(handle) + self.enqueue_android_wakeup_for_delayed_tasks() + return ret + + def run_forever_cooperatively(self): + """Configure the event loop so it is started, doing as little work as possible to + ensure that. Most Android interop objects are created lazily so that the cost of + event loop interop is not paid by apps that don't use the event loop.""" + # Based on `BaseEventLoop.run_forever()` in CPython. + if self.is_running(): + raise RuntimeError("Refusing to start since loop is already running.") + if self._closed: + raise RuntimeError("Event loop is closed. Create a new object.") + self._set_coroutine_origin_tracking(self._debug) + self._thread_id = threading.get_ident() + + self._old_agen_hooks = sys.get_asyncgen_hooks() + sys.set_asyncgen_hooks( + firstiter=self._asyncgen_firstiter_hook, + finalizer=self._asyncgen_finalizer_hook, + ) + asyncio.events._set_running_loop(self) + + # Schedule any tasks which were added before the loop started. + self.enqueue_android_wakeup_for_delayed_tasks() + + def enqueue_android_wakeup_for_delayed_tasks(self): + """Ask Android to wake us up when delayed tasks are ready to be handled. + + Since this is effectively the actual event loop, it also handles stopping the + loop. + """ + # If we are supposed to stop, actually stop. + if self._stopping: + self._stopping = False + self._thread_id = None + asyncio.events._set_running_loop(None) + self._set_coroutine_origin_tracking(False) + sys.set_asyncgen_hooks(*self._old_agen_hooks) + # Remove Android event loop interop objects. + self.android_interop = None + return + + # If we have actually already stopped, then do nothing. + if self._thread_id is None: + return + + timeout = self._get_next_delayed_task_wakeup() + if timeout is None: + # No delayed tasks. + return + + # Ask Android to wake us up to run delayed tasks. Running delayed tasks also + # checks for other tasks that require wakeup by calling this method. The fact that + # running delayed tasks can trigger the next wakeup is what makes this event loop a "loop." + self.android_interop.call_later(self.run_delayed_tasks, timeout * 1000) + + def _set_coroutine_origin_tracking(self, debug): + # If running on Python 3.7 or 3.8, integrate with upstream event loop's debug feature, allowing + # unawaited coroutines to have some useful info logged. See https://bugs.python.org/issue32591 + if hasattr(super(), "_set_coroutine_origin_tracking"): + super()._set_coroutine_origin_tracking(debug) + + def _get_next_delayed_task_wakeup(self): + """Compute the time to sleep before we should be woken up to handle delayed tasks.""" + # This is based heavily on the CPython's implementation of `BaseEventLoop._run_once()` + # before it blocks on `select()`. + _MIN_SCHEDULED_TIMER_HANDLES = 100 + _MIN_CANCELLED_TIMER_HANDLES_FRACTION = 0.5 + MAXIMUM_SELECT_TIMEOUT = 24 * 3600 + + sched_count = len(self._scheduled) + if ( + sched_count > _MIN_SCHEDULED_TIMER_HANDLES + and self._timer_cancelled_count / sched_count + > _MIN_CANCELLED_TIMER_HANDLES_FRACTION + ): + # Remove delayed calls that were cancelled if their number + # is too high + new_scheduled = [] + for handle in self._scheduled: + if handle._cancelled: + handle._scheduled = False + else: + new_scheduled.append(handle) + + heapq.heapify(new_scheduled) + self._scheduled = new_scheduled + self._timer_cancelled_count = 0 + else: + # Remove delayed calls that were cancelled from head of queue. + while self._scheduled and self._scheduled[0]._cancelled: + self._timer_cancelled_count -= 1 + handle = heapq.heappop(self._scheduled) + handle._scheduled = False + + timeout = None + if self._ready or self._stopping: + if self._debug: + print("AndroidEventLoop: self.ready is", self._ready) + timeout = 0 + elif self._scheduled: + # Compute the desired timeout. + when = self._scheduled[0]._when + timeout = min(max(0, when - self.time()), MAXIMUM_SELECT_TIMEOUT) + + return timeout + + def run_delayed_tasks(self): + """Android-specific: Run any delayed tasks that have become ready. Additionally, + check if there are more delayed tasks to execute in the future; if so, schedule + the next wakeup. + """ + # Based heavily on `BaseEventLoop._run_once()` from CPython -- specifically, the part + # after blocking on `select()`. + # Handle 'later' callbacks that are ready. + end_time = self.time() + self._clock_resolution + while self._scheduled: + handle = self._scheduled[0] + if handle._when >= end_time: + break + handle = heapq.heappop(self._scheduled) + handle._scheduled = False + self._ready.append(handle) + + # This is the only place where callbacks are actually *called*. + # All other places just add them to ready. + # Note: We run all currently scheduled callbacks, but not any + # callbacks scheduled by callbacks run this time around -- + # they will be run the next time (after another I/O poll). + # Use an idiom that is thread-safe without using locks. + ntodo = len(self._ready) + for i in range(ntodo): + handle = self._ready.popleft() + if handle._cancelled: + continue + if self._debug: + try: + self._current_handle = handle + t0 = self.time() + handle._run() + dt = self.time() - t0 + if dt >= self.slow_callback_duration: + asyncio.log.logger.warning( + "Executing %s took %.3f seconds", + asyncio.base_events._format_handle(handle), + dt, + ) + finally: + self._current_handle = None + else: + handle._run() + handle = None # Needed to break cycles when an exception occurs. + + # End code borrowed from CPython, within this method. + self.enqueue_android_wakeup_for_delayed_tasks() + + +class AndroidInterop: + """Encapsulate details of Android event loop cooperation.""" + + def __init__(self): + # `_runnable_by_fn` is a one-to-one mapping from Python callables to Java Runnables. + # This allows us to avoid creating more than one Java object per Python callable, which + # would prevent removeCallbacks from working. + self._runnable_by_fn = {} + # The handler must be created on the Android UI thread. + self.handler = Handler() + + def get_or_create_runnable(self, fn): + if fn in self._runnable_by_fn: + return self._runnable_by_fn[fn] + + self._runnable_by_fn[fn] = PythonRunnable(fn) + return self._runnable_by_fn[fn] + + def call_later(self, fn, timeout_millis): + """Enqueue a Python callable `fn` to be run after `timeout_millis` milliseconds.""" + runnable = self.get_or_create_runnable(fn) + self.handler.removeCallbacks(runnable) + self.handler.postDelayed(runnable, int(timeout_millis)) + + +class PythonRunnable(dynamic_proxy(Runnable)): + """Bind a specific Python callable in a Java `Runnable`.""" + + def __init__(self, fn): + super().__init__() + self._fn = fn + + def run(self): + self._fn() + + +class AndroidSelector(selectors.SelectSelector): + """Subclass of selectors.Selector which cooperates with the Android event loop + to learn when file descriptors become ready for I/O. + + AndroidSelector's `select()` raises NotImplementedError; see its comments.""" + + def __init__(self, loop): + super().__init__() + self.loop = loop + # Lazily-created AndroidSelectorFileDescriptorEventsListener. + self._file_descriptor_event_listener = None + # Keep a `_debug` flag so that a developer can modify it for more debug printing. + self._debug = False + + @property + def file_descriptor_event_listener(self): + if self._file_descriptor_event_listener is not None: + return self._file_descriptor_event_listener + self._file_descriptor_event_listener = ( + AndroidSelectorFileDescriptorEventsListener( + android_selector=self, + ) + ) + return self._file_descriptor_event_listener + + @property + def message_queue(self): + return Looper.getMainLooper().getQueue() + + # File descriptors can be registered and unregistered by the event loop. + # The events for which we listen can be modified. For register & unregister, + # we mostly rely on the parent class. For modify(), the parent class calls + # unregister() and register(), so we rely on that as well. + + def register(self, fileobj, events, data=None): + if self._debug: + print( + "register() fileobj={fileobj} events={events} data={data}".format( + fileobj=fileobj, events=events, data=data + ) + ) + ret = super().register(fileobj, events, data=data) + self.register_with_android(fileobj, events) + return ret + + def unregister(self, fileobj): + self.message_queue.removeOnFileDescriptorEventListener(_create_java_fd(fileobj)) + return super().unregister(fileobj) + + def reregister_with_android_soon(self, fileobj): + def _reregister(): + # If the fileobj got unregistered, exit early. + key = self._key_from_fd(fileobj) + if key is None: + if self._debug: + print( + "reregister_with_android_soon reregister_temporarily_ignored_fd exiting early; key=None" + ) + return + if self._debug: + print( + "reregister_with_android_soon reregistering key={key}".format( + key=key + ) + ) + self.register_with_android(key.fd, key.events) + + # Use `call_later(0, fn)` to ensure the Python event loop runs to completion before re-registering. + self.loop.call_later(0, _reregister) + + def register_with_android(self, fileobj, events): + if self._debug: + print( + "register_with_android() fileobj={fileobj} events={events}".format( + fileobj=fileobj, events=events + ) + ) + # `events` is a bitset comprised of `selectors.EVENT_READ` and `selectors.EVENT_WRITE`. + # Register this FD for read and/or write events from Android. + self.message_queue.addOnFileDescriptorEventListener( + _create_java_fd(fileobj), + events, # Passing `events` as-is because Android and Python use the same values for read & write events. + self.file_descriptor_event_listener, + ) + + def handle_fd_wakeup(self, fd, events): + """Accept a FD and the events that it is ready for (read and/or write). + + Filter the events to just those that are registered, then notify the loop.""" + key = self._key_from_fd(fd) + if key is None: + print( + "Warning: handle_fd_wakeup: wakeup for unregistered fd={fd}".format( + fd=fd + ) + ) + return + + key_event_pairs = [] + for event_type in (selectors.EVENT_READ, selectors.EVENT_WRITE): + if events & event_type and key.events & event_type: + key_event_pairs.append((key, event_type)) + if key_event_pairs: + if self._debug: + print( + "handle_fd_wakeup() calling parent for key_event_pairs={key_event_pairs}".format( + key_event_pairs=key_event_pairs + ) + ) + # Call superclass private method to notify. + self.loop._process_events(key_event_pairs) + else: + print( + "Warning: handle_fd_wakeup(): unnecessary wakeup fd={fd} events={events} key={key}".format( + fd=fd, events=events, key=key + ) + ) + + # This class declines to implement the `select()` method, purely as + # a safety mechanism. On Android, this would be an error -- it would result + # in the app freezing, triggering an App Not Responding pop-up from the + # platform, and the user killing the app. + # + # Instead, the AndroidEventLoop cooperates with the native Android event + # loop to be woken up to get work done as needed. + def select(self, *args, **kwargs): + raise NotImplementedError("AndroidSelector refuses to select(); see comments.") + + +class AndroidSelectorFileDescriptorEventsListener( + dynamic_proxy(MessageQueue.OnFileDescriptorEventListener) +): + """Notify an `AndroidSelector` instance when file descriptors become readable/writable.""" + + def __init__(self, android_selector): + super().__init__() + self.android_selector = android_selector + # Keep a `_debug` flag so that a developer can modify it for more debug printing. + self._debug = False + + def onFileDescriptorEvents(self, fd_obj, events): + """Receive a Java FileDescriptor object and notify the Python event loop that the FD + is ready for read and/or write. + + As an implementation detail, this relies on the fact that Android EVENT_INPUT and Python + selectors.EVENT_READ have the same value (1) and Android EVENT_OUTPUT and Python + selectors.EVENT_WRITE have the same value (2).""" + # Call hidden (non-private) method to get the numeric FD, so we can pass that to Python. + fd = getattr(fd_obj, "getInt$")() + if self._debug: + print( + "onFileDescriptorEvents woke up for fd={fd} events={events}".format( + fd=fd, events=events + ) + ) + # Tell the Python event loop that the FD is ready for read and/or write. + self.android_selector.handle_fd_wakeup(fd, events) + # Tell Android we don't want any more wake-ups from this FD until the event loop runs. + # To do that, we return 0. + # + # We also need Python to request wake-ups once the event loop has finished. + self.android_selector.reregister_with_android_soon(fd) + return 0 + + +def _create_java_fd(int_fd): + """Given a numeric file descriptor, create a `java.io.FileDescriptor` object.""" + # On Android, the class exposes hidden (non-private) methods `getInt$()` and `setInt$()`. Because + # they aren't valid Python identifier names, we need to use `getattr()` to grab them. + # See e.g. https://android.googlesource.com/platform/prebuilts/fullsdk/sources/android-28/+/refs/heads/master/java/io/FileDescriptor.java#149 # noqa: E501 + java_fd = FileDescriptor() + getattr(java_fd, "setInt$")(int_fd) + return java_fd diff --git a/android/src/toga_android/screen.py b/android/src/toga_android/screen.py index facf7a3b92..16f785172f 100644 --- a/android/src/toga_android/screen.py +++ b/android/src/toga_android/screen.py @@ -1,7 +1,6 @@ +from android.view import WindowInsets, WindowManager from toga.screen import Screen as ScreenInterface -from .android.view import WindowInsets, WindowManager - class Screen: _instances = {} diff --git a/android/src/toga_android/widgets/base.py b/android/src/toga_android/widgets/base.py index f10cd75f3c..9fae28706f 100644 --- a/android/src/toga_android/widgets/base.py +++ b/android/src/toga_android/widgets/base.py @@ -1,16 +1,16 @@ from abc import ABC, abstractmethod from decimal import ROUND_HALF_EVEN, ROUND_UP, Decimal +from org.beeware.android import MainActivity from travertino.size import at_least +from android.graphics import PorterDuff, PorterDuffColorFilter, Rect +from android.graphics.drawable import ColorDrawable, InsetDrawable +from android.view import Gravity, View +from android.widget import RelativeLayout from toga.constants import CENTER, JUSTIFY, LEFT, RIGHT, TRANSPARENT from ..colors import native_color -from ..libs.activity import MainActivity -from ..libs.android.graphics import PorterDuff__Mode, PorterDuffColorFilter, Rect -from ..libs.android.graphics.drawable import ColorDrawable, InsetDrawable -from ..libs.android.view import Gravity, View -from ..libs.android.widget import RelativeLayout__LayoutParams class Scalable: @@ -19,20 +19,22 @@ class Scalable: def init_scale(self, context): # The baseline DPI is 160: # https://developer.android.com/training/multiscreen/screendensities - self.scale = context.getResources().getDisplayMetrics().densityDpi / 160 + self.dpi_scale = context.getResources().getDisplayMetrics().densityDpi / 160 # Convert CSS pixels to native pixels def scale_in(self, value, rounding=SCALE_DEFAULT_ROUNDING): - return self.scale_round(value * self.scale, rounding) + return self.scale_round(value * self.dpi_scale, rounding) # Convert native pixels to CSS pixels def scale_out(self, value, rounding=SCALE_DEFAULT_ROUNDING): if isinstance(value, at_least): return at_least(self.scale_out(value.value, rounding)) else: - return self.scale_round(value / self.scale, rounding) + return self.scale_round(value / self.dpi_scale, rounding) def scale_round(self, value, rounding): + if rounding is None: + return value return int(Decimal(value).to_integral(rounding)) @@ -55,9 +57,9 @@ def __init__(self, interface): # Some widgets, e.g. TextView, may throw an exception if we call measure() # before setting LayoutParams. self.native.setLayoutParams( - RelativeLayout__LayoutParams( - RelativeLayout__LayoutParams.WRAP_CONTENT, - RelativeLayout__LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT, ) ) @@ -157,7 +159,7 @@ def set_background_filter(self, value): self.native.getBackground().setColorFilter( None if value in (None, TRANSPARENT) - else PorterDuffColorFilter(native_color(value), PorterDuff__Mode.SRC_IN) + else PorterDuffColorFilter(native_color(value), PorterDuff.Mode.SRC_IN) ) def set_alignment(self, alignment): diff --git a/android/src/toga_android/widgets/box.py b/android/src/toga_android/widgets/box.py index c1d5b62912..2a584a130f 100644 --- a/android/src/toga_android/widgets/box.py +++ b/android/src/toga_android/widgets/box.py @@ -1,6 +1,7 @@ from travertino.size import at_least -from ..libs.android.widget import RelativeLayout +from android.widget import RelativeLayout + from .base import Widget diff --git a/android/src/toga_android/widgets/button.py b/android/src/toga_android/widgets/button.py index de313a31e8..ccc167fc5c 100644 --- a/android/src/toga_android/widgets/button.py +++ b/android/src/toga_android/widgets/button.py @@ -1,11 +1,13 @@ +from java import dynamic_proxy from travertino.size import at_least -from ..libs.android.view import OnClickListener, View__MeasureSpec -from ..libs.android.widget import Button as A_Button +from android.view import View +from android.widget import Button as A_Button + from .label import TextViewWidget -class TogaOnClickListener(OnClickListener): +class TogaOnClickListener(dynamic_proxy(View.OnClickListener)): def __init__(self, button_impl): super().__init__() self.button_impl = button_impl @@ -36,8 +38,8 @@ def set_background_color(self, value): def rehint(self): self.native.measure( - View__MeasureSpec.UNSPECIFIED, - View__MeasureSpec.UNSPECIFIED, + View.MeasureSpec.UNSPECIFIED, + View.MeasureSpec.UNSPECIFIED, ) self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth()) self.interface.intrinsic.height = self.native.getMeasuredHeight() diff --git a/android/src/toga_android/widgets/canvas.py b/android/src/toga_android/widgets/canvas.py index 681e897d77..47cbb4508d 100644 --- a/android/src/toga_android/widgets/canvas.py +++ b/android/src/toga_android/widgets/canvas.py @@ -1,110 +1,125 @@ -import math +from math import degrees, pi +from java import dynamic_proxy, jint +from java.io import ByteArrayOutputStream +from org.beeware.android import DrawHandlerView, IDrawHandler from travertino.size import at_least -from ..libs import activity -from ..libs.android.graphics import ( +from android.graphics import ( + Bitmap, + Canvas as A_Canvas, DashPathEffect, Matrix, Paint, - Paint__Style, Path, - Path__Direction, ) +from android.view import MotionEvent, View +from toga.widgets.canvas import Baseline, FillRule + +from ..colors import native_color from .base import Widget -class DrawHandler(activity.IDrawHandler): - def __init__(self, interface): - self.interface = interface +class DrawHandler(dynamic_proxy(IDrawHandler)): + def __init__(self, impl): super().__init__() + self.impl = impl + self.interface = impl.interface def handleDraw(self, canvas): - canvas.save() - self.interface._draw(self.interface._impl, path=Path(), canvas=canvas) + self.impl.reset_transform(canvas) + self.interface.context._draw(self.impl, path=Path(), canvas=canvas) + + +class TouchListener(dynamic_proxy(View.OnTouchListener)): + def __init__(self, impl): + super().__init__() + self.impl = impl + self.interface = impl.interface + + def onTouch(self, canvas, event): + x, y = map(self.impl.scale_out, (event.getX(), event.getY())) + if (action := event.getAction()) == MotionEvent.ACTION_DOWN: + self.interface.on_press(None, x, y) + elif action == MotionEvent.ACTION_MOVE: + self.interface.on_drag(None, x, y) + elif action == MotionEvent.ACTION_UP: + self.interface.on_release(None, x, y) + else: # pragma: no cover + return False + return True class Canvas(Widget): def create(self): - # Our native widget is a DrawHandlerView, which delegates drawing to DrawHandler, - # so we can pass the `android.graphics.Canvas` around as `canvas`. - self.native = activity.DrawHandlerView( - self._native_activity.getApplicationContext() - ) - self.native.setDrawHandler(DrawHandler(self.interface)) + self.native = DrawHandlerView(self._native_activity) + self.native.setDrawHandler(DrawHandler(self)) + self.native.setOnTouchListener(TouchListener(self)) - def set_hidden(self, hidden): - self.interface.factory.not_implemented("Canvas.set_hidden()") + def set_bounds(self, x, y, width, height): + super().set_bounds(x, y, width, height) + self.interface.on_resize(None, width=width, height=height) def redraw(self): - pass - - def set_on_press(self, handler): - self.interface.factory.not_implemented("Canvas.set_on_press()") - - def set_on_release(self, handler): - self.interface.factory.not_implemented("Canvas.set_on_release()") - - def set_on_drag(self, handler): - self.interface.factory.not_implemented("Canvas.set_on_drag()") + self.native.invalidate() - def set_on_alt_press(self, handler): - self.interface.factory.not_implemented("Canvas.set_on_alt_press()") + # Context management - def set_on_alt_release(self, handler): - self.interface.factory.not_implemented("Canvas.set_on_alt_release()") + def push_context(self, canvas, **kwargs): + canvas.save() - def set_on_alt_drag(self, handler): - self.interface.factory.not_implemented("Canvas.set_on_alt_drag()") + def pop_context(self, canvas, **kwargs): + canvas.restore() # Basic paths - def new_path(self, *args, **kwargs): - self.interface.factory.not_implemented("Canvas.new_path()") + def begin_path(self, path, **kwargs): + path.reset() - def closed_path(self, x, y, path, *args, **kwargs): + def close_path(self, path, **kwargs): path.close() - def move_to(self, x, y, path, *args, **kwargs): - path.moveTo(self.container.scale * x, self.container.scale * y) + def move_to(self, x, y, path, **kwargs): + path.moveTo(x, y) + + def line_to(self, x, y, path, **kwargs): + self._ensure_subpath(x, y, path) + path.lineTo(x, y) - def line_to(self, x, y, path, *args, **kwargs): - path.lineTo(self.container.scale * x, self.container.scale * y) + def _ensure_subpath(self, x, y, path): + if path.isEmpty(): + self.move_to(x, y, path) # Basic shapes - def bezier_curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y, path, *args, **kwargs): - path.cubicTo( - cp1x * self.container.scale, - cp1y * self.container.scale, - cp2x * self.container.scale, - cp2y * self.container.scale, - x * self.container.scale, - y * self.container.scale, - ) + def bezier_curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y, path, **kwargs): + self._ensure_subpath(cp1x, cp1y, path) + path.cubicTo(cp1x, cp1y, cp2x, cp2y, x, y) - def quadratic_curve_to(self, cpx, cpy, x, y, path, *args, **kwargs): - path.quadTo( - cpx * self.container.scale, - cpy * self.container.scale, - x * self.container.scale, - y * self.container.scale, - ) + def quadratic_curve_to(self, cpx, cpy, x, y, path, **kwargs): + self._ensure_subpath(cpx, cpy, path) + path.quadTo(cpx, cpy, x, y) - def arc( - self, x, y, radius, startangle, endangle, anticlockwise, path, *args, **kwargs - ): - sweep_angle = endangle - startangle + def arc(self, x, y, radius, startangle, endangle, anticlockwise, path, **kwargs): + sweepangle = endangle - startangle if anticlockwise: - sweep_angle -= math.radians(360) + if sweepangle > 0: + sweepangle -= 2 * pi + else: + if sweepangle < 0: + sweepangle += 2 * pi + + # HTML says sweep angles should be clamped at +/- 360 degrees, but Android uses + # mod 360 instead, so 360 would cause the circle to completely disappear. + limit = 359.999 # Must be less than 360 in 32-bit floating point. path.arcTo( - self.container.scale * (x - radius), - self.container.scale * (y - radius), - self.container.scale * (x + radius), - self.container.scale * (y + radius), - math.degrees(startangle), - math.degrees(sweep_angle), - False, + x - radius, + y - radius, + x + radius, + y + radius, + degrees(startangle), + max(-limit, min(degrees(sweepangle), limit)), + False, # forceMoveTo ) def ellipse( @@ -118,104 +133,138 @@ def ellipse( endangle, anticlockwise, path, - *args, - **kwargs + **kwargs, ): - sweep_angle = endangle - startangle - if anticlockwise: - sweep_angle -= math.radians(360) - ellipse_path = Path() - ellipse_path.addArc( - self.container.scale * (x - radiusx), - self.container.scale * (y - radiusy), - self.container.scale * (x + radiusx), - self.container.scale * (y + radiusy), - math.degrees(startangle), - math.degrees(sweep_angle), - ) - rotation_matrix = Matrix() - rotation_matrix.postRotate( - math.degrees(rotation), - self.container.scale * x, - self.container.scale * y, - ) - ellipse_path.transform(rotation_matrix) - path.addPath(ellipse_path) - - def rect(self, x, y, width, height, path, *args, **kwargs): - path.addRect( - self.container.scale * x, - self.container.scale * y, - self.container.scale * (x + width), - self.container.scale * (y + height), - Path__Direction.CW, - ) + matrix = Matrix() + matrix.postScale(radiusx, radiusy) + matrix.postRotate(degrees(rotation)) + matrix.postTranslate(x, y) + + # Creating the ellipse as a separate path and then using addPath would make it a + # disconnected contour. And there's no way to extract the segments from a path + # until getPathIterator in API level 34. So this is the simplest solution I + # could find. + inverse = Matrix() + matrix.invert(inverse) + path.transform(inverse) + self.arc(0, 0, 1, startangle, endangle, anticlockwise, path) + path.transform(matrix) + + def rect(self, x, y, width, height, path, **kwargs): + path.addRect(x, y, x + width, y + height, Path.Direction.CW) # Drawing Paths - def fill(self, color, fill_rule, preserve, path, canvas, *args, **kwargs): + def fill(self, color, fill_rule, path, canvas, **kwargs): draw_paint = Paint() draw_paint.setAntiAlias(True) - draw_paint.setStyle(Paint__Style.FILL) - if color is None: - a, r, g, b = 255, 0, 0, 0 - else: - a, r, g, b = round(color.a * 255), int(color.r), int(color.g), int(color.b) - draw_paint.setARGB(a, r, g, b) - + draw_paint.setStyle(Paint.Style.FILL) + draw_paint.setColor(jint(native_color(color))) + + path.setFillType( + { + FillRule.EVENODD: Path.FillType.EVEN_ODD, + FillRule.NONZERO: Path.FillType.WINDING, + }.get(fill_rule, Path.FillType.WINDING) + ) canvas.drawPath(path, draw_paint) path.reset() - def stroke(self, color, line_width, line_dash, path, canvas, *args, **kwargs): + def stroke(self, color, line_width, line_dash, path, canvas, **kwargs): draw_paint = Paint() draw_paint.setAntiAlias(True) - draw_paint.setStrokeWidth(self.container.scale * line_width) - draw_paint.setStyle(Paint__Style.STROKE) - if color is None: - a, r, g, b = 255, 0, 0, 0 - else: - a, r, g, b = round(color.a * 255), int(color.r), int(color.g), int(color.b) + draw_paint.setStyle(Paint.Style.STROKE) + draw_paint.setColor(jint(native_color(color))) + + # The stroke respects the canvas transform, so we don't need to scale it here. + draw_paint.setStrokeWidth(line_width) if line_dash is not None: - draw_paint.setPathEffect( - DashPathEffect( - [(self.container.scale * float(d)) for d in line_dash], 0.0 - ) - ) - draw_paint.setARGB(a, r, g, b) + draw_paint.setPathEffect(DashPathEffect(line_dash, 0)) canvas.drawPath(path, draw_paint) path.reset() # Transformations - def rotate(self, radians, canvas, *args, **kwargs): - canvas.rotate(math.degrees(radians)) + def rotate(self, radians, canvas, **kwargs): + canvas.rotate(degrees(radians)) - def scale(self, sx, sy, canvas, *args, **kwargs): - canvas.scale(float(sx), float(sy)) + def scale(self, sx, sy, canvas, **kwargs): + canvas.scale(sx, sy) - def translate(self, tx, ty, canvas, *args, **kwargs): - canvas.translate(self.container.scale * tx, self.container.scale * ty) + def translate(self, tx, ty, canvas, **kwargs): + canvas.translate(tx, ty) - def reset_transform(self, canvas, *args, **kwargs): - canvas.restore() - canvas.save() + def reset_transform(self, canvas, **kwargs): + canvas.setMatrix(None) + self.scale(self.dpi_scale, self.dpi_scale, canvas) # Text - def measure_text(self, text, font, tight=False): - self.interface.factory.not_implemented("Canvas.measure_text") + def measure_text(self, text, font): + paint = self._text_paint(font) + sizes = [paint.measureText(line) for line in text.splitlines()] + return ( + max(size for size in sizes), + paint.getFontSpacing() * len(sizes), + ) - def write_text(self, text, x, y, font, *args, **kwargs): - self.interface.factory.not_implemented("Canvas.write_text") + def write_text(self, text, x, y, font, baseline, canvas, **kwargs): + lines = text.splitlines() + paint = self._text_paint(font) + line_height = paint.getFontSpacing() + total_height = line_height * len(lines) + + # paint.ascent returns a negative number. + if baseline == Baseline.TOP: + top = y - paint.ascent() + elif baseline == Baseline.MIDDLE: + top = y - paint.ascent() - (total_height / 2) + elif baseline == Baseline.BOTTOM: + top = y - paint.ascent() - total_height + else: + # Default to Baseline.ALPHABETIC + top = y + + for line_num, line in enumerate(text.splitlines()): + # FILL_AND_STROKE doesn't allow separate colors, so we have to draw twice. + def draw(): + canvas.drawText(line, x, top + (line_height * line_num), paint) + + if (color := kwargs.get("fill_color")) is not None: + paint.setStyle(Paint.Style.FILL) + paint.setColor(jint(native_color(color))) + draw() + if (color := kwargs.get("stroke_color")) is not None: + paint.setStyle(Paint.Style.STROKE) + paint.setStrokeWidth(kwargs["line_width"]) + paint.setColor(jint(native_color(color))) + draw() + + def _text_paint(self, font): + # font.size applies the scale factor, and the canvas transformation matrix + # will apply it again, so we need to cancel one of those with a scale_out. + paint = Paint() + paint.setTypeface(font.typeface()) + paint.setTextSize(self.scale_out(font.size())) + return paint def get_image_data(self): - self.interface.factory.not_implemented("Canvas.get_image_data()") - - # Rehint - - def set_on_resize(self, handler): - self.interface.factory.not_implemented("Canvas.on_resize") + bitmap = Bitmap.createBitmap( + self.native.getWidth(), self.native.getHeight(), Bitmap.Config.ARGB_8888 + ) + canvas = A_Canvas(bitmap) + background = self.native.getBackground() + if background: + background.draw(canvas) + self.native.draw(canvas) + + stream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream) + return bytes(stream.toByteArray()) + + def set_background_color(self, value): + self.set_background_simple(value) def rehint(self): self.interface.intrinsic.width = at_least(0) diff --git a/android/src/toga_android/widgets/dateinput.py b/android/src/toga_android/widgets/dateinput.py index 6a60c445e1..2f3e000c4e 100644 --- a/android/src/toga_android/widgets/dateinput.py +++ b/android/src/toga_android/widgets/dateinput.py @@ -1,10 +1,10 @@ from datetime import date, datetime, time -from ..libs.android import R__drawable -from ..libs.android.widget import ( - DatePickerDialog, - DatePickerDialog__OnDateSetListener as OnDateSetListener, -) +from java import dynamic_proxy + +from android import R +from android.app import DatePickerDialog + from .internal.pickers import PickerBase @@ -16,7 +16,7 @@ def native_date(py_date): return int(datetime.combine(py_date, time.min).timestamp() * 1000) -class DatePickerListener(OnDateSetListener): +class DatePickerListener(dynamic_proxy(DatePickerDialog.OnDateSetListener)): def __init__(self, impl): super().__init__() self.impl = impl @@ -31,7 +31,7 @@ def onDateSet(self, view, year, month_0, day): class DateInput(PickerBase): @classmethod def _get_icon(cls): - return R__drawable.ic_menu_my_calendar + return R.drawable.ic_menu_my_calendar def create(self): super().create() diff --git a/android/src/toga_android/widgets/detailedlist.py b/android/src/toga_android/widgets/detailedlist.py index 3357fa1541..bdcc487eb8 100644 --- a/android/src/toga_android/widgets/detailedlist.py +++ b/android/src/toga_android/widgets/detailedlist.py @@ -1,30 +1,20 @@ from dataclasses import dataclass +from androidx.swiperefreshlayout.widget import SwipeRefreshLayout +from java import dynamic_proxy from travertino.size import at_least -from ..libs.android import R__attr, R__color -from ..libs.android.app import AlertDialog__Builder -from ..libs.android.content import DialogInterface__OnClickListener -from ..libs.android.graphics import Rect -from ..libs.android.view import Gravity, OnClickListener, OnLongClickListener -from ..libs.android.widget import ( - ImageView, - ImageView__ScaleType, - LinearLayout, - LinearLayout__LayoutParams, - RelativeLayout, - RelativeLayout__LayoutParams, - ScrollView, - TextView, -) -from ..libs.androidx.swiperefreshlayout import ( - SwipeRefreshLayout, - SwipeRefreshLayout__OnRefreshListener, -) +from android import R +from android.app import AlertDialog +from android.content import DialogInterface +from android.graphics import Rect +from android.view import Gravity, View +from android.widget import ImageView, LinearLayout, RelativeLayout, ScrollView, TextView + from .base import Widget -class DetailedListOnClickListener(OnClickListener): +class DetailedListOnClickListener(dynamic_proxy(View.OnClickListener)): def __init__(self, impl, row_number): super().__init__() self.impl = impl @@ -42,7 +32,7 @@ class Action: enabled: bool -class DetailedListOnLongClickListener(OnLongClickListener): +class DetailedListOnLongClickListener(dynamic_proxy(View.OnLongClickListener)): def __init__(self, impl, row_number): super().__init__() self.impl = impl @@ -72,7 +62,7 @@ def onLongClick(self, _view): if actions: row = self.interface.data[self.row_number] - AlertDialog__Builder(self.impl._native_activity).setItems( + AlertDialog.Builder(self.impl._native_activity).setItems( [action.name for action in actions], DetailedListActionListener(actions, row), ).show() @@ -80,7 +70,7 @@ def onLongClick(self, _view): return True -class DetailedListActionListener(DialogInterface__OnClickListener): +class DetailedListActionListener(dynamic_proxy(DialogInterface.OnClickListener)): def __init__(self, actions, row): super().__init__() self.actions = actions @@ -90,7 +80,7 @@ def onClick(self, dialog, which): self.actions[which].handler(None, row=self.row) -class OnRefreshListener(SwipeRefreshLayout__OnRefreshListener): +class OnRefreshListener(dynamic_proxy(SwipeRefreshLayout.OnRefreshListener)): def __init__(self, interface): super().__init__() self._interface = interface @@ -102,7 +92,7 @@ def onRefresh(self): class DetailedList(Widget): def create(self): # get the selection color from the current theme - attrs = [R__attr.colorBackground, R__attr.colorControlHighlight] + attrs = [R.attr.colorBackground, R.attr.colorControlHighlight] typed_array = self._native_activity.obtainStyledAttributes(attrs) self.color_unselected = typed_array.getColor(0, 0) self.color_selected = typed_array.getColor(1, 0) @@ -112,9 +102,9 @@ def create(self): self._refresh_layout.setOnRefreshListener(OnRefreshListener(self.interface)) self._scroll_view = ScrollView(self._native_activity) - match_parent = LinearLayout__LayoutParams( - LinearLayout__LayoutParams.MATCH_PARENT, - LinearLayout__LayoutParams.MATCH_PARENT, + match_parent = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT, ) self._refresh_layout.addView(self._scroll_view, match_parent) @@ -144,23 +134,23 @@ def _make_row(self, container, i, row): icon_image_view = ImageView(self._native_activity) if icon is not None: icon_image_view.setImageBitmap(icon._impl.native) - icon_layout_params = RelativeLayout__LayoutParams( - RelativeLayout__LayoutParams.WRAP_CONTENT, - RelativeLayout__LayoutParams.WRAP_CONTENT, + icon_layout_params = RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT, ) icon_width = self.scale_in(50) icon_margin = self.scale_in(10) icon_layout_params.width = icon_width icon_layout_params.setMargins(icon_margin, 0, icon_margin, 0) icon_layout_params.height = row_height - icon_image_view.setScaleType(ImageView__ScaleType.FIT_CENTER) + icon_image_view.setScaleType(ImageView.ScaleType.FIT_CENTER) row_view.addView(icon_image_view, icon_layout_params) # Create layout to show top_text and bottom_text. text_container = LinearLayout(self._native_activity) - text_container_params = RelativeLayout__LayoutParams( - RelativeLayout__LayoutParams.WRAP_CONTENT, - RelativeLayout__LayoutParams.WRAP_CONTENT, + text_container_params = RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT, ) text_container_params.height = row_height text_container_params.setMargins(icon_width + (2 * icon_margin), 0, 0, 0) @@ -178,24 +168,24 @@ def get_string(value): top_text.setText(get_string(title)) top_text.setTextSize(20.0) top_text.setTextColor( - self._native_activity.getResources().getColor(R__color.black) + self._native_activity.getResources().getColor(R.color.black) ) bottom_text = TextView(self._native_activity) bottom_text.setTextColor( - self._native_activity.getResources().getColor(R__color.black) + self._native_activity.getResources().getColor(R.color.black) ) bottom_text.setText(get_string(subtitle)) bottom_text.setTextSize(16.0) - top_text_params = LinearLayout__LayoutParams( - RelativeLayout__LayoutParams.WRAP_CONTENT, - RelativeLayout__LayoutParams.MATCH_PARENT, + top_text_params = LinearLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.MATCH_PARENT, ) top_text_params.weight = 1.0 top_text.setGravity(Gravity.BOTTOM) text_container.addView(top_text, top_text_params) - bottom_text_params = LinearLayout__LayoutParams( - RelativeLayout__LayoutParams.WRAP_CONTENT, - RelativeLayout__LayoutParams.MATCH_PARENT, + bottom_text_params = LinearLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.MATCH_PARENT, ) bottom_text_params.weight = 1.0 bottom_text.setGravity(Gravity.TOP) diff --git a/android/src/toga_android/widgets/imageview.py b/android/src/toga_android/widgets/imageview.py index 401df7da02..e70bd38392 100644 --- a/android/src/toga_android/widgets/imageview.py +++ b/android/src/toga_android/widgets/imageview.py @@ -1,6 +1,6 @@ +from android.widget import ImageView as A_ImageView from toga.widgets.imageview import rehint_imageview -from ..libs.android.widget import ImageView as A_ImageView from .base import Widget diff --git a/android/src/toga_android/widgets/internal/pickers.py b/android/src/toga_android/widgets/internal/pickers.py index 286199643a..470f1b05e5 100644 --- a/android/src/toga_android/widgets/internal/pickers.py +++ b/android/src/toga_android/widgets/internal/pickers.py @@ -1,13 +1,15 @@ from abc import ABC, abstractmethod +from java import dynamic_proxy from travertino.size import at_least -from ...libs.android.view import OnClickListener, View__MeasureSpec -from ...libs.android.widget import EditText +from android.view import View +from android.widget import EditText + from ..label import TextViewWidget -class TogaPickerClickListener(OnClickListener): +class TogaPickerClickListener(dynamic_proxy(View.OnClickListener)): def __init__(self, impl): super().__init__() self.impl = impl @@ -40,7 +42,5 @@ def create(self): def rehint(self): self.interface.intrinsic.width = at_least(300) - self.native.measure( - View__MeasureSpec.UNSPECIFIED, View__MeasureSpec.UNSPECIFIED - ) + self.native.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) self.interface.intrinsic.height = self.native.getMeasuredHeight() diff --git a/android/src/toga_android/widgets/label.py b/android/src/toga_android/widgets/label.py index 267091fb7c..756fa51e0c 100644 --- a/android/src/toga_android/widgets/label.py +++ b/android/src/toga_android/widgets/label.py @@ -1,15 +1,21 @@ from travertino.size import at_least +from android.os import Build +from android.text import Layout +from android.util import TypedValue +from android.view import Gravity, View +from android.widget import TextView from toga.constants import JUSTIFY from toga_android.colors import native_color -from ..libs.android.os import Build -from ..libs.android.text import Layout -from ..libs.android.view import Gravity, View__MeasureSpec -from ..libs.android.widget import TextView from .base import Widget, align +def set_textview_font(tv, font, default_typeface, default_size): + tv.setTypeface(font.typeface(default=default_typeface)) + tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, font.size(default=default_size)) + + class TextViewWidget(Widget): def cache_textview_defaults(self): self._default_text_color = self.native.getCurrentTextColor() @@ -17,7 +23,9 @@ def cache_textview_defaults(self): self._default_typeface = self.native.getTypeface() def set_font(self, font): - font._impl.apply(self.native, self._default_text_size, self._default_typeface) + set_textview_font( + self.native, font._impl, self._default_typeface, self._default_text_size + ) def set_background_color(self, value): # In the case of EditText, this causes any custom color to hide the bottom border @@ -58,15 +66,13 @@ def set_text(self, value): def rehint(self): # Ask the Android TextView first for its minimum possible height. # This is the height with word-wrapping disabled. - self.native.measure( - View__MeasureSpec.UNSPECIFIED, View__MeasureSpec.UNSPECIFIED - ) + self.native.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) min_height = self.native.getMeasuredHeight() self.interface.intrinsic.height = min_height # Ask it how wide it would be if it had to be the minimum height. self.native.measure( - View__MeasureSpec.UNSPECIFIED, - View__MeasureSpec.makeMeasureSpec(min_height, View__MeasureSpec.AT_MOST), + View.MeasureSpec.UNSPECIFIED, + View.MeasureSpec.makeMeasureSpec(min_height, View.MeasureSpec.AT_MOST), ) self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth()) diff --git a/android/src/toga_android/widgets/multilinetextinput.py b/android/src/toga_android/widgets/multilinetextinput.py index 96275768ba..a3ee4a1591 100644 --- a/android/src/toga_android/widgets/multilinetextinput.py +++ b/android/src/toga_android/widgets/multilinetextinput.py @@ -1,7 +1,8 @@ from travertino.size import at_least -from ..libs.android.text import InputType -from ..libs.android.view import Gravity +from android.text import InputType +from android.view import Gravity + from .textinput import TextInput diff --git a/android/src/toga_android/widgets/numberinput.py b/android/src/toga_android/widgets/numberinput.py index 8692001027..e91050e8e2 100644 --- a/android/src/toga_android/widgets/numberinput.py +++ b/android/src/toga_android/widgets/numberinput.py @@ -1,8 +1,8 @@ from decimal import InvalidOperation +from android.text import InputType from toga.widgets.numberinput import _clean_decimal -from ..libs.android.text import InputType from .textinput import TextInput diff --git a/android/src/toga_android/widgets/passwordinput.py b/android/src/toga_android/widgets/passwordinput.py index 9562573576..928ae48915 100644 --- a/android/src/toga_android/widgets/passwordinput.py +++ b/android/src/toga_android/widgets/passwordinput.py @@ -1,4 +1,5 @@ -from ..libs.android.text import InputType +from android.text import InputType + from .textinput import TextInput diff --git a/android/src/toga_android/widgets/progressbar.py b/android/src/toga_android/widgets/progressbar.py index d1b847bbbf..539f1ce1d6 100644 --- a/android/src/toga_android/widgets/progressbar.py +++ b/android/src/toga_android/widgets/progressbar.py @@ -1,9 +1,9 @@ from travertino.size import at_least -from ..libs.android import R__attr -from ..libs.android.util import AttributeSet -from ..libs.android.view import View__MeasureSpec -from ..libs.android.widget import ProgressBar as A_ProgressBar +from android import R +from android.view import View +from android.widget import ProgressBar as A_ProgressBar + from .base import Widget # Implementation notes @@ -35,9 +35,7 @@ class ProgressBar(Widget): def create(self): progressbar = A_ProgressBar( - self._native_activity, - AttributeSet.__null__, - R__attr.progressBarStyleHorizontal, + self._native_activity, None, R.attr.progressBarStyleHorizontal ) self.native = progressbar @@ -92,8 +90,8 @@ def set_value(self, value): def rehint(self): self.native.measure( - View__MeasureSpec.UNSPECIFIED, - View__MeasureSpec.UNSPECIFIED, + View.MeasureSpec.UNSPECIFIED, + View.MeasureSpec.UNSPECIFIED, ) self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth()) self.interface.intrinsic.height = self.native.getMeasuredHeight() diff --git a/android/src/toga_android/widgets/scrollcontainer.py b/android/src/toga_android/widgets/scrollcontainer.py index 4f388e0d72..1e727774f4 100644 --- a/android/src/toga_android/widgets/scrollcontainer.py +++ b/android/src/toga_android/widgets/scrollcontainer.py @@ -1,22 +1,16 @@ from decimal import ROUND_DOWN +from java import dynamic_proxy from travertino.size import at_least +from android.view import Gravity, View +from android.widget import HorizontalScrollView, LinearLayout, ScrollView + from ..container import Container -from ..libs.android.view import ( - Gravity, - View__OnScrollChangeListener, - View__OnTouchListener, -) -from ..libs.android.widget import ( - HorizontalScrollView, - LinearLayout__LayoutParams, - ScrollView, -) from .base import Widget -class TogaOnTouchListener(View__OnTouchListener): +class TogaOnTouchListener(dynamic_proxy(View.OnTouchListener)): def __init__(self): super().__init__() self.is_scrolling_enabled = True @@ -28,7 +22,7 @@ def onTouch(self, view, motion_event): return True -class TogaOnScrollListener(View__OnScrollChangeListener): +class TogaOnScrollListener(dynamic_proxy(View.OnScrollChangeListener)): def __init__(self, impl): super().__init__() self.impl = impl @@ -42,9 +36,9 @@ def create(self): scroll_listener = TogaOnScrollListener(self) self.native = self.vScrollView = ScrollView(self._native_activity) - vScrollView_layout_params = LinearLayout__LayoutParams( - LinearLayout__LayoutParams.MATCH_PARENT, - LinearLayout__LayoutParams.MATCH_PARENT, + vScrollView_layout_params = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT, ) vScrollView_layout_params.gravity = Gravity.TOP self.vScrollView.setLayoutParams(vScrollView_layout_params) @@ -53,9 +47,9 @@ def create(self): self.vScrollView.setOnScrollChangeListener(scroll_listener) self.hScrollView = HorizontalScrollView(self._native_activity) - hScrollView_layout_params = LinearLayout__LayoutParams( - LinearLayout__LayoutParams.MATCH_PARENT, - LinearLayout__LayoutParams.MATCH_PARENT, + hScrollView_layout_params = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT, ) hScrollView_layout_params.gravity = Gravity.LEFT self.hScrollListener = TogaOnTouchListener() diff --git a/android/src/toga_android/widgets/selection.py b/android/src/toga_android/widgets/selection.py index b1d9b6be2a..2e79e93382 100644 --- a/android/src/toga_android/widgets/selection.py +++ b/android/src/toga_android/widgets/selection.py @@ -1,12 +1,14 @@ +from java import dynamic_proxy from travertino.size import at_least -from ..libs.android import R__layout -from ..libs.android.view import Gravity, View__MeasureSpec -from ..libs.android.widget import ArrayAdapter, OnItemSelectedListener, Spinner -from .base import Widget, align +from android import R +from android.view import View +from android.widget import AdapterView, ArrayAdapter, Spinner +from .base import Widget -class TogaOnItemSelectedListener(OnItemSelectedListener): + +class TogaOnItemSelectedListener(dynamic_proxy(AdapterView.OnItemSelectedListener)): def __init__(self, impl): super().__init__() self.impl = impl @@ -24,10 +26,8 @@ class Selection(Widget): def create(self): self.native = Spinner(self._native_activity, Spinner.MODE_DROPDOWN) self.native.setOnItemSelectedListener(TogaOnItemSelectedListener(impl=self)) - self.adapter = ArrayAdapter( - self._native_activity, R__layout.simple_spinner_item - ) - self.adapter.setDropDownViewResource(R__layout.simple_spinner_dropdown_item) + self.adapter = ArrayAdapter(self._native_activity, R.layout.simple_spinner_item) + self.adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item) self.native.setAdapter(self.adapter) self.last_selection = None @@ -84,11 +84,6 @@ def clear(self): self.on_change(None) def rehint(self): - self.native.measure( - View__MeasureSpec.UNSPECIFIED, View__MeasureSpec.UNSPECIFIED - ) + self.native.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth()) self.interface.intrinsic.height = self.native.getMeasuredHeight() - - def set_alignment(self, value): - self.native.setGravity(Gravity.CENTER_VERTICAL | align(value)) diff --git a/android/src/toga_android/widgets/slider.py b/android/src/toga_android/widgets/slider.py index 94ded9d0d0..6f75168977 100644 --- a/android/src/toga_android/widgets/slider.py +++ b/android/src/toga_android/widgets/slider.py @@ -1,10 +1,11 @@ +from java import dynamic_proxy from travertino.size import at_least import toga +from android import R +from android.view import View +from android.widget import SeekBar -from ..libs.android import R__attr, R__style -from ..libs.android.view import View__MeasureSpec -from ..libs.android.widget import SeekBar, SeekBar__OnSeekBarChangeListener from .base import Widget # Implementation notes @@ -14,7 +15,7 @@ # used to convert between integers and floats. -class TogaOnSeekBarChangeListener(SeekBar__OnSeekBarChangeListener): +class TogaOnSeekBarChangeListener(dynamic_proxy(SeekBar.OnSeekBarChangeListener)): def __init__(self, impl): super().__init__() self.impl = impl @@ -59,14 +60,12 @@ def set_ticks_visible(self, visible): def _load_tick_drawable(self): attrs = self._native_activity.obtainStyledAttributes( - R__style.Widget_Material_SeekBar_Discrete, [R__attr.tickMark] + R.style.Widget_Material_SeekBar_Discrete, [R.attr.tickMark] ) Slider.TICK_DRAWABLE = attrs.getDrawable(0) attrs.recycle() def rehint(self): - self.native.measure( - View__MeasureSpec.UNSPECIFIED, View__MeasureSpec.UNSPECIFIED - ) + self.native.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth()) self.interface.intrinsic.height = self.native.getMeasuredHeight() diff --git a/android/src/toga_android/widgets/switch.py b/android/src/toga_android/widgets/switch.py index 28fe8e9ce9..e843cd94bd 100644 --- a/android/src/toga_android/widgets/switch.py +++ b/android/src/toga_android/widgets/switch.py @@ -1,14 +1,13 @@ +from java import dynamic_proxy from travertino.size import at_least -from ..libs.android.view import View__MeasureSpec -from ..libs.android.widget import ( - CompoundButton__OnCheckedChangeListener, - Switch as A_Switch, -) +from android.view import View +from android.widget import CompoundButton, Switch as A_Switch + from .label import TextViewWidget -class OnCheckedChangeListener(CompoundButton__OnCheckedChangeListener): +class OnCheckedChangeListener(dynamic_proxy(CompoundButton.OnCheckedChangeListener)): def __init__(self, impl): super().__init__() self._impl = impl @@ -44,8 +43,6 @@ def set_value(self, value): self.native.setChecked(bool(value)) def rehint(self): - self.native.measure( - View__MeasureSpec.UNSPECIFIED, View__MeasureSpec.UNSPECIFIED - ) + self.native.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth()) self.interface.intrinsic.height = self.native.getMeasuredHeight() diff --git a/android/src/toga_android/widgets/table.py b/android/src/toga_android/widgets/table.py index f8fa4c77c4..849addeff0 100644 --- a/android/src/toga_android/widgets/table.py +++ b/android/src/toga_android/widgets/table.py @@ -1,25 +1,19 @@ from warnings import warn +from java import dynamic_proxy from travertino.size import at_least import toga +from android import R +from android.graphics import Rect, Typeface +from android.view import Gravity, View +from android.widget import LinearLayout, ScrollView, TableLayout, TableRow, TextView -from ..libs.android import R__attr -from ..libs.android.graphics import Rect, Typeface -from ..libs.android.view import Gravity, OnClickListener, OnLongClickListener -from ..libs.android.widget import ( - LinearLayout__LayoutParams, - ScrollView, - TableLayout, - TableLayout__Layoutparams, - TableRow, - TableRow__Layoutparams, - TextView, -) from .base import Widget +from .label import set_textview_font -class TogaOnClickListener(OnClickListener): +class TogaOnClickListener(dynamic_proxy(View.OnClickListener)): def __init__(self, impl): super().__init__() self.impl = impl @@ -37,7 +31,7 @@ def onClick(self, view): self.impl.interface.on_select(None) -class TogaOnLongClickListener(OnLongClickListener): +class TogaOnLongClickListener(dynamic_proxy(View.OnLongClickListener)): def __init__(self, impl): super().__init__() self.impl = impl @@ -59,7 +53,7 @@ class Table(Widget): def create(self): # get the selection color from the current theme - attrs = [R__attr.colorBackground, R__attr.colorControlHighlight] + attrs = [R.attr.colorBackground, R.attr.colorControlHighlight] typed_array = self._native_activity.obtainStyledAttributes(attrs) self.color_unselected = typed_array.getColor(0, 0) self.color_selected = typed_array.getColor(1, 0) @@ -67,17 +61,17 @@ def create(self): # add vertical scroll view self.native = vscroll_view = ScrollView(self._native_activity) - vscroll_view_layout_params = LinearLayout__LayoutParams( - LinearLayout__LayoutParams.MATCH_PARENT, - LinearLayout__LayoutParams.MATCH_PARENT, + vscroll_view_layout_params = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT, ) vscroll_view_layout_params.gravity = Gravity.TOP vscroll_view.setLayoutParams(vscroll_view_layout_params) self.table_layout = TableLayout(self._native_activity) - table_layout_params = TableLayout__Layoutparams( - TableLayout__Layoutparams.MATCH_PARENT, - TableLayout__Layoutparams.WRAP_CONTENT, + table_layout_params = TableLayout.LayoutParams( + TableLayout.LayoutParams.MATCH_PARENT, + TableLayout.LayoutParams.WRAP_CONTENT, ) # add table layout to scrollbox @@ -113,15 +107,18 @@ def clear_selection(self): def create_table_header(self): table_row = TableRow(self._native_activity) - table_row_params = TableRow__Layoutparams( - TableRow__Layoutparams.MATCH_PARENT, TableRow__Layoutparams.WRAP_CONTENT + table_row_params = TableRow.LayoutParams( + TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT ) table_row.setLayoutParams(table_row_params) for col_index in range(len(self.interface._accessors)): text_view = TextView(self._native_activity) text_view.setText(self.interface.headings[col_index]) - self._font_impl.apply( - text_view, text_view.getTextSize(), text_view.getTypeface() + set_textview_font( + text_view, + self._font_impl, + text_view.getTypeface(), + text_view.getTextSize(), ) text_view.setTypeface( Typeface.create( @@ -129,8 +126,8 @@ def create_table_header(self): text_view.getTypeface().getStyle() | Typeface.BOLD, ) ) - text_view_params = TableRow__Layoutparams( - TableRow__Layoutparams.MATCH_PARENT, TableRow__Layoutparams.WRAP_CONTENT + text_view_params = TableRow.LayoutParams( + TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT ) text_view_params.setMargins(10, 5, 10, 5) # left, top, right, bottom text_view_params.gravity = Gravity.START @@ -140,8 +137,8 @@ def create_table_header(self): def create_table_row(self, row_index): table_row = TableRow(self._native_activity) - table_row_params = TableRow__Layoutparams( - TableRow__Layoutparams.MATCH_PARENT, TableRow__Layoutparams.WRAP_CONTENT + table_row_params = TableRow.LayoutParams( + TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT ) table_row.setLayoutParams(table_row_params) table_row.setClickable(True) @@ -152,11 +149,14 @@ def create_table_row(self, row_index): for col_index in range(len(self.interface._accessors)): text_view = TextView(self._native_activity) text_view.setText(self.get_data_value(row_index, col_index)) - self._font_impl.apply( - text_view, text_view.getTextSize(), text_view.getTypeface() + set_textview_font( + text_view, + self._font_impl, + text_view.getTypeface(), + text_view.getTextSize(), ) - text_view_params = TableRow__Layoutparams( - TableRow__Layoutparams.MATCH_PARENT, TableRow__Layoutparams.WRAP_CONTENT + text_view_params = TableRow.LayoutParams( + TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT ) text_view_params.setMargins(10, 5, 10, 5) # left, top, right, bottom text_view_params.gravity = Gravity.START diff --git a/android/src/toga_android/widgets/textinput.py b/android/src/toga_android/widgets/textinput.py index 760592024f..24d1529026 100644 --- a/android/src/toga_android/widgets/textinput.py +++ b/android/src/toga_android/widgets/textinput.py @@ -1,19 +1,15 @@ +from java import dynamic_proxy from travertino.size import at_least +from android.text import InputType, TextWatcher +from android.view import Gravity, View +from android.widget import EditText from toga_android.keys import toga_key -from ..libs.android.text import InputType, TextWatcher -from ..libs.android.view import ( - Gravity, - OnKeyListener, - View__MeasureSpec, - View__OnFocusChangeListener, -) -from ..libs.android.widget import EditText from .label import TextViewWidget -class TogaTextWatcher(TextWatcher): +class TogaTextWatcher(dynamic_proxy(TextWatcher)): def __init__(self, impl): super().__init__() self.impl = impl @@ -28,7 +24,7 @@ def onTextChanged(self, _charSequence, _start, _before, _count): pass -class TogaKeyListener(OnKeyListener): +class TogaKeyListener(dynamic_proxy(View.OnKeyListener)): def __init__(self, impl): super().__init__() self.impl = impl @@ -46,7 +42,7 @@ def onKey(self, _view, _key, _event): return False -class TogaFocusListener(View__OnFocusChangeListener): +class TogaFocusListener(dynamic_proxy(View.OnFocusChangeListener)): def __init__(self, impl): super().__init__() self.impl = impl @@ -81,9 +77,19 @@ def set_readonly(self, readonly): if readonly: # Implicitly calls setFocusableInTouchMode(False) self.native.setFocusable(False) + # Add TYPE_TEXT_FLAG_NO_SUGGESTIONS to the input type to disable suggestions + input_type = ( + self.native.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + ) + self.native.setInputType(input_type) else: # Implicitly calls setFocusable(True) self.native.setFocusableInTouchMode(True) + # Remove TYPE_TEXT_FLAG_NO_SUGGESTIONS to enable suggestions + input_type = ( + self.native.getInputType() & ~InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + ) + self.native.setInputType(input_type) def get_placeholder(self): return str(self.native.getHint()) @@ -118,7 +124,5 @@ def _on_lose_focus(self): def rehint(self): self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH) - self.native.measure( - View__MeasureSpec.UNSPECIFIED, View__MeasureSpec.UNSPECIFIED - ) + self.native.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) self.interface.intrinsic.height = self.native.getMeasuredHeight() diff --git a/android/src/toga_android/widgets/timeinput.py b/android/src/toga_android/widgets/timeinput.py index 6d46f19b87..05d9744ae9 100644 --- a/android/src/toga_android/widgets/timeinput.py +++ b/android/src/toga_android/widgets/timeinput.py @@ -1,14 +1,14 @@ from datetime import time -from ..libs.android import R__drawable -from ..libs.android.widget import ( - TimePickerDialog, - TimePickerDialog__OnTimeSetListener as OnTimeSetListener, -) +from java import dynamic_proxy + +from android import R +from android.app import TimePickerDialog + from .internal.pickers import PickerBase -class TimePickerListener(OnTimeSetListener): +class TimePickerListener(dynamic_proxy(TimePickerDialog.OnTimeSetListener)): def __init__(self, impl): super().__init__() self.impl = impl @@ -23,7 +23,7 @@ def onTimeSet(self, view, hour, minute): class TimeInput(PickerBase): @classmethod def _get_icon(cls): - return R__drawable.ic_menu_recent_history + return R.drawable.ic_menu_recent_history def create(self): super().create() diff --git a/android/src/toga_android/widgets/webview.py b/android/src/toga_android/widgets/webview.py index 5aa72ebe26..343fadd954 100644 --- a/android/src/toga_android/widgets/webview.py +++ b/android/src/toga_android/widgets/webview.py @@ -1,14 +1,15 @@ import json +from java import dynamic_proxy from travertino.size import at_least +from android.webkit import ValueCallback, WebView as A_WebView, WebViewClient from toga.widgets.webview import JavaScriptResult -from ..libs.android.webkit import ValueCallback, WebView as A_WebView, WebViewClient from .base import Widget -class ReceiveString(ValueCallback): +class ReceiveString(dynamic_proxy(ValueCallback)): def __init__(self, future, on_result): super().__init__() self.future = future diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 1da3543e5a..ad86742d05 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -1,12 +1,15 @@ from decimal import ROUND_UP +from java import dynamic_proxy + +from android import R +from android.view import ViewTreeObserver + from .container import Container -from .libs.android import R__id -from .libs.android.view import ViewTreeObserver__OnGlobalLayoutListener from .screen import Screen as ScreenImpl -class LayoutListener(ViewTreeObserver__OnGlobalLayoutListener): +class LayoutListener(dynamic_proxy(ViewTreeObserver.OnGlobalLayoutListener)): def __init__(self, window): super().__init__() self.window = window @@ -29,7 +32,7 @@ def __init__(self, interface, title, position, size): def set_app(self, app): self.app = app - native_parent = app.native.findViewById(R__id.content) + native_parent = app.native.findViewById(R.id.content) self.init_container(native_parent) native_parent.getViewTreeObserver().addOnGlobalLayoutListener( LayoutListener(self) diff --git a/android/tests_backend/app.py b/android/tests_backend/app.py index 0ff7a5cace..0e7527e14c 100644 --- a/android/tests_backend/app.py +++ b/android/tests_backend/app.py @@ -1,6 +1,6 @@ from pathlib import Path -from toga_android.libs.activity import MainActivity +from org.beeware.android import MainActivity from .probe import BaseProbe diff --git a/android/tests_backend/fonts.py b/android/tests_backend/fonts.py new file mode 100644 index 0000000000..63014f7875 --- /dev/null +++ b/android/tests_backend/fonts.py @@ -0,0 +1,107 @@ +from concurrent.futures import ThreadPoolExecutor + +from fontTools.ttLib import TTFont +from java import jint +from java.lang import Integer, Long + +from android.graphics import Typeface +from android.graphics.fonts import FontFamily +from android.util import TypedValue +from toga.fonts import ( + BOLD, + ITALIC, + MESSAGE, + NORMAL, + OBLIQUE, + SMALL_CAPS, + SYSTEM, + SYSTEM_DEFAULT_FONT_SIZE, +) + +SYSTEM_FONTS = {} +nativeGetFamily = new_FontFamily = None + + +def load_fontmap(): + field = Typeface.getClass().getDeclaredField("sSystemFontMap") + field.setAccessible(True) + fontmap = field.get(None) + + for name in fontmap.keySet().toArray(): + typeface = fontmap.get(name) + SYSTEM_FONTS[typeface] = name + for native_style in [ + Typeface.BOLD, + Typeface.ITALIC, + Typeface.BOLD | Typeface.ITALIC, + ]: + SYSTEM_FONTS[Typeface.create(typeface, native_style)] = name + + +def reflect_font_methods(): + global nativeGetFamily, new_FontFamily + + # Bypass non-SDK interface restrictions by looking them up on a background thread + # with no Java stack frames (https://stackoverflow.com/a/61600526). + with ThreadPoolExecutor() as executor: + nativeGetFamily = executor.submit( + Typeface.getClass().getDeclaredMethod, + "nativeGetFamily", + Long.TYPE, + Integer.TYPE, + ).result() + nativeGetFamily.setAccessible(True) + + new_FontFamily = executor.submit( + FontFamily.getClass().getConstructor, Long.TYPE + ).result() + + +class FontMixin: + supports_custom_fonts = True + + def assert_font_options(self, weight=NORMAL, style=NORMAL, variant=NORMAL): + assert (BOLD if self.typeface.isBold() else NORMAL) == weight + + if style == OBLIQUE: + print("Interpreting OBLIQUE font as ITALIC") + assert self.typeface.isItalic() + else: + assert (ITALIC if self.typeface.isItalic() else NORMAL) == style + + if variant == SMALL_CAPS: + print("Ignoring SMALL CAPS font test") + else: + assert NORMAL == variant + + def assert_font_size(self, expected): + if expected == SYSTEM_DEFAULT_FONT_SIZE: + expected = self.default_font_size * (72 / 96) + assert round(self.text_size) == round( + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + expected * (96 / 72), + self.native.getResources().getDisplayMetrics(), + ) + ) + + def assert_font_family(self, expected): + if not SYSTEM_FONTS: + load_fontmap() + + if actual := SYSTEM_FONTS.get(self.typeface): + assert actual == { + SYSTEM: self.default_font_family, + MESSAGE: "sans-serif", + }.get(expected, expected) + else: + if not nativeGetFamily: + reflect_font_methods() + family_ptr = nativeGetFamily.invoke( + None, self.typeface.native_instance, jint(0) + ) + family = new_FontFamily.newInstance(family_ptr) + assert family.getSize() == 1 + + font = TTFont(family.getFont(0).getFile().getPath()) + assert font["name"].getDebugName(1) == expected diff --git a/android/tests_backend/probe.py b/android/tests_backend/probe.py index 64cc383b25..39c3f3608e 100644 --- a/android/tests_backend/probe.py +++ b/android/tests_backend/probe.py @@ -1,16 +1,7 @@ import asyncio -from toga.fonts import SYSTEM - class BaseProbe: - def assert_font_family(self, expected): - actual = self.font.family - if expected == SYSTEM: - assert actual == "sans-serif" - else: - assert actual == expected - async def redraw(self, message=None, delay=None): """Request a redraw of the app, waiting until that redraw has completed.""" # If we're running slow, wait for a second diff --git a/android/tests_backend/widgets/base.py b/android/tests_backend/widgets/base.py index eab0dd2a13..b59bd8b56a 100644 --- a/android/tests_backend/widgets/base.py +++ b/android/tests_backend/widgets/base.py @@ -1,5 +1,6 @@ import asyncio +import pytest from java import dynamic_proxy from pytest import approx @@ -20,6 +21,7 @@ from toga.colors import TRANSPARENT from toga.style.pack import JUSTIFY, LEFT +from ..fonts import FontMixin from ..probe import BaseProbe from .properties import toga_color, toga_vertical_alignment @@ -33,11 +35,15 @@ def onGlobalLayout(self): self.event.set() -class SimpleProbe(BaseProbe): +class SimpleProbe(BaseProbe, FontMixin): + default_font_family = "sans-serif" + default_font_size = 14 + def __init__(self, widget): super().__init__() self.app = widget.app self.widget = widget + self.impl = widget._impl self.native = widget._impl.native self.layout_listener = LayoutListener() self.native.getViewTreeObserver().addOnGlobalLayoutListener( @@ -226,6 +232,12 @@ def is_hidden(self): def has_focus(self): return self.widget.app._impl.native.getCurrentFocus() == self.native + async def undo(self): + pytest.skip("Undo not supported on this platform") + + async def redo(self): + pytest.skip("Redo not supported on this platform") + def find_view_by_type(root, cls): assert isinstance(root, View) diff --git a/android/tests_backend/widgets/button.py b/android/tests_backend/widgets/button.py index 6cdd2fecc5..27ab6c8f5e 100644 --- a/android/tests_backend/widgets/button.py +++ b/android/tests_backend/widgets/button.py @@ -1,7 +1,6 @@ from java import jclass from toga.colors import TRANSPARENT -from toga.fonts import SYSTEM from .label import LabelProbe @@ -10,12 +9,8 @@ class ButtonProbe(LabelProbe): native_class = jclass("android.widget.Button") - def assert_font_family(self, expected): - actual = self.font.family - if expected == SYSTEM: - assert actual == "sans-serif-medium" - else: - assert actual == expected + # Heavier than sans-serif, but lighter than sans-serif bold + default_font_family = "sans-serif-medium" @property def background_color(self): diff --git a/android/tests_backend/widgets/canvas.py b/android/tests_backend/widgets/canvas.py new file mode 100644 index 0000000000..380bc9c3f2 --- /dev/null +++ b/android/tests_backend/widgets/canvas.py @@ -0,0 +1,50 @@ +from io import BytesIO + +import pytest +from org.beeware.android import DrawHandlerView +from PIL import Image + +from android.os import SystemClock +from android.view import MotionEvent + +from .base import SimpleProbe + + +class CanvasProbe(SimpleProbe): + native_class = DrawHandlerView + + def reference_variant(self, reference): + if reference in {"multiline_text", "write_text"}: + return f"{reference}-android" + return reference + + def get_image(self): + return Image.open(BytesIO(self.impl.get_image_data())) + + def assert_image_size(self, image, width, height): + assert image.width == width * self.scale_factor + assert image.height == height * self.scale_factor + + def motion_event(self, action, x, y): + time = SystemClock.uptimeMillis() + super().motion_event( + time, time, action, x * self.scale_factor, y * self.scale_factor + ) + + async def mouse_press(self, x, y): + self.motion_event(MotionEvent.ACTION_DOWN, x, y) + self.motion_event(MotionEvent.ACTION_UP, x, y) + + async def mouse_activate(self, x, y): + pytest.skip("Activation not supported on this platform") + + async def mouse_drag(self, x1, y1, x2, y2): + self.motion_event(MotionEvent.ACTION_DOWN, x1, y1) + self.motion_event(MotionEvent.ACTION_MOVE, (x1 + x2) / 2, (y1 + y2) / 2) + self.motion_event(MotionEvent.ACTION_UP, x2, y2) + + async def alt_mouse_press(self, x, y): + pytest.skip("Alternate handling not supported on this platform") + + async def alt_mouse_drag(self, x1, y1, x2, y2): + pytest.skip("Alternate handling not supported on this platform") diff --git a/android/tests_backend/widgets/imageview.py b/android/tests_backend/widgets/imageview.py index 8f1e4208ee..4b4091f8ee 100644 --- a/android/tests_backend/widgets/imageview.py +++ b/android/tests_backend/widgets/imageview.py @@ -9,3 +9,8 @@ class ImageViewProbe(SimpleProbe): @property def preserve_aspect_ratio(self): return self.native.getScaleType() == ImageView.ScaleType.FIT_CENTER + + def assert_image_size(self, width, height): + # Android internally scales the image to the container, + # so there's no image size check required. + pass diff --git a/android/tests_backend/widgets/label.py b/android/tests_backend/widgets/label.py index df5fa9d0a0..93c5145f33 100644 --- a/android/tests_backend/widgets/label.py +++ b/android/tests_backend/widgets/label.py @@ -3,7 +3,7 @@ from android.os import Build from .base import SimpleProbe -from .properties import toga_alignment, toga_color, toga_font +from .properties import toga_alignment, toga_color class LabelProbe(SimpleProbe): @@ -19,12 +19,12 @@ def text(self): return str(self.native.getText()) @property - def font(self): - return toga_font( - self.native.getTypeface(), - self.native.getTextSize(), - self.native.getResources(), - ) + def typeface(self): + return self.native.getTypeface() + + @property + def text_size(self): + return self.native.getTextSize() @property def alignment(self): diff --git a/android/tests_backend/widgets/numberinput.py b/android/tests_backend/widgets/numberinput.py index 716e1eb56c..018ffe8582 100644 --- a/android/tests_backend/widgets/numberinput.py +++ b/android/tests_backend/widgets/numberinput.py @@ -1,4 +1,4 @@ -from pytest import xfail +import pytest from .textinput import TextInputProbe @@ -16,7 +16,10 @@ def clear_input(self): self.native.setText("") async def increment(self): - xfail("This backend doesn't support stepped increments") + pytest.xfail("This backend doesn't support stepped increments") async def decrement(self): - xfail("This backend doesn't support stepped increments") + pytest.xfail("This backend doesn't support stepped increments") + + def set_cursor_at_end(self): + pytest.skip("Cursor positioning not supported on this platform") diff --git a/android/tests_backend/widgets/passwordinput.py b/android/tests_backend/widgets/passwordinput.py index 0ec4805f97..baba02b7ce 100644 --- a/android/tests_backend/widgets/passwordinput.py +++ b/android/tests_backend/widgets/passwordinput.py @@ -1,13 +1,5 @@ -from toga.fonts import SYSTEM - from .textinput import TextInputProbe class PasswordInputProbe(TextInputProbe): - # In password mode, the EditText defaults to monospace. - def assert_font_family(self, expected): - actual = self.font.family - if expected == SYSTEM: - assert actual == "monospace" - else: - assert actual == expected + default_font_family = "monospace" diff --git a/android/tests_backend/widgets/properties.py b/android/tests_backend/widgets/properties.py index cfabddf847..c202caecbe 100644 --- a/android/tests_backend/widgets/properties.py +++ b/android/tests_backend/widgets/properties.py @@ -1,18 +1,11 @@ from java import jint -from travertino.fonts import Font -from android.graphics import Color, Typeface +from android.graphics import Color from android.os import Build from android.text import Layout -from android.util import TypedValue from android.view import Gravity from toga.colors import TRANSPARENT, rgba from toga.constants import BOTTOM, CENTER, JUSTIFY, LEFT, RIGHT, TOP -from toga.fonts import ( - BOLD, - ITALIC, - NORMAL, -) def toga_color(color_int): @@ -29,45 +22,6 @@ def toga_color(color_int): ) -DECLARED_FONTS = {} - - -def load_fontmap(): - field = Typeface.getClass().getDeclaredField("sSystemFontMap") - field.setAccessible(True) - fontmap = field.get(None) - - for name in fontmap.keySet().toArray(): - typeface = fontmap.get(name) - DECLARED_FONTS[typeface] = name - for native_style in [ - Typeface.BOLD, - Typeface.ITALIC, - Typeface.BOLD | Typeface.ITALIC, - ]: - DECLARED_FONTS[Typeface.create(typeface, native_style)] = name - - -def toga_font(typeface, size, resources): - # Android provides font details in pixels; that size needs to be converted to SP (see - # notes in toga_android/fonts.py). - pixels_per_sp = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_SP, 1, resources.getDisplayMetrics() - ) - - # Ensure we have a map of typeface to font names - if not DECLARED_FONTS: - load_fontmap() - - return Font( - family=DECLARED_FONTS[typeface], - size=round(size / pixels_per_sp), - style=ITALIC if typeface.isItalic() else NORMAL, - variant=NORMAL, - weight=BOLD if typeface.isBold() else NORMAL, - ) - - def toga_alignment(gravity, justification_mode=None): horizontal_gravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK if (Build.VERSION.SDK_INT < 26) or ( diff --git a/android/tests_backend/widgets/selection.py b/android/tests_backend/widgets/selection.py index f68b21f6d1..8925cf272f 100644 --- a/android/tests_backend/widgets/selection.py +++ b/android/tests_backend/widgets/selection.py @@ -3,7 +3,6 @@ from android.widget import Spinner from .base import SimpleProbe -from .properties import toga_alignment class SelectionProbe(SimpleProbe): @@ -15,14 +14,18 @@ def assert_resizes_on_content_change(self): @property def alignment(self): - return toga_alignment(self.native.getGravity()) + xfail("Can't change the alignment of Selection on this backend") @property def color(self): xfail("Can't change the color of Selection on this backend") @property - def font(self): + def typeface(self): + xfail("Can't change the font of Selection on this backend") + + @property + def text_size(self): xfail("Can't change the font of Selection on this backend") @property diff --git a/android/tests_backend/widgets/table.py b/android/tests_backend/widgets/table.py index a00de1269c..d27084314f 100644 --- a/android/tests_backend/widgets/table.py +++ b/android/tests_backend/widgets/table.py @@ -3,7 +3,6 @@ from android.widget import ScrollView, TableLayout, TextView from .base import SimpleProbe -from .properties import toga_font HEADER = "HEADER" @@ -90,6 +89,9 @@ async def activate_row(self, row): self._row_view(row).performLongClick() @property - def font(self): - tv = self._row_view(0).getChildAt(0) - return toga_font(tv.getTypeface(), tv.getTextSize(), tv.getResources()) + def typeface(self): + return self._row_view(0).getChildAt(0).getTypeface() + + @property + def text_size(self): + return self._row_view(0).getChildAt(0).getTextSize() diff --git a/android/tests_backend/widgets/textinput.py b/android/tests_backend/widgets/textinput.py index 2bf66e7fb7..149765bbea 100644 --- a/android/tests_backend/widgets/textinput.py +++ b/android/tests_backend/widgets/textinput.py @@ -1,3 +1,4 @@ +import pytest from java import jclass from android.os import SystemClock @@ -10,6 +11,7 @@ class TextInputProbe(LabelProbe): native_class = jclass("android.widget.EditText") + default_font_size = 18 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -37,6 +39,22 @@ def readonly(self): focusable_in_touch_mode = self.native.isFocusableInTouchMode() if focusable != focusable_in_touch_mode: raise ValueError(f"invalid state: {focusable=}, {focusable_in_touch_mode=}") + + # Check if TYPE_TEXT_FLAG_NO_SUGGESTIONS is set in the input type + input_type = self.native.getInputType() + if input_type & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS: + # TYPE_TEXT_FLAG_NO_SUGGESTIONS is set + if focusable: + raise ValueError( + "TYPE_TEXT_FLAG_NO_SUGGESTIONS is not set on the input." + ) + else: + # TYPE_TEXT_FLAG_NO_SUGGESTIONS is not set + if not focusable: + raise ValueError( + "TYPE_TEXT_FLAG_NO_SUGGESTIONS has been set when the input is readonly." + ) + return not focusable async def type_character(self, char): @@ -63,3 +81,6 @@ async def type_character(self, char): 0, # metaState ) ) + + def set_cursor_at_end(self): + pytest.skip("Cursor positioning not supported on this platform") From 3d008f69c4a79a31f08a29b0afae297a9368f4c1 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 14 Oct 2023 00:04:50 -0700 Subject: [PATCH 038/102] Re: Modifying to base on latest main branch. --- android/src/toga_android/app.py | 2 -- android/src/toga_android/window.py | 1 - 2 files changed, 3 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 837c94b313..042be0b9be 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -5,13 +5,11 @@ import toga from android.graphics.drawable import Drawable -from android.hardware import DisplayManager from android.media import RingtoneManager from android.view import Menu, MenuItem from toga.command import Group from .libs import events -from .screen import Screen as ScreenImpl from .window import Window # `MainWindow` is defined here in `app.py`, not `window.py`, to mollify the test suite. diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index ad86742d05..2045056d40 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -6,7 +6,6 @@ from android.view import ViewTreeObserver from .container import Container -from .screen import Screen as ScreenImpl class LayoutListener(dynamic_proxy(ViewTreeObserver.OnGlobalLayoutListener)): From 1cefcc065d24b13ee5609a1f357d3aa343bc4f78 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 14 Oct 2023 00:07:29 -0700 Subject: [PATCH 039/102] Re: Modifying to base on latest main branch. --- android/src/toga_android/app.py | 2 ++ android/src/toga_android/window.py | 1 + 2 files changed, 3 insertions(+) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 042be0b9be..837c94b313 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -5,11 +5,13 @@ import toga from android.graphics.drawable import Drawable +from android.hardware import DisplayManager from android.media import RingtoneManager from android.view import Menu, MenuItem from toga.command import Group from .libs import events +from .screen import Screen as ScreenImpl from .window import Window # `MainWindow` is defined here in `app.py`, not `window.py`, to mollify the test suite. diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 2045056d40..ad86742d05 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -6,6 +6,7 @@ from android.view import ViewTreeObserver from .container import Container +from .screen import Screen as ScreenImpl class LayoutListener(dynamic_proxy(ViewTreeObserver.OnGlobalLayoutListener)): From dea6d778a83046e6d90f190d0ba29ad94f302cfd Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 31 Oct 2023 18:16:20 -0700 Subject: [PATCH 040/102] Merge Fixes --- android/src/toga_android/app.py | 6 +----- android/src/toga_android/screen.py | 1 + dummy/src/toga_dummy/app.py | 7 ++++--- dummy/src/toga_dummy/window.py | 4 ++-- gtk/src/toga_gtk/app.py | 1 + 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index de7869c6a4..04f7e4bf72 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -2,16 +2,12 @@ import sys from android.graphics.drawable import BitmapDrawable +from android.hardware import DisplayManager from android.media import RingtoneManager from android.view import Menu, MenuItem from java import dynamic_proxy from org.beeware.android import IPythonApp, MainActivity -import toga -from android.graphics.drawable import Drawable -from android.hardware import DisplayManager -from android.media import RingtoneManager -from android.view import Menu, MenuItem from toga.command import GROUP_BREAK, SECTION_BREAK, Command, Group from .libs import events diff --git a/android/src/toga_android/screen.py b/android/src/toga_android/screen.py index 16f785172f..21e80c0681 100644 --- a/android/src/toga_android/screen.py +++ b/android/src/toga_android/screen.py @@ -1,4 +1,5 @@ from android.view import WindowInsets, WindowManager + from toga.screen import Screen as ScreenInterface diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index 65c2b1a8ee..bef0e5ca1a 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -3,7 +3,7 @@ from pathlib import Path from .screen import Screen as ScreenImpl -from .utils import LoggedObject, not_required, not_required_on +from .utils import LoggedObject, not_required_on from .window import Window @@ -64,10 +64,10 @@ def show_cursor(self): def hide_cursor(self): self._action("hide_cursor") - + def simulate_exit(self): self.interface.on_exit() - + @not_required_on("mobile", "web") def get_screens(self): return [ @@ -75,6 +75,7 @@ def get_screens(self): ScreenImpl(native="secondary_screen"), ] + class DocumentApp(App): def create(self): self._action("create DocumentApp") diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index ecbdbb14b8..5e2d4f33b6 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -1,4 +1,4 @@ -from .utils import LoggedObject +from .utils import LoggedObject, not_required_on class Container: @@ -96,7 +96,7 @@ def set_full_screen(self, is_full_screen): def simulate_close(self): self.interface.on_close() - + @not_required_on("mobile", "web") def get_current_screen(self): self._get_value("screen") diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 262288698f..313e86d1be 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -1,4 +1,5 @@ import asyncio +import os import signal import sys from pathlib import Path From 84b1bfd8a031ae6370ceb065a0573db774004462 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 9 Nov 2023 08:50:14 -0800 Subject: [PATCH 041/102] Updating to latest main branch --- android/src/toga_android/window.py | 2 +- cocoa/src/toga_cocoa/window.py | 2 +- gtk/src/toga_gtk/window.py | 2 +- iOS/src/toga_iOS/window.py | 2 +- winforms/src/toga_winforms/window.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index e9cb706161..1c3ab1d8e7 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -119,4 +119,4 @@ def get_image_data(self): stream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream) - return bytes(stream.toByteArray()) \ No newline at end of file + return bytes(stream.toByteArray()) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index f71ccab65a..c1cda1ccc5 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -317,4 +317,4 @@ def get_image_data(self): NSBitmapImageFileType.PNG, properties=None, ) - return nsdata_to_bytes(data) \ No newline at end of file + return nsdata_to_bytes(data) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index ff3d2e61a2..ce1ae60e1f 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -183,4 +183,4 @@ def get_image_data(self): return buffer else: # pragma: nocover # This shouldn't ever happen, and it's difficult to manufacture in test conditions - raise ValueError(f"Unable to generate screenshot of {self}") \ No newline at end of file + raise ValueError(f"Unable to generate screenshot of {self}") diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index b4a6977b5e..d13e8b57b1 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -188,4 +188,4 @@ def render(context): # Convert back into a UIGraphics final_image = UIImage.imageWithCGImage(cropped_image) # Convert into PNG data. - return nsdata_to_bytes(NSData(uikit.UIImagePNGRepresentation(final_image))) \ No newline at end of file + return nsdata_to_bytes(NSData(uikit.UIImagePNGRepresentation(final_image))) diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 6c9ec583c5..bdad3341dc 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -199,4 +199,4 @@ def get_image_data(self): stream = MemoryStream() bitmap.Save(stream, ImageFormat.Png) - return stream.ToArray() \ No newline at end of file + return stream.ToArray() From 333f282ca29fe64997d4d7a7eacc7e7686990562 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 14 Jan 2024 05:38:25 -0800 Subject: [PATCH 042/102] Modified dummy backend --- dummy/src/toga_dummy/app.py | 3 +-- dummy/src/toga_dummy/screen.py | 3 +-- dummy/src/toga_dummy/window.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index bef0e5ca1a..68e53a87b0 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -3,7 +3,7 @@ from pathlib import Path from .screen import Screen as ScreenImpl -from .utils import LoggedObject, not_required_on +from .utils import LoggedObject from .window import Window @@ -68,7 +68,6 @@ def hide_cursor(self): def simulate_exit(self): self.interface.on_exit() - @not_required_on("mobile", "web") def get_screens(self): return [ ScreenImpl(native="primary_screen"), diff --git a/dummy/src/toga_dummy/screen.py b/dummy/src/toga_dummy/screen.py index 2686406d4a..3352e66ec5 100644 --- a/dummy/src/toga_dummy/screen.py +++ b/dummy/src/toga_dummy/screen.py @@ -1,9 +1,8 @@ from toga.screen import Screen as ScreenInterface -from .utils import LoggedObject, not_required, not_required_on # noqa +from .utils import LoggedObject # noqa -@not_required_on("mobile", "web") class Screen(LoggedObject): _instances = {} diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 0966dd1bcf..c96d51b394 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -2,7 +2,7 @@ import toga_dummy -from .utils import LoggedObject, not_required_on +from .utils import LoggedObject class Container: @@ -106,6 +106,5 @@ def set_full_screen(self, is_full_screen): def simulate_close(self): self.interface.on_close() - @not_required_on("mobile", "web") def get_current_screen(self): self._get_value("screen") From ddbe0332f7568b2be7b4225f750ef4de385d9638 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 14 Jan 2024 04:37:37 -0800 Subject: [PATCH 043/102] Added some tests --- android/tests_backend/window.py | 5 ++++ cocoa/tests_backend/window.py | 4 +++ gtk/tests_backend/window.py | 4 +++ iOS/tests_backend/window.py | 4 +++ testbed/tests/test_app.py | 8 +++++ testbed/tests/test_screen.py | 51 ++++++++++++++++++++++++++++++++ testbed/tests/test_window.py | 6 ++++ winforms/tests_backend/screen.py | 28 ++++++++++++++++++ winforms/tests_backend/window.py | 5 ++++ 9 files changed, 115 insertions(+) create mode 100644 testbed/tests/test_screen.py create mode 100644 winforms/tests_backend/screen.py diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index bc283f43a3..e489f935be 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -1,6 +1,8 @@ import pytest from androidx.appcompat import R as appcompat_R +from toga_android.screen import Screen as ScreenImpl + from .probe import BaseProbe @@ -87,3 +89,6 @@ def assert_toolbar_item(self, index, label, tooltip, has_icon, enabled): def press_toolbar_button(self, index): self.native.onOptionsItemSelected(self._toolbar_items()[index]) + + def assert_screen_implementation_type(self, screen): + assert isinstance(screen, ScreenImpl) diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 9eeba9ff29..ea7cc47a86 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -14,6 +14,7 @@ NSWindow, NSWindowStyleMask, ) +from toga_cocoa.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -261,3 +262,6 @@ def press_toolbar_button(self, index): restype=None, argtypes=[objc_id], ) + + def assert_screen_implementation_type(self, screen): + assert isinstance(screen, ScreenImpl) diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index a90b60f8ef..2a90455396 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -3,6 +3,7 @@ from unittest.mock import Mock from toga_gtk.libs import Gdk, Gtk +from toga_gtk.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -251,3 +252,6 @@ def assert_toolbar_item(self, index, label, tooltip, has_icon, enabled): def press_toolbar_button(self, index): item = self.impl.native_toolbar.get_nth_item(index) item.emit("clicked") + + def assert_screen_implementation_type(self, screen): + assert isinstance(screen, ScreenImpl) diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py index 08f9a34295..b9144bdab0 100644 --- a/iOS/tests_backend/window.py +++ b/iOS/tests_backend/window.py @@ -1,6 +1,7 @@ import pytest from toga_iOS.libs import UIApplication, UIWindow +from toga_iOS.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -77,3 +78,6 @@ async def close_select_folder_dialog(self, dialog, result, multiple_select): def has_toolbar(self): pytest.skip("Toolbars not implemented on iOS") + + def assert_screen_implementation_type(self, screen): + assert isinstance(screen, ScreenImpl) diff --git a/testbed/tests/test_app.py b/testbed/tests/test_app.py index 326860a6a8..fca11b11f1 100644 --- a/testbed/tests/test_app.py +++ b/testbed/tests/test_app.py @@ -4,6 +4,7 @@ import toga from toga.colors import CORNFLOWERBLUE, FIREBRICK, REBECCAPURPLE +from toga.screen import Screen as ScreenInterface from toga.style.pack import Pack from .test_window import window_probe @@ -550,3 +551,10 @@ async def test_beep(app): # can be invoked without raising an error, but there's no way to verify that the app # actually made a noise. app.beep() + + +async def test_screens(app, app_probe): + assert type(app.screens) is list + for screen in app.screens: + assert isinstance(screen, ScreenInterface) + # app_probe.assert_scree diff --git a/testbed/tests/test_screen.py b/testbed/tests/test_screen.py new file mode 100644 index 0000000000..1066a8154b --- /dev/null +++ b/testbed/tests/test_screen.py @@ -0,0 +1,51 @@ +from importlib import import_module + +import pytest + +from toga.screen import Screen as ScreenInterface + + +@pytest.fixture +def screen_probe_list(app): + module = import_module("tests_backend.screen") + return [getattr(module, "ScreenProbe")(screen) for screen in app.screens] + + +async def test_type(screen_probe_list): + for screen_probe in screen_probe_list: + assert isinstance(screen_probe.screen, ScreenInterface) + screen_probe.assert_implementation_type() + screen_probe.assert_native_type() + + +async def test_name(screen_probe_list): + for screen_probe in screen_probe_list: + assert isinstance(screen_probe.screen.name, str) + screen_probe.assert_name() + + +async def test_origin(screen_probe_list): + for screen_probe in screen_probe_list: + origin = screen_probe.screen.origin + assert ( + isinstance(origin, tuple) + and len(origin) == 2 + and all(isinstance(val, int) for val in origin) + ) + screen_probe.assert_origin() + + +async def test_size(screen_probe_list): + for screen_probe in screen_probe_list: + size = screen_probe.screen.size + assert ( + isinstance(size, tuple) + and len(size) == 2 + and all(isinstance(val, int) for val in size) + ) + screen_probe.assert_size() + + +# async def test_as_image(screen_probe_list): +# for screen_probe in screen_probe_list: +# screenshot = screen_probe.screen.as_image() diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 70067ce9c5..dac1bd11de 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -12,6 +12,7 @@ import toga from toga.colors import CORNFLOWERBLUE, GOLDENROD, REBECCAPURPLE +from toga.screen import Screen as ScreenInterface from toga.style.pack import COLUMN, Pack @@ -487,6 +488,11 @@ async def test_as_image(main_window, main_window_probe): main_window_probe.assert_image_size(screenshot.size, main_window_probe.content_size) +async def test_screen(main_window, main_window_probe): + assert isinstance(main_window.screen, ScreenInterface) + main_window_probe.assert_screen_implementation_type(main_window.screen._impl) + + ######################################################################################## # Dialog tests ######################################################################################## diff --git a/winforms/tests_backend/screen.py b/winforms/tests_backend/screen.py new file mode 100644 index 0000000000..91d6b82f30 --- /dev/null +++ b/winforms/tests_backend/screen.py @@ -0,0 +1,28 @@ +from System.Windows.Forms import Screen as WinFormsScreen + +from toga_winforms.screen import Screen as ScreenImpl + +from .probe import BaseProbe + + +class ScreenProbe(BaseProbe): + def __init__(self, screen): + super().__init__() + self.screen = screen + self._impl = screen._impl + self.native = screen._impl.native + + def assert_implementation_type(self): + assert isinstance(self._impl, ScreenImpl) + + def assert_native_type(self): + assert isinstance(self.native, WinFormsScreen) + + def assert_name(self): + assert self.screen.name == self.native.DeviceName + + def assert_origin(self): + assert self.screen.origin == (self.native.Bounds.X, self.native.Bounds.Y) + + def assert_size(self): + assert self.screen.size == (self.native.Bounds.Width, self.native.Bounds.Height) diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 5fc6ae3c94..4eede4c15d 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -11,6 +11,8 @@ ToolStripSeparator, ) +from toga_winforms.screen import Screen as ScreenImpl + from .probe import BaseProbe @@ -148,3 +150,6 @@ def assert_toolbar_item(self, index, label, tooltip, has_icon, enabled): def press_toolbar_button(self, index): self._native_toolbar_item(index).OnClick(EventArgs.Empty) + + def assert_screen_implementation_type(self, screen): + assert isinstance(screen, ScreenImpl) From ae66c37f1aebfef3986af364ce9e016e7ffce3bf Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 14 Jan 2024 18:45:23 -0800 Subject: [PATCH 044/102] Added some tests --- core/src/toga/screen.py | 10 +++++++--- winforms/src/toga_winforms/screen.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/src/toga/screen.py b/core/src/toga/screen.py index 0dde3b9717..d837d67cd8 100644 --- a/core/src/toga/screen.py +++ b/core/src/toga/screen.py @@ -1,6 +1,10 @@ +from typing import TYPE_CHECKING + +from toga.images import Image from toga.platform import get_platform_factory -from .images import Image +if TYPE_CHECKING: + from toga.images import ImageT class Screen: @@ -23,5 +27,5 @@ def size(self): """The size of the screen, as a ``(width, height)`` tuple.""" return self._impl.get_size() - def as_image(self): - return Image(data=self._impl.get_image_data()) + def as_image(self, format: type[ImageT] = Image) -> ImageT: + return Image(self._impl.get_image_data()).as_format(format) diff --git a/winforms/src/toga_winforms/screen.py b/winforms/src/toga_winforms/screen.py index eaebf36763..168bd3758c 100644 --- a/winforms/src/toga_winforms/screen.py +++ b/winforms/src/toga_winforms/screen.py @@ -41,4 +41,4 @@ def get_image_data(self): graphics.CopyFromScreen(source_point, destination_point, copy_size) stream = MemoryStream() bitmap.Save(stream, Imaging.ImageFormat.Png) - return stream.ToArray() + return bytes(stream.ToArray()) From dc67e97fcb86274b501c08edf7106be69b8d5be3 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 14 Jan 2024 18:52:12 -0800 Subject: [PATCH 045/102] Modified some tests --- core/src/toga/screen.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/src/toga/screen.py b/core/src/toga/screen.py index d837d67cd8..e0ce3addc8 100644 --- a/core/src/toga/screen.py +++ b/core/src/toga/screen.py @@ -1,11 +1,6 @@ -from typing import TYPE_CHECKING - from toga.images import Image from toga.platform import get_platform_factory -if TYPE_CHECKING: - from toga.images import ImageT - class Screen: def __init__(self, _impl): @@ -27,5 +22,5 @@ def size(self): """The size of the screen, as a ``(width, height)`` tuple.""" return self._impl.get_size() - def as_image(self, format: type[ImageT] = Image) -> ImageT: + def as_image(self, format: Image): return Image(self._impl.get_image_data()).as_format(format) From 325957320da5b6e9c046ec1880e9a3c87c11bac4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 14 Jan 2024 19:00:15 -0800 Subject: [PATCH 046/102] Modified some tests --- testbed/tests/test_screen.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/testbed/tests/test_screen.py b/testbed/tests/test_screen.py index 1066a8154b..0bc5cb4907 100644 --- a/testbed/tests/test_screen.py +++ b/testbed/tests/test_screen.py @@ -46,6 +46,8 @@ async def test_size(screen_probe_list): screen_probe.assert_size() -# async def test_as_image(screen_probe_list): -# for screen_probe in screen_probe_list: -# screenshot = screen_probe.screen.as_image() +async def test_as_image(screen_probe_list): + for screen_probe in screen_probe_list: + screenshot = screen_probe.screen.as_image() + # TODO: Check screenshot size with actual screen size + print(screenshot) From b9b448e734db6bfebb9441399b58d3594a2197d4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 15 Jan 2024 08:56:06 -0800 Subject: [PATCH 047/102] Added tests for other platforms. --- android/tests_backend/app.py | 4 ++++ android/tests_backend/screen.py | 31 +++++++++++++++++++++++++++ android/tests_backend/window.py | 2 +- cocoa/tests_backend/app.py | 4 ++++ cocoa/tests_backend/screen.py | 30 ++++++++++++++++++++++++++ cocoa/tests_backend/window.py | 2 +- core/src/toga/screen.py | 2 +- dummy/src/toga_dummy/window.py | 3 ++- gtk/tests_backend/app.py | 4 ++++ gtk/tests_backend/screen.py | 36 ++++++++++++++++++++++++++++++++ gtk/tests_backend/window.py | 2 +- iOS/tests_backend/app.py | 4 ++++ iOS/tests_backend/screen.py | 31 +++++++++++++++++++++++++++ iOS/tests_backend/window.py | 2 +- testbed/tests/test_app.py | 4 ++-- testbed/tests/test_window.py | 2 +- winforms/tests_backend/app.py | 4 ++++ winforms/tests_backend/window.py | 2 +- 18 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 android/tests_backend/screen.py create mode 100644 cocoa/tests_backend/screen.py create mode 100644 gtk/tests_backend/screen.py create mode 100644 iOS/tests_backend/screen.py diff --git a/android/tests_backend/app.py b/android/tests_backend/app.py index 76365fcc4d..9be462d7e3 100644 --- a/android/tests_backend/app.py +++ b/android/tests_backend/app.py @@ -5,6 +5,7 @@ from pytest import xfail from toga import Group +from toga_android.screen import Screen as ScreenImpl from .probe import BaseProbe from .window import WindowProbe @@ -106,3 +107,6 @@ def rotate(self): self.native.findViewById( R.id.content ).getViewTreeObserver().dispatchOnGlobalLayout() + + def assert_screen_implementation_type(self, screen): + assert isinstance(screen._impl, ScreenImpl) diff --git a/android/tests_backend/screen.py b/android/tests_backend/screen.py new file mode 100644 index 0000000000..8f75c56073 --- /dev/null +++ b/android/tests_backend/screen.py @@ -0,0 +1,31 @@ +import pytest +from android.view import Display + +from toga_cocoa.screen import Screen as ScreenImpl + +from .probe import BaseProbe + + +class ScreenProbe(BaseProbe): + def __init__(self, screen): + super().__init__() + self.screen = screen + self._impl = screen._impl + self.native = screen._impl.native + + def assert_implementation_type(self): + assert isinstance(self._impl, ScreenImpl) + + def assert_native_type(self): + print(type(self.native)) + assert isinstance(self.native, Display) + + def assert_name(self): + assert self.screen.name == self.native.getName() + + def assert_origin(self): + assert self.screen.origin == (0, 0) + + def assert_size(self): + # assert self.screen.size == (frame_native.size.width, frame_native.size.height) + pytest.skip("TODO: Check screen size") diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index e489f935be..59043ef2c7 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -91,4 +91,4 @@ def press_toolbar_button(self, index): self.native.onOptionsItemSelected(self._toolbar_items()[index]) def assert_screen_implementation_type(self, screen): - assert isinstance(screen, ScreenImpl) + assert isinstance(screen._impl, ScreenImpl) diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index 8c82b68a4f..667bc3e81b 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -10,6 +10,7 @@ NSEventType, NSWindow, ) +from toga_cocoa.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -175,3 +176,6 @@ def keystroke(self, combination): keyCode=key_code, ) return toga_key(event) + + def assert_screen_implementation_type(self, screen): + assert isinstance(screen._impl, ScreenImpl) diff --git a/cocoa/tests_backend/screen.py b/cocoa/tests_backend/screen.py new file mode 100644 index 0000000000..2d3fdb56af --- /dev/null +++ b/cocoa/tests_backend/screen.py @@ -0,0 +1,30 @@ +from toga_cocoa.libs import NSScreen +from toga_cocoa.screen import Screen as ScreenImpl + +from .probe import BaseProbe + + +class ScreenProbe(BaseProbe): + def __init__(self, screen): + super().__init__() + self.screen = screen + self._impl = screen._impl + self.native = screen._impl.native + + def assert_implementation_type(self): + assert isinstance(self._impl, ScreenImpl) + + def assert_native_type(self): + print(type(self.native)) + assert isinstance(self.native, NSScreen) + + def assert_name(self): + assert self.screen.name == self.native.localizedName + + def assert_origin(self): + frame_native = self.native.frame + assert self.screen.origin == (frame_native.origin.x, frame_native.origin.y) + + def assert_size(self): + frame_native = self.native.frame + assert self.screen.size == (frame_native.size.width, frame_native.size.height) diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index ea7cc47a86..5b24b5141b 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -264,4 +264,4 @@ def press_toolbar_button(self, index): ) def assert_screen_implementation_type(self, screen): - assert isinstance(screen, ScreenImpl) + assert isinstance(screen._impl, ScreenImpl) diff --git a/core/src/toga/screen.py b/core/src/toga/screen.py index e0ce3addc8..c2a3a7091e 100644 --- a/core/src/toga/screen.py +++ b/core/src/toga/screen.py @@ -22,5 +22,5 @@ def size(self): """The size of the screen, as a ``(width, height)`` tuple.""" return self._impl.get_size() - def as_image(self, format: Image): + def as_image(self, format=Image): return Image(self._impl.get_image_data()).as_format(format) diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index c96d51b394..54726827a1 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -2,6 +2,7 @@ import toga_dummy +from .screen import Screen as ScreenImpl from .utils import LoggedObject @@ -107,4 +108,4 @@ def simulate_close(self): self.interface.on_close() def get_current_screen(self): - self._get_value("screen") + return ScreenImpl(native="primary_screen") diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index 10dcc058b6..0575940ab4 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -4,6 +4,7 @@ from toga_gtk.keys import gtk_accel, toga_key from toga_gtk.libs import Gdk, Gtk +from toga_gtk.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -154,3 +155,6 @@ def keystroke(self, combination): event.state = state return toga_key(event) + + def assert_screen_implementation_type(self, screen): + assert isinstance(screen._impl, ScreenImpl) diff --git a/gtk/tests_backend/screen.py b/gtk/tests_backend/screen.py new file mode 100644 index 0000000000..99c3703727 --- /dev/null +++ b/gtk/tests_backend/screen.py @@ -0,0 +1,36 @@ +import os + +from gi.repository import GdkX11 + +from toga_gtk.screen import Screen as ScreenImpl + +from .probe import BaseProbe + + +class ScreenProbe(BaseProbe): + def __init__(self, screen): + super().__init__() + self.screen = screen + self._impl = screen._impl + self.native = screen._impl.native + + def assert_implementation_type(self): + assert isinstance(self._impl, ScreenImpl) + + def assert_native_type(self): + if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": + assert isinstance(self.native, GdkX11.X11Monitor) + else: + # TODO: Check for the wayland monitor native type + pass + + def assert_name(self): + assert self.screen.name == self.native.get_model() + + def assert_origin(self): + geometry = self.native.get_geometry() + assert self.screen.origin == (geometry.x, geometry.y) + + def assert_size(self): + geometry = self.native.get_geometry() + assert self.screen.size == (geometry.width, geometry.height) diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index 2a90455396..58fa74beee 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -254,4 +254,4 @@ def press_toolbar_button(self, index): item.emit("clicked") def assert_screen_implementation_type(self, screen): - assert isinstance(screen, ScreenImpl) + assert isinstance(screen._impl, ScreenImpl) diff --git a/iOS/tests_backend/app.py b/iOS/tests_backend/app.py index 98a4ba0369..b2fe05c616 100644 --- a/iOS/tests_backend/app.py +++ b/iOS/tests_backend/app.py @@ -8,6 +8,7 @@ NSSearchPathDomainMask, UIApplication, ) +from toga_iOS.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -69,3 +70,6 @@ def terminate(self): def rotate(self): self.native = self.app._impl.native self.native.delegate.application(self.native, didChangeStatusBarOrientation=0) + + def assert_screen_implementation_type(self, screen): + assert isinstance(screen._impl, ScreenImpl) diff --git a/iOS/tests_backend/screen.py b/iOS/tests_backend/screen.py new file mode 100644 index 0000000000..61e2bd4cd0 --- /dev/null +++ b/iOS/tests_backend/screen.py @@ -0,0 +1,31 @@ +from toga_cocoa.screen import Screen as ScreenImpl +from toga_iOS.libs import UIScreen + +from .probe import BaseProbe + + +class ScreenProbe(BaseProbe): + def __init__(self, screen): + super().__init__() + self.screen = screen + self._impl = screen._impl + self.native = screen._impl.native + + def assert_implementation_type(self): + assert isinstance(self._impl, ScreenImpl) + + def assert_native_type(self): + print(type(self.native)) + assert isinstance(self.native, UIScreen) + + def assert_name(self): + assert self.screen.name == "iOS Screen" + + def assert_origin(self): + assert self.screen.origin == (0, 0) + + def assert_size(self): + assert self.screen.size == ( + self.native.bounds.size.width, + self.native.bounds.size.height, + ) diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py index b9144bdab0..0e13222ae4 100644 --- a/iOS/tests_backend/window.py +++ b/iOS/tests_backend/window.py @@ -80,4 +80,4 @@ def has_toolbar(self): pytest.skip("Toolbars not implemented on iOS") def assert_screen_implementation_type(self, screen): - assert isinstance(screen, ScreenImpl) + assert isinstance(screen._impl, ScreenImpl) diff --git a/testbed/tests/test_app.py b/testbed/tests/test_app.py index fca11b11f1..aa19c27c0f 100644 --- a/testbed/tests/test_app.py +++ b/testbed/tests/test_app.py @@ -554,7 +554,7 @@ async def test_beep(app): async def test_screens(app, app_probe): - assert type(app.screens) is list + assert isinstance(app.screens, list) for screen in app.screens: assert isinstance(screen, ScreenInterface) - # app_probe.assert_scree + app_probe.assert_screen_implementation_type(screen) diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index dac1bd11de..2951c53c38 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -490,7 +490,7 @@ async def test_as_image(main_window, main_window_probe): async def test_screen(main_window, main_window_probe): assert isinstance(main_window.screen, ScreenInterface) - main_window_probe.assert_screen_implementation_type(main_window.screen._impl) + main_window_probe.assert_screen_implementation_type(main_window.screen) ######################################################################################## diff --git a/winforms/tests_backend/app.py b/winforms/tests_backend/app.py index 4a2640e538..14eeee4cd5 100644 --- a/winforms/tests_backend/app.py +++ b/winforms/tests_backend/app.py @@ -8,6 +8,7 @@ from System.Windows.Forms import Application, Cursor from toga_winforms.keys import toga_to_winforms_key, winforms_to_toga_key +from toga_winforms.screen import Screen as ScreenImpl from .probe import BaseProbe from .window import WindowProbe @@ -154,3 +155,6 @@ def activate_menu_minimize(self): def keystroke(self, combination): return winforms_to_toga_key(toga_to_winforms_key(combination)) + + def assert_screen_implementation_type(self, screen): + assert isinstance(screen._impl, ScreenImpl) diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 4eede4c15d..2b6d65a45a 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -152,4 +152,4 @@ def press_toolbar_button(self, index): self._native_toolbar_item(index).OnClick(EventArgs.Empty) def assert_screen_implementation_type(self, screen): - assert isinstance(screen, ScreenImpl) + assert isinstance(screen._impl, ScreenImpl) From 045404a2e5b4dac2a6e71dcd3694a475b45a03bc Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 16 Jan 2024 21:33:50 -0500 Subject: [PATCH 048/102] Modified some tests --- android/tests_backend/screen.py | 7 +++++-- cocoa/src/toga_cocoa/screen.py | 6 +++--- cocoa/tests_backend/screen.py | 5 +++++ gtk/tests_backend/screen.py | 9 +++++++++ iOS/tests_backend/screen.py | 7 ++++++- testbed/tests/test_screen.py | 4 +--- winforms/tests_backend/screen.py | 5 +++++ 7 files changed, 34 insertions(+), 9 deletions(-) diff --git a/android/tests_backend/screen.py b/android/tests_backend/screen.py index 8f75c56073..5a4fe59e8d 100644 --- a/android/tests_backend/screen.py +++ b/android/tests_backend/screen.py @@ -1,7 +1,7 @@ import pytest from android.view import Display -from toga_cocoa.screen import Screen as ScreenImpl +from toga_android.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -28,4 +28,7 @@ def assert_origin(self): def assert_size(self): # assert self.screen.size == (frame_native.size.width, frame_native.size.height) - pytest.skip("TODO: Check screen size") + pytest.xfail("TODO: Check screen size") + + def assert_screen_as_image_size(self): + pytest.xfail("Screen.as_image() is not supported on wayland.") diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index 1debca7a65..939a33ea01 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -22,15 +22,15 @@ def __new__(cls, native): return instance def get_name(self): - return self.native.localizedName + return str(self.native.localizedName) def get_origin(self): frame_native = self.native.frame - return (frame_native.origin.x, frame_native.origin.y) + return (int(frame_native.origin.x), int(frame_native.origin.y)) def get_size(self): frame_native = self.native.frame - return (frame_native.size.width, frame_native.size.height) + return (int(frame_native.size.width), int(frame_native.size.height)) def get_image_data(self): image = core_graphics.CGDisplayCreateImage( diff --git a/cocoa/tests_backend/screen.py b/cocoa/tests_backend/screen.py index 2d3fdb56af..6d383b34b9 100644 --- a/cocoa/tests_backend/screen.py +++ b/cocoa/tests_backend/screen.py @@ -1,3 +1,5 @@ +import pytest + from toga_cocoa.libs import NSScreen from toga_cocoa.screen import Screen as ScreenImpl @@ -28,3 +30,6 @@ def assert_origin(self): def assert_size(self): frame_native = self.native.frame assert self.screen.size == (frame_native.size.width, frame_native.size.height) + + def assert_screen_as_image_size(self): + pytest.xfail("Screen.as_image() is not supported on wayland.") diff --git a/gtk/tests_backend/screen.py b/gtk/tests_backend/screen.py index 99c3703727..1d49a3cd14 100644 --- a/gtk/tests_backend/screen.py +++ b/gtk/tests_backend/screen.py @@ -1,5 +1,6 @@ import os +import pytest from gi.repository import GdkX11 from toga_gtk.screen import Screen as ScreenImpl @@ -34,3 +35,11 @@ def assert_origin(self): def assert_size(self): geometry = self.native.get_geometry() assert self.screen.size == (geometry.width, geometry.height) + + def assert_screen_as_image_size(self): + if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": + screenshot = self.screen.as_image() + print(screenshot) + # TODO: Check screenshot size with actual screen size + else: + pytest.xfail("Screen.as_image() is not supported on wayland.") diff --git a/iOS/tests_backend/screen.py b/iOS/tests_backend/screen.py index 61e2bd4cd0..2e8b4b6112 100644 --- a/iOS/tests_backend/screen.py +++ b/iOS/tests_backend/screen.py @@ -1,5 +1,7 @@ -from toga_cocoa.screen import Screen as ScreenImpl +import pytest + from toga_iOS.libs import UIScreen +from toga_iOS.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -29,3 +31,6 @@ def assert_size(self): self.native.bounds.size.width, self.native.bounds.size.height, ) + + def assert_screen_as_image_size(self): + pytest.xfail("Screen.as_image() is not supported on wayland.") diff --git a/testbed/tests/test_screen.py b/testbed/tests/test_screen.py index 0bc5cb4907..23afee722e 100644 --- a/testbed/tests/test_screen.py +++ b/testbed/tests/test_screen.py @@ -48,6 +48,4 @@ async def test_size(screen_probe_list): async def test_as_image(screen_probe_list): for screen_probe in screen_probe_list: - screenshot = screen_probe.screen.as_image() - # TODO: Check screenshot size with actual screen size - print(screenshot) + screen_probe.assert_screen_as_image_size() diff --git a/winforms/tests_backend/screen.py b/winforms/tests_backend/screen.py index 91d6b82f30..7e1cf37fce 100644 --- a/winforms/tests_backend/screen.py +++ b/winforms/tests_backend/screen.py @@ -26,3 +26,8 @@ def assert_origin(self): def assert_size(self): assert self.screen.size == (self.native.Bounds.Width, self.native.Bounds.Height) + + def assert_screen_as_image_size(self): + screenshot = self.screen.as_image() + print(screenshot) + # TODO: Check screenshot size with actual screen size From 8634eea6748ad6cedd26841f5c7fe8ca7c3b4f1f Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 17 Jan 2024 05:28:48 -0800 Subject: [PATCH 049/102] Fixed tests --- android/tests_backend/screen.py | 2 +- cocoa/tests_backend/screen.py | 6 +++--- gtk/src/toga_gtk/app.py | 3 ++- gtk/src/toga_gtk/screen.py | 4 ++-- iOS/src/toga_iOS/screen.py | 2 +- iOS/tests_backend/screen.py | 6 +++--- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/android/tests_backend/screen.py b/android/tests_backend/screen.py index 5a4fe59e8d..7f971d1e86 100644 --- a/android/tests_backend/screen.py +++ b/android/tests_backend/screen.py @@ -31,4 +31,4 @@ def assert_size(self): pytest.xfail("TODO: Check screen size") def assert_screen_as_image_size(self): - pytest.xfail("Screen.as_image() is not supported on wayland.") + pytest.xfail("Screen.as_image() is not implemented on wayland.") diff --git a/cocoa/tests_backend/screen.py b/cocoa/tests_backend/screen.py index 6d383b34b9..eeffde318c 100644 --- a/cocoa/tests_backend/screen.py +++ b/cocoa/tests_backend/screen.py @@ -1,5 +1,3 @@ -import pytest - from toga_cocoa.libs import NSScreen from toga_cocoa.screen import Screen as ScreenImpl @@ -32,4 +30,6 @@ def assert_size(self): assert self.screen.size == (frame_native.size.width, frame_native.size.height) def assert_screen_as_image_size(self): - pytest.xfail("Screen.as_image() is not supported on wayland.") + screenshot = self.screen.as_image() + print(screenshot) + # TODO: Check screenshot size with actual screen size diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 3aaac1cfcb..2b747234bc 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -191,7 +191,8 @@ def main_loop(self): def get_screens(self): display = Gdk.Display.get_default() - if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": + # CI runs on wayland + if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": # pragma: no cover primary_screen = ScreenImpl(display.get_primary_monitor()) screen_list = [primary_screen] + [ ScreenImpl(native=display.get_monitor(i)) diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index c5248c2600..4ea0054a2c 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -29,8 +29,8 @@ def get_size(self): geometry = self.native.get_geometry() return geometry.width, geometry.height - def get_image_data(self): - if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": + def get_image_data(self): # CI runs on wayland + if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": # pragma: no cover # Only works for x11 display = self.native.get_display() screen = display.get_default_screen() diff --git a/iOS/src/toga_iOS/screen.py b/iOS/src/toga_iOS/screen.py index b0dee923f0..524b46cf09 100644 --- a/iOS/src/toga_iOS/screen.py +++ b/iOS/src/toga_iOS/screen.py @@ -21,7 +21,7 @@ def get_origin(self): return (0, 0) def get_size(self): - return self.native.bounds.size.width, self.native.bounds.size.height + return int(self.native.bounds.size.width), int(self.native.bounds.size.height) def get_image_data(self): self.interface.factory.not_implemented("Screen.get_image_data()") diff --git a/iOS/tests_backend/screen.py b/iOS/tests_backend/screen.py index 2e8b4b6112..6815742a17 100644 --- a/iOS/tests_backend/screen.py +++ b/iOS/tests_backend/screen.py @@ -28,9 +28,9 @@ def assert_origin(self): def assert_size(self): assert self.screen.size == ( - self.native.bounds.size.width, - self.native.bounds.size.height, + int(self.native.bounds.size.width), + int(self.native.bounds.size.height), ) def assert_screen_as_image_size(self): - pytest.xfail("Screen.as_image() is not supported on wayland.") + pytest.xfail("Screen.as_image() is not implemented on iOS.") From 175a5c0b809373359f41197e08ba30ef93e5d2a2 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 17 Jan 2024 05:40:27 -0800 Subject: [PATCH 050/102] Corrected tests --- gtk/src/toga_gtk/screen.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index 4ea0054a2c..2c6fa64b61 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -48,4 +48,3 @@ def get_image_data(self): # CI runs on wayland else: # Not implemented for wayland self.interface.factory.not_implemented("Screen.get_image_data()") - return None From 0d011539fd5768030fc01b23060fbd6ac55457dc Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 17 Jan 2024 09:26:24 -0800 Subject: [PATCH 051/102] Fixed tests --- android/src/toga_android/app.py | 6 ++++-- android/src/toga_android/screen.py | 12 +----------- android/src/toga_android/window.py | 5 ++++- android/tests_backend/screen.py | 9 ++++----- testbed/tests/test_screen.py | 6 +++++- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index e9299f522d..4fb06236fa 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -1,8 +1,8 @@ import asyncio import sys +from android.content import Context from android.graphics.drawable import BitmapDrawable -from android.hardware import DisplayManager from android.media import RingtoneManager from android.view import Menu, MenuItem from java import dynamic_proxy @@ -286,5 +286,7 @@ def show_cursor(self): pass def get_screens(self): - screen_list = DisplayManager.getDisplays() + context = self.native.getApplicationContext() + display_manager = context.getSystemService(Context.DISPLAY_SERVICE) + screen_list = display_manager.getDisplays() return [ScreenImpl(screen) for screen in screen_list] diff --git a/android/src/toga_android/screen.py b/android/src/toga_android/screen.py index 21e80c0681..f868bd7fab 100644 --- a/android/src/toga_android/screen.py +++ b/android/src/toga_android/screen.py @@ -1,5 +1,3 @@ -from android.view import WindowInsets, WindowManager - from toga.screen import Screen as ScreenInterface @@ -23,15 +21,7 @@ def get_origin(self): return (0, 0) def get_size(self): - metrics = WindowManager.getCurrentWindowMetrics() - window_insets = metrics.getWindowInsets() - insets = window_insets.getInsetsIgnoringVisibility( - WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout() - ) - insets_width = insets.right + insets.left - insets_height = insets.top + insets.bottom - bounds = metrics.getBounds() - return (bounds.width() - insets_width, bounds.height() - insets_height) + return self.native.getWidth(), self.native.getHeight() def get_image_data(self): self.interface.factory.not_implemented("Screen.get_image_data()") diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 1c3ab1d8e7..5fc9716b29 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -1,6 +1,7 @@ from decimal import ROUND_UP from android import R +from android.content import Context from android.graphics import ( Bitmap, Canvas as A_Canvas, @@ -105,7 +106,9 @@ def set_full_screen(self, is_full_screen): self.interface.factory.not_implemented("Window.set_full_screen()") def get_current_screen(self): - return ScreenImpl(self.app.native.getContext().getResources().getDisplay()) + context = self.app.native.getApplicationContext() + window_manager = context.getSystemService(Context.WINDOW_SERVICE) + return ScreenImpl(window_manager.getDefaultDisplay()) def get_image_data(self): bitmap = Bitmap.createBitmap( diff --git a/android/tests_backend/screen.py b/android/tests_backend/screen.py index 7f971d1e86..c8c7bf70b0 100644 --- a/android/tests_backend/screen.py +++ b/android/tests_backend/screen.py @@ -7,8 +7,8 @@ class ScreenProbe(BaseProbe): - def __init__(self, screen): - super().__init__() + def __init__(self, app, screen): + super().__init__(app) self.screen = screen self._impl = screen._impl self.native = screen._impl.native @@ -27,8 +27,7 @@ def assert_origin(self): assert self.screen.origin == (0, 0) def assert_size(self): - # assert self.screen.size == (frame_native.size.width, frame_native.size.height) - pytest.xfail("TODO: Check screen size") + assert self.screen.size == (self.native.getWidth(), self.native.getHeight()) def assert_screen_as_image_size(self): - pytest.xfail("Screen.as_image() is not implemented on wayland.") + pytest.xfail("Screen.as_image() is not implemented on Android.") diff --git a/testbed/tests/test_screen.py b/testbed/tests/test_screen.py index 23afee722e..90b75b4fe3 100644 --- a/testbed/tests/test_screen.py +++ b/testbed/tests/test_screen.py @@ -2,13 +2,17 @@ import pytest +from toga.platform import current_platform from toga.screen import Screen as ScreenInterface @pytest.fixture def screen_probe_list(app): module = import_module("tests_backend.screen") - return [getattr(module, "ScreenProbe")(screen) for screen in app.screens] + if current_platform == "android": + return [getattr(module, "ScreenProbe")(app, screen) for screen in app.screens] + else: + return [getattr(module, "ScreenProbe")(screen) for screen in app.screens] async def test_type(screen_probe_list): From efe2b699f20f57c565b8d71de2537c8edd285f23 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 17 Jan 2024 11:10:04 -0800 Subject: [PATCH 052/102] Added tests for core --- core/src/toga/platform.py | 2 +- core/tests/app/test_app.py | 9 +++++++++ core/tests/test_screen.py | 25 +++++++++++++++++++++++++ core/tests/test_window.py | 18 ++++++++++++++++++ dummy/src/toga_dummy/screen.py | 8 +++++++- 5 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 core/tests/test_screen.py diff --git a/core/src/toga/platform.py b/core/src/toga/platform.py index 7fb438ad62..159829ca06 100644 --- a/core/src/toga/platform.py +++ b/core/src/toga/platform.py @@ -8,7 +8,7 @@ else: # Before Python 3.10, entry_points did not support the group argument; # so, the backport package must be used on older versions. - from importlib_metadata import entry_points + from importlib_metadata import entry_points # pragma: no cover # Map python sys.platform with toga platforms names diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index deac2063f5..1ac1ee4562 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -8,6 +8,8 @@ import pytest import toga +from toga.screen import Screen as ScreenInterface +from toga_dummy.screen import Screen as ScreenImpl from toga_dummy.utils import ( assert_action_not_performed, assert_action_performed, @@ -621,3 +623,10 @@ def test_deprecated_name(): assert app.formal_name == "Test App" with pytest.warns(DeprecationWarning, match=name_warning): assert app.name == "Test App" + + +def test_screens(app): + assert isinstance(app.screens, list) + for screen in app.screens: + assert isinstance(screen, ScreenInterface) + assert isinstance(screen._impl, ScreenImpl) diff --git a/core/tests/test_screen.py b/core/tests/test_screen.py new file mode 100644 index 0000000000..a1b83e13f0 --- /dev/null +++ b/core/tests/test_screen.py @@ -0,0 +1,25 @@ +from toga_dummy.utils import assert_action_performed + + +def test_name(app): + assert app.screens[0].name == "primary_screen" + assert app.screens[1].name == "secondary_screen" + + +def test_origin(app): + assert app.screens[0].origin == (0, 0) + assert app.screens[1].origin == (-1920, 0) + + +def test_size(app): + assert app.screens[0].size == (1920, 1080) + assert app.screens[1].size == (1920, 1080) + + +# Same as for the window as_image() test. +def test_as_image(app): + """A screen can be captured as an image""" + image = app.screens[0].as_image() + assert_action_performed(app.screens[0], "get image data") + # Don't need to check the raw data; just check it's the right size. + assert image.size == (318, 346) diff --git a/core/tests/test_window.py b/core/tests/test_window.py index f925dafa16..e90d674972 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -4,6 +4,8 @@ import pytest import toga +from toga.screen import Screen as ScreenInterface +from toga_dummy.screen import Screen as ScreenImpl from toga_dummy.utils import ( assert_action_not_performed, assert_action_performed, @@ -354,6 +356,22 @@ def test_as_image(window): assert image.size == (318, 346) +def test_screen(window, app): + assert isinstance(window.screen, ScreenInterface) + assert isinstance(window.screen._impl, ScreenImpl) + # Cannot actually change window.screen, so just check + # the window positions as a substitute for moving the + # window between the screens. + assert window.position == (100, 100) + window.screen = app.screens[1] + assert window.position == (-1820, 100) + + +def test_screen_position(window, app): + window.screen_position = (0, 0) + assert window.screen_position == (0, 0) + + def test_info_dialog(window, app): """An info dialog can be shown""" on_result_handler = Mock() diff --git a/dummy/src/toga_dummy/screen.py b/dummy/src/toga_dummy/screen.py index 3352e66ec5..4330922be9 100644 --- a/dummy/src/toga_dummy/screen.py +++ b/dummy/src/toga_dummy/screen.py @@ -1,3 +1,6 @@ +from pathlib import Path + +import toga_dummy from toga.screen import Screen as ScreenInterface from .utils import LoggedObject # noqa @@ -28,5 +31,8 @@ def get_origin(self): def get_size(self): return (1920, 1080) + # Same as for the window as_image(). def get_image_data(self): - self._action("get_image_data") + self._action("get image data") + path = Path(toga_dummy.__file__).parent / "resources/screenshot.png" + return path.read_bytes() From 3fb01b2641ba8674b5f37e85b4589513a750d535 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 19 Jan 2024 10:23:18 -0800 Subject: [PATCH 053/102] Fixed core tests --- core/src/toga/platform.py | 2 +- core/tests/app/test_app.py | 19 ++++++++++++++++ core/tests/test_screen.py | 25 --------------------- core/tests/test_window.py | 40 +++++++++++++++++++++++++++++---- dummy/src/toga_dummy/app.py | 20 +++++++++++++++-- dummy/src/toga_dummy/screen.py | 25 ++++++++++++--------- dummy/src/toga_dummy/window.py | 3 ++- output_image.png | Bin 0 -> 10888 bytes 8 files changed, 90 insertions(+), 44 deletions(-) delete mode 100644 core/tests/test_screen.py create mode 100644 output_image.png diff --git a/core/src/toga/platform.py b/core/src/toga/platform.py index 159829ca06..7fb438ad62 100644 --- a/core/src/toga/platform.py +++ b/core/src/toga/platform.py @@ -8,7 +8,7 @@ else: # Before Python 3.10, entry_points did not support the group argument; # so, the backport package must be used on older versions. - from importlib_metadata import entry_points # pragma: no cover + from importlib_metadata import entry_points # Map python sys.platform with toga platforms names diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 1ac1ee4562..673beba8bb 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -630,3 +630,22 @@ def test_screens(app): for screen in app.screens: assert isinstance(screen, ScreenInterface) assert isinstance(screen._impl, ScreenImpl) + + # Test screen names: + assert app.screens[0].name == "Primary Screen" + assert app.screens[1].name == "Secondary Screen" + + # Test screen origin: + assert app.screens[0].origin == (0, 0) + assert app.screens[1].origin == (-1366, -768) + + # Test screen sizes: + assert app.screens[0].size == (1920, 1080) + assert app.screens[1].size == (1366, 768) + + # Test screen as_image(): + """A screen can be captured as an image""" + screenshot = app.screens[0].as_image() + assert_action_performed(app.screens[0], "get image data") + # Don't need to check the raw data; just check it's the right screen size. + assert screenshot.size == app.screens[0].size diff --git a/core/tests/test_screen.py b/core/tests/test_screen.py deleted file mode 100644 index a1b83e13f0..0000000000 --- a/core/tests/test_screen.py +++ /dev/null @@ -1,25 +0,0 @@ -from toga_dummy.utils import assert_action_performed - - -def test_name(app): - assert app.screens[0].name == "primary_screen" - assert app.screens[1].name == "secondary_screen" - - -def test_origin(app): - assert app.screens[0].origin == (0, 0) - assert app.screens[1].origin == (-1920, 0) - - -def test_size(app): - assert app.screens[0].size == (1920, 1080) - assert app.screens[1].size == (1920, 1080) - - -# Same as for the window as_image() test. -def test_as_image(app): - """A screen can be captured as an image""" - image = app.screens[0].as_image() - assert_action_performed(app.screens[0], "get image data") - # Don't need to check the raw data; just check it's the right size. - assert image.size == (318, 346) diff --git a/core/tests/test_window.py b/core/tests/test_window.py index e90d674972..8c4ad7df5b 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -362,14 +362,46 @@ def test_screen(window, app): # Cannot actually change window.screen, so just check # the window positions as a substitute for moving the # window between the screens. + # `window.screen` will return `Secondary Screen` + assert window.screen == app.screens[1] assert window.position == (100, 100) - window.screen = app.screens[1] - assert window.position == (-1820, 100) + window.screen = app.screens[0] + assert window.position == (1466, 868) def test_screen_position(window, app): - window.screen_position = (0, 0) - assert window.screen_position == (0, 0) + # _________________________________________________ + # Display Setup: | + # ________________________________________________| + # |--1366--| | + # (-1366,-768) _________ | + # | | | | + # 768 |Secondary| | + # | | Screen | | + # | |_________|(0,0) | + # _________ | + # | | | | + # 1080 | Primary | | + # | | Screen | | + # | |_________|(1920,1080) | + # |---1920--| | + # ________________________________________________| + # Assumptions: | + # ________________________________________________| + # * `window.screen` will return `Secondary Screen`| + # as window is on secondary screen to better | + # test out the differences between | + # `window.position` & `window.screen_position`. | + # ________________________________________________| + initial_position = window.position + window.position = (-100, -100) + assert window.position != initial_position + assert window.position == (-100, -100) + + assert window.screen_position == (1266, 668) + window.screen_position = (100, 100) + assert window.position == (-1266, -668) + assert window.screen_position == (100, 100) def test_info_dialog(window, app): diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index 68e53a87b0..42872224a5 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -69,9 +69,25 @@ def simulate_exit(self): self.interface.on_exit() def get_screens(self): + # _________________________________________________ + # Display Setup: | + # ________________________________________________| + # |--1366--| | + # (-1366,-768) _________ | + # | | | | + # 768 |Secondary| | + # | | Screen | | + # | |_________|(0,0) | + # _________ | + # | | | | + # 1080 | Primary | | + # | | Screen | | + # | |_________|(1920,1080) | + # |---1920--| | + # ________________________________________________| return [ - ScreenImpl(native="primary_screen"), - ScreenImpl(native="secondary_screen"), + ScreenImpl(native=("Primary Screen", (0, 0), (1920, 1080))), + ScreenImpl(native=("Secondary Screen", (-1366, -768), (1366, 768))), ] diff --git a/dummy/src/toga_dummy/screen.py b/dummy/src/toga_dummy/screen.py index 4330922be9..6d7bf57cd2 100644 --- a/dummy/src/toga_dummy/screen.py +++ b/dummy/src/toga_dummy/screen.py @@ -1,6 +1,5 @@ -from pathlib import Path +from PIL import Image, ImageDraw -import toga_dummy from toga.screen import Screen as ScreenInterface from .utils import LoggedObject # noqa @@ -9,6 +8,11 @@ class Screen(LoggedObject): _instances = {} + # native: tuple = ( + # name: str, + # origin: tuple(x:int, y:int), + # size: tuple(width:int, height:int) + # ) def __new__(cls, native): if native in cls._instances: return cls._instances[native] @@ -20,19 +24,18 @@ def __new__(cls, native): return instance def get_name(self): - return self.native + return self.native[0] def get_origin(self): - if self.native == "primary_screen": - return (0, 0) - else: - return (-1920, 0) + return self.native[1] def get_size(self): - return (1920, 1080) + return self.native[2] - # Same as for the window as_image(). def get_image_data(self): self._action("get image data") - path = Path(toga_dummy.__file__).parent / "resources/screenshot.png" - return path.read_bytes() + + img = Image.new("RGB", self.native[2], "white") + draw = ImageDraw.Draw(img) + draw.text((0, 0), self.native[0], fill="black") # text = self.native[0] + return img diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 54726827a1..10f5c0e153 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -108,4 +108,5 @@ def simulate_close(self): self.interface.on_close() def get_current_screen(self): - return ScreenImpl(native="primary_screen") + # `window.screen` will return `Secondary Screen` + return ScreenImpl(native=("Secondary Screen", (-1366, -768), (1366, 768))) diff --git a/output_image.png b/output_image.png new file mode 100644 index 0000000000000000000000000000000000000000..c34aeb407c7a975decc2ec6473b2d2b0c1166043 GIT binary patch literal 10888 zcmeHN_g9nYw*D;0;HaFVB4PnOiU>$isZxYflG+J9PHBu+ou{DbPT zAF8YVio0)j^v&HrWY0Dk?0Ktw&_3_-6|;_=(ec-IZuEIDcuxD)(O1u|KO5ib{@1~l z^dF9Ym5_*XEOb#{m*f(R$A8BDOr*OF@VA>3QLu~rv?XEWX%Ag`Jv~jSK0p81Ryao0 z=c~P(O>pB*&4G33v3}SNzma=uf2`GD4F_wpu*L^#oVcb7YjU_|3I4Aw+R6yufNU)H zAQKW*9Yj0|N&V>2uWzq3|Gq);sl0I*YqB-dGF8{Bf3~Duo$)2ZqSAMyq_k9W+rFnSgg3eY`F-cEU0vyhA@jr4Uj6wwtC#A-*lq3YwT!}AgOGXW@bD;u zg?c8AIh{o`5t2$vN-BIu97+6j2X;y!cMJkn7rx!-$u#EAmOhkLC>-3fb+2w6oe(aM z7cAx#*5sb8+!XZY<;#~L%uyw4f{`!=BrT|;7D2LqO@ zKJAp1rL^3;pm)S7t;%<#hBqpK{AG?Hy@dm%9x)1-26*;sSvXpvgKzCz1-wp5`YzAb zvKu{-f(Zo*M;-a23q6wQUS03qnwL-g^2;x6*0mu_lYq&V1{{B7ex%l=Hn=lY&u_Gj z3op8Dzd;#ZEJE)Xu3Ahc39g>3ls=!LePgifMtKD;TTaZ<5T9^sx;H8Ap}D{@j>Fjp zZ5$m3EB&Ym4*F4>wua5*WI;<*PqadN6vAiULfzgNjm=e3-EUXijK6CTC_2^Pu0*44$>eNGR2ESz2xN~swlYe zu7urJ`QF?pN$ID%kN@^5T8>@+Z)6nX{yty!rOfJK3(sxQPL(CX(tszCu4tk zeO57sOfar(3;eIo#R~6%a*8Xz-^s6!a~2sBKZCN$K&M-yVvJWV>SWX>y)Fxw(B4a;Rci{Gu!ow{i)`lt8wl?R&&#P?Vy}Jo{9Xl(JHKg6D@r zKm74;si>=0ujbhoOUt7(C6N`>K2jS}#CKWu?gf<1^CQ7HeEV6z zaTv=PI_2HZbI0$EcBsJnxPo^ckD?OEkaY3>z{vI*s z=P*twnl{eP&WE%(Oe!X{*g(%kJf%mvRN{`#7|cQqAmPlzZ92ZgRWNhOu3>|BWW1y%^a;)H6iyGI8j6C z>3w+)Vme+#2hhdH8#~(D_t%9ksTNkfLLa|r%Whroiy23Z$Q0v(H*XGOFyduW{@uHG zo0^(-C|PO41jAX3!&f?5Tjfp$xLXQSa}#1rc;CAWnMOld@Q*YMUIZO7yxNvvFuW6a zW3ve)Smq>aIYc3~qggQu_MN113dte)#aNw3CVHlhXLr_tA1% zVawldb`cY&r>D<6*b+MNL{H^}4~w#+ZuyHv7KS(eSfh|6;*}P}rKZ+~v1eFHm{8W_ z>_91hdD=EsVNu9qrvl>u8L9B~Y&96_*ajmvf}GYh!D7#=SFfgfvaq4FM7p!Fo?gg6 z$<;pbA%`|*B&RkZ^~UFyjc?53pDJJ?dBynEhxl{)2UJz_o>p!?<&T1N+=dl`@~x##J-moZr;#HnHWvF)8v;!>M~(zh61A4Xne`6$AGY$@ zj7qon4`u`|eVsM5!*`&cE`gmoiH^9A=g05gzu(8Tb@;h$qrZ?%@f$3g?s$(*W_rjv zGDaM6)jGd9h+KBP`_;M)o4kiA3T!+& zi5UFYyWiol`BO7?Jq1pc+$DOEV|gx?6v-}Ghv@G`neuqV>cV8BwF%c9CI(=Z%@?Ls zz>~s37N6*iX0x`yox6>0`(LG+=m$=9z!Ve-uBgLk`b93KwoYVOrPqK25F%KuCBJIC zDEec3d|YbBk;CH6H4*Ic`4^)A)$Z433WmY?o9U4YjgnVBMdz@-*!7fqQHX*`rJ)KR zReB$ST!^XL_OCmR-5D0i!^6V@f#BYvU7kIAb|4T`iZc>|1y*4MSeTlc0&qvlZrHY2 z7uB)afK_0~gT8$UoOQpT#c*O3Z1AN4Pzv!IXPDPy2J)y#DXQfpc!vO zchjRrEGZ^C#~GQxI3{BwPtug<2Fv%FMBEMz4(6}S?bY)Y40sS_cJ4gpT(uByZDMY+ z3Y>X;po9jfQYoQ_Idfuy=LXpyc37twg|a#tmzS4KaJ-0V3#=#L=Y+mu5BB=5&WDh~83mQP*FV3kNZxQKH;*p)Z=}k3B3V%ompNT|M)oGBh>^K)@BUiBDyc@sz0R4Nm6%7Vd*^F zx<~tB&8-`zqQaTTk;#yC?(PC&xQtWT^~JAOVsMKeWn1ZmuJr~=N=mc`psbf19UWsa z0s9x!QCx3CPN~%}8~8S^&(m|RwkK6mBl#Hd62=cuxDs6Q+s8)y9P#c568j+uj=O!y-y*M3O_M~?T|82S1LF6-%j!=%9TfDtXSQ9O$vo~h%l zL)0~=uiD|7rIBAhfJiqiKJlv)i};DgZ($5TfA^elZz*#Aq zKUOXAJ$<@ShvFRnub2iAyGY!O7nn(exaMV^e{ljFgq9gl7V7)3;QJ1sgjefW?pY2H z0803EAWmw%ZDFxds9_a zB^aWRmp`A8wiEC$YHD*Zm6q}mK+md?`w2-&{ZNxhzU(#y6C-e@OFko|FARb_C5ps7 zU%u3YFj3xC+%o_5?dy`q>)+k~8H`OoDDdt6{7dc08sxUN+`%nFm3~BBA4fn*k!w8x zm>(GI5z*<_UzeANVk0+y;6;%C!e04+z`&vg-psv5;3)ga;j>TnJORr%?zJD-adG^k zRFsg<5r5Ka_+`=Ds&*bG52Wp0BVI_s8yEE(r`i`dX4GZ8+SmGco=PIwXd&+;&ck`0 zPEJjsya-S2$-+&^aB8VS-==#~purLBJ_ps?ZG0vP5BB+9WB7b^*5*8*uFjE~ARwz% zaA$Z{JTc$7nk9)@J#A_=OC{mklhjljKBxLMybx%M0L&$71w1%))V@YoCvFsdj& zTo>f{Bt3P{cK2)7jzohkChFgOWfPD5Q=@rzclT&{V*{`a6EPT3cT8kLdU`roqx+#S zt2gQCA>em-V~tUCBYkeI6xd;X@2|xw`Ft1?Z?H8`4RD^Iy%APcT^Y%)jXrBX#LFfb=+YXq5IVTw1p6ShL>)L?G9tKV1-GVa! zWsd~XyM(i)WVm)kutbiL)5-*sFJn~y^5qxiyLKH1_*n1GnRP;PHMolFY5HJb z%oK15nqL9eGu{Wd%P>=EN{pn}0*&?Om1$8MfSl6$iyQP1M0`jK{aHdCHJJW~3XxUb z10^9Knf07e!{FJACq3q1NS+$bUwr$UDNJ`9Mom?92p!PNz(HQV6d9YG@O{C|Q5Tdo zpA!hE*QagO_8HW6JItN`ljm5uPbQ*k{JgS)+M-Mg;-9h+dB@Fm0HBAl!^VD>1f6&b z)dgdqak2OS*D@%pz_p$SRwGsWh7HRMyu&dSm7jN?=T_Y=@C49(l(q`Bea|QYWHX8= zy^%JBHduITCCv33kv95i1nKg!OwpD*{@Vwjkr2>F7~Bkj9Uk5ti0BK!Ae!G^o5dWF zt^l2W5E7~m56@X`lnHHiWA8s1aQIuu1iRe#B*@r>ywr7fu7h(DX_xs?BOzh{4_Sup z#X|@XaF^nnZB2gqTy=9Ig^I0K2~u|Qpk7WczJM$E5$TnKCSnx9JUUeeTm`Ehj|qcF z7bh97;A?HjByK4x4WY;Y$L&X1^@zE0O4mN~ zP`yZN{M=d{ z;?Pqy;KDl#zW@Ly8z-7$l%lmgd$U0Uq@susEb~s^%xl2a%Ax*H8rd2205@e3L?<7D zLu-^@19xhJw9$pAJZ)`f`~FBjxx*EvXO#b8YjOE-Y^*GWZ8gv&jX zjgXKJORvlgg?xM@);=pEgElpl zX|(!juitr*;tFJr+~WDW5HY1)vxt#3o~59X*Z(PB<(v?F*je!EEH94NJvT=#fyay5u_$K!-3Xq6L3N; z;N^R0ykv7oVq8Z5$lEJsEarQ^dUpTMmNuu+`T|oojXC+%+uc`M0wa`l{zaS-;Y3d_ z1&}d<=JHT1x6J|6F40Kz7%TX#H~W`|+xF+OI>312`IY!o!Ol~VK3|@W>I$|Gh&!sH z0s+w`BZ&aM7?NApz8n4d1K{9Pb<96nKG!@Ls;QyDT=@1rF~tL9*rTC(tjsFiAblsJM)|RPJSU6b37LqsN)q!)oar8q?pLOE&jTR=SB?Y^#oz@K3GIo2XGATrhXC;D znpKw@?`9Fga_?G@1gv&Q20u5A!E1tr{iRV(#S5Ow!8P=1(->fFiN$*K=#h$kYfTVC z9u^ami(sScPDgE`_rW{f|Mw;W<{I?`Z84EzK9w5Kt~M|*04+C+6x=qwHdN|CPEfXm zRRR`9c+%Prc15YaH92WL@PjT~j6~mG#{ft$M)VnoQJ;XNwr>Ip^@4SllM>KvDr>yd zq18I^SXM(GtV5FnCj(X&xsWIov6Uf={<#MJydf3kn>*mGeL{h!8@Qc*gt;D=0UC`~ z4s-}j(7M)f8-fvrI~fniOiV}s6<2_z5F{##q+?)%W&p{bVGRXM*tKid5poas7SPYd zv{X3=?I1C!Mkb(E5VwFSGL5`oW??r_M@w=7S%_We2F4DBF;5E|nW>t1YG`9}y%>%O z8u~)ZgD^*7p(%8^+bl3J5PBpw{Pz06*1hO-)9|g>zj?Uge|Lwn`QWMGBBiO=#fB@K|;ae4xAz!-#*O7RA zMM=jl#SI^`3Wvp@?dU5klQBi7E8!Kt0n@10&Xxquubf982G?%iL;}YPcyaI~3~Is} z1j06xy*XCSz`zENf>g@<{;;31J zMWo+Ai6XZLQa!}YxBpq%vPb)SqJyh>s1Q4mqU*zhBqF`vK>KR7KBT&)`#-9>UPO`k z$XQI808AFJ4!}(00Yo9r3`3IhTA>u<*$(*fNFnbLR!r;%)|FAUKa4lK{Bb0VQSf+| zn&+1{=7}EJ`4;Nc?pzivWca38(2>@bmgl-YgFV(5oTvzdQXYh|MiZC^Jd^ml8Kwrh zmv^!cB!`lJX{qT)4d;l(k9PX?u;4%;1?dqim`c!Z@kcBPu%W{W#%00XI)CR~ILs zWVH|!W@l$-W)B}abY=X*pJ0JjM>!1+v1aHB)#S#PH(*9rAwA@G)%4v0?8EZ78O8vm zX9;UL?dzXO!XZ)tNN*liI71wU(mh!+IEBoZnVYwQYNNgfWDs3q50*2)zJltbHVZ@r z4(15k6Tm3oJ3l`khN38vomak?$Wl~P1WOGlqynhm0#vSG{qW z6M9l09t1hCQTp$1U95e*VeKm(0Ee}YwbyE}_ObTb$J%QjYop@0hJ*hfHCPZM-f0hv UZx#gRp=~ucwfd>>znAa+4>)k%%m4rY literal 0 HcmV?d00001 From 133048e3730f27f2133b494161e89a6d3aff2305 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 19 Jan 2024 20:04:20 -0800 Subject: [PATCH 054/102] Fixed Tests --- android/src/toga_android/app.py | 2 +- android/src/toga_android/screen.py | 13 ++++++-- android/src/toga_android/window.py | 2 +- android/tests_backend/screen.py | 13 ++++---- cocoa/src/toga_cocoa/libs/core_graphics.py | 5 ++- cocoa/src/toga_cocoa/libs/foundation.py | 4 +++ cocoa/src/toga_cocoa/screen.py | 36 ++++++++++++---------- cocoa/tests_backend/screen.py | 5 --- core/tests/app/test_app.py | 20 +----------- core/tests/app/test_screen.py | 24 +++++++++++++++ core/tests/test_window.py | 4 +-- gtk/src/toga_gtk/screen.py | 4 +-- gtk/tests_backend/screen.py | 9 ------ iOS/tests_backend/screen.py | 5 --- testbed/tests/app/__init__.py | 0 testbed/tests/{ => app}/test_screen.py | 11 ++++++- testbed/tests/test_app.py | 16 ++++++++++ testbed/tests/test_window.py | 10 ++++++ winforms/tests_backend/screen.py | 5 --- 19 files changed, 108 insertions(+), 80 deletions(-) create mode 100644 core/tests/app/test_screen.py create mode 100644 testbed/tests/app/__init__.py rename testbed/tests/{ => app}/test_screen.py (77%) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 4fb06236fa..d7fc267929 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -289,4 +289,4 @@ def get_screens(self): context = self.native.getApplicationContext() display_manager = context.getSystemService(Context.DISPLAY_SERVICE) screen_list = display_manager.getDisplays() - return [ScreenImpl(screen) for screen in screen_list] + return [ScreenImpl(self, screen) for screen in screen_list] diff --git a/android/src/toga_android/screen.py b/android/src/toga_android/screen.py index f868bd7fab..1e1d88c83a 100644 --- a/android/src/toga_android/screen.py +++ b/android/src/toga_android/screen.py @@ -1,10 +1,12 @@ from toga.screen import Screen as ScreenInterface +from .widgets.base import Scalable -class Screen: + +class Screen(Scalable): _instances = {} - def __new__(cls, native): + def __new__(cls, app, native): if native in cls._instances: return cls._instances[native] else: @@ -12,6 +14,8 @@ def __new__(cls, native): instance.interface = ScreenInterface(_impl=instance) instance.native = native cls._instances[native] = instance + cls.app = app + instance.init_scale(instance.app.native) return instance def get_name(self): @@ -21,7 +25,10 @@ def get_origin(self): return (0, 0) def get_size(self): - return self.native.getWidth(), self.native.getHeight() + return ( + self.scale_out(self.native.getWidth()), + self.scale_out(self.native.getHeight()), + ) def get_image_data(self): self.interface.factory.not_implemented("Screen.get_image_data()") diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 5fc9716b29..3fa3c39eae 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -108,7 +108,7 @@ def set_full_screen(self, is_full_screen): def get_current_screen(self): context = self.app.native.getApplicationContext() window_manager = context.getSystemService(Context.WINDOW_SERVICE) - return ScreenImpl(window_manager.getDefaultDisplay()) + return ScreenImpl(self.app, window_manager.getDefaultDisplay()) def get_image_data(self): bitmap = Bitmap.createBitmap( diff --git a/android/tests_backend/screen.py b/android/tests_backend/screen.py index c8c7bf70b0..7569fd1625 100644 --- a/android/tests_backend/screen.py +++ b/android/tests_backend/screen.py @@ -1,17 +1,18 @@ -import pytest from android.view import Display from toga_android.screen import Screen as ScreenImpl +from toga_android.widgets.base import Scalable from .probe import BaseProbe -class ScreenProbe(BaseProbe): +class ScreenProbe(BaseProbe, Scalable): def __init__(self, app, screen): super().__init__(app) self.screen = screen self._impl = screen._impl self.native = screen._impl.native + self.init_scale(app._impl.native) def assert_implementation_type(self): assert isinstance(self._impl, ScreenImpl) @@ -27,7 +28,7 @@ def assert_origin(self): assert self.screen.origin == (0, 0) def assert_size(self): - assert self.screen.size == (self.native.getWidth(), self.native.getHeight()) - - def assert_screen_as_image_size(self): - pytest.xfail("Screen.as_image() is not implemented on Android.") + assert self.screen.size == ( + self.scale_out(self.native.getWidth()), + self.scale_out(self.native.getHeight()), + ) diff --git a/cocoa/src/toga_cocoa/libs/core_graphics.py b/cocoa/src/toga_cocoa/libs/core_graphics.py index 091af2509d..5fffb57be7 100644 --- a/cocoa/src/toga_cocoa/libs/core_graphics.py +++ b/cocoa/src/toga_cocoa/libs/core_graphics.py @@ -222,7 +222,7 @@ class CGEventRef(c_void_p): kCGBitmapByteOrder32Big = 4 << 12 ###################################################################### -# Quartz functions +# CoreGraphics.h CGDirectDisplayID = c_uint32 @@ -231,8 +231,7 @@ class CGEventRef(c_void_p): core_graphics.CGMainDisplayID.argtypes = None -class CGImageRef(c_void_p): - pass +CGImageRef = c_void_p register_preferred_encoding(b"^{CGImage=}", CGImageRef) diff --git a/cocoa/src/toga_cocoa/libs/foundation.py b/cocoa/src/toga_cocoa/libs/foundation.py index ffbf612f60..c2bac3a3d9 100644 --- a/cocoa/src/toga_cocoa/libs/foundation.py +++ b/cocoa/src/toga_cocoa/libs/foundation.py @@ -51,3 +51,7 @@ ###################################################################### # NSValue.h NSNumber = ObjCClass("NSNumber") + +###################################################################### +# NSAutoreleasePool.h +NSAutoreleasePool = ObjCClass("NSAutoreleasePool") diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index 939a33ea01..ff5a593bef 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -1,9 +1,7 @@ -from ctypes import POINTER, c_char, cast - from toga.screen import Screen as ScreenInterface from toga_cocoa.libs import ( - NSBitmapImageFileType, - NSBitmapImageRep, + NSAutoreleasePool, + NSImage, core_graphics, ) @@ -33,18 +31,22 @@ def get_size(self): return (int(frame_native.size.width), int(frame_native.size.height)) def get_image_data(self): - image = core_graphics.CGDisplayCreateImage( - core_graphics.CGMainDisplayID(), + # Retrieve the device description dictionary for the NSScreen + device_description = self.native.deviceDescription() + # Extract the CGDirectDisplayID from the device description + cg_direct_display_id = device_description.objectForKey_("NSScreenNumber") + + cg_image = core_graphics.CGDisplayCreateImage( + cg_direct_display_id, self.native.frame, ) - bitmap_rep = NSBitmapImageRep.alloc().initWithCGImage(image) - data = bitmap_rep.representationUsingType( - NSBitmapImageFileType.PNG, - properties=None, - ) - - # data is an NSData object that has .bytes as a c_void_p, and a .length. Cast to - # POINTER(c_char) to get an addressable array of bytes, and slice that array to - # the known length. We don't use c_char_p because it has handling of NUL - # termination, and POINTER(c_char) allows array subscripting. - return cast(data.bytes, POINTER(c_char))[: data.length] + # Create an autorelease pool to manage memory + pool = NSAutoreleasePool.alloc().init() + # Get the size of the CGImage + size = cg_image.size() + # Create an NSImage from the CGImage + ns_image = NSImage.alloc().initWithCGImage_size_(cg_image, size) + # Drain the autorelease pool to release memory + pool.release() + + return ns_image diff --git a/cocoa/tests_backend/screen.py b/cocoa/tests_backend/screen.py index eeffde318c..2d3fdb56af 100644 --- a/cocoa/tests_backend/screen.py +++ b/cocoa/tests_backend/screen.py @@ -28,8 +28,3 @@ def assert_origin(self): def assert_size(self): frame_native = self.native.frame assert self.screen.size == (frame_native.size.width, frame_native.size.height) - - def assert_screen_as_image_size(self): - screenshot = self.screen.as_image() - print(screenshot) - # TODO: Check screenshot size with actual screen size diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 673beba8bb..50194feca3 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -630,22 +630,4 @@ def test_screens(app): for screen in app.screens: assert isinstance(screen, ScreenInterface) assert isinstance(screen._impl, ScreenImpl) - - # Test screen names: - assert app.screens[0].name == "Primary Screen" - assert app.screens[1].name == "Secondary Screen" - - # Test screen origin: - assert app.screens[0].origin == (0, 0) - assert app.screens[1].origin == (-1366, -768) - - # Test screen sizes: - assert app.screens[0].size == (1920, 1080) - assert app.screens[1].size == (1366, 768) - - # Test screen as_image(): - """A screen can be captured as an image""" - screenshot = app.screens[0].as_image() - assert_action_performed(app.screens[0], "get image data") - # Don't need to check the raw data; just check it's the right screen size. - assert screenshot.size == app.screens[0].size + # Other screen tests are in `app/test_screen.py` diff --git a/core/tests/app/test_screen.py b/core/tests/app/test_screen.py new file mode 100644 index 0000000000..7a52bdb2ac --- /dev/null +++ b/core/tests/app/test_screen.py @@ -0,0 +1,24 @@ +from toga_dummy.utils import assert_action_performed + + +def test_name(app): + assert app.screens[0].name == "Primary Screen" + assert app.screens[1].name == "Secondary Screen" + + +def test_origin(app): + assert app.screens[0].origin == (0, 0) + assert app.screens[1].origin == (-1366, -768) + + +def test_size(app): + assert app.screens[0].size == (1920, 1080) + assert app.screens[1].size == (1366, 768) + + +def test_as_image(app): + """A screen can be captured as an image""" + screenshot = app.screens[0].as_image() + assert_action_performed(app.screens[0], "get image data") + # Don't need to check the raw data; just check it's the right screen size. + assert screenshot.size == app.screens[0].size diff --git a/core/tests/test_window.py b/core/tests/test_window.py index 8c4ad7df5b..fa95ca254e 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -386,9 +386,7 @@ def test_screen_position(window, app): # | |_________|(1920,1080) | # |---1920--| | # ________________________________________________| - # Assumptions: | - # ________________________________________________| - # * `window.screen` will return `Secondary Screen`| + # `window.screen` will return `Secondary Screen` | # as window is on secondary screen to better | # test out the differences between | # `window.position` & `window.screen_position`. | diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index 2c6fa64b61..f97dbe5fc7 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -46,5 +46,5 @@ def get_image_data(self): # CI runs on wayland print("Failed to save screenshot to buffer.") return None else: - # Not implemented for wayland - self.interface.factory.not_implemented("Screen.get_image_data()") + # Not implemented on wayland due to wayland security policies. + self.interface.factory.not_implemented("Screen.get_image_data() on Wayland") diff --git a/gtk/tests_backend/screen.py b/gtk/tests_backend/screen.py index 1d49a3cd14..99c3703727 100644 --- a/gtk/tests_backend/screen.py +++ b/gtk/tests_backend/screen.py @@ -1,6 +1,5 @@ import os -import pytest from gi.repository import GdkX11 from toga_gtk.screen import Screen as ScreenImpl @@ -35,11 +34,3 @@ def assert_origin(self): def assert_size(self): geometry = self.native.get_geometry() assert self.screen.size == (geometry.width, geometry.height) - - def assert_screen_as_image_size(self): - if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": - screenshot = self.screen.as_image() - print(screenshot) - # TODO: Check screenshot size with actual screen size - else: - pytest.xfail("Screen.as_image() is not supported on wayland.") diff --git a/iOS/tests_backend/screen.py b/iOS/tests_backend/screen.py index 6815742a17..f6db3bc837 100644 --- a/iOS/tests_backend/screen.py +++ b/iOS/tests_backend/screen.py @@ -1,5 +1,3 @@ -import pytest - from toga_iOS.libs import UIScreen from toga_iOS.screen import Screen as ScreenImpl @@ -31,6 +29,3 @@ def assert_size(self): int(self.native.bounds.size.width), int(self.native.bounds.size.height), ) - - def assert_screen_as_image_size(self): - pytest.xfail("Screen.as_image() is not implemented on iOS.") diff --git a/testbed/tests/app/__init__.py b/testbed/tests/app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/testbed/tests/test_screen.py b/testbed/tests/app/test_screen.py similarity index 77% rename from testbed/tests/test_screen.py rename to testbed/tests/app/test_screen.py index 90b75b4fe3..a7e8618f06 100644 --- a/testbed/tests/test_screen.py +++ b/testbed/tests/app/test_screen.py @@ -1,3 +1,4 @@ +import os from importlib import import_module import pytest @@ -52,4 +53,12 @@ async def test_size(screen_probe_list): async def test_as_image(screen_probe_list): for screen_probe in screen_probe_list: - screen_probe.assert_screen_as_image_size() + if current_platform in {"android", "iOS"}: + pytest.xfail("Screen.as_image is not implemented on current platform.") + elif ( + current_platform == "linux" + and os.environ.get("XDG_SESSION_TYPE", "").lower() != "x11" + ): + pytest.xfail("Screen.as_image() is not supported on wayland.") + screenshot = screen_probe.screen.as_image() + assert screenshot.size == screen_probe.screen.size diff --git a/testbed/tests/test_app.py b/testbed/tests/test_app.py index aa19c27c0f..4b433d3a3b 100644 --- a/testbed/tests/test_app.py +++ b/testbed/tests/test_app.py @@ -553,8 +553,24 @@ async def test_beep(app): app.beep() +# Test primary screen `origin` and `name` & `origin` uniqueness of other screens. async def test_screens(app, app_probe): assert isinstance(app.screens, list) for screen in app.screens: assert isinstance(screen, ScreenInterface) app_probe.assert_screen_implementation_type(screen) + + # Get the origin of screen 0 + assert app.screens[0].origin == (0, 0) + + # Check for unique names + screen_names = set() + unique_names = all( + screen.name not in screen_names and not screen_names.add(screen.name) + for screen in app.screens + ) + assert unique_names is True + + # Check that the origin of every other screen is not "0,0" + origins_not_zero = all(screen.origin != (0, 0) for screen in app.screens[1:]) + assert origins_not_zero is True diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 2951c53c38..de679ae6c1 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -488,9 +488,19 @@ async def test_as_image(main_window, main_window_probe): main_window_probe.assert_image_size(screenshot.size, main_window_probe.content_size) +# Test the `origin`, `position` and `screen_position`. async def test_screen(main_window, main_window_probe): assert isinstance(main_window.screen, ScreenInterface) main_window_probe.assert_screen_implementation_type(main_window.screen) + if main_window.screen.origin == (0, 0): + if toga.platform.current_platform in {"android", "iOS"}: + pytest.xfail("Window.position is non functional on current platform.") + initial_position = main_window.position + main_window.position = (200, 200) + assert main_window.position != initial_position + # position and screen_position should be equal on screen with origin (0, 0) + assert main_window.position == (200, 200) + assert main_window.screen_position == (200, 200) ######################################################################################## diff --git a/winforms/tests_backend/screen.py b/winforms/tests_backend/screen.py index 7e1cf37fce..91d6b82f30 100644 --- a/winforms/tests_backend/screen.py +++ b/winforms/tests_backend/screen.py @@ -26,8 +26,3 @@ def assert_origin(self): def assert_size(self): assert self.screen.size == (self.native.Bounds.Width, self.native.Bounds.Height) - - def assert_screen_as_image_size(self): - screenshot = self.screen.as_image() - print(screenshot) - # TODO: Check screenshot size with actual screen size From 4247276816261a46aed0a72e7a0c582a9c8fac9a Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 20 Jan 2024 17:24:51 -0800 Subject: [PATCH 055/102] Fixed `as_image()` and `toga.Image` on cocoa --- cocoa/src/toga_cocoa/images.py | 10 ++++------ cocoa/src/toga_cocoa/libs/core_graphics.py | 8 ++++++++ cocoa/src/toga_cocoa/screen.py | 10 +++++++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/cocoa/src/toga_cocoa/images.py b/cocoa/src/toga_cocoa/images.py index 034f5ba581..6c5f02df00 100644 --- a/cocoa/src/toga_cocoa/images.py +++ b/cocoa/src/toga_cocoa/images.py @@ -55,13 +55,11 @@ def get_height(self): return self.native.size.height def get_data(self): - return nsdata_to_bytes( - NSBitmapImageRep.representationOfImageRepsInArray( - self.native.representations, - usingType=NSBitmapImageFileType.PNG, - properties=None, - ) + bitmap_rep = NSBitmapImageRep.imageRepWithData_(self.native.TIFFRepresentation) + image_data = bitmap_rep.representationUsingType( + NSBitmapImageFileType.PNG, properties=None ) + return nsdata_to_bytes(image_data) def save(self, path): path = Path(path) diff --git a/cocoa/src/toga_cocoa/libs/core_graphics.py b/cocoa/src/toga_cocoa/libs/core_graphics.py index 5fffb57be7..bcda2b8bc9 100644 --- a/cocoa/src/toga_cocoa/libs/core_graphics.py +++ b/cocoa/src/toga_cocoa/libs/core_graphics.py @@ -239,3 +239,11 @@ class CGEventRef(c_void_p): # CGImageRef CGDisplayCreateImage(CGDirectDisplayID displayID, CGRect rect); core_graphics.CGDisplayCreateImage.restype = CGImageRef core_graphics.CGDisplayCreateImage.argtypes = [CGDirectDisplayID, CGRect] + +CGImageGetWidth = core_graphics.CGImageGetWidth +CGImageGetWidth.argtypes = [c_void_p] +CGImageGetWidth.restype = c_size_t + +CGImageGetHeight = core_graphics.CGImageGetHeight +CGImageGetHeight.argtypes = [c_void_p] +CGImageGetHeight.restype = c_size_t diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index ff5a593bef..dee8f7398a 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -1,5 +1,7 @@ from toga.screen import Screen as ScreenInterface from toga_cocoa.libs import ( + CGImageGetHeight, + CGImageGetWidth, NSAutoreleasePool, NSImage, core_graphics, @@ -32,9 +34,11 @@ def get_size(self): def get_image_data(self): # Retrieve the device description dictionary for the NSScreen - device_description = self.native.deviceDescription() + device_description = self.native.deviceDescription # Extract the CGDirectDisplayID from the device description - cg_direct_display_id = device_description.objectForKey_("NSScreenNumber") + cg_direct_display_id = device_description.objectForKey_( + "NSScreenNumber" + ).unsignedIntValue cg_image = core_graphics.CGDisplayCreateImage( cg_direct_display_id, @@ -43,7 +47,7 @@ def get_image_data(self): # Create an autorelease pool to manage memory pool = NSAutoreleasePool.alloc().init() # Get the size of the CGImage - size = cg_image.size() + size = CGImageGetWidth(cg_image), CGImageGetHeight(cg_image) # Create an NSImage from the CGImage ns_image = NSImage.alloc().initWithCGImage_size_(cg_image, size) # Drain the autorelease pool to release memory From 94f368eee2f44d568e8257ef68475adabe89e696 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 20 Jan 2024 19:48:42 -0800 Subject: [PATCH 056/102] Added implementation and tests for `textual` --- testbed/tests/app/test_screen.py | 2 +- textual/src/toga_textual/app.py | 4 ++++ textual/src/toga_textual/screen.py | 27 +++++++++++++++++++++++++++ textual/src/toga_textual/window.py | 4 ++++ textual/tests_backend/screen.py | 28 ++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 textual/src/toga_textual/screen.py create mode 100644 textual/tests_backend/screen.py diff --git a/testbed/tests/app/test_screen.py b/testbed/tests/app/test_screen.py index a7e8618f06..056f5ea44f 100644 --- a/testbed/tests/app/test_screen.py +++ b/testbed/tests/app/test_screen.py @@ -53,7 +53,7 @@ async def test_size(screen_probe_list): async def test_as_image(screen_probe_list): for screen_probe in screen_probe_list: - if current_platform in {"android", "iOS"}: + if current_platform in {"android", "iOS", "textual"}: pytest.xfail("Screen.as_image is not implemented on current platform.") elif ( current_platform == "linux" diff --git a/textual/src/toga_textual/app.py b/textual/src/toga_textual/app.py index ff71825494..e7790a7af8 100644 --- a/textual/src/toga_textual/app.py +++ b/textual/src/toga_textual/app.py @@ -2,6 +2,7 @@ from textual.app import App as TextualApp +from .screen import Screen as ScreenImpl from .window import Window @@ -69,6 +70,9 @@ def show_cursor(self): def hide_cursor(self): pass + def get_screens(self): + return [ScreenImpl(window._impl.native) for window in self.interface.windows] + class DocumentApp(App): pass diff --git a/textual/src/toga_textual/screen.py b/textual/src/toga_textual/screen.py new file mode 100644 index 0000000000..3fa1d22260 --- /dev/null +++ b/textual/src/toga_textual/screen.py @@ -0,0 +1,27 @@ +from toga.screen import Screen as ScreenInterface + + +class Screen: + _instances = {} + + def __new__(cls, native): + if native in cls._instances: + return cls._instances[native] + else: + instance = super().__new__(cls) + instance.interface = ScreenInterface(_impl=instance) + instance.native = native + cls._instances[native] = instance + return instance + + def get_name(self): + return "Textual Screen" + + def get_origin(self): + return (0, 0) + + def get_size(self): + return (self.native.size.width, self.native.size.height) + + def get_image_data(self): + self.interface.factory.not_implemented("Screen.get_image_data()") diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index 239a8568c1..9400019989 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -7,6 +7,7 @@ from textual.widgets import Button as TextualButton from .container import Container +from .screen import Screen as ScreenImpl class WindowCloseButton(TextualButton): @@ -168,3 +169,6 @@ def close(self): def set_full_screen(self, is_full_screen): pass + + def get_current_screen(self): + return ScreenImpl(self.native) diff --git a/textual/tests_backend/screen.py b/textual/tests_backend/screen.py new file mode 100644 index 0000000000..ec2c2b4a96 --- /dev/null +++ b/textual/tests_backend/screen.py @@ -0,0 +1,28 @@ +from toga_textual.screen import Screen as ScreenImpl +from toga_textual.window import TogaWindow + +from .probe import BaseProbe + + +class ScreenProbe(BaseProbe): + def __init__(self, screen): + super().__init__() + self.screen = screen + self._impl = screen._impl + self.native = screen._impl.native + + def assert_implementation_type(self): + assert isinstance(self._impl, ScreenImpl) + + def assert_native_type(self): + print(type(self.native)) + assert isinstance(self.native, TogaWindow) + + def assert_name(self): + assert self.screen.name == "Textual Screen" + + def assert_origin(self): + assert self.screen.origin == (0, 0) + + def assert_size(self): + assert self.screen.size == (self.native.size.width, self.native.size.height) From 1cf64f37a0182e2b99c970b6292eba3b87c63664 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 20 Jan 2024 20:01:05 -0800 Subject: [PATCH 057/102] Fixed textual tests --- testbed/tests/test_window.py | 2 +- textual/tests_backend/screen.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index de679ae6c1..1ed16b62de 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -493,7 +493,7 @@ async def test_screen(main_window, main_window_probe): assert isinstance(main_window.screen, ScreenInterface) main_window_probe.assert_screen_implementation_type(main_window.screen) if main_window.screen.origin == (0, 0): - if toga.platform.current_platform in {"android", "iOS"}: + if toga.platform.current_platform in {"android", "iOS", "textual"}: pytest.xfail("Window.position is non functional on current platform.") initial_position = main_window.position main_window.position = (200, 200) diff --git a/textual/tests_backend/screen.py b/textual/tests_backend/screen.py index ec2c2b4a96..dd256cccaa 100644 --- a/textual/tests_backend/screen.py +++ b/textual/tests_backend/screen.py @@ -1,10 +1,9 @@ from toga_textual.screen import Screen as ScreenImpl -from toga_textual.window import TogaWindow -from .probe import BaseProbe +from textual.screen import Screen as TextualScreen -class ScreenProbe(BaseProbe): +class ScreenProbe: def __init__(self, screen): super().__init__() self.screen = screen @@ -16,7 +15,7 @@ def assert_implementation_type(self): def assert_native_type(self): print(type(self.native)) - assert isinstance(self.native, TogaWindow) + assert isinstance(self.native, TextualScreen) def assert_name(self): assert self.screen.name == "Textual Screen" From 6fd9fa817819954af9c62900d2c23345288ab9a6 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 21 Jan 2024 06:23:36 -0800 Subject: [PATCH 058/102] Added an explanatory comment --- core/src/toga/window.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 6b055e7737..e37834063c 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -331,6 +331,15 @@ def position(self) -> tuple[int, int]: :ref:`CSS pixels `.""" absolute_origin = self._app.screens[0].origin absolute_window_position = self._impl.get_position() + # Using the name `assumed_absolute_window_position` due to the + # differences of the position of the origin on different platforms. + + # For example, On cocoa the origin is at the bottom left corner + # while on most platforms the origin is at the top left. But, for + # toga, the position of the origin will always be at the top left. + + # So, the actual absolute window position on cocoa will be different from + # the absolute position that toga works with and provides to the user. assumed_absolute_window_position = ( absolute_window_position[0] - absolute_origin[0], absolute_window_position[1] - absolute_origin[1], From 1aa4b137f81f912684c10fe01738aab934d27770 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 21 Jan 2024 06:23:52 -0800 Subject: [PATCH 059/102] Added an explanatory comment --- core/src/toga/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index e37834063c..c149f9e214 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -328,7 +328,7 @@ def size(self, size: tuple[int, int]) -> None: @property def position(self) -> tuple[int, int]: """Absolute position of the window, as a ``(x, y)`` tuple coordinates, in - :ref:`CSS pixels `.""" + :ref:`CSS pixels `. The origin is at the top left corner.""" absolute_origin = self._app.screens[0].origin absolute_window_position = self._impl.get_position() # Using the name `assumed_absolute_window_position` due to the From a1b626e1753e5775e0d24189fa0531a0ae20b3de Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 21 Jan 2024 06:42:04 -0800 Subject: [PATCH 060/102] Added explanatory comments to tests --- core/tests/app/test_app.py | 1 + core/tests/app/test_screen.py | 3 +++ core/tests/test_window.py | 3 +++ testbed/tests/app/test_screen.py | 5 +++++ testbed/tests/test_app.py | 2 ++ testbed/tests/test_window.py | 3 +++ 6 files changed, 17 insertions(+) diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 50194feca3..610d74f9e0 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -626,6 +626,7 @@ def test_deprecated_name(): def test_screens(app): + """All available screens can be accessed using `App.screens`""" assert isinstance(app.screens, list) for screen in app.screens: assert isinstance(screen, ScreenInterface) diff --git a/core/tests/app/test_screen.py b/core/tests/app/test_screen.py index 7a52bdb2ac..ea2e68ded7 100644 --- a/core/tests/app/test_screen.py +++ b/core/tests/app/test_screen.py @@ -2,16 +2,19 @@ def test_name(app): + """The name of the screens can be retrieved""" assert app.screens[0].name == "Primary Screen" assert app.screens[1].name == "Secondary Screen" def test_origin(app): + """The origin of the screens can be retrieved""" assert app.screens[0].origin == (0, 0) assert app.screens[1].origin == (-1366, -768) def test_size(app): + """The size of the screens can be retrieved""" assert app.screens[0].size == (1920, 1080) assert app.screens[1].size == (1366, 768) diff --git a/core/tests/test_window.py b/core/tests/test_window.py index fa95ca254e..90350da526 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -357,6 +357,7 @@ def test_as_image(window): def test_screen(window, app): + """A window can be moved to a different screen""" assert isinstance(window.screen, ScreenInterface) assert isinstance(window.screen._impl, ScreenImpl) # Cannot actually change window.screen, so just check @@ -370,6 +371,8 @@ def test_screen(window, app): def test_screen_position(window, app): + """A window can be moved using both the + absolute position and relative screen position""" # _________________________________________________ # Display Setup: | # ________________________________________________| diff --git a/testbed/tests/app/test_screen.py b/testbed/tests/app/test_screen.py index 056f5ea44f..b9136672ce 100644 --- a/testbed/tests/app/test_screen.py +++ b/testbed/tests/app/test_screen.py @@ -17,6 +17,7 @@ def screen_probe_list(app): async def test_type(screen_probe_list): + """The type of the implementation and native classes should be correct""" for screen_probe in screen_probe_list: assert isinstance(screen_probe.screen, ScreenInterface) screen_probe.assert_implementation_type() @@ -24,12 +25,14 @@ async def test_type(screen_probe_list): async def test_name(screen_probe_list): + """The name of the screens can be retrieved""" for screen_probe in screen_probe_list: assert isinstance(screen_probe.screen.name, str) screen_probe.assert_name() async def test_origin(screen_probe_list): + """The origin of the screens can be retrieved""" for screen_probe in screen_probe_list: origin = screen_probe.screen.origin assert ( @@ -41,6 +44,7 @@ async def test_origin(screen_probe_list): async def test_size(screen_probe_list): + """The size of the screens can be retrieved""" for screen_probe in screen_probe_list: size = screen_probe.screen.size assert ( @@ -52,6 +56,7 @@ async def test_size(screen_probe_list): async def test_as_image(screen_probe_list): + """A screen can be captured as an image""" for screen_probe in screen_probe_list: if current_platform in {"android", "iOS", "textual"}: pytest.xfail("Screen.as_image is not implemented on current platform.") diff --git a/testbed/tests/test_app.py b/testbed/tests/test_app.py index 4b433d3a3b..52054b2093 100644 --- a/testbed/tests/test_app.py +++ b/testbed/tests/test_app.py @@ -555,6 +555,8 @@ async def test_beep(app): # Test primary screen `origin` and `name` & `origin` uniqueness of other screens. async def test_screens(app, app_probe): + """A screen should have unique origin and name and + the primary screen should have the origin (0,0)""" assert isinstance(app.screens, list) for screen in app.screens: assert isinstance(screen, ScreenInterface) diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 1ed16b62de..894be1f68d 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -490,6 +490,9 @@ async def test_as_image(main_window, main_window_probe): # Test the `origin`, `position` and `screen_position`. async def test_screen(main_window, main_window_probe): + """A window can be moved to a different screen + and can be moved using both the absolute position + and relative screen position""" assert isinstance(main_window.screen, ScreenInterface) main_window_probe.assert_screen_implementation_type(main_window.screen) if main_window.screen.origin == (0, 0): From d5d1572871bf51d71de55f1c7601555ea1ae017c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 21 Jan 2024 10:51:12 -0800 Subject: [PATCH 061/102] Miscellaneous fixes --- android/src/toga_android/app.py | 2 +- android/src/toga_android/screen.py | 8 ++++---- android/src/toga_android/window.py | 2 +- cocoa/src/toga_cocoa/app.py | 2 +- cocoa/src/toga_cocoa/screen.py | 8 ++++---- cocoa/src/toga_cocoa/window.py | 2 +- core/src/toga/app.py | 3 ++- core/src/toga/screen.py | 14 ++++++++++---- dummy/src/toga_dummy/app.py | 2 +- dummy/src/toga_dummy/screen.py | 8 ++++---- dummy/src/toga_dummy/window.py | 2 +- gtk/src/toga_gtk/app.py | 2 +- gtk/src/toga_gtk/screen.py | 11 ++++++----- gtk/src/toga_gtk/window.py | 2 +- iOS/src/toga_iOS/app.py | 2 +- iOS/src/toga_iOS/screen.py | 6 +++--- iOS/src/toga_iOS/window.py | 2 +- textual/src/toga_textual/app.py | 2 +- textual/src/toga_textual/screen.py | 6 +++--- textual/src/toga_textual/window.py | 2 +- web/src/toga_web/app.py | 2 +- web/src/toga_web/screen.py | 6 +++--- web/src/toga_web/window.py | 2 +- winforms/src/toga_winforms/app.py | 2 +- winforms/src/toga_winforms/screen.py | 10 +++++----- winforms/src/toga_winforms/window.py | 2 +- 26 files changed, 60 insertions(+), 52 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index d7fc267929..4d7a97945c 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -285,7 +285,7 @@ def hide_cursor(self): def show_cursor(self): pass - def get_screens(self): + def get_screens(self) -> tuple[ScreenImpl, ...]: context = self.native.getApplicationContext() display_manager = context.getSystemService(Context.DISPLAY_SERVICE) screen_list = display_manager.getDisplays() diff --git a/android/src/toga_android/screen.py b/android/src/toga_android/screen.py index 1e1d88c83a..2bae7e810e 100644 --- a/android/src/toga_android/screen.py +++ b/android/src/toga_android/screen.py @@ -18,13 +18,13 @@ def __new__(cls, app, native): instance.init_scale(instance.app.native) return instance - def get_name(self): - return self.native.getName() + def get_name(self) -> str: + return str(self.native.getName()) - def get_origin(self): + def get_origin(self) -> tuple[int, int]: return (0, 0) - def get_size(self): + def get_size(self) -> tuple[int, int]: return ( self.scale_out(self.native.getWidth()), self.scale_out(self.native.getHeight()), diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 3fa3c39eae..d56e556c0a 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -105,7 +105,7 @@ def close(self): def set_full_screen(self, is_full_screen): self.interface.factory.not_implemented("Window.set_full_screen()") - def get_current_screen(self): + def get_current_screen(self) -> ScreenImpl: context = self.app.native.getApplicationContext() window_manager = context.getSystemService(Context.WINDOW_SERVICE) return ScreenImpl(self.app, window_manager.getDefaultDisplay()) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 8be3825772..dee45896c2 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -403,7 +403,7 @@ def _submenu(self, group, menubar): def main_loop(self): self.loop.run_forever(lifecycle=CocoaLifecycle(self.native)) - def get_screens(self): + def get_screens(self) -> tuple[ScreenImpl, ...]: return [ScreenImpl(native=screen) for screen in NSScreen.screens] def set_main_window(self, window): diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index dee8f7398a..964f904157 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -21,18 +21,18 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self): + def get_name(self) -> str: return str(self.native.localizedName) - def get_origin(self): + def get_origin(self) -> tuple[int, int]: frame_native = self.native.frame return (int(frame_native.origin.x), int(frame_native.origin.y)) - def get_size(self): + def get_size(self) -> tuple[int, int]: frame_native = self.native.frame return (int(frame_native.size.width), int(frame_native.size.height)) - def get_image_data(self): + def get_image_data(self) -> NSImage: # Retrieve the device description dictionary for the NSScreen device_description = self.native.deviceDescription # Extract the CGDirectDisplayID from the device description diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 391dd0671f..1c378bff0d 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -307,7 +307,7 @@ def cocoa_windowShouldClose(self): def close(self): self.native.close() - def get_current_screen(self): + def get_current_screen(self) -> ScreenImpl: return ScreenImpl(self.native.screen) def get_image_data(self): diff --git a/core/src/toga/app.py b/core/src/toga/app.py index c70ff99f6c..fc8c9cd1ac 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -27,6 +27,7 @@ from toga.icons import Icon from toga.paths import Paths from toga.platform import get_platform_factory +from toga.screen import Screen from toga.widgets.base import Widget from toga.window import Window @@ -497,7 +498,7 @@ def _create_impl(self): self.factory.App(interface=self) @property - def screens(self): + def screens(self) -> tuple[Screen, ...]: """Returns a list of available screens.""" return [screen.interface for screen in self._impl.get_screens()] diff --git a/core/src/toga/screen.py b/core/src/toga/screen.py index c2a3a7091e..2995f6e628 100644 --- a/core/src/toga/screen.py +++ b/core/src/toga/screen.py @@ -8,19 +8,25 @@ def __init__(self, _impl): self.factory = get_platform_factory() @property - def name(self): + def name(self) -> str: """Unique name of the screen.""" return self._impl.get_name() @property - def origin(self): + def origin(self) -> tuple[int, int]: """The absolute coordinates of the screen's origin, as a ``(x, y)`` tuple.""" return self._impl.get_origin() @property - def size(self): + def size(self) -> tuple[int, int]: """The size of the screen, as a ``(width, height)`` tuple.""" return self._impl.get_size() - def as_image(self, format=Image): + def as_image(self, format=Image) -> Image: + """Render the current contents of the screen as an image. + + :param format: Format to provide. Defaults to :class:`~toga.images.Image`; also + supports :any:`PIL.Image.Image` if Pillow is installed + :returns: An image containing the screen content, in the format requested. + """ return Image(self._impl.get_image_data()).as_format(format) diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index 42872224a5..c239c6ca97 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -68,7 +68,7 @@ def hide_cursor(self): def simulate_exit(self): self.interface.on_exit() - def get_screens(self): + def get_screens(self) -> tuple[ScreenImpl, ...]: # _________________________________________________ # Display Setup: | # ________________________________________________| diff --git a/dummy/src/toga_dummy/screen.py b/dummy/src/toga_dummy/screen.py index 6d7bf57cd2..436f635a2c 100644 --- a/dummy/src/toga_dummy/screen.py +++ b/dummy/src/toga_dummy/screen.py @@ -23,16 +23,16 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self): + def get_name(self) -> str: return self.native[0] - def get_origin(self): + def get_origin(self) -> tuple[int, int]: return self.native[1] - def get_size(self): + def get_size(self) -> tuple[int, int]: return self.native[2] - def get_image_data(self): + def get_image_data(self) -> Image: self._action("get image data") img = Image.new("RGB", self.native[2], "white") diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 10f5c0e153..024db540b6 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -107,6 +107,6 @@ def set_full_screen(self, is_full_screen): def simulate_close(self): self.interface.on_close() - def get_current_screen(self): + def get_current_screen(self) -> ScreenImpl: # `window.screen` will return `Secondary Screen` return ScreenImpl(native=("Secondary Screen", (-1366, -768), (1366, 768))) diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 2b747234bc..470c9e106e 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -189,7 +189,7 @@ def main_loop(self): self.loop.run_forever(application=self.native) - def get_screens(self): + def get_screens(self) -> tuple[ScreenImpl, ...]: display = Gdk.Display.get_default() # CI runs on wayland if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": # pragma: no cover diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index f97dbe5fc7..ff5d414a1b 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -18,18 +18,18 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self): - return self.native.get_model() + def get_name(self) -> str: + return str(self.native.get_model()) - def get_origin(self): + def get_origin(self) -> tuple[int, int]: geometry = self.native.get_geometry() return geometry.x, geometry.y - def get_size(self): + def get_size(self) -> tuple[int, int]: geometry = self.native.get_geometry() return geometry.width, geometry.height - def get_image_data(self): # CI runs on wayland + def get_image_data(self) -> bytes | None: if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": # pragma: no cover # Only works for x11 display = self.native.get_display() @@ -45,6 +45,7 @@ def get_image_data(self): # CI runs on wayland else: print("Failed to save screenshot to buffer.") return None + # CI runs on wayland else: # Not implemented on wayland due to wayland security policies. self.interface.factory.not_implemented("Screen.get_image_data() on Wayland") diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 6cea0d01e9..74fef807db 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -164,7 +164,7 @@ def set_full_screen(self, is_full_screen): else: self.native.unfullscreen() - def get_current_screen(self): + def get_current_screen(self) -> ScreenImpl: display = Gdk.Display.get_default() monitor_native = display.get_monitor_at_window(self.native.get_window()) return ScreenImpl(monitor_native) diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index 45fedfe82c..ebb476e94e 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -123,5 +123,5 @@ def show_cursor(self): # No-op; mobile doesn't support cursors pass - def get_screens(self): + def get_screens(self) -> ScreenImpl: return [ScreenImpl(UIScreen.mainScreen)] diff --git a/iOS/src/toga_iOS/screen.py b/iOS/src/toga_iOS/screen.py index 524b46cf09..2689808152 100644 --- a/iOS/src/toga_iOS/screen.py +++ b/iOS/src/toga_iOS/screen.py @@ -14,13 +14,13 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self): + def get_name(self) -> str: return "iOS Screen" - def get_origin(self): + def get_origin(self) -> tuple[int, int]: return (0, 0) - def get_size(self): + def get_size(self) -> tuple[int, int]: return int(self.native.bounds.size.width), int(self.native.bounds.size.height) def get_image_data(self): diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index d13e8b57b1..9d1148db0c 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -117,7 +117,7 @@ def set_full_screen(self, is_full_screen): def close(self): pass - def get_current_screen(self): + def get_current_screen(self) -> ScreenImpl: return ScreenImpl(UIScreen.mainScreen) def get_image_data(self): diff --git a/textual/src/toga_textual/app.py b/textual/src/toga_textual/app.py index e7790a7af8..8a04a05960 100644 --- a/textual/src/toga_textual/app.py +++ b/textual/src/toga_textual/app.py @@ -70,7 +70,7 @@ def show_cursor(self): def hide_cursor(self): pass - def get_screens(self): + def get_screens(self) -> tuple[ScreenImpl, ...]: return [ScreenImpl(window._impl.native) for window in self.interface.windows] diff --git a/textual/src/toga_textual/screen.py b/textual/src/toga_textual/screen.py index 3fa1d22260..37fee40192 100644 --- a/textual/src/toga_textual/screen.py +++ b/textual/src/toga_textual/screen.py @@ -14,13 +14,13 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self): + def get_name(self) -> str: return "Textual Screen" - def get_origin(self): + def get_origin(self) -> tuple[int, int]: return (0, 0) - def get_size(self): + def get_size(self) -> tuple[int, int]: return (self.native.size.width, self.native.size.height) def get_image_data(self): diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index 9400019989..553cc8639c 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -170,5 +170,5 @@ def close(self): def set_full_screen(self, is_full_screen): pass - def get_current_screen(self): + def get_current_screen(self) -> ScreenImpl: return ScreenImpl(self.native) diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index b359a137d0..ee2084e179 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -212,5 +212,5 @@ def show_cursor(self): def hide_cursor(self): self.interface.factory.not_implemented("App.hide_cursor()") - def get_screens(self): + def get_screens(self) -> ScreenImpl: return [ScreenImpl(js.document.documentElement)] diff --git a/web/src/toga_web/screen.py b/web/src/toga_web/screen.py index dd876bbae4..169c39714e 100644 --- a/web/src/toga_web/screen.py +++ b/web/src/toga_web/screen.py @@ -14,13 +14,13 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self): + def get_name(self) -> str: return "Web Screen" - def get_origin(self): + def get_origin(self) -> tuple[int, int]: return (0, 0) - def get_size(self): + def get_size(self) -> tuple[int, int]: return self.native.clientWidth, self.native.clientHeight def get_image_data(self): diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index 9595636404..27dd4a460b 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -80,5 +80,5 @@ def set_size(self, size): def set_full_screen(self, is_full_screen): self.interface.factory.not_implemented("Window.set_full_screen()") - def get_current_screen(self): + def get_current_screen(self) -> ScreenImpl: return ScreenImpl(js.document.documentElement) diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 10b661fd95..62542912fb 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -308,7 +308,7 @@ def exit(self): # pragma: no cover self._is_exiting = True self.native.Exit() - def get_screens(self): + def get_screens(self) -> tuple[ScreenImpl, ...]: primary_screen = ScreenImpl(WinForms.Screen.PrimaryScreen) screen_list = [primary_screen] + [ ScreenImpl(native=screen) diff --git a/winforms/src/toga_winforms/screen.py b/winforms/src/toga_winforms/screen.py index 168bd3758c..6e8d42873e 100644 --- a/winforms/src/toga_winforms/screen.py +++ b/winforms/src/toga_winforms/screen.py @@ -23,16 +23,16 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self): - return self.native.DeviceName + def get_name(self) -> str: + return str(self.native.DeviceName) - def get_origin(self): + def get_origin(self) -> tuple[int, int]: return self.native.Bounds.X, self.native.Bounds.Y - def get_size(self): + def get_size(self) -> tuple[int, int]: return self.native.Bounds.Width, self.native.Bounds.Height - def get_image_data(self): + def get_image_data(self) -> bytes: bitmap = Bitmap(*self.get_size()) graphics = Graphics.FromImage(bitmap) source_point = Point(*self.get_origin()) diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index fb06dfa03c..c34556f91d 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -190,7 +190,7 @@ def resize_content(self): self.native.ClientSize.Height - vertical_shift, ) - def get_current_screen(self): + def get_current_screen(self) -> ScreenImpl: return ScreenImpl(WinForms.Screen.FromControl(self.native)) def get_image_data(self): From 1b8a52ddbd091800567da5b92b3e1e5c0e8ea8ee Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 21 Jan 2024 11:26:03 -0800 Subject: [PATCH 062/102] Corrected tests --- android/src/toga_android/app.py | 3 ++- android/src/toga_android/screen.py | 6 ++++-- cocoa/src/toga_cocoa/app.py | 3 ++- cocoa/src/toga_cocoa/screen.py | 6 ++++-- core/src/toga/app.py | 1 + core/src/toga/screen.py | 6 ++++-- dummy/src/toga_dummy/app.py | 3 ++- dummy/src/toga_dummy/screen.py | 6 ++++-- gtk/src/toga_gtk/app.py | 3 ++- gtk/src/toga_gtk/screen.py | 5 +++-- iOS/src/toga_iOS/app.py | 3 ++- iOS/src/toga_iOS/screen.py | 6 ++++-- textual/src/toga_textual/app.py | 3 ++- textual/src/toga_textual/screen.py | 6 ++++-- web/src/toga_web/app.py | 4 +++- web/src/toga_web/screen.py | 6 ++++-- winforms/src/toga_winforms/app.py | 3 ++- winforms/src/toga_winforms/screen.py | 6 ++++-- 18 files changed, 53 insertions(+), 26 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 4d7a97945c..efea0e5b29 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -1,5 +1,6 @@ import asyncio import sys +from typing import Tuple from android.content import Context from android.graphics.drawable import BitmapDrawable @@ -285,7 +286,7 @@ def hide_cursor(self): def show_cursor(self): pass - def get_screens(self) -> tuple[ScreenImpl, ...]: + def get_screens(self) -> Tuple[ScreenImpl, ...]: context = self.native.getApplicationContext() display_manager = context.getSystemService(Context.DISPLAY_SERVICE) screen_list = display_manager.getDisplays() diff --git a/android/src/toga_android/screen.py b/android/src/toga_android/screen.py index 2bae7e810e..954297e5c9 100644 --- a/android/src/toga_android/screen.py +++ b/android/src/toga_android/screen.py @@ -1,3 +1,5 @@ +from typing import Tuple + from toga.screen import Screen as ScreenInterface from .widgets.base import Scalable @@ -21,10 +23,10 @@ def __new__(cls, app, native): def get_name(self) -> str: return str(self.native.getName()) - def get_origin(self) -> tuple[int, int]: + def get_origin(self) -> Tuple[int, int]: return (0, 0) - def get_size(self) -> tuple[int, int]: + def get_size(self) -> Tuple[int, int]: return ( self.scale_out(self.native.getWidth()), self.scale_out(self.native.getHeight()), diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index dee45896c2..7bd8e13c6a 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -3,6 +3,7 @@ import os import sys from pathlib import Path +from typing import Tuple from urllib.parse import unquote, urlparse from rubicon.objc.eventloop import CocoaLifecycle, EventLoopPolicy @@ -403,7 +404,7 @@ def _submenu(self, group, menubar): def main_loop(self): self.loop.run_forever(lifecycle=CocoaLifecycle(self.native)) - def get_screens(self) -> tuple[ScreenImpl, ...]: + def get_screens(self) -> Tuple[ScreenImpl, ...]: return [ScreenImpl(native=screen) for screen in NSScreen.screens] def set_main_window(self, window): diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index 964f904157..e1ae2a745a 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -1,3 +1,5 @@ +from typing import Tuple + from toga.screen import Screen as ScreenInterface from toga_cocoa.libs import ( CGImageGetHeight, @@ -24,11 +26,11 @@ def __new__(cls, native): def get_name(self) -> str: return str(self.native.localizedName) - def get_origin(self) -> tuple[int, int]: + def get_origin(self) -> Tuple[int, int]: frame_native = self.native.frame return (int(frame_native.origin.x), int(frame_native.origin.y)) - def get_size(self) -> tuple[int, int]: + def get_size(self) -> Tuple[int, int]: frame_native = self.native.frame return (int(frame_native.size.width), int(frame_native.size.height)) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index fc8c9cd1ac..ccd8738051 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -34,6 +34,7 @@ if TYPE_CHECKING: from toga.icons import IconContent + # Make sure deprecation warnings are shown by default warnings.filterwarnings("default", category=DeprecationWarning) diff --git a/core/src/toga/screen.py b/core/src/toga/screen.py index 2995f6e628..e597d2588b 100644 --- a/core/src/toga/screen.py +++ b/core/src/toga/screen.py @@ -1,3 +1,5 @@ +from typing import Tuple + from toga.images import Image from toga.platform import get_platform_factory @@ -13,12 +15,12 @@ def name(self) -> str: return self._impl.get_name() @property - def origin(self) -> tuple[int, int]: + def origin(self) -> Tuple[int, int]: """The absolute coordinates of the screen's origin, as a ``(x, y)`` tuple.""" return self._impl.get_origin() @property - def size(self) -> tuple[int, int]: + def size(self) -> Tuple[int, int]: """The size of the screen, as a ``(width, height)`` tuple.""" return self._impl.get_size() diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index c239c6ca97..7fae00889d 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -1,6 +1,7 @@ import asyncio import sys from pathlib import Path +from typing import Tuple from .screen import Screen as ScreenImpl from .utils import LoggedObject @@ -68,7 +69,7 @@ def hide_cursor(self): def simulate_exit(self): self.interface.on_exit() - def get_screens(self) -> tuple[ScreenImpl, ...]: + def get_screens(self) -> Tuple[ScreenImpl, ...]: # _________________________________________________ # Display Setup: | # ________________________________________________| diff --git a/dummy/src/toga_dummy/screen.py b/dummy/src/toga_dummy/screen.py index 436f635a2c..d3aafe1261 100644 --- a/dummy/src/toga_dummy/screen.py +++ b/dummy/src/toga_dummy/screen.py @@ -1,3 +1,5 @@ +from typing import Tuple + from PIL import Image, ImageDraw from toga.screen import Screen as ScreenInterface @@ -26,10 +28,10 @@ def __new__(cls, native): def get_name(self) -> str: return self.native[0] - def get_origin(self) -> tuple[int, int]: + def get_origin(self) -> Tuple[int, int]: return self.native[1] - def get_size(self) -> tuple[int, int]: + def get_size(self) -> Tuple[int, int]: return self.native[2] def get_image_data(self) -> Image: diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 470c9e106e..acadc555b7 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -3,6 +3,7 @@ import signal import sys from pathlib import Path +from typing import Tuple import gbulb @@ -189,7 +190,7 @@ def main_loop(self): self.loop.run_forever(application=self.native) - def get_screens(self) -> tuple[ScreenImpl, ...]: + def get_screens(self) -> Tuple[ScreenImpl, ...]: display = Gdk.Display.get_default() # CI runs on wayland if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": # pragma: no cover diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index ff5d414a1b..f06217c7d5 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -1,4 +1,5 @@ import os +from typing import Tuple from toga.screen import Screen as ScreenInterface @@ -21,11 +22,11 @@ def __new__(cls, native): def get_name(self) -> str: return str(self.native.get_model()) - def get_origin(self) -> tuple[int, int]: + def get_origin(self) -> Tuple[int, int]: geometry = self.native.get_geometry() return geometry.x, geometry.y - def get_size(self) -> tuple[int, int]: + def get_size(self) -> Tuple[int, int]: geometry = self.native.get_geometry() return geometry.width, geometry.height diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index ebb476e94e..760ce2dbec 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -1,4 +1,5 @@ import asyncio +from typing import Tuple from rubicon.objc import objc_method from rubicon.objc.eventloop import EventLoopPolicy, iOSLifecycle @@ -123,5 +124,5 @@ def show_cursor(self): # No-op; mobile doesn't support cursors pass - def get_screens(self) -> ScreenImpl: + def get_screens(self) -> Tuple[ScreenImpl, ...]: return [ScreenImpl(UIScreen.mainScreen)] diff --git a/iOS/src/toga_iOS/screen.py b/iOS/src/toga_iOS/screen.py index 2689808152..3b47d41540 100644 --- a/iOS/src/toga_iOS/screen.py +++ b/iOS/src/toga_iOS/screen.py @@ -1,3 +1,5 @@ +from typing import Tuple + from toga.screen import Screen as ScreenInterface @@ -17,10 +19,10 @@ def __new__(cls, native): def get_name(self) -> str: return "iOS Screen" - def get_origin(self) -> tuple[int, int]: + def get_origin(self) -> Tuple[int, int]: return (0, 0) - def get_size(self) -> tuple[int, int]: + def get_size(self) -> Tuple[int, int]: return int(self.native.bounds.size.width), int(self.native.bounds.size.height) def get_image_data(self): diff --git a/textual/src/toga_textual/app.py b/textual/src/toga_textual/app.py index 8a04a05960..31df693d0d 100644 --- a/textual/src/toga_textual/app.py +++ b/textual/src/toga_textual/app.py @@ -1,4 +1,5 @@ import asyncio +from typing import Tuple from textual.app import App as TextualApp @@ -70,7 +71,7 @@ def show_cursor(self): def hide_cursor(self): pass - def get_screens(self) -> tuple[ScreenImpl, ...]: + def get_screens(self) -> Tuple[ScreenImpl, ...]: return [ScreenImpl(window._impl.native) for window in self.interface.windows] diff --git a/textual/src/toga_textual/screen.py b/textual/src/toga_textual/screen.py index 37fee40192..d7042005a7 100644 --- a/textual/src/toga_textual/screen.py +++ b/textual/src/toga_textual/screen.py @@ -1,3 +1,5 @@ +from typing import Tuple + from toga.screen import Screen as ScreenInterface @@ -17,10 +19,10 @@ def __new__(cls, native): def get_name(self) -> str: return "Textual Screen" - def get_origin(self) -> tuple[int, int]: + def get_origin(self) -> Tuple[int, int]: return (0, 0) - def get_size(self) -> tuple[int, int]: + def get_size(self) -> Tuple[int, int]: return (self.native.size.width, self.native.size.height) def get_image_data(self): diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index ee2084e179..1b06d6e16b 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -1,3 +1,5 @@ +from typing import Tuple + import toga from toga.command import Separator from toga_web.libs import create_element, js @@ -212,5 +214,5 @@ def show_cursor(self): def hide_cursor(self): self.interface.factory.not_implemented("App.hide_cursor()") - def get_screens(self) -> ScreenImpl: + def get_screens(self) -> Tuple[ScreenImpl, ...]: return [ScreenImpl(js.document.documentElement)] diff --git a/web/src/toga_web/screen.py b/web/src/toga_web/screen.py index 169c39714e..eeea53ba2e 100644 --- a/web/src/toga_web/screen.py +++ b/web/src/toga_web/screen.py @@ -1,3 +1,5 @@ +from typing import Tuple + from toga.screen import Screen as ScreenInterface @@ -17,10 +19,10 @@ def __new__(cls, native): def get_name(self) -> str: return "Web Screen" - def get_origin(self) -> tuple[int, int]: + def get_origin(self) -> Tuple[int, int]: return (0, 0) - def get_size(self) -> tuple[int, int]: + def get_size(self) -> Tuple[int, int]: return self.native.clientWidth, self.native.clientHeight def get_image_data(self): diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 62542912fb..6c070d6b2e 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -3,6 +3,7 @@ import sys import threading from ctypes import windll +from typing import Tuple import System.Windows.Forms as WinForms from System import Environment, Threading @@ -308,7 +309,7 @@ def exit(self): # pragma: no cover self._is_exiting = True self.native.Exit() - def get_screens(self) -> tuple[ScreenImpl, ...]: + def get_screens(self) -> Tuple[ScreenImpl, ...]: primary_screen = ScreenImpl(WinForms.Screen.PrimaryScreen) screen_list = [primary_screen] + [ ScreenImpl(native=screen) diff --git a/winforms/src/toga_winforms/screen.py b/winforms/src/toga_winforms/screen.py index 6e8d42873e..513d281ded 100644 --- a/winforms/src/toga_winforms/screen.py +++ b/winforms/src/toga_winforms/screen.py @@ -1,3 +1,5 @@ +from typing import Tuple + from System.Drawing import ( Bitmap, Graphics, @@ -26,10 +28,10 @@ def __new__(cls, native): def get_name(self) -> str: return str(self.native.DeviceName) - def get_origin(self) -> tuple[int, int]: + def get_origin(self) -> Tuple[int, int]: return self.native.Bounds.X, self.native.Bounds.Y - def get_size(self) -> tuple[int, int]: + def get_size(self) -> Tuple[int, int]: return self.native.Bounds.Width, self.native.Bounds.Height def get_image_data(self) -> bytes: From 1b89c95df3a3ac4f6bc1c9031c739208d577b1cd Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 24 Jan 2024 23:20:59 -0800 Subject: [PATCH 063/102] Removed type annotations and other fixes --- android/src/toga_android/app.py | 3 +-- android/src/toga_android/screen.py | 10 ++++------ android/src/toga_android/window.py | 2 +- android/tests_backend/app.py | 4 ---- android/tests_backend/screen.py | 7 ------- android/tests_backend/window.py | 5 ----- cocoa/src/toga_cocoa/app.py | 3 +-- cocoa/src/toga_cocoa/screen.py | 10 ++++------ cocoa/src/toga_cocoa/window.py | 2 +- cocoa/tests_backend/app.py | 4 ---- cocoa/tests_backend/screen.py | 7 ------- cocoa/tests_backend/window.py | 4 ---- core/src/toga/screen.py | 6 +++--- core/src/toga/window.py | 12 ++---------- core/tests/app/test_app.py | 11 ----------- core/tests/test_window.py | 7 +------ dummy/src/toga_dummy/app.py | 3 +-- dummy/src/toga_dummy/screen.py | 10 ++++------ dummy/src/toga_dummy/window.py | 2 +- gtk/src/toga_gtk/app.py | 3 +-- gtk/src/toga_gtk/screen.py | 11 +++++------ gtk/src/toga_gtk/window.py | 2 +- gtk/tests_backend/app.py | 4 ---- gtk/tests_backend/screen.py | 7 ------- gtk/tests_backend/window.py | 4 ---- iOS/src/toga_iOS/app.py | 3 +-- iOS/src/toga_iOS/screen.py | 9 ++++----- iOS/src/toga_iOS/window.py | 2 +- iOS/tests_backend/app.py | 4 ---- iOS/tests_backend/screen.py | 7 ------- iOS/tests_backend/window.py | 4 ---- output_image.png | Bin 10888 -> 0 bytes testbed/tests/app/test_screen.py | 9 --------- testbed/tests/test_app.py | 8 +------- testbed/tests/test_window.py | 8 ++------ textual/src/toga_textual/app.py | 3 +-- textual/src/toga_textual/screen.py | 8 +++----- textual/src/toga_textual/window.py | 2 +- textual/tests_backend/screen.py | 8 -------- web/src/toga_web/app.py | 4 +--- web/src/toga_web/screen.py | 8 +++----- web/src/toga_web/window.py | 2 +- winforms/src/toga_winforms/app.py | 3 +-- winforms/src/toga_winforms/screen.py | 12 +++++------- winforms/src/toga_winforms/window.py | 2 +- winforms/tests_backend/app.py | 4 ---- winforms/tests_backend/screen.py | 7 ------- winforms/tests_backend/window.py | 5 ----- 48 files changed, 57 insertions(+), 208 deletions(-) delete mode 100644 output_image.png diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index efea0e5b29..d7fc267929 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -1,6 +1,5 @@ import asyncio import sys -from typing import Tuple from android.content import Context from android.graphics.drawable import BitmapDrawable @@ -286,7 +285,7 @@ def hide_cursor(self): def show_cursor(self): pass - def get_screens(self) -> Tuple[ScreenImpl, ...]: + def get_screens(self): context = self.native.getApplicationContext() display_manager = context.getSystemService(Context.DISPLAY_SERVICE) screen_list = display_manager.getDisplays() diff --git a/android/src/toga_android/screen.py b/android/src/toga_android/screen.py index 954297e5c9..1e1d88c83a 100644 --- a/android/src/toga_android/screen.py +++ b/android/src/toga_android/screen.py @@ -1,5 +1,3 @@ -from typing import Tuple - from toga.screen import Screen as ScreenInterface from .widgets.base import Scalable @@ -20,13 +18,13 @@ def __new__(cls, app, native): instance.init_scale(instance.app.native) return instance - def get_name(self) -> str: - return str(self.native.getName()) + def get_name(self): + return self.native.getName() - def get_origin(self) -> Tuple[int, int]: + def get_origin(self): return (0, 0) - def get_size(self) -> Tuple[int, int]: + def get_size(self): return ( self.scale_out(self.native.getWidth()), self.scale_out(self.native.getHeight()), diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index d56e556c0a..3fa3c39eae 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -105,7 +105,7 @@ def close(self): def set_full_screen(self, is_full_screen): self.interface.factory.not_implemented("Window.set_full_screen()") - def get_current_screen(self) -> ScreenImpl: + def get_current_screen(self): context = self.app.native.getApplicationContext() window_manager = context.getSystemService(Context.WINDOW_SERVICE) return ScreenImpl(self.app, window_manager.getDefaultDisplay()) diff --git a/android/tests_backend/app.py b/android/tests_backend/app.py index 9be462d7e3..76365fcc4d 100644 --- a/android/tests_backend/app.py +++ b/android/tests_backend/app.py @@ -5,7 +5,6 @@ from pytest import xfail from toga import Group -from toga_android.screen import Screen as ScreenImpl from .probe import BaseProbe from .window import WindowProbe @@ -107,6 +106,3 @@ def rotate(self): self.native.findViewById( R.id.content ).getViewTreeObserver().dispatchOnGlobalLayout() - - def assert_screen_implementation_type(self, screen): - assert isinstance(screen._impl, ScreenImpl) diff --git a/android/tests_backend/screen.py b/android/tests_backend/screen.py index 7569fd1625..b7de2cd18a 100644 --- a/android/tests_backend/screen.py +++ b/android/tests_backend/screen.py @@ -1,6 +1,5 @@ from android.view import Display -from toga_android.screen import Screen as ScreenImpl from toga_android.widgets.base import Scalable from .probe import BaseProbe @@ -13,12 +12,6 @@ def __init__(self, app, screen): self._impl = screen._impl self.native = screen._impl.native self.init_scale(app._impl.native) - - def assert_implementation_type(self): - assert isinstance(self._impl, ScreenImpl) - - def assert_native_type(self): - print(type(self.native)) assert isinstance(self.native, Display) def assert_name(self): diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index 59043ef2c7..bc283f43a3 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -1,8 +1,6 @@ import pytest from androidx.appcompat import R as appcompat_R -from toga_android.screen import Screen as ScreenImpl - from .probe import BaseProbe @@ -89,6 +87,3 @@ def assert_toolbar_item(self, index, label, tooltip, has_icon, enabled): def press_toolbar_button(self, index): self.native.onOptionsItemSelected(self._toolbar_items()[index]) - - def assert_screen_implementation_type(self, screen): - assert isinstance(screen._impl, ScreenImpl) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 7bd8e13c6a..8be3825772 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -3,7 +3,6 @@ import os import sys from pathlib import Path -from typing import Tuple from urllib.parse import unquote, urlparse from rubicon.objc.eventloop import CocoaLifecycle, EventLoopPolicy @@ -404,7 +403,7 @@ def _submenu(self, group, menubar): def main_loop(self): self.loop.run_forever(lifecycle=CocoaLifecycle(self.native)) - def get_screens(self) -> Tuple[ScreenImpl, ...]: + def get_screens(self): return [ScreenImpl(native=screen) for screen in NSScreen.screens] def set_main_window(self, window): diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index e1ae2a745a..dee8f7398a 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -1,5 +1,3 @@ -from typing import Tuple - from toga.screen import Screen as ScreenInterface from toga_cocoa.libs import ( CGImageGetHeight, @@ -23,18 +21,18 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self) -> str: + def get_name(self): return str(self.native.localizedName) - def get_origin(self) -> Tuple[int, int]: + def get_origin(self): frame_native = self.native.frame return (int(frame_native.origin.x), int(frame_native.origin.y)) - def get_size(self) -> Tuple[int, int]: + def get_size(self): frame_native = self.native.frame return (int(frame_native.size.width), int(frame_native.size.height)) - def get_image_data(self) -> NSImage: + def get_image_data(self): # Retrieve the device description dictionary for the NSScreen device_description = self.native.deviceDescription # Extract the CGDirectDisplayID from the device description diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 1c378bff0d..391dd0671f 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -307,7 +307,7 @@ def cocoa_windowShouldClose(self): def close(self): self.native.close() - def get_current_screen(self) -> ScreenImpl: + def get_current_screen(self): return ScreenImpl(self.native.screen) def get_image_data(self): diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index 667bc3e81b..8c82b68a4f 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -10,7 +10,6 @@ NSEventType, NSWindow, ) -from toga_cocoa.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -176,6 +175,3 @@ def keystroke(self, combination): keyCode=key_code, ) return toga_key(event) - - def assert_screen_implementation_type(self, screen): - assert isinstance(screen._impl, ScreenImpl) diff --git a/cocoa/tests_backend/screen.py b/cocoa/tests_backend/screen.py index 2d3fdb56af..ec665748e9 100644 --- a/cocoa/tests_backend/screen.py +++ b/cocoa/tests_backend/screen.py @@ -1,5 +1,4 @@ from toga_cocoa.libs import NSScreen -from toga_cocoa.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -10,12 +9,6 @@ def __init__(self, screen): self.screen = screen self._impl = screen._impl self.native = screen._impl.native - - def assert_implementation_type(self): - assert isinstance(self._impl, ScreenImpl) - - def assert_native_type(self): - print(type(self.native)) assert isinstance(self.native, NSScreen) def assert_name(self): diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 5b24b5141b..9eeba9ff29 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -14,7 +14,6 @@ NSWindow, NSWindowStyleMask, ) -from toga_cocoa.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -262,6 +261,3 @@ def press_toolbar_button(self, index): restype=None, argtypes=[objc_id], ) - - def assert_screen_implementation_type(self, screen): - assert isinstance(screen._impl, ScreenImpl) diff --git a/core/src/toga/screen.py b/core/src/toga/screen.py index e597d2588b..c8415cbbf1 100644 --- a/core/src/toga/screen.py +++ b/core/src/toga/screen.py @@ -1,4 +1,4 @@ -from typing import Tuple +from __future__ import annotations from toga.images import Image from toga.platform import get_platform_factory @@ -15,12 +15,12 @@ def name(self) -> str: return self._impl.get_name() @property - def origin(self) -> Tuple[int, int]: + def origin(self) -> tuple[int, int]: """The absolute coordinates of the screen's origin, as a ``(x, y)`` tuple.""" return self._impl.get_origin() @property - def size(self) -> Tuple[int, int]: + def size(self) -> tuple[int, int]: """The size of the screen, as a ``(width, height)`` tuple.""" return self._impl.get_size() diff --git a/core/src/toga/window.py b/core/src/toga/window.py index c149f9e214..c18b9f9466 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -331,20 +331,12 @@ def position(self) -> tuple[int, int]: :ref:`CSS pixels `. The origin is at the top left corner.""" absolute_origin = self._app.screens[0].origin absolute_window_position = self._impl.get_position() - # Using the name `assumed_absolute_window_position` due to the - # differences of the position of the origin on different platforms. - # For example, On cocoa the origin is at the bottom left corner - # while on most platforms the origin is at the top left. But, for - # toga, the position of the origin will always be at the top left. - - # So, the actual absolute window position on cocoa will be different from - # the absolute position that toga works with and provides to the user. - assumed_absolute_window_position = ( + window_position = ( absolute_window_position[0] - absolute_origin[0], absolute_window_position[1] - absolute_origin[1], ) - return assumed_absolute_window_position + return window_position @position.setter def position(self, position: tuple[int, int]) -> None: diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 610d74f9e0..deac2063f5 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -8,8 +8,6 @@ import pytest import toga -from toga.screen import Screen as ScreenInterface -from toga_dummy.screen import Screen as ScreenImpl from toga_dummy.utils import ( assert_action_not_performed, assert_action_performed, @@ -623,12 +621,3 @@ def test_deprecated_name(): assert app.formal_name == "Test App" with pytest.warns(DeprecationWarning, match=name_warning): assert app.name == "Test App" - - -def test_screens(app): - """All available screens can be accessed using `App.screens`""" - assert isinstance(app.screens, list) - for screen in app.screens: - assert isinstance(screen, ScreenInterface) - assert isinstance(screen._impl, ScreenImpl) - # Other screen tests are in `app/test_screen.py` diff --git a/core/tests/test_window.py b/core/tests/test_window.py index 90350da526..cd6a417506 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -4,8 +4,6 @@ import pytest import toga -from toga.screen import Screen as ScreenInterface -from toga_dummy.screen import Screen as ScreenImpl from toga_dummy.utils import ( assert_action_not_performed, assert_action_performed, @@ -358,8 +356,6 @@ def test_as_image(window): def test_screen(window, app): """A window can be moved to a different screen""" - assert isinstance(window.screen, ScreenInterface) - assert isinstance(window.screen._impl, ScreenImpl) # Cannot actually change window.screen, so just check # the window positions as a substitute for moving the # window between the screens. @@ -371,8 +367,7 @@ def test_screen(window, app): def test_screen_position(window, app): - """A window can be moved using both the - absolute position and relative screen position""" + """The window can be relocated using absolute and relative screen positions.""" # _________________________________________________ # Display Setup: | # ________________________________________________| diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index 7fae00889d..42872224a5 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -1,7 +1,6 @@ import asyncio import sys from pathlib import Path -from typing import Tuple from .screen import Screen as ScreenImpl from .utils import LoggedObject @@ -69,7 +68,7 @@ def hide_cursor(self): def simulate_exit(self): self.interface.on_exit() - def get_screens(self) -> Tuple[ScreenImpl, ...]: + def get_screens(self): # _________________________________________________ # Display Setup: | # ________________________________________________| diff --git a/dummy/src/toga_dummy/screen.py b/dummy/src/toga_dummy/screen.py index d3aafe1261..6d7bf57cd2 100644 --- a/dummy/src/toga_dummy/screen.py +++ b/dummy/src/toga_dummy/screen.py @@ -1,5 +1,3 @@ -from typing import Tuple - from PIL import Image, ImageDraw from toga.screen import Screen as ScreenInterface @@ -25,16 +23,16 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self) -> str: + def get_name(self): return self.native[0] - def get_origin(self) -> Tuple[int, int]: + def get_origin(self): return self.native[1] - def get_size(self) -> Tuple[int, int]: + def get_size(self): return self.native[2] - def get_image_data(self) -> Image: + def get_image_data(self): self._action("get image data") img = Image.new("RGB", self.native[2], "white") diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 024db540b6..10f5c0e153 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -107,6 +107,6 @@ def set_full_screen(self, is_full_screen): def simulate_close(self): self.interface.on_close() - def get_current_screen(self) -> ScreenImpl: + def get_current_screen(self): # `window.screen` will return `Secondary Screen` return ScreenImpl(native=("Secondary Screen", (-1366, -768), (1366, 768))) diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index acadc555b7..2b747234bc 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -3,7 +3,6 @@ import signal import sys from pathlib import Path -from typing import Tuple import gbulb @@ -190,7 +189,7 @@ def main_loop(self): self.loop.run_forever(application=self.native) - def get_screens(self) -> Tuple[ScreenImpl, ...]: + def get_screens(self): display = Gdk.Display.get_default() # CI runs on wayland if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": # pragma: no cover diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index f06217c7d5..9d63e1c91b 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -1,5 +1,4 @@ import os -from typing import Tuple from toga.screen import Screen as ScreenInterface @@ -19,18 +18,18 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self) -> str: - return str(self.native.get_model()) + def get_name(self): + return self.native.get_model() - def get_origin(self) -> Tuple[int, int]: + def get_origin(self): geometry = self.native.get_geometry() return geometry.x, geometry.y - def get_size(self) -> Tuple[int, int]: + def get_size(self): geometry = self.native.get_geometry() return geometry.width, geometry.height - def get_image_data(self) -> bytes | None: + def get_image_data(self): if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": # pragma: no cover # Only works for x11 display = self.native.get_display() diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 74fef807db..6cea0d01e9 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -164,7 +164,7 @@ def set_full_screen(self, is_full_screen): else: self.native.unfullscreen() - def get_current_screen(self) -> ScreenImpl: + def get_current_screen(self): display = Gdk.Display.get_default() monitor_native = display.get_monitor_at_window(self.native.get_window()) return ScreenImpl(monitor_native) diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index 0575940ab4..10dcc058b6 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -4,7 +4,6 @@ from toga_gtk.keys import gtk_accel, toga_key from toga_gtk.libs import Gdk, Gtk -from toga_gtk.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -155,6 +154,3 @@ def keystroke(self, combination): event.state = state return toga_key(event) - - def assert_screen_implementation_type(self, screen): - assert isinstance(screen._impl, ScreenImpl) diff --git a/gtk/tests_backend/screen.py b/gtk/tests_backend/screen.py index 99c3703727..745b0270e2 100644 --- a/gtk/tests_backend/screen.py +++ b/gtk/tests_backend/screen.py @@ -2,8 +2,6 @@ from gi.repository import GdkX11 -from toga_gtk.screen import Screen as ScreenImpl - from .probe import BaseProbe @@ -13,11 +11,6 @@ def __init__(self, screen): self.screen = screen self._impl = screen._impl self.native = screen._impl.native - - def assert_implementation_type(self): - assert isinstance(self._impl, ScreenImpl) - - def assert_native_type(self): if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": assert isinstance(self.native, GdkX11.X11Monitor) else: diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index 58fa74beee..a90b60f8ef 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -3,7 +3,6 @@ from unittest.mock import Mock from toga_gtk.libs import Gdk, Gtk -from toga_gtk.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -252,6 +251,3 @@ def assert_toolbar_item(self, index, label, tooltip, has_icon, enabled): def press_toolbar_button(self, index): item = self.impl.native_toolbar.get_nth_item(index) item.emit("clicked") - - def assert_screen_implementation_type(self, screen): - assert isinstance(screen._impl, ScreenImpl) diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index 760ce2dbec..45fedfe82c 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -1,5 +1,4 @@ import asyncio -from typing import Tuple from rubicon.objc import objc_method from rubicon.objc.eventloop import EventLoopPolicy, iOSLifecycle @@ -124,5 +123,5 @@ def show_cursor(self): # No-op; mobile doesn't support cursors pass - def get_screens(self) -> Tuple[ScreenImpl, ...]: + def get_screens(self): return [ScreenImpl(UIScreen.mainScreen)] diff --git a/iOS/src/toga_iOS/screen.py b/iOS/src/toga_iOS/screen.py index 3b47d41540..93f6a1859d 100644 --- a/iOS/src/toga_iOS/screen.py +++ b/iOS/src/toga_iOS/screen.py @@ -1,5 +1,3 @@ -from typing import Tuple - from toga.screen import Screen as ScreenInterface @@ -16,13 +14,14 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self) -> str: + def get_name(self): + # Return a dummy name as UIScreen object has no name related attributes. return "iOS Screen" - def get_origin(self) -> Tuple[int, int]: + def get_origin(self): return (0, 0) - def get_size(self) -> Tuple[int, int]: + def get_size(self): return int(self.native.bounds.size.width), int(self.native.bounds.size.height) def get_image_data(self): diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index 9d1148db0c..d13e8b57b1 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -117,7 +117,7 @@ def set_full_screen(self, is_full_screen): def close(self): pass - def get_current_screen(self) -> ScreenImpl: + def get_current_screen(self): return ScreenImpl(UIScreen.mainScreen) def get_image_data(self): diff --git a/iOS/tests_backend/app.py b/iOS/tests_backend/app.py index b2fe05c616..98a4ba0369 100644 --- a/iOS/tests_backend/app.py +++ b/iOS/tests_backend/app.py @@ -8,7 +8,6 @@ NSSearchPathDomainMask, UIApplication, ) -from toga_iOS.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -70,6 +69,3 @@ def terminate(self): def rotate(self): self.native = self.app._impl.native self.native.delegate.application(self.native, didChangeStatusBarOrientation=0) - - def assert_screen_implementation_type(self, screen): - assert isinstance(screen._impl, ScreenImpl) diff --git a/iOS/tests_backend/screen.py b/iOS/tests_backend/screen.py index f6db3bc837..af996ad9fb 100644 --- a/iOS/tests_backend/screen.py +++ b/iOS/tests_backend/screen.py @@ -1,5 +1,4 @@ from toga_iOS.libs import UIScreen -from toga_iOS.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -10,12 +9,6 @@ def __init__(self, screen): self.screen = screen self._impl = screen._impl self.native = screen._impl.native - - def assert_implementation_type(self): - assert isinstance(self._impl, ScreenImpl) - - def assert_native_type(self): - print(type(self.native)) assert isinstance(self.native, UIScreen) def assert_name(self): diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py index 0e13222ae4..08f9a34295 100644 --- a/iOS/tests_backend/window.py +++ b/iOS/tests_backend/window.py @@ -1,7 +1,6 @@ import pytest from toga_iOS.libs import UIApplication, UIWindow -from toga_iOS.screen import Screen as ScreenImpl from .probe import BaseProbe @@ -78,6 +77,3 @@ async def close_select_folder_dialog(self, dialog, result, multiple_select): def has_toolbar(self): pytest.skip("Toolbars not implemented on iOS") - - def assert_screen_implementation_type(self, screen): - assert isinstance(screen._impl, ScreenImpl) diff --git a/output_image.png b/output_image.png deleted file mode 100644 index c34aeb407c7a975decc2ec6473b2d2b0c1166043..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10888 zcmeHN_g9nYw*D;0;HaFVB4PnOiU>$isZxYflG+J9PHBu+ou{DbPT zAF8YVio0)j^v&HrWY0Dk?0Ktw&_3_-6|;_=(ec-IZuEIDcuxD)(O1u|KO5ib{@1~l z^dF9Ym5_*XEOb#{m*f(R$A8BDOr*OF@VA>3QLu~rv?XEWX%Ag`Jv~jSK0p81Ryao0 z=c~P(O>pB*&4G33v3}SNzma=uf2`GD4F_wpu*L^#oVcb7YjU_|3I4Aw+R6yufNU)H zAQKW*9Yj0|N&V>2uWzq3|Gq);sl0I*YqB-dGF8{Bf3~Duo$)2ZqSAMyq_k9W+rFnSgg3eY`F-cEU0vyhA@jr4Uj6wwtC#A-*lq3YwT!}AgOGXW@bD;u zg?c8AIh{o`5t2$vN-BIu97+6j2X;y!cMJkn7rx!-$u#EAmOhkLC>-3fb+2w6oe(aM z7cAx#*5sb8+!XZY<;#~L%uyw4f{`!=BrT|;7D2LqO@ zKJAp1rL^3;pm)S7t;%<#hBqpK{AG?Hy@dm%9x)1-26*;sSvXpvgKzCz1-wp5`YzAb zvKu{-f(Zo*M;-a23q6wQUS03qnwL-g^2;x6*0mu_lYq&V1{{B7ex%l=Hn=lY&u_Gj z3op8Dzd;#ZEJE)Xu3Ahc39g>3ls=!LePgifMtKD;TTaZ<5T9^sx;H8Ap}D{@j>Fjp zZ5$m3EB&Ym4*F4>wua5*WI;<*PqadN6vAiULfzgNjm=e3-EUXijK6CTC_2^Pu0*44$>eNGR2ESz2xN~swlYe zu7urJ`QF?pN$ID%kN@^5T8>@+Z)6nX{yty!rOfJK3(sxQPL(CX(tszCu4tk zeO57sOfar(3;eIo#R~6%a*8Xz-^s6!a~2sBKZCN$K&M-yVvJWV>SWX>y)Fxw(B4a;Rci{Gu!ow{i)`lt8wl?R&&#P?Vy}Jo{9Xl(JHKg6D@r zKm74;si>=0ujbhoOUt7(C6N`>K2jS}#CKWu?gf<1^CQ7HeEV6z zaTv=PI_2HZbI0$EcBsJnxPo^ckD?OEkaY3>z{vI*s z=P*twnl{eP&WE%(Oe!X{*g(%kJf%mvRN{`#7|cQqAmPlzZ92ZgRWNhOu3>|BWW1y%^a;)H6iyGI8j6C z>3w+)Vme+#2hhdH8#~(D_t%9ksTNkfLLa|r%Whroiy23Z$Q0v(H*XGOFyduW{@uHG zo0^(-C|PO41jAX3!&f?5Tjfp$xLXQSa}#1rc;CAWnMOld@Q*YMUIZO7yxNvvFuW6a zW3ve)Smq>aIYc3~qggQu_MN113dte)#aNw3CVHlhXLr_tA1% zVawldb`cY&r>D<6*b+MNL{H^}4~w#+ZuyHv7KS(eSfh|6;*}P}rKZ+~v1eFHm{8W_ z>_91hdD=EsVNu9qrvl>u8L9B~Y&96_*ajmvf}GYh!D7#=SFfgfvaq4FM7p!Fo?gg6 z$<;pbA%`|*B&RkZ^~UFyjc?53pDJJ?dBynEhxl{)2UJz_o>p!?<&T1N+=dl`@~x##J-moZr;#HnHWvF)8v;!>M~(zh61A4Xne`6$AGY$@ zj7qon4`u`|eVsM5!*`&cE`gmoiH^9A=g05gzu(8Tb@;h$qrZ?%@f$3g?s$(*W_rjv zGDaM6)jGd9h+KBP`_;M)o4kiA3T!+& zi5UFYyWiol`BO7?Jq1pc+$DOEV|gx?6v-}Ghv@G`neuqV>cV8BwF%c9CI(=Z%@?Ls zz>~s37N6*iX0x`yox6>0`(LG+=m$=9z!Ve-uBgLk`b93KwoYVOrPqK25F%KuCBJIC zDEec3d|YbBk;CH6H4*Ic`4^)A)$Z433WmY?o9U4YjgnVBMdz@-*!7fqQHX*`rJ)KR zReB$ST!^XL_OCmR-5D0i!^6V@f#BYvU7kIAb|4T`iZc>|1y*4MSeTlc0&qvlZrHY2 z7uB)afK_0~gT8$UoOQpT#c*O3Z1AN4Pzv!IXPDPy2J)y#DXQfpc!vO zchjRrEGZ^C#~GQxI3{BwPtug<2Fv%FMBEMz4(6}S?bY)Y40sS_cJ4gpT(uByZDMY+ z3Y>X;po9jfQYoQ_Idfuy=LXpyc37twg|a#tmzS4KaJ-0V3#=#L=Y+mu5BB=5&WDh~83mQP*FV3kNZxQKH;*p)Z=}k3B3V%ompNT|M)oGBh>^K)@BUiBDyc@sz0R4Nm6%7Vd*^F zx<~tB&8-`zqQaTTk;#yC?(PC&xQtWT^~JAOVsMKeWn1ZmuJr~=N=mc`psbf19UWsa z0s9x!QCx3CPN~%}8~8S^&(m|RwkK6mBl#Hd62=cuxDs6Q+s8)y9P#c568j+uj=O!y-y*M3O_M~?T|82S1LF6-%j!=%9TfDtXSQ9O$vo~h%l zL)0~=uiD|7rIBAhfJiqiKJlv)i};DgZ($5TfA^elZz*#Aq zKUOXAJ$<@ShvFRnub2iAyGY!O7nn(exaMV^e{ljFgq9gl7V7)3;QJ1sgjefW?pY2H z0803EAWmw%ZDFxds9_a zB^aWRmp`A8wiEC$YHD*Zm6q}mK+md?`w2-&{ZNxhzU(#y6C-e@OFko|FARb_C5ps7 zU%u3YFj3xC+%o_5?dy`q>)+k~8H`OoDDdt6{7dc08sxUN+`%nFm3~BBA4fn*k!w8x zm>(GI5z*<_UzeANVk0+y;6;%C!e04+z`&vg-psv5;3)ga;j>TnJORr%?zJD-adG^k zRFsg<5r5Ka_+`=Ds&*bG52Wp0BVI_s8yEE(r`i`dX4GZ8+SmGco=PIwXd&+;&ck`0 zPEJjsya-S2$-+&^aB8VS-==#~purLBJ_ps?ZG0vP5BB+9WB7b^*5*8*uFjE~ARwz% zaA$Z{JTc$7nk9)@J#A_=OC{mklhjljKBxLMybx%M0L&$71w1%))V@YoCvFsdj& zTo>f{Bt3P{cK2)7jzohkChFgOWfPD5Q=@rzclT&{V*{`a6EPT3cT8kLdU`roqx+#S zt2gQCA>em-V~tUCBYkeI6xd;X@2|xw`Ft1?Z?H8`4RD^Iy%APcT^Y%)jXrBX#LFfb=+YXq5IVTw1p6ShL>)L?G9tKV1-GVa! zWsd~XyM(i)WVm)kutbiL)5-*sFJn~y^5qxiyLKH1_*n1GnRP;PHMolFY5HJb z%oK15nqL9eGu{Wd%P>=EN{pn}0*&?Om1$8MfSl6$iyQP1M0`jK{aHdCHJJW~3XxUb z10^9Knf07e!{FJACq3q1NS+$bUwr$UDNJ`9Mom?92p!PNz(HQV6d9YG@O{C|Q5Tdo zpA!hE*QagO_8HW6JItN`ljm5uPbQ*k{JgS)+M-Mg;-9h+dB@Fm0HBAl!^VD>1f6&b z)dgdqak2OS*D@%pz_p$SRwGsWh7HRMyu&dSm7jN?=T_Y=@C49(l(q`Bea|QYWHX8= zy^%JBHduITCCv33kv95i1nKg!OwpD*{@Vwjkr2>F7~Bkj9Uk5ti0BK!Ae!G^o5dWF zt^l2W5E7~m56@X`lnHHiWA8s1aQIuu1iRe#B*@r>ywr7fu7h(DX_xs?BOzh{4_Sup z#X|@XaF^nnZB2gqTy=9Ig^I0K2~u|Qpk7WczJM$E5$TnKCSnx9JUUeeTm`Ehj|qcF z7bh97;A?HjByK4x4WY;Y$L&X1^@zE0O4mN~ zP`yZN{M=d{ z;?Pqy;KDl#zW@Ly8z-7$l%lmgd$U0Uq@susEb~s^%xl2a%Ax*H8rd2205@e3L?<7D zLu-^@19xhJw9$pAJZ)`f`~FBjxx*EvXO#b8YjOE-Y^*GWZ8gv&jX zjgXKJORvlgg?xM@);=pEgElpl zX|(!juitr*;tFJr+~WDW5HY1)vxt#3o~59X*Z(PB<(v?F*je!EEH94NJvT=#fyay5u_$K!-3Xq6L3N; z;N^R0ykv7oVq8Z5$lEJsEarQ^dUpTMmNuu+`T|oojXC+%+uc`M0wa`l{zaS-;Y3d_ z1&}d<=JHT1x6J|6F40Kz7%TX#H~W`|+xF+OI>312`IY!o!Ol~VK3|@W>I$|Gh&!sH z0s+w`BZ&aM7?NApz8n4d1K{9Pb<96nKG!@Ls;QyDT=@1rF~tL9*rTC(tjsFiAblsJM)|RPJSU6b37LqsN)q!)oar8q?pLOE&jTR=SB?Y^#oz@K3GIo2XGATrhXC;D znpKw@?`9Fga_?G@1gv&Q20u5A!E1tr{iRV(#S5Ow!8P=1(->fFiN$*K=#h$kYfTVC z9u^ami(sScPDgE`_rW{f|Mw;W<{I?`Z84EzK9w5Kt~M|*04+C+6x=qwHdN|CPEfXm zRRR`9c+%Prc15YaH92WL@PjT~j6~mG#{ft$M)VnoQJ;XNwr>Ip^@4SllM>KvDr>yd zq18I^SXM(GtV5FnCj(X&xsWIov6Uf={<#MJydf3kn>*mGeL{h!8@Qc*gt;D=0UC`~ z4s-}j(7M)f8-fvrI~fniOiV}s6<2_z5F{##q+?)%W&p{bVGRXM*tKid5poas7SPYd zv{X3=?I1C!Mkb(E5VwFSGL5`oW??r_M@w=7S%_We2F4DBF;5E|nW>t1YG`9}y%>%O z8u~)ZgD^*7p(%8^+bl3J5PBpw{Pz06*1hO-)9|g>zj?Uge|Lwn`QWMGBBiO=#fB@K|;ae4xAz!-#*O7RA zMM=jl#SI^`3Wvp@?dU5klQBi7E8!Kt0n@10&Xxquubf982G?%iL;}YPcyaI~3~Is} z1j06xy*XCSz`zENf>g@<{;;31J zMWo+Ai6XZLQa!}YxBpq%vPb)SqJyh>s1Q4mqU*zhBqF`vK>KR7KBT&)`#-9>UPO`k z$XQI808AFJ4!}(00Yo9r3`3IhTA>u<*$(*fNFnbLR!r;%)|FAUKa4lK{Bb0VQSf+| zn&+1{=7}EJ`4;Nc?pzivWca38(2>@bmgl-YgFV(5oTvzdQXYh|MiZC^Jd^ml8Kwrh zmv^!cB!`lJX{qT)4d;l(k9PX?u;4%;1?dqim`c!Z@kcBPu%W{W#%00XI)CR~ILs zWVH|!W@l$-W)B}abY=X*pJ0JjM>!1+v1aHB)#S#PH(*9rAwA@G)%4v0?8EZ78O8vm zX9;UL?dzXO!XZ)tNN*liI71wU(mh!+IEBoZnVYwQYNNgfWDs3q50*2)zJltbHVZ@r z4(15k6Tm3oJ3l`khN38vomak?$Wl~P1WOGlqynhm0#vSG{qW z6M9l09t1hCQTp$1U95e*VeKm(0Ee}YwbyE}_ObTb$J%QjYop@0hJ*hfHCPZM-f0hv UZx#gRp=~ucwfd>>znAa+4>)k%%m4rY diff --git a/testbed/tests/app/test_screen.py b/testbed/tests/app/test_screen.py index b9136672ce..35a291b224 100644 --- a/testbed/tests/app/test_screen.py +++ b/testbed/tests/app/test_screen.py @@ -4,7 +4,6 @@ import pytest from toga.platform import current_platform -from toga.screen import Screen as ScreenInterface @pytest.fixture @@ -16,14 +15,6 @@ def screen_probe_list(app): return [getattr(module, "ScreenProbe")(screen) for screen in app.screens] -async def test_type(screen_probe_list): - """The type of the implementation and native classes should be correct""" - for screen_probe in screen_probe_list: - assert isinstance(screen_probe.screen, ScreenInterface) - screen_probe.assert_implementation_type() - screen_probe.assert_native_type() - - async def test_name(screen_probe_list): """The name of the screens can be retrieved""" for screen_probe in screen_probe_list: diff --git a/testbed/tests/test_app.py b/testbed/tests/test_app.py index 52054b2093..92e09e7d79 100644 --- a/testbed/tests/test_app.py +++ b/testbed/tests/test_app.py @@ -4,7 +4,6 @@ import toga from toga.colors import CORNFLOWERBLUE, FIREBRICK, REBECCAPURPLE -from toga.screen import Screen as ScreenInterface from toga.style.pack import Pack from .test_window import window_probe @@ -555,12 +554,7 @@ async def test_beep(app): # Test primary screen `origin` and `name` & `origin` uniqueness of other screens. async def test_screens(app, app_probe): - """A screen should have unique origin and name and - the primary screen should have the origin (0,0)""" - assert isinstance(app.screens, list) - for screen in app.screens: - assert isinstance(screen, ScreenInterface) - app_probe.assert_screen_implementation_type(screen) + """Screens must have unique origins and names, with the primary screen at (0,0).""" # Get the origin of screen 0 assert app.screens[0].origin == (0, 0) diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 894be1f68d..0891e1d8ad 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -12,7 +12,6 @@ import toga from toga.colors import CORNFLOWERBLUE, GOLDENROD, REBECCAPURPLE -from toga.screen import Screen as ScreenInterface from toga.style.pack import COLUMN, Pack @@ -490,11 +489,8 @@ async def test_as_image(main_window, main_window_probe): # Test the `origin`, `position` and `screen_position`. async def test_screen(main_window, main_window_probe): - """A window can be moved to a different screen - and can be moved using both the absolute position - and relative screen position""" - assert isinstance(main_window.screen, ScreenInterface) - main_window_probe.assert_screen_implementation_type(main_window.screen) + """The window can be relocated to another screen, using both absolute and relative screen positions.""" + if main_window.screen.origin == (0, 0): if toga.platform.current_platform in {"android", "iOS", "textual"}: pytest.xfail("Window.position is non functional on current platform.") diff --git a/textual/src/toga_textual/app.py b/textual/src/toga_textual/app.py index 31df693d0d..e7790a7af8 100644 --- a/textual/src/toga_textual/app.py +++ b/textual/src/toga_textual/app.py @@ -1,5 +1,4 @@ import asyncio -from typing import Tuple from textual.app import App as TextualApp @@ -71,7 +70,7 @@ def show_cursor(self): def hide_cursor(self): pass - def get_screens(self) -> Tuple[ScreenImpl, ...]: + def get_screens(self): return [ScreenImpl(window._impl.native) for window in self.interface.windows] diff --git a/textual/src/toga_textual/screen.py b/textual/src/toga_textual/screen.py index d7042005a7..3fa1d22260 100644 --- a/textual/src/toga_textual/screen.py +++ b/textual/src/toga_textual/screen.py @@ -1,5 +1,3 @@ -from typing import Tuple - from toga.screen import Screen as ScreenInterface @@ -16,13 +14,13 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self) -> str: + def get_name(self): return "Textual Screen" - def get_origin(self) -> Tuple[int, int]: + def get_origin(self): return (0, 0) - def get_size(self) -> Tuple[int, int]: + def get_size(self): return (self.native.size.width, self.native.size.height) def get_image_data(self): diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index 553cc8639c..9400019989 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -170,5 +170,5 @@ def close(self): def set_full_screen(self, is_full_screen): pass - def get_current_screen(self) -> ScreenImpl: + def get_current_screen(self): return ScreenImpl(self.native) diff --git a/textual/tests_backend/screen.py b/textual/tests_backend/screen.py index dd256cccaa..b2480eeaa6 100644 --- a/textual/tests_backend/screen.py +++ b/textual/tests_backend/screen.py @@ -1,5 +1,3 @@ -from toga_textual.screen import Screen as ScreenImpl - from textual.screen import Screen as TextualScreen @@ -9,12 +7,6 @@ def __init__(self, screen): self.screen = screen self._impl = screen._impl self.native = screen._impl.native - - def assert_implementation_type(self): - assert isinstance(self._impl, ScreenImpl) - - def assert_native_type(self): - print(type(self.native)) assert isinstance(self.native, TextualScreen) def assert_name(self): diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index 1b06d6e16b..b359a137d0 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -1,5 +1,3 @@ -from typing import Tuple - import toga from toga.command import Separator from toga_web.libs import create_element, js @@ -214,5 +212,5 @@ def show_cursor(self): def hide_cursor(self): self.interface.factory.not_implemented("App.hide_cursor()") - def get_screens(self) -> Tuple[ScreenImpl, ...]: + def get_screens(self): return [ScreenImpl(js.document.documentElement)] diff --git a/web/src/toga_web/screen.py b/web/src/toga_web/screen.py index eeea53ba2e..dd876bbae4 100644 --- a/web/src/toga_web/screen.py +++ b/web/src/toga_web/screen.py @@ -1,5 +1,3 @@ -from typing import Tuple - from toga.screen import Screen as ScreenInterface @@ -16,13 +14,13 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self) -> str: + def get_name(self): return "Web Screen" - def get_origin(self) -> Tuple[int, int]: + def get_origin(self): return (0, 0) - def get_size(self) -> Tuple[int, int]: + def get_size(self): return self.native.clientWidth, self.native.clientHeight def get_image_data(self): diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index 27dd4a460b..9595636404 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -80,5 +80,5 @@ def set_size(self, size): def set_full_screen(self, is_full_screen): self.interface.factory.not_implemented("Window.set_full_screen()") - def get_current_screen(self) -> ScreenImpl: + def get_current_screen(self): return ScreenImpl(js.document.documentElement) diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 6c070d6b2e..10b661fd95 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -3,7 +3,6 @@ import sys import threading from ctypes import windll -from typing import Tuple import System.Windows.Forms as WinForms from System import Environment, Threading @@ -309,7 +308,7 @@ def exit(self): # pragma: no cover self._is_exiting = True self.native.Exit() - def get_screens(self) -> Tuple[ScreenImpl, ...]: + def get_screens(self): primary_screen = ScreenImpl(WinForms.Screen.PrimaryScreen) screen_list = [primary_screen] + [ ScreenImpl(native=screen) diff --git a/winforms/src/toga_winforms/screen.py b/winforms/src/toga_winforms/screen.py index 513d281ded..168bd3758c 100644 --- a/winforms/src/toga_winforms/screen.py +++ b/winforms/src/toga_winforms/screen.py @@ -1,5 +1,3 @@ -from typing import Tuple - from System.Drawing import ( Bitmap, Graphics, @@ -25,16 +23,16 @@ def __new__(cls, native): cls._instances[native] = instance return instance - def get_name(self) -> str: - return str(self.native.DeviceName) + def get_name(self): + return self.native.DeviceName - def get_origin(self) -> Tuple[int, int]: + def get_origin(self): return self.native.Bounds.X, self.native.Bounds.Y - def get_size(self) -> Tuple[int, int]: + def get_size(self): return self.native.Bounds.Width, self.native.Bounds.Height - def get_image_data(self) -> bytes: + def get_image_data(self): bitmap = Bitmap(*self.get_size()) graphics = Graphics.FromImage(bitmap) source_point = Point(*self.get_origin()) diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index c34556f91d..fb06dfa03c 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -190,7 +190,7 @@ def resize_content(self): self.native.ClientSize.Height - vertical_shift, ) - def get_current_screen(self) -> ScreenImpl: + def get_current_screen(self): return ScreenImpl(WinForms.Screen.FromControl(self.native)) def get_image_data(self): diff --git a/winforms/tests_backend/app.py b/winforms/tests_backend/app.py index 14eeee4cd5..4a2640e538 100644 --- a/winforms/tests_backend/app.py +++ b/winforms/tests_backend/app.py @@ -8,7 +8,6 @@ from System.Windows.Forms import Application, Cursor from toga_winforms.keys import toga_to_winforms_key, winforms_to_toga_key -from toga_winforms.screen import Screen as ScreenImpl from .probe import BaseProbe from .window import WindowProbe @@ -155,6 +154,3 @@ def activate_menu_minimize(self): def keystroke(self, combination): return winforms_to_toga_key(toga_to_winforms_key(combination)) - - def assert_screen_implementation_type(self, screen): - assert isinstance(screen._impl, ScreenImpl) diff --git a/winforms/tests_backend/screen.py b/winforms/tests_backend/screen.py index 91d6b82f30..890d606a29 100644 --- a/winforms/tests_backend/screen.py +++ b/winforms/tests_backend/screen.py @@ -1,7 +1,5 @@ from System.Windows.Forms import Screen as WinFormsScreen -from toga_winforms.screen import Screen as ScreenImpl - from .probe import BaseProbe @@ -11,11 +9,6 @@ def __init__(self, screen): self.screen = screen self._impl = screen._impl self.native = screen._impl.native - - def assert_implementation_type(self): - assert isinstance(self._impl, ScreenImpl) - - def assert_native_type(self): assert isinstance(self.native, WinFormsScreen) def assert_name(self): diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 2b6d65a45a..5fc6ae3c94 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -11,8 +11,6 @@ ToolStripSeparator, ) -from toga_winforms.screen import Screen as ScreenImpl - from .probe import BaseProbe @@ -150,6 +148,3 @@ def assert_toolbar_item(self, index, label, tooltip, has_icon, enabled): def press_toolbar_button(self, index): self._native_toolbar_item(index).OnClick(EventArgs.Empty) - - def assert_screen_implementation_type(self, screen): - assert isinstance(screen._impl, ScreenImpl) From d015c2c5c289b13b45cc0fe35be1084ee2a164c3 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Wed, 24 Jan 2024 23:36:49 -0800 Subject: [PATCH 064/102] Empty commit for CI From 6406d5757b3a54482611df4bc9210a7420015f9a Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Wed, 31 Jan 2024 03:45:01 -0800 Subject: [PATCH 065/102] Update cocoa/src/toga_cocoa/screen.py Co-authored-by: Russell Keith-Magee --- cocoa/src/toga_cocoa/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index dee8f7398a..b991551c5f 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -49,7 +49,7 @@ def get_image_data(self): # Get the size of the CGImage size = CGImageGetWidth(cg_image), CGImageGetHeight(cg_image) # Create an NSImage from the CGImage - ns_image = NSImage.alloc().initWithCGImage_size_(cg_image, size) + ns_image = NSImage.alloc().initWithCGImage(cg_image, size=size) # Drain the autorelease pool to release memory pool.release() From eb84c07421939d0ac904ecbdfd7675157c2c1b44 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Wed, 31 Jan 2024 03:45:24 -0800 Subject: [PATCH 066/102] Update core/tests/test_window.py Co-authored-by: Russell Keith-Magee --- core/tests/test_window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/tests/test_window.py b/core/tests/test_window.py index cd6a417506..f33b70a0e3 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -393,8 +393,9 @@ def test_screen_position(window, app): window.position = (-100, -100) assert window.position != initial_position assert window.position == (-100, -100) - assert window.screen_position == (1266, 668) + + # Move the window to a new position. window.screen_position = (100, 100) assert window.position == (-1266, -668) assert window.screen_position == (100, 100) From 6747b3b1cef07bc17a39ae120b2fc41bc0ae5595 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Wed, 31 Jan 2024 03:47:46 -0800 Subject: [PATCH 067/102] Update testbed/tests/test_app.py Co-authored-by: Russell Keith-Magee --- testbed/tests/test_app.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/testbed/tests/test_app.py b/testbed/tests/test_app.py index 92e09e7d79..25644ba234 100644 --- a/testbed/tests/test_app.py +++ b/testbed/tests/test_app.py @@ -560,12 +560,9 @@ async def test_screens(app, app_probe): assert app.screens[0].origin == (0, 0) # Check for unique names - screen_names = set() - unique_names = all( - screen.name not in screen_names and not screen_names.add(screen.name) - for screen in app.screens - ) - assert unique_names is True + screen_names = [s.name for s in app.screens] + unique_names = set(screen_names) + assert len(screen_names) == len(unique_names) # Check that the origin of every other screen is not "0,0" origins_not_zero = all(screen.origin != (0, 0) for screen in app.screens[1:]) From 2d0022487c07ced88e2bff2cadbc2495e625df1d Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 31 Jan 2024 08:57:21 -0800 Subject: [PATCH 068/102] Fixed tests as per recommendation --- android/tests_backend/screen.py | 14 ++----- cocoa/src/toga_cocoa/libs/foundation.py | 4 -- cocoa/src/toga_cocoa/screen.py | 5 --- cocoa/tests_backend/screen.py | 12 +----- core/tests/test_window.py | 22 +--------- dummy/src/toga_dummy/app.py | 5 +++ gtk/src/toga_gtk/screen.py | 11 +++-- gtk/tests_backend/screen.py | 18 ++++---- iOS/tests_backend/screen.py | 15 ++----- testbed/tests/{ => app}/test_app.py | 2 +- testbed/tests/app/test_screen.py | 36 +++++++--------- testbed/tests/test_window.py | 55 ++++++++++++++++++------- textual/tests_backend/screen.py | 12 ++---- winforms/tests_backend/screen.py | 10 +---- 14 files changed, 88 insertions(+), 133 deletions(-) rename testbed/tests/{ => app}/test_app.py (99%) diff --git a/android/tests_backend/screen.py b/android/tests_backend/screen.py index b7de2cd18a..fb5b4ab6b3 100644 --- a/android/tests_backend/screen.py +++ b/android/tests_backend/screen.py @@ -1,3 +1,4 @@ +import pytest from android.view import Display from toga_android.widgets.base import Scalable @@ -14,14 +15,5 @@ def __init__(self, app, screen): self.init_scale(app._impl.native) assert isinstance(self.native, Display) - def assert_name(self): - assert self.screen.name == self.native.getName() - - def assert_origin(self): - assert self.screen.origin == (0, 0) - - def assert_size(self): - assert self.screen.size == ( - self.scale_out(self.native.getWidth()), - self.scale_out(self.native.getHeight()), - ) + def get_screenshot(self): + pytest.skip("Screen.as_image is not implemented on Android.") diff --git a/cocoa/src/toga_cocoa/libs/foundation.py b/cocoa/src/toga_cocoa/libs/foundation.py index c2bac3a3d9..ffbf612f60 100644 --- a/cocoa/src/toga_cocoa/libs/foundation.py +++ b/cocoa/src/toga_cocoa/libs/foundation.py @@ -51,7 +51,3 @@ ###################################################################### # NSValue.h NSNumber = ObjCClass("NSNumber") - -###################################################################### -# NSAutoreleasePool.h -NSAutoreleasePool = ObjCClass("NSAutoreleasePool") diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screen.py index b991551c5f..2c5e6fe9b2 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screen.py @@ -2,7 +2,6 @@ from toga_cocoa.libs import ( CGImageGetHeight, CGImageGetWidth, - NSAutoreleasePool, NSImage, core_graphics, ) @@ -44,13 +43,9 @@ def get_image_data(self): cg_direct_display_id, self.native.frame, ) - # Create an autorelease pool to manage memory - pool = NSAutoreleasePool.alloc().init() # Get the size of the CGImage size = CGImageGetWidth(cg_image), CGImageGetHeight(cg_image) # Create an NSImage from the CGImage ns_image = NSImage.alloc().initWithCGImage(cg_image, size=size) - # Drain the autorelease pool to release memory - pool.release() return ns_image diff --git a/cocoa/tests_backend/screen.py b/cocoa/tests_backend/screen.py index ec665748e9..c5a7c4d2d3 100644 --- a/cocoa/tests_backend/screen.py +++ b/cocoa/tests_backend/screen.py @@ -11,13 +11,5 @@ def __init__(self, screen): self.native = screen._impl.native assert isinstance(self.native, NSScreen) - def assert_name(self): - assert self.screen.name == self.native.localizedName - - def assert_origin(self): - frame_native = self.native.frame - assert self.screen.origin == (frame_native.origin.x, frame_native.origin.y) - - def assert_size(self): - frame_native = self.native.frame - assert self.screen.size == (frame_native.size.width, frame_native.size.height) + def get_screenshot(self): + return self.screen.as_image() diff --git a/core/tests/test_window.py b/core/tests/test_window.py index f33b70a0e3..bd10287bb9 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -368,27 +368,7 @@ def test_screen(window, app): def test_screen_position(window, app): """The window can be relocated using absolute and relative screen positions.""" - # _________________________________________________ - # Display Setup: | - # ________________________________________________| - # |--1366--| | - # (-1366,-768) _________ | - # | | | | - # 768 |Secondary| | - # | | Screen | | - # | |_________|(0,0) | - # _________ | - # | | | | - # 1080 | Primary | | - # | | Screen | | - # | |_________|(1920,1080) | - # |---1920--| | - # ________________________________________________| - # `window.screen` will return `Secondary Screen` | - # as window is on secondary screen to better | - # test out the differences between | - # `window.position` & `window.screen_position`. | - # ________________________________________________| + # Details about screen layout are in toga_dummy=>app.py=>get_screens() initial_position = window.position window.position = (-100, -100) assert window.position != initial_position diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index 42872224a5..d665b295e8 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -85,6 +85,11 @@ def get_screens(self): # | |_________|(1920,1080) | # |---1920--| | # ________________________________________________| + # `window.screen` will return `Secondary Screen` | + # as window is on secondary screen to better | + # test out the differences between | + # `window.position` & `window.screen_position`. | + # ________________________________________________| return [ ScreenImpl(native=("Primary Screen", (0, 0), (1920, 1080))), ScreenImpl(native=("Secondary Screen", (-1366, -768), (1366, 768))), diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index 9d63e1c91b..5c28a9c574 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -30,8 +30,11 @@ def get_size(self): return geometry.width, geometry.height def get_image_data(self): - if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": # pragma: no cover - # Only works for x11 + if "WAYLAND_DISPLAY" in os.environ: + # Not implemented on wayland due to wayland security policies. + self.interface.factory.not_implemented("Screen.get_image_data() on Wayland") + else: + # Only works for Xorg display = self.native.get_display() screen = display.get_default_screen() window = screen.get_root_window() @@ -45,7 +48,3 @@ def get_image_data(self): else: print("Failed to save screenshot to buffer.") return None - # CI runs on wayland - else: - # Not implemented on wayland due to wayland security policies. - self.interface.factory.not_implemented("Screen.get_image_data() on Wayland") diff --git a/gtk/tests_backend/screen.py b/gtk/tests_backend/screen.py index 745b0270e2..c07ad20ae2 100644 --- a/gtk/tests_backend/screen.py +++ b/gtk/tests_backend/screen.py @@ -1,5 +1,6 @@ import os +import pytest from gi.repository import GdkX11 from .probe import BaseProbe @@ -14,16 +15,11 @@ def __init__(self, screen): if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": assert isinstance(self.native, GdkX11.X11Monitor) else: - # TODO: Check for the wayland monitor native type + # TODO: Check for the other monitor native types pass - def assert_name(self): - assert self.screen.name == self.native.get_model() - - def assert_origin(self): - geometry = self.native.get_geometry() - assert self.screen.origin == (geometry.x, geometry.y) - - def assert_size(self): - geometry = self.native.get_geometry() - assert self.screen.size == (geometry.width, geometry.height) + def get_screenshot(self): + if "WAYLAND_DISPLAY" in os.environ: + pytest.skip("Screen.as_image() is not implemented on wayland.") + else: + return self.screen.as_image() diff --git a/iOS/tests_backend/screen.py b/iOS/tests_backend/screen.py index af996ad9fb..15b199ce70 100644 --- a/iOS/tests_backend/screen.py +++ b/iOS/tests_backend/screen.py @@ -1,3 +1,5 @@ +import pytest + from toga_iOS.libs import UIScreen from .probe import BaseProbe @@ -11,14 +13,5 @@ def __init__(self, screen): self.native = screen._impl.native assert isinstance(self.native, UIScreen) - def assert_name(self): - assert self.screen.name == "iOS Screen" - - def assert_origin(self): - assert self.screen.origin == (0, 0) - - def assert_size(self): - assert self.screen.size == ( - int(self.native.bounds.size.width), - int(self.native.bounds.size.height), - ) + def get_screenshot(self): + pytest.skip("Screen.as_image is not implemented on iOS.") diff --git a/testbed/tests/test_app.py b/testbed/tests/app/test_app.py similarity index 99% rename from testbed/tests/test_app.py rename to testbed/tests/app/test_app.py index 25644ba234..32e40b32ce 100644 --- a/testbed/tests/test_app.py +++ b/testbed/tests/app/test_app.py @@ -6,7 +6,7 @@ from toga.colors import CORNFLOWERBLUE, FIREBRICK, REBECCAPURPLE from toga.style.pack import Pack -from .test_window import window_probe +from ..test_window import window_probe @pytest.fixture diff --git a/testbed/tests/app/test_screen.py b/testbed/tests/app/test_screen.py index 35a291b224..42386dff0f 100644 --- a/testbed/tests/app/test_screen.py +++ b/testbed/tests/app/test_screen.py @@ -1,4 +1,3 @@ -import os from importlib import import_module import pytest @@ -15,46 +14,39 @@ def screen_probe_list(app): return [getattr(module, "ScreenProbe")(screen) for screen in app.screens] -async def test_name(screen_probe_list): +async def test_name(app): """The name of the screens can be retrieved""" - for screen_probe in screen_probe_list: - assert isinstance(screen_probe.screen.name, str) - screen_probe.assert_name() + for screen in app.screens: + # Just check that it returns a string as the name will be platform specific. + assert isinstance(screen.name, str) -async def test_origin(screen_probe_list): +async def test_origin(app): """The origin of the screens can be retrieved""" - for screen_probe in screen_probe_list: - origin = screen_probe.screen.origin + for screen in app.screens: + origin = screen.origin assert ( isinstance(origin, tuple) and len(origin) == 2 and all(isinstance(val, int) for val in origin) ) - screen_probe.assert_origin() -async def test_size(screen_probe_list): +async def test_size(app): """The size of the screens can be retrieved""" - for screen_probe in screen_probe_list: - size = screen_probe.screen.size + for screen in app.screens: + size = screen.size assert ( isinstance(size, tuple) and len(size) == 2 - and all(isinstance(val, int) for val in size) + # Check that neither the width or height is zero. + and all(isinstance(val, int) and val > 0 for val in size) ) - screen_probe.assert_size() async def test_as_image(screen_probe_list): """A screen can be captured as an image""" for screen_probe in screen_probe_list: - if current_platform in {"android", "iOS", "textual"}: - pytest.xfail("Screen.as_image is not implemented on current platform.") - elif ( - current_platform == "linux" - and os.environ.get("XDG_SESSION_TYPE", "").lower() != "x11" - ): - pytest.xfail("Screen.as_image() is not supported on wayland.") - screenshot = screen_probe.screen.as_image() + # Using a probe for test as feature is not implemented on some platforms. + screenshot = screen_probe.get_screenshot() assert screenshot.size == screen_probe.screen.size diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 0891e1d8ad..284d9bcd9e 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -152,6 +152,17 @@ async def test_full_screen(main_window, main_window_probe): main_window.full_screen = False await main_window_probe.wait_for_window("Full screen is a no-op") + # Test the `origin`, `position` and `screen_position`. + async def test_screen(main_window, main_window_probe): + """The window can be relocated to another screen, using both absolute and relative screen positions.""" + assert main_window.screen.origin == (0, 0) + initial_size = main_window.size + main_window.position = (150, 50) + await main_window_probe.wait_for_window("Main window can't be moved") + assert main_window.size == initial_size + assert main_window.position == (0, 0) + assert main_window.screen_position == (0, 0) + else: #################################################################################### # Desktop platform tests @@ -479,6 +490,35 @@ async def test_full_screen(second_window, second_window_probe): assert not second_window_probe.is_full_screen assert second_window_probe.content_size == initial_content_size + # Test the `position`, `screen_position` and `screen`. + @pytest.mark.parametrize( + "second_window_kwargs", + [dict(title="Secondary Window", position=(200, 150))], + ) + async def test_screen(second_window, second_window_probe): + """The window can be relocated to another screen, using both absolute and relative screen positions.""" + + initial_position = second_window.position + + # Move the window using absolute position. + second_window.position = (200, 200) + await second_window_probe.wait_for_window("Secondary window has been moved") + assert second_window.position != initial_position + + # `position` and `screen_position` will be same as the window will be in primary screen. + assert second_window.position == (200, 200) + assert second_window.screen_position == (200, 200) + + # Move the window between available screens and assert its `screen_position` + for screen in second_window.app.screens: + second_window.screen = screen + await second_window_probe.wait_for_window("Secondary window has been moved") + assert second_window.screen == screen + assert second_window.screen_position == ( + second_window.position[0] - screen.origin[0], + second_window.position[1] - screen.origin[1], + ) + async def test_as_image(main_window, main_window_probe): """The window can be captured as a screenshot""" @@ -487,21 +527,6 @@ async def test_as_image(main_window, main_window_probe): main_window_probe.assert_image_size(screenshot.size, main_window_probe.content_size) -# Test the `origin`, `position` and `screen_position`. -async def test_screen(main_window, main_window_probe): - """The window can be relocated to another screen, using both absolute and relative screen positions.""" - - if main_window.screen.origin == (0, 0): - if toga.platform.current_platform in {"android", "iOS", "textual"}: - pytest.xfail("Window.position is non functional on current platform.") - initial_position = main_window.position - main_window.position = (200, 200) - assert main_window.position != initial_position - # position and screen_position should be equal on screen with origin (0, 0) - assert main_window.position == (200, 200) - assert main_window.screen_position == (200, 200) - - ######################################################################################## # Dialog tests ######################################################################################## diff --git a/textual/tests_backend/screen.py b/textual/tests_backend/screen.py index b2480eeaa6..5951eb515e 100644 --- a/textual/tests_backend/screen.py +++ b/textual/tests_backend/screen.py @@ -1,3 +1,5 @@ +import pytest + from textual.screen import Screen as TextualScreen @@ -9,11 +11,5 @@ def __init__(self, screen): self.native = screen._impl.native assert isinstance(self.native, TextualScreen) - def assert_name(self): - assert self.screen.name == "Textual Screen" - - def assert_origin(self): - assert self.screen.origin == (0, 0) - - def assert_size(self): - assert self.screen.size == (self.native.size.width, self.native.size.height) + def get_screenshot(self): + pytest.skip("Screen.as_image is not implemented on textual.") diff --git a/winforms/tests_backend/screen.py b/winforms/tests_backend/screen.py index 890d606a29..08d89a6ee7 100644 --- a/winforms/tests_backend/screen.py +++ b/winforms/tests_backend/screen.py @@ -11,11 +11,5 @@ def __init__(self, screen): self.native = screen._impl.native assert isinstance(self.native, WinFormsScreen) - def assert_name(self): - assert self.screen.name == self.native.DeviceName - - def assert_origin(self): - assert self.screen.origin == (self.native.Bounds.X, self.native.Bounds.Y) - - def assert_size(self): - assert self.screen.size == (self.native.Bounds.Width, self.native.Bounds.Height) + def get_screenshot(self): + return self.screen.as_image() From 00af21b4580f2ecb2db63f50c04ac45d259bbf78 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 31 Jan 2024 09:11:21 -0800 Subject: [PATCH 069/102] Miscellaneous Fix --- gtk/src/toga_gtk/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screen.py index 5c28a9c574..d06ccc379c 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screen.py @@ -45,6 +45,6 @@ def get_image_data(self): success, buffer = screenshot.save_to_bufferv("png", [], []) if success: return bytes(buffer) - else: + else: # pragma: no cover print("Failed to save screenshot to buffer.") return None From c460833644fc3c6832ee31d7522d3b01368c5bbc Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Thu, 1 Feb 2024 04:18:24 -0800 Subject: [PATCH 070/102] Update changes/1930.feature.rst Co-authored-by: Russell Keith-Magee --- changes/1930.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/1930.feature.rst b/changes/1930.feature.rst index 87814368e4..f3a7076122 100644 --- a/changes/1930.feature.rst +++ b/changes/1930.feature.rst @@ -1 +1 @@ -APIs for detecting multiple displays or screens and setting windows on them were added. +Toga apps can now access details about the screens attached to the computer. Window position APIs have been extended to allow for placement on a specific screen, and positioning relative to a specific screen. From d2a266da057071f245f90b6af8dea531ee303323 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 1 Feb 2024 05:16:47 -0800 Subject: [PATCH 071/102] Misc commit --- core/src/toga/app.py | 2 +- core/src/toga/screen.py | 7 ++++++- gtk/src/toga_gtk/app.py | 14 +++++++------- gtk/tests_backend/screen.py | 12 ++++++++++-- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index ccd8738051..edaa14263d 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -499,7 +499,7 @@ def _create_impl(self): self.factory.App(interface=self) @property - def screens(self) -> tuple[Screen, ...]: + def screens(self) -> list[Screen, ...]: """Returns a list of available screens.""" return [screen.interface for screen in self._impl.get_screens()] diff --git a/core/src/toga/screen.py b/core/src/toga/screen.py index c8415cbbf1..01fe869e88 100644 --- a/core/src/toga/screen.py +++ b/core/src/toga/screen.py @@ -1,8 +1,13 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from toga.images import Image from toga.platform import get_platform_factory +if TYPE_CHECKING: + from toga.images import ImageT + class Screen: def __init__(self, _impl): @@ -24,7 +29,7 @@ def size(self) -> tuple[int, int]: """The size of the screen, as a ``(width, height)`` tuple.""" return self._impl.get_size() - def as_image(self, format=Image) -> Image: + def as_image(self, format: type[ImageT] = Image) -> ImageT: """Render the current contents of the screen as an image. :param format: Format to provide. Defaults to :class:`~toga.images.Image`; also diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index b7d71182dd..c3b5d99a46 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -191,20 +191,20 @@ def main_loop(self): def get_screens(self): display = Gdk.Display.get_default() - # CI runs on wayland - if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": # pragma: no cover - primary_screen = ScreenImpl(display.get_primary_monitor()) - screen_list = [primary_screen] + [ + if "WAYLAND_DISPLAY" in os.environ: # pragma: no cover + # `get_primary_monitor()` doesn't work on wayland, so return as it is. + return [ ScreenImpl(native=display.get_monitor(i)) for i in range(display.get_n_monitors()) - if display.get_monitor(i) != primary_screen.native ] - return screen_list else: - return [ + primary_screen = ScreenImpl(display.get_primary_monitor()) + screen_list = [primary_screen] + [ ScreenImpl(native=display.get_monitor(i)) for i in range(display.get_n_monitors()) + if display.get_monitor(i) != primary_screen.native ] + return screen_list def set_main_window(self, window): pass diff --git a/gtk/tests_backend/screen.py b/gtk/tests_backend/screen.py index c07ad20ae2..b1a619f2c8 100644 --- a/gtk/tests_backend/screen.py +++ b/gtk/tests_backend/screen.py @@ -12,11 +12,19 @@ def __init__(self, screen): self.screen = screen self._impl = screen._impl self.native = screen._impl.native - if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11": + # Using XDG_SESSION_TYPE to detect specific native monitor types + # Use WAYLAND_DISPLAY for everything else + session_type = os.environ.get("XDG_SESSION_TYPE", "").lower() + if session_type == "x11": assert isinstance(self.native, GdkX11.X11Monitor) + elif session_type == "wayland": + # For wayland, the native type is __gi__.GdkWaylandMonitor + # But cannot be imported directly. + pass else: + print(session_type) + assert session_type == "a" # TODO: Check for the other monitor native types - pass def get_screenshot(self): if "WAYLAND_DISPLAY" in os.environ: From 2361fdfb19746a66b2197b2e2a5b0438e6071cfe Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 1 Feb 2024 06:23:38 -0800 Subject: [PATCH 072/102] Misc commit --- gtk/tests_backend/screen.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gtk/tests_backend/screen.py b/gtk/tests_backend/screen.py index b1a619f2c8..ab02da5b9f 100644 --- a/gtk/tests_backend/screen.py +++ b/gtk/tests_backend/screen.py @@ -19,11 +19,11 @@ def __init__(self, screen): assert isinstance(self.native, GdkX11.X11Monitor) elif session_type == "wayland": # For wayland, the native type is __gi__.GdkWaylandMonitor - # But cannot be imported directly. + # But it cannot be imported directly. pass else: - print(session_type) - assert session_type == "a" + assert self.native == "a" + # session_type is "" for CI # TODO: Check for the other monitor native types def get_screenshot(self): From 4ed3ed38573a945a6f4569d0731ffb78a88d9734 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Thu, 1 Feb 2024 06:28:23 -0800 Subject: [PATCH 073/102] Empty commit for CI From be8fa670624f86c10497467100b95feb0caa1dd7 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 1 Feb 2024 07:56:28 -0800 Subject: [PATCH 074/102] Various fixes --- android/tests_backend/screen.py | 3 ++- cocoa/tests_backend/screen.py | 7 ++++--- core/tests/app/test_screen.py | 21 +++++++++++++++++---- gtk/tests_backend/screen.py | 23 ++++++++++------------- iOS/tests_backend/screen.py | 5 +++-- testbed/tests/app/test_screen.py | 22 ++++++++++++++-------- testbed/tests/test_window.py | 4 +++- winforms/tests_backend/screen.py | 8 +++++--- 8 files changed, 58 insertions(+), 35 deletions(-) diff --git a/android/tests_backend/screen.py b/android/tests_backend/screen.py index fb5b4ab6b3..e031c4317e 100644 --- a/android/tests_backend/screen.py +++ b/android/tests_backend/screen.py @@ -1,6 +1,7 @@ import pytest from android.view import Display +from toga.images import Image as TogaImage from toga_android.widgets.base import Scalable from .probe import BaseProbe @@ -15,5 +16,5 @@ def __init__(self, app, screen): self.init_scale(app._impl.native) assert isinstance(self.native, Display) - def get_screenshot(self): + def get_screenshot(self, format=TogaImage): pytest.skip("Screen.as_image is not implemented on Android.") diff --git a/cocoa/tests_backend/screen.py b/cocoa/tests_backend/screen.py index c5a7c4d2d3..9a20af536c 100644 --- a/cocoa/tests_backend/screen.py +++ b/cocoa/tests_backend/screen.py @@ -1,15 +1,16 @@ +from toga.images import Image as TogaImage from toga_cocoa.libs import NSScreen from .probe import BaseProbe class ScreenProbe(BaseProbe): - def __init__(self, screen): + def __init__(self, app, screen): super().__init__() self.screen = screen self._impl = screen._impl self.native = screen._impl.native assert isinstance(self.native, NSScreen) - def get_screenshot(self): - return self.screen.as_image() + def get_screenshot(self, format=TogaImage): + return self.screen.as_image(format=format) diff --git a/core/tests/app/test_screen.py b/core/tests/app/test_screen.py index ea2e68ded7..f42923277b 100644 --- a/core/tests/app/test_screen.py +++ b/core/tests/app/test_screen.py @@ -1,3 +1,6 @@ +from PIL.Image import Image as PILImage + +from toga.images import Image as TogaImage from toga_dummy.utils import assert_action_performed @@ -21,7 +24,17 @@ def test_size(app): def test_as_image(app): """A screen can be captured as an image""" - screenshot = app.screens[0].as_image() - assert_action_performed(app.screens[0], "get image data") - # Don't need to check the raw data; just check it's the right screen size. - assert screenshot.size == app.screens[0].size + for screen in app.screens: + # `as_image()` should default to `toga.images.Image` as format. + toga_image_screenshot = screen.as_image() + assert_action_performed(screen, "get image data") + # Check if returned image is of type `toga.images.Image`. + assert isinstance(toga_image_screenshot, TogaImage) + # Don't need to check the raw data; just check it's the right screen size. + assert toga_image_screenshot.size == screen.size + + pil_screenshot = screen.as_image(format=PILImage) + assert_action_performed(screen, "get image data") + # Check if returned image is of type `PIL.Image.Image`. + assert isinstance(pil_screenshot, PILImage) + assert pil_screenshot.size == screen.size diff --git a/gtk/tests_backend/screen.py b/gtk/tests_backend/screen.py index ab02da5b9f..0428c8f7f7 100644 --- a/gtk/tests_backend/screen.py +++ b/gtk/tests_backend/screen.py @@ -3,31 +3,28 @@ import pytest from gi.repository import GdkX11 +from toga.images import Image as TogaImage + from .probe import BaseProbe class ScreenProbe(BaseProbe): - def __init__(self, screen): + def __init__(self, app, screen): super().__init__() self.screen = screen self._impl = screen._impl self.native = screen._impl.native - # Using XDG_SESSION_TYPE to detect specific native monitor types - # Use WAYLAND_DISPLAY for everything else - session_type = os.environ.get("XDG_SESSION_TYPE", "").lower() - if session_type == "x11": - assert isinstance(self.native, GdkX11.X11Monitor) - elif session_type == "wayland": - # For wayland, the native type is __gi__.GdkWaylandMonitor + if "WAYLAND_DISPLAY" in os.environ: + # TODO: + # For wayland, the native type is `__gi__.GdkWaylandMonitor` # But it cannot be imported directly. pass else: - assert self.native == "a" - # session_type is "" for CI - # TODO: Check for the other monitor native types + assert isinstance(self.native, GdkX11.X11Monitor) + # For CI, the native type is also GdkX11.X11Monitor - def get_screenshot(self): + def get_screenshot(self, format=TogaImage): if "WAYLAND_DISPLAY" in os.environ: pytest.skip("Screen.as_image() is not implemented on wayland.") else: - return self.screen.as_image() + return self.screen.as_image(format=format) diff --git a/iOS/tests_backend/screen.py b/iOS/tests_backend/screen.py index 15b199ce70..1493015c1e 100644 --- a/iOS/tests_backend/screen.py +++ b/iOS/tests_backend/screen.py @@ -1,17 +1,18 @@ import pytest +from toga.images import Image as TogaImage from toga_iOS.libs import UIScreen from .probe import BaseProbe class ScreenProbe(BaseProbe): - def __init__(self, screen): + def __init__(self, app, screen): super().__init__() self.screen = screen self._impl = screen._impl self.native = screen._impl.native assert isinstance(self.native, UIScreen) - def get_screenshot(self): + def get_screenshot(self, format=TogaImage): pytest.skip("Screen.as_image is not implemented on iOS.") diff --git a/testbed/tests/app/test_screen.py b/testbed/tests/app/test_screen.py index 42386dff0f..3e753b7c52 100644 --- a/testbed/tests/app/test_screen.py +++ b/testbed/tests/app/test_screen.py @@ -1,17 +1,15 @@ from importlib import import_module import pytest +from PIL.Image import Image as PILImage -from toga.platform import current_platform +from toga.images import Image as TogaImage @pytest.fixture def screen_probe_list(app): module = import_module("tests_backend.screen") - if current_platform == "android": - return [getattr(module, "ScreenProbe")(app, screen) for screen in app.screens] - else: - return [getattr(module, "ScreenProbe")(screen) for screen in app.screens] + return [getattr(module, "ScreenProbe")(app, screen) for screen in app.screens] async def test_name(app): @@ -46,7 +44,15 @@ async def test_size(app): async def test_as_image(screen_probe_list): """A screen can be captured as an image""" + # Using a probe for test as the feature is not implemented on some platforms. for screen_probe in screen_probe_list: - # Using a probe for test as feature is not implemented on some platforms. - screenshot = screen_probe.get_screenshot() - assert screenshot.size == screen_probe.screen.size + # `get_screenshot()` should default to `toga.images.Image` as format. + toga_image_screenshot = screen_probe.get_screenshot() + # Check if returned image is of type `toga.images.Image`. + assert isinstance(toga_image_screenshot, TogaImage) + assert toga_image_screenshot.size == screen_probe.screen.size + + pil_screenshot = screen_probe.get_screenshot(format=PILImage) + # Check if returned image is of type `PIL.Image.Image`. + assert isinstance(pil_screenshot, PILImage) + assert pil_screenshot.size == screen_probe.screen.size diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 284d9bcd9e..a6595edeec 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -512,7 +512,9 @@ async def test_screen(second_window, second_window_probe): # Move the window between available screens and assert its `screen_position` for screen in second_window.app.screens: second_window.screen = screen - await second_window_probe.wait_for_window("Secondary window has been moved") + await second_window_probe.wait_for_window( + f"Secondary window has been moved to {screen.name}" + ) assert second_window.screen == screen assert second_window.screen_position == ( second_window.position[0] - screen.origin[0], diff --git a/winforms/tests_backend/screen.py b/winforms/tests_backend/screen.py index 08d89a6ee7..7342f28e9b 100644 --- a/winforms/tests_backend/screen.py +++ b/winforms/tests_backend/screen.py @@ -1,15 +1,17 @@ from System.Windows.Forms import Screen as WinFormsScreen +from toga.images import Image as TogaImage + from .probe import BaseProbe class ScreenProbe(BaseProbe): - def __init__(self, screen): + def __init__(self, app, screen): super().__init__() self.screen = screen self._impl = screen._impl self.native = screen._impl.native assert isinstance(self.native, WinFormsScreen) - def get_screenshot(self): - return self.screen.as_image() + def get_screenshot(self, format=TogaImage): + return self.screen.as_image(format=format) From c78d2d41d7ebc23f209dbbbe8bc1034cd1b57481 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 1 Feb 2024 10:32:24 -0800 Subject: [PATCH 075/102] Added s_image() implementation for Android --- android/src/toga_android/screen.py | 21 ++++++++++++++++++--- android/tests_backend/screen.py | 3 +-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/android/src/toga_android/screen.py b/android/src/toga_android/screen.py index 1e1d88c83a..484b43ed67 100644 --- a/android/src/toga_android/screen.py +++ b/android/src/toga_android/screen.py @@ -1,3 +1,8 @@ +from android.graphics import ( + Bitmap, + Canvas as A_Canvas, +) + from toga.screen import Screen as ScreenInterface from .widgets.base import Scalable @@ -26,9 +31,19 @@ def get_origin(self): def get_size(self): return ( - self.scale_out(self.native.getWidth()), - self.scale_out(self.native.getHeight()), + # Using scaling(scale_out) produces wrong results. + self.native.getWidth(), + self.native.getHeight(), ) def get_image_data(self): - self.interface.factory.not_implemented("Screen.get_image_data()") + # Get the root view of the current activity + root_view = self.app.native.getWindow().getDecorView().getRootView() + bitmap = Bitmap.createBitmap( + *self.get_size(), + Bitmap.Config.ARGB_8888, + ) + canvas = A_Canvas(bitmap) + root_view.draw(canvas) + + return bitmap diff --git a/android/tests_backend/screen.py b/android/tests_backend/screen.py index e031c4317e..a51b5a8867 100644 --- a/android/tests_backend/screen.py +++ b/android/tests_backend/screen.py @@ -1,4 +1,3 @@ -import pytest from android.view import Display from toga.images import Image as TogaImage @@ -17,4 +16,4 @@ def __init__(self, app, screen): assert isinstance(self.native, Display) def get_screenshot(self, format=TogaImage): - pytest.skip("Screen.as_image is not implemented on Android.") + return self.screen.as_image(format=format) From 1867bbee12a407ca5dd6c301b1e1fb57f769d1b4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 1 Feb 2024 11:01:47 -0800 Subject: [PATCH 076/102] Added s_image() implementation for iOS --- iOS/src/toga_iOS/screen.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/iOS/src/toga_iOS/screen.py b/iOS/src/toga_iOS/screen.py index 93f6a1859d..f50f65fb46 100644 --- a/iOS/src/toga_iOS/screen.py +++ b/iOS/src/toga_iOS/screen.py @@ -1,4 +1,7 @@ +from rubicon.objc import Block, objc_id + from toga.screen import Screen as ScreenInterface +from toga_iOS.libs import UIGraphicsImageRenderer, UIImage class Screen: @@ -25,4 +28,15 @@ def get_size(self): return int(self.native.bounds.size.width), int(self.native.bounds.size.height) def get_image_data(self): - self.interface.factory.not_implemented("Screen.get_image_data()") + renderer = UIGraphicsImageRenderer.alloc().initWithSize(self.native.bounds.size) + + def render(context): + self.native.drawViewHierarchyInRect( + self.native.bounds, afterScreenUpdates=True + ) + + # Render the full image + full_image = UIImage.imageWithData( + renderer.PNGDataWithActions(Block(render, None, objc_id)) + ) + return full_image From 616e2e2e27175cafee4702a2d34e311d787b6e49 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 1 Feb 2024 14:38:15 -0800 Subject: [PATCH 077/102] Fixed iOS implementation --- iOS/src/toga_iOS/screen.py | 12 +++++------- iOS/tests_backend/screen.py | 4 +--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/iOS/src/toga_iOS/screen.py b/iOS/src/toga_iOS/screen.py index f50f65fb46..acc770a8ed 100644 --- a/iOS/src/toga_iOS/screen.py +++ b/iOS/src/toga_iOS/screen.py @@ -28,15 +28,13 @@ def get_size(self): return int(self.native.bounds.size.width), int(self.native.bounds.size.height) def get_image_data(self): - renderer = UIGraphicsImageRenderer.alloc().initWithSize(self.native.bounds.size) + ui_view = self.native.snapshotViewAfterScreenUpdates_(True) + renderer = UIGraphicsImageRenderer.alloc().initWithSize(ui_view.bounds.size) def render(context): - self.native.drawViewHierarchyInRect( - self.native.bounds, afterScreenUpdates=True - ) + ui_view.drawViewHierarchyInRect(ui_view.bounds, afterScreenUpdates=True) - # Render the full image - full_image = UIImage.imageWithData( + ui_image = UIImage.imageWithData( renderer.PNGDataWithActions(Block(render, None, objc_id)) ) - return full_image + return ui_image diff --git a/iOS/tests_backend/screen.py b/iOS/tests_backend/screen.py index 1493015c1e..c8d6118141 100644 --- a/iOS/tests_backend/screen.py +++ b/iOS/tests_backend/screen.py @@ -1,5 +1,3 @@ -import pytest - from toga.images import Image as TogaImage from toga_iOS.libs import UIScreen @@ -15,4 +13,4 @@ def __init__(self, app, screen): assert isinstance(self.native, UIScreen) def get_screenshot(self, format=TogaImage): - pytest.skip("Screen.as_image is not implemented on iOS.") + return self.screen.as_image(format=format) From a2a72d269a9de9becf7436a52b6e49c78c04ea16 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 1 Feb 2024 15:12:48 -0800 Subject: [PATCH 078/102] Fixed iOS implementation --- iOS/src/toga_iOS/screen.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/iOS/src/toga_iOS/screen.py b/iOS/src/toga_iOS/screen.py index acc770a8ed..edf08434ad 100644 --- a/iOS/src/toga_iOS/screen.py +++ b/iOS/src/toga_iOS/screen.py @@ -25,7 +25,9 @@ def get_origin(self): return (0, 0) def get_size(self): - return int(self.native.bounds.size.width), int(self.native.bounds.size.height) + return int(self.native.nativeBounds.size.width), int( + self.native.nativeBounds.size.height + ) def get_image_data(self): ui_view = self.native.snapshotViewAfterScreenUpdates_(True) From 5cea9af75db4b6afea7c11c26c0c11e29e95b628 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 2 Feb 2024 10:16:00 +0800 Subject: [PATCH 079/102] Minor cleanups and renames. --- android/src/toga_android/app.py | 2 +- android/src/toga_android/{screen.py => screens.py} | 2 +- android/src/toga_android/window.py | 2 +- android/tests_backend/{screen.py => screens.py} | 4 +++- cocoa/src/toga_cocoa/app.py | 2 +- cocoa/src/toga_cocoa/{screen.py => screens.py} | 2 +- cocoa/src/toga_cocoa/window.py | 2 +- cocoa/tests_backend/{screen.py => screens.py} | 2 +- core/src/toga/app.py | 4 ++-- core/src/toga/{screen.py => screens.py} | 5 +++-- core/src/toga/window.py | 2 +- core/tests/app/{test_screen.py => test_screens.py} | 12 +++++++++--- dummy/src/toga_dummy/app.py | 2 +- dummy/src/toga_dummy/{screen.py => screens.py} | 2 +- dummy/src/toga_dummy/window.py | 2 +- gtk/src/toga_gtk/app.py | 2 +- gtk/src/toga_gtk/{screen.py => screens.py} | 2 +- gtk/src/toga_gtk/window.py | 2 +- gtk/tests_backend/{screen.py => screens.py} | 8 +++----- iOS/src/toga_iOS/app.py | 2 +- iOS/src/toga_iOS/{screen.py => screens.py} | 7 ++++--- iOS/src/toga_iOS/window.py | 2 +- iOS/tests_backend/{screen.py => screens.py} | 2 +- .../tests/app/{test_screen.py => test_screens.py} | 11 ++++++----- textual/src/toga_textual/app.py | 2 +- textual/src/toga_textual/{screen.py => screens.py} | 2 +- textual/src/toga_textual/window.py | 2 +- textual/tests_backend/{screen.py => screens.py} | 0 web/src/toga_web/app.py | 2 +- web/src/toga_web/{screen.py => screens.py} | 2 +- web/src/toga_web/window.py | 2 +- winforms/src/toga_winforms/app.py | 2 +- winforms/src/toga_winforms/{screen.py => screens.py} | 2 +- winforms/src/toga_winforms/window.py | 2 +- winforms/tests_backend/{screen.py => screens.py} | 2 +- 35 files changed, 57 insertions(+), 48 deletions(-) rename android/src/toga_android/{screen.py => screens.py} (96%) rename android/tests_backend/{screen.py => screens.py} (88%) rename cocoa/src/toga_cocoa/{screen.py => screens.py} (96%) rename cocoa/tests_backend/{screen.py => screens.py} (91%) rename core/src/toga/{screen.py => screens.py} (85%) rename core/tests/app/{test_screen.py => test_screens.py} (82%) rename dummy/src/toga_dummy/{screen.py => screens.py} (95%) rename gtk/src/toga_gtk/{screen.py => screens.py} (96%) rename gtk/tests_backend/{screen.py => screens.py} (73%) rename iOS/src/toga_iOS/{screen.py => screens.py} (86%) rename iOS/tests_backend/{screen.py => screens.py} (91%) rename testbed/tests/app/{test_screen.py => test_screens.py} (86%) rename textual/src/toga_textual/{screen.py => screens.py} (93%) rename textual/tests_backend/{screen.py => screens.py} (100%) rename web/src/toga_web/{screen.py => screens.py} (92%) rename winforms/src/toga_winforms/{screen.py => screens.py} (95%) rename winforms/tests_backend/{screen.py => screens.py} (92%) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index d7fc267929..ed0a75186b 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -11,7 +11,7 @@ from toga.command import Command, Group, Separator from .libs import events -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl from .window import Window diff --git a/android/src/toga_android/screen.py b/android/src/toga_android/screens.py similarity index 96% rename from android/src/toga_android/screen.py rename to android/src/toga_android/screens.py index 484b43ed67..09fdd1d21f 100644 --- a/android/src/toga_android/screen.py +++ b/android/src/toga_android/screens.py @@ -3,7 +3,7 @@ Canvas as A_Canvas, ) -from toga.screen import Screen as ScreenInterface +from toga.screens import Screen as ScreenInterface from .widgets.base import Scalable diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 3fa3c39eae..5e8f1ffe1b 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -11,7 +11,7 @@ from java.io import ByteArrayOutputStream from .container import Container -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl class LayoutListener(dynamic_proxy(ViewTreeObserver.OnGlobalLayoutListener)): diff --git a/android/tests_backend/screen.py b/android/tests_backend/screens.py similarity index 88% rename from android/tests_backend/screen.py rename to android/tests_backend/screens.py index a51b5a8867..ebae1b2006 100644 --- a/android/tests_backend/screen.py +++ b/android/tests_backend/screens.py @@ -1,5 +1,6 @@ from android.view import Display +import toga from toga.images import Image as TogaImage from toga_android.widgets.base import Scalable @@ -7,7 +8,8 @@ class ScreenProbe(BaseProbe, Scalable): - def __init__(self, app, screen): + def __init__(self, screen): + app = toga.App.app super().__init__(app) self.screen = screen self._impl = screen._impl diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 8be3825772..1811f5f702 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -37,7 +37,7 @@ objc_method, objc_property, ) -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl from .window import Window diff --git a/cocoa/src/toga_cocoa/screen.py b/cocoa/src/toga_cocoa/screens.py similarity index 96% rename from cocoa/src/toga_cocoa/screen.py rename to cocoa/src/toga_cocoa/screens.py index 2c5e6fe9b2..47651d8028 100644 --- a/cocoa/src/toga_cocoa/screen.py +++ b/cocoa/src/toga_cocoa/screens.py @@ -1,4 +1,4 @@ -from toga.screen import Screen as ScreenInterface +from toga.screens import Screen as ScreenInterface from toga_cocoa.libs import ( CGImageGetHeight, CGImageGetWidth, diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 391dd0671f..a4e554ef86 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -18,7 +18,7 @@ objc_property, ) -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl def toolbar_identifier(cmd): diff --git a/cocoa/tests_backend/screen.py b/cocoa/tests_backend/screens.py similarity index 91% rename from cocoa/tests_backend/screen.py rename to cocoa/tests_backend/screens.py index 9a20af536c..19745e40b1 100644 --- a/cocoa/tests_backend/screen.py +++ b/cocoa/tests_backend/screens.py @@ -5,7 +5,7 @@ class ScreenProbe(BaseProbe): - def __init__(self, app, screen): + def __init__(self, screen): super().__init__() self.screen = screen self._impl = screen._impl diff --git a/core/src/toga/app.py b/core/src/toga/app.py index edaa14263d..fd3d318c78 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -27,7 +27,7 @@ from toga.icons import Icon from toga.paths import Paths from toga.platform import get_platform_factory -from toga.screen import Screen +from toga.screens import Screen from toga.widgets.base import Widget from toga.window import Window @@ -499,7 +499,7 @@ def _create_impl(self): self.factory.App(interface=self) @property - def screens(self) -> list[Screen, ...]: + def screens(self) -> list[Screen]: """Returns a list of available screens.""" return [screen.interface for screen in self._impl.get_screens()] diff --git a/core/src/toga/screen.py b/core/src/toga/screens.py similarity index 85% rename from core/src/toga/screen.py rename to core/src/toga/screens.py index 01fe869e88..dbdd061cf7 100644 --- a/core/src/toga/screen.py +++ b/core/src/toga/screens.py @@ -32,8 +32,9 @@ def size(self) -> tuple[int, int]: def as_image(self, format: type[ImageT] = Image) -> ImageT: """Render the current contents of the screen as an image. - :param format: Format to provide. Defaults to :class:`~toga.images.Image`; also - supports :any:`PIL.Image.Image` if Pillow is installed + :param format: Format for the resulting image. Defaults to + :class:`~toga.images.Image`; also supports :any:`PIL.Image.Image` if Pillow + is installed :returns: An image containing the screen content, in the format requested. """ return Image(self._impl.get_image_data()).as_format(format) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 357c3b43d4..ca9df146b0 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from toga.app import App from toga.images import ImageT - from toga.screen import Screen + from toga.screens import Screen from toga.widgets.base import Widget diff --git a/core/tests/app/test_screen.py b/core/tests/app/test_screens.py similarity index 82% rename from core/tests/app/test_screen.py rename to core/tests/app/test_screens.py index f42923277b..566891df09 100644 --- a/core/tests/app/test_screen.py +++ b/core/tests/app/test_screens.py @@ -28,13 +28,19 @@ def test_as_image(app): # `as_image()` should default to `toga.images.Image` as format. toga_image_screenshot = screen.as_image() assert_action_performed(screen, "get image data") - # Check if returned image is of type `toga.images.Image`. + + # Check if returned image is of type `toga.images.Image`, and the right size assert isinstance(toga_image_screenshot, TogaImage) - # Don't need to check the raw data; just check it's the right screen size. assert toga_image_screenshot.size == screen.size + +def test_as_image_format(app): + """A screen can be captured as an image in a non-default format""" + for screen in app.screens: + # Capture image in PIL format pil_screenshot = screen.as_image(format=PILImage) assert_action_performed(screen, "get image data") - # Check if returned image is of type `PIL.Image.Image`. + + # Check if returned image is of type `PIL.Image.Image`, and the right size assert isinstance(pil_screenshot, PILImage) assert pil_screenshot.size == screen.size diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index d665b295e8..7e95948625 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -2,7 +2,7 @@ import sys from pathlib import Path -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl from .utils import LoggedObject from .window import Window diff --git a/dummy/src/toga_dummy/screen.py b/dummy/src/toga_dummy/screens.py similarity index 95% rename from dummy/src/toga_dummy/screen.py rename to dummy/src/toga_dummy/screens.py index 6d7bf57cd2..efde573938 100644 --- a/dummy/src/toga_dummy/screen.py +++ b/dummy/src/toga_dummy/screens.py @@ -1,6 +1,6 @@ from PIL import Image, ImageDraw -from toga.screen import Screen as ScreenInterface +from toga.screens import Screen as ScreenInterface from .utils import LoggedObject # noqa diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 10f5c0e153..2965d7c340 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -2,7 +2,7 @@ import toga_dummy -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl from .utils import LoggedObject diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index c3b5d99a46..812995764c 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -12,7 +12,7 @@ from .keys import gtk_accel from .libs import TOGA_DEFAULT_STYLES, Gdk, Gio, GLib, Gtk -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl from .window import Window diff --git a/gtk/src/toga_gtk/screen.py b/gtk/src/toga_gtk/screens.py similarity index 96% rename from gtk/src/toga_gtk/screen.py rename to gtk/src/toga_gtk/screens.py index d06ccc379c..c095e4f7a5 100644 --- a/gtk/src/toga_gtk/screen.py +++ b/gtk/src/toga_gtk/screens.py @@ -1,6 +1,6 @@ import os -from toga.screen import Screen as ScreenInterface +from toga.screens import Screen as ScreenInterface from .libs import Gdk diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 6cea0d01e9..a8f48ab26d 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -2,7 +2,7 @@ from .container import TogaContainer from .libs import Gdk, Gtk -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl class Window: diff --git a/gtk/tests_backend/screen.py b/gtk/tests_backend/screens.py similarity index 73% rename from gtk/tests_backend/screen.py rename to gtk/tests_backend/screens.py index 0428c8f7f7..399544797c 100644 --- a/gtk/tests_backend/screen.py +++ b/gtk/tests_backend/screens.py @@ -9,19 +9,17 @@ class ScreenProbe(BaseProbe): - def __init__(self, app, screen): + def __init__(self, screen): super().__init__() self.screen = screen self._impl = screen._impl self.native = screen._impl.native if "WAYLAND_DISPLAY" in os.environ: - # TODO: - # For wayland, the native type is `__gi__.GdkWaylandMonitor` - # But it cannot be imported directly. + # The native display type on Wayland is `__gi__.GdkWaylandMonitor` + # However, that class can't be imported directly. pass else: assert isinstance(self.native, GdkX11.X11Monitor) - # For CI, the native type is also GdkX11.X11Monitor def get_screenshot(self, format=TogaImage): if "WAYLAND_DISPLAY" in os.environ: diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index 45fedfe82c..3f3f058cfc 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -6,7 +6,7 @@ from toga_iOS.libs import UIResponder, UIScreen, av_foundation from toga_iOS.window import Window -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl class MainWindow(Window): diff --git a/iOS/src/toga_iOS/screen.py b/iOS/src/toga_iOS/screens.py similarity index 86% rename from iOS/src/toga_iOS/screen.py rename to iOS/src/toga_iOS/screens.py index edf08434ad..6e47883004 100644 --- a/iOS/src/toga_iOS/screen.py +++ b/iOS/src/toga_iOS/screens.py @@ -1,6 +1,6 @@ from rubicon.objc import Block, objc_id -from toga.screen import Screen as ScreenInterface +from toga.screens import Screen as ScreenInterface from toga_iOS.libs import UIGraphicsImageRenderer, UIImage @@ -25,8 +25,9 @@ def get_origin(self): return (0, 0) def get_size(self): - return int(self.native.nativeBounds.size.width), int( - self.native.nativeBounds.size.height + return ( + int(self.native.nativeBounds.size.width), + int(self.native.nativeBounds.size.height), ) def get_image_data(self): diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index d13e8b57b1..c4996b0a58 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -19,7 +19,7 @@ uikit, ) -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl class Window: diff --git a/iOS/tests_backend/screen.py b/iOS/tests_backend/screens.py similarity index 91% rename from iOS/tests_backend/screen.py rename to iOS/tests_backend/screens.py index c8d6118141..453a6bca35 100644 --- a/iOS/tests_backend/screen.py +++ b/iOS/tests_backend/screens.py @@ -5,7 +5,7 @@ class ScreenProbe(BaseProbe): - def __init__(self, app, screen): + def __init__(self, screen): super().__init__() self.screen = screen self._impl = screen._impl diff --git a/testbed/tests/app/test_screen.py b/testbed/tests/app/test_screens.py similarity index 86% rename from testbed/tests/app/test_screen.py rename to testbed/tests/app/test_screens.py index 3e753b7c52..05eee2e2a9 100644 --- a/testbed/tests/app/test_screen.py +++ b/testbed/tests/app/test_screens.py @@ -7,9 +7,9 @@ @pytest.fixture -def screen_probe_list(app): - module = import_module("tests_backend.screen") - return [getattr(module, "ScreenProbe")(app, screen) for screen in app.screens] +def screen_probes(app): + module = import_module("tests_backend.screens") + return [getattr(module, "ScreenProbe")(screen) for screen in app.screens] async def test_name(app): @@ -42,16 +42,17 @@ async def test_size(app): ) -async def test_as_image(screen_probe_list): +async def test_as_image(screen_probes): """A screen can be captured as an image""" # Using a probe for test as the feature is not implemented on some platforms. - for screen_probe in screen_probe_list: + for screen_probe in screen_probes: # `get_screenshot()` should default to `toga.images.Image` as format. toga_image_screenshot = screen_probe.get_screenshot() # Check if returned image is of type `toga.images.Image`. assert isinstance(toga_image_screenshot, TogaImage) assert toga_image_screenshot.size == screen_probe.screen.size + # Capture screenshot in PIL format pil_screenshot = screen_probe.get_screenshot(format=PILImage) # Check if returned image is of type `PIL.Image.Image`. assert isinstance(pil_screenshot, PILImage) diff --git a/textual/src/toga_textual/app.py b/textual/src/toga_textual/app.py index e7790a7af8..5c21bdab5e 100644 --- a/textual/src/toga_textual/app.py +++ b/textual/src/toga_textual/app.py @@ -2,7 +2,7 @@ from textual.app import App as TextualApp -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl from .window import Window diff --git a/textual/src/toga_textual/screen.py b/textual/src/toga_textual/screens.py similarity index 93% rename from textual/src/toga_textual/screen.py rename to textual/src/toga_textual/screens.py index 3fa1d22260..21abb3489e 100644 --- a/textual/src/toga_textual/screen.py +++ b/textual/src/toga_textual/screens.py @@ -1,4 +1,4 @@ -from toga.screen import Screen as ScreenInterface +from toga.screens import Screen as ScreenInterface class Screen: diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index 9400019989..09a6daf381 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -7,7 +7,7 @@ from textual.widgets import Button as TextualButton from .container import Container -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl class WindowCloseButton(TextualButton): diff --git a/textual/tests_backend/screen.py b/textual/tests_backend/screens.py similarity index 100% rename from textual/tests_backend/screen.py rename to textual/tests_backend/screens.py diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index b359a137d0..07ed538ff6 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -3,7 +3,7 @@ from toga_web.libs import create_element, js from toga_web.window import Window -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl class MainWindow(Window): diff --git a/web/src/toga_web/screen.py b/web/src/toga_web/screens.py similarity index 92% rename from web/src/toga_web/screen.py rename to web/src/toga_web/screens.py index dd876bbae4..15e5d49ce1 100644 --- a/web/src/toga_web/screen.py +++ b/web/src/toga_web/screens.py @@ -1,4 +1,4 @@ -from toga.screen import Screen as ScreenInterface +from toga.screens import Screen as ScreenInterface class Screen: diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index 9595636404..b78c4028cf 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -1,6 +1,6 @@ from toga_web.libs import create_element, js -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl class Window: diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 10b661fd95..1bf3aa0b30 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -18,7 +18,7 @@ from .keys import toga_to_winforms_key from .libs.proactor import WinformsProactorEventLoop from .libs.wrapper import WeakrefCallable -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl from .window import Window diff --git a/winforms/src/toga_winforms/screen.py b/winforms/src/toga_winforms/screens.py similarity index 95% rename from winforms/src/toga_winforms/screen.py rename to winforms/src/toga_winforms/screens.py index 168bd3758c..ad919aaf50 100644 --- a/winforms/src/toga_winforms/screen.py +++ b/winforms/src/toga_winforms/screens.py @@ -7,7 +7,7 @@ ) from System.IO import MemoryStream -from toga.screen import Screen as ScreenInterface +from toga.screens import Screen as ScreenInterface class Screen: diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index fb06dfa03c..a23882e290 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -7,7 +7,7 @@ from .container import Container from .libs.wrapper import WeakrefCallable -from .screen import Screen as ScreenImpl +from .screens import Screen as ScreenImpl from .widgets.base import Scalable diff --git a/winforms/tests_backend/screen.py b/winforms/tests_backend/screens.py similarity index 92% rename from winforms/tests_backend/screen.py rename to winforms/tests_backend/screens.py index 7342f28e9b..c6fdfc9c55 100644 --- a/winforms/tests_backend/screen.py +++ b/winforms/tests_backend/screens.py @@ -6,7 +6,7 @@ class ScreenProbe(BaseProbe): - def __init__(self, app, screen): + def __init__(self, screen): super().__init__() self.screen = screen self._impl = screen._impl From 858a9da64a458018309d5d8f1c169c2ea679054c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 2 Feb 2024 10:16:14 +0800 Subject: [PATCH 080/102] Add docs for Screen class. --- docs/reference/api/index.rst | 2 ++ docs/reference/api/screens.rst | 39 +++++++++++++++++++++ docs/reference/data/widgets_by_platform.csv | 8 ++++- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 docs/reference/api/screens.rst diff --git a/docs/reference/api/index.rst b/docs/reference/api/index.rst index 7de538407c..02030dc87e 100644 --- a/docs/reference/api/index.rst +++ b/docs/reference/api/index.rst @@ -96,6 +96,7 @@ Device and Hardware Usage Description ==================================================================== ======================================================================== :doc:`Camera ` A sensor that can capture photos and/or video. + :doc:`Screen ` A representation of a screen attached to a device. ==================================================================== ======================================================================== Other @@ -121,3 +122,4 @@ Other widgets/index constants keys + screens diff --git a/docs/reference/api/screens.rst b/docs/reference/api/screens.rst new file mode 100644 index 0000000000..b3f97d4728 --- /dev/null +++ b/docs/reference/api/screens.rst @@ -0,0 +1,39 @@ +Screen +====== + +A representation of a screen attached to a device. + +.. 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: '(?!(Screen|Device)$)'} + +Usage +----- + +An app will always have access to at least one screen. The :any:`toga.App.screens` +attribute will return the list of all available screens; the screen at index 0 will be +the "primary" screen. Screen sizes and positions are given in CSS pixels. + +.. code-block:: python + + # Print the size of the primary screen. + print(my_app.screens[0].size) + + # Print the identifying name of the second screen + print(my_app.screens[1].name) + + +Notes +----- + +* When using the GTK backend under Wayland, the screen at index 0 may not be the primary + screen. This because the separation of concerns enforced by Wayland makes determining + the primary screen unreliable. + +Reference +--------- + +.. autoclass:: toga.screens.Screen diff --git a/docs/reference/data/widgets_by_platform.csv b/docs/reference/data/widgets_by_platform.csv index aec6e3dbdb..78b98b0e1b 100644 --- a/docs/reference/data/widgets_by_platform.csv +++ b/docs/reference/data/widgets_by_platform.csv @@ -29,8 +29,14 @@ ScrollContainer,Layout Widget,:class:`~toga.ScrollContainer`,A container that ca SplitContainer,Layout Widget,:class:`~toga.SplitContainer`,A container that divides an area into two panels with a movable border,|y|,|y|,|y|,,,, OptionContainer,Layout Widget,:class:`~toga.OptionContainer`,A container that can display multiple labeled tabs of content,|y|,|y|,|y|,,,, Camera,Hardware,:class:`~toga.hardware.camera.Camera`,A sensor that can capture photos and/or video.,|y|,,,|y|,,, +Screen,Hardware,:class:`~toga.screens.Screen`,A representation of a screen attached to a device.,|y|,|y|,|y|,|y|,|y|,|b|,|b| App Paths,Resource,:class:`~toga.paths.Paths`,A mechanism for obtaining platform-appropriate filesystem locations for an application.,|y|,|y|,|y|,|y|,|y|,,|b| -Font,Resource,:class:`~toga.Font`,A text font,|y|,|y|,|y|,|y|,|y|,, Command,Resource,:class:`~toga.Command`,Command,|y|,|y|,|y|,,|y|,, +Font,Resource,:class:`~toga.Font`,A text font,|y|,|y|,|y|,|y|,|y|,, Icon,Resource,:class:`~toga.Icon`,"A small, square image, used to provide easily identifiable visual context to a widget.",|y|,|y|,|y|,|y|,|y|,,|b| Image,Resource,:class:`~toga.Image`,Graphical content of arbitrary size.,|y|,|y|,|y|,|y|,|y|,, +ListSource,Resource,:class:`~toga.sources.ListSource`,A data source describing an ordered list of data.,|y|,|y|,|y|,|y|,|y|,|y|,|y| +Source,Resource,:class:`~toga.sources.Source`,A base class for data source implementations.,|y|,|y|,|y|,|y|,|y|,|y|,|y| +TreeSource,Resource,:class:`~toga.sources.TreeSource`,A data source describing an ordered hierarchical tree of data.,|y|,|y|,|y|,|y|,|y|,|y|,|y| +Validators,Resource,:ref:`Validators `,A mechanism for validating that input meets a given set of criteria.,|y|,|y|,|y|,|y|,|y|,|y|,|y| +ValueSource,Resource,:class:`~toga.sources.ValueSource`,A data source describing a single value.,|y|,|y|,|y|,|y|,|y|,|y|,|y| From 95c28cf2f9466d9d968b23cd855ebb1280c4010b Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 2 Feb 2024 10:28:15 +0800 Subject: [PATCH 081/102] Use probe mechanism for inspecting image size. --- cocoa/tests_backend/probe.py | 3 ++- gtk/tests_backend/probe.py | 3 ++- iOS/tests_backend/probe.py | 3 ++- testbed/tests/app/test_screens.py | 25 +++++++++++++++---------- winforms/tests_backend/probe.py | 4 +++- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/cocoa/tests_backend/probe.py b/cocoa/tests_backend/probe.py index df70de74c7..865cf7cb62 100644 --- a/cocoa/tests_backend/probe.py +++ b/cocoa/tests_backend/probe.py @@ -4,6 +4,7 @@ from rubicon.objc import SEL, NSArray, NSObject, ObjCClass, objc_method from rubicon.objc.api import NSString +import toga from toga_cocoa.libs.appkit import appkit NSRunLoop = ObjCClass("NSRunLoop") @@ -48,7 +49,7 @@ async def post_event(self, event, delay=None): async def redraw(self, message=None, delay=None): """Request a redraw of the app, waiting until that redraw has completed.""" - if self.app.run_slow: + if toga.App.app.run_slow: # If we're running slow, wait for a second print("Waiting for redraw" if message is None else message) delay = 1 diff --git a/gtk/tests_backend/probe.py b/gtk/tests_backend/probe.py index abb09434fd..e87eca7bfd 100644 --- a/gtk/tests_backend/probe.py +++ b/gtk/tests_backend/probe.py @@ -1,5 +1,6 @@ import asyncio +import toga from toga_gtk.libs import Gtk @@ -14,7 +15,7 @@ async def redraw(self, message=None, delay=None): Gtk.main_iteration_do(blocking=False) # If we're running slow, wait for a second - if self.app.run_slow: + if toga.App.app.run_slow: print("Waiting for redraw" if message is None else message) delay = 1 diff --git a/iOS/tests_backend/probe.py b/iOS/tests_backend/probe.py index c3a4653d11..c7dc5764a7 100644 --- a/iOS/tests_backend/probe.py +++ b/iOS/tests_backend/probe.py @@ -1,5 +1,6 @@ import asyncio +import toga from toga_iOS.libs import NSRunLoop, UIScreen @@ -7,7 +8,7 @@ class BaseProbe: async def redraw(self, message=None, delay=None): """Request a redraw of the app, waiting until that redraw has completed.""" # If we're running slow, wait for a second - if self.app.run_slow: + if toga.App.app.run_slow: print("Waiting for redraw" if message is None else message) delay = 1 diff --git a/testbed/tests/app/test_screens.py b/testbed/tests/app/test_screens.py index 05eee2e2a9..55ea3ce740 100644 --- a/testbed/tests/app/test_screens.py +++ b/testbed/tests/app/test_screens.py @@ -1,15 +1,13 @@ from importlib import import_module -import pytest from PIL.Image import Image as PILImage from toga.images import Image as TogaImage -@pytest.fixture -def screen_probes(app): +def screen_probe(screen): module = import_module("tests_backend.screens") - return [getattr(module, "ScreenProbe")(screen) for screen in app.screens] + return getattr(module, "ScreenProbe")(screen) async def test_name(app): @@ -42,18 +40,25 @@ async def test_size(app): ) -async def test_as_image(screen_probes): +async def test_as_image(app): """A screen can be captured as an image""" # Using a probe for test as the feature is not implemented on some platforms. - for screen_probe in screen_probes: + for screen in app.screens: + probe = screen_probe(screen) + # `get_screenshot()` should default to `toga.images.Image` as format. - toga_image_screenshot = screen_probe.get_screenshot() + toga_image_screenshot = probe.get_screenshot() + await probe.redraw(f"Screenshot of {screen} has been taken") # Check if returned image is of type `toga.images.Image`. assert isinstance(toga_image_screenshot, TogaImage) - assert toga_image_screenshot.size == screen_probe.screen.size + probe.assert_image_size( + toga_image_screenshot.size, + probe.screen.size, + ) # Capture screenshot in PIL format - pil_screenshot = screen_probe.get_screenshot(format=PILImage) + pil_screenshot = probe.get_screenshot(format=PILImage) + await probe.redraw(f"Screenshot of {screen} has been taken in PIL format") # Check if returned image is of type `PIL.Image.Image`. assert isinstance(pil_screenshot, PILImage) - assert pil_screenshot.size == screen_probe.screen.size + probe.assert_image_size(pil_screenshot.size, probe.screen.size) diff --git a/winforms/tests_backend/probe.py b/winforms/tests_backend/probe.py index bc02d4bba2..0a2e36564a 100644 --- a/winforms/tests_backend/probe.py +++ b/winforms/tests_backend/probe.py @@ -2,6 +2,8 @@ from System.Windows.Forms import SendKeys +import toga + KEY_CODES = { f"<{name}>": f"{{{name.upper()}}}" for name in ["esc", "up", "down", "left", "right"] @@ -19,7 +21,7 @@ async def redraw(self, message=None, delay=None): # Winforms style changes always take effect immediately. # If we're running slow, wait for a second - if self.app.run_slow: + if toga.App.app.run_slow: delay = 1 if delay: From b320ba9eac6755ad4c0909689d8b275cc47a7895 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 3 Feb 2024 06:24:01 -0800 Subject: [PATCH 082/102] Fixed tests --- android/src/toga_android/screens.py | 8 +++----- iOS/src/toga_iOS/screens.py | 4 ++-- winforms/tests_backend/probe.py | 5 ++++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/android/src/toga_android/screens.py b/android/src/toga_android/screens.py index 09fdd1d21f..39dc07fa8e 100644 --- a/android/src/toga_android/screens.py +++ b/android/src/toga_android/screens.py @@ -30,17 +30,15 @@ def get_origin(self): return (0, 0) def get_size(self): - return ( - # Using scaling(scale_out) produces wrong results. - self.native.getWidth(), - self.native.getHeight(), + return tuple( + map(self.scale_out, (self.native.getWidth(), self.native.getHeight())) ) def get_image_data(self): # Get the root view of the current activity root_view = self.app.native.getWindow().getDecorView().getRootView() bitmap = Bitmap.createBitmap( - *self.get_size(), + *map(self.scale_in, self.get_size()), Bitmap.Config.ARGB_8888, ) canvas = A_Canvas(bitmap) diff --git a/iOS/src/toga_iOS/screens.py b/iOS/src/toga_iOS/screens.py index 6e47883004..c81f2b08c4 100644 --- a/iOS/src/toga_iOS/screens.py +++ b/iOS/src/toga_iOS/screens.py @@ -26,8 +26,8 @@ def get_origin(self): def get_size(self): return ( - int(self.native.nativeBounds.size.width), - int(self.native.nativeBounds.size.height), + int(self.native.bounds.size.width), + int(self.native.bounds.size.height), ) def get_image_data(self): diff --git a/winforms/tests_backend/probe.py b/winforms/tests_backend/probe.py index 0a2e36564a..f6495899be 100644 --- a/winforms/tests_backend/probe.py +++ b/winforms/tests_backend/probe.py @@ -1,5 +1,7 @@ import asyncio +from System import IntPtr +from System.Drawing import Graphics from System.Windows.Forms import SendKeys import toga @@ -30,7 +32,8 @@ async def redraw(self, message=None, delay=None): @property def scale_factor(self): - return self.native.CreateGraphics().DpiX / 96 + # Does the same thing as `return self.native.CreateGraphics().DpiX / 96` + return Graphics.FromHdc(Graphics.FromHwnd(IntPtr.Zero).GetHdc()).DpiX / 96 async def type_character(self, char, *, shift=False, ctrl=False, alt=False): try: From f16ba4f15811050a34deeb04e6352ac2a44a5ba9 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 3 Feb 2024 11:16:56 -0800 Subject: [PATCH 083/102] Fixed cocoa implementation --- cocoa/src/toga_cocoa/screens.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/screens.py b/cocoa/src/toga_cocoa/screens.py index 47651d8028..7ab1f70282 100644 --- a/cocoa/src/toga_cocoa/screens.py +++ b/cocoa/src/toga_cocoa/screens.py @@ -44,7 +44,10 @@ def get_image_data(self): self.native.frame, ) # Get the size of the CGImage - size = CGImageGetWidth(cg_image), CGImageGetHeight(cg_image) + size = ( + CGImageGetWidth(cg_image) / self.native.backingScaleFactor, + CGImageGetHeight(cg_image) / self.native.backingScaleFactor, + ) # Create an NSImage from the CGImage ns_image = NSImage.alloc().initWithCGImage(cg_image, size=size) From 04e0e631074ee43bb9a2fb77e4d8295cd2c9df11 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 3 Feb 2024 20:03:56 -0800 Subject: [PATCH 084/102] Fixed cocoa implementation --- android/tests_backend/probe.py | 2 +- cocoa/src/toga_cocoa/screens.py | 5 +---- cocoa/tests_backend/probe.py | 13 +++++++++---- gtk/tests_backend/probe.py | 2 +- iOS/tests_backend/probe.py | 2 +- testbed/tests/app/test_screens.py | 2 +- winforms/tests_backend/probe.py | 2 +- 7 files changed, 15 insertions(+), 13 deletions(-) diff --git a/android/tests_backend/probe.py b/android/tests_backend/probe.py index d21380ee43..ce53b4ed94 100644 --- a/android/tests_backend/probe.py +++ b/android/tests_backend/probe.py @@ -94,7 +94,7 @@ async def redraw(self, message=None, delay=0): print("Waiting for redraw" if message is None else message) await asyncio.sleep(delay) - def assert_image_size(self, image_size, size): + def assert_image_size(self, image_size, size, screen=None): # Sizes are approximate because of scaling inconsistencies. assert image_size == ( approx(size[0] * self.scale_factor, abs=2), diff --git a/cocoa/src/toga_cocoa/screens.py b/cocoa/src/toga_cocoa/screens.py index 7ab1f70282..47651d8028 100644 --- a/cocoa/src/toga_cocoa/screens.py +++ b/cocoa/src/toga_cocoa/screens.py @@ -44,10 +44,7 @@ def get_image_data(self): self.native.frame, ) # Get the size of the CGImage - size = ( - CGImageGetWidth(cg_image) / self.native.backingScaleFactor, - CGImageGetHeight(cg_image) / self.native.backingScaleFactor, - ) + size = CGImageGetWidth(cg_image), CGImageGetHeight(cg_image) # Create an NSImage from the CGImage ns_image = NSImage.alloc().initWithCGImage(cg_image, size=size) diff --git a/cocoa/tests_backend/probe.py b/cocoa/tests_backend/probe.py index 865cf7cb62..10a23a180f 100644 --- a/cocoa/tests_backend/probe.py +++ b/cocoa/tests_backend/probe.py @@ -61,7 +61,12 @@ async def redraw(self, message=None, delay=None): # for at least one iteration. `runUntilDate:None` does this. NSRunLoop.currentRunLoop.runUntilDate(None) - def assert_image_size(self, image_size, size): - # Cocoa reports image sizing in the natural screen coordinates, not the size of - # the backing store. - assert image_size == size + def assert_image_size(self, image_size, size, screen=None): + if isinstance(screen, toga.screens.Screen): + # Screenshots are captured in native device resolution, not in CSS pixels. + scale = int(screen._impl.native.backingScaleFactor) + assert image_size == (size[0] * scale, size[1] * scale) + else: + # Cocoa reports image sizing in the natural screen coordinates, not the size of + # the backing store. + assert image_size == size diff --git a/gtk/tests_backend/probe.py b/gtk/tests_backend/probe.py index e87eca7bfd..484027d955 100644 --- a/gtk/tests_backend/probe.py +++ b/gtk/tests_backend/probe.py @@ -22,5 +22,5 @@ async def redraw(self, message=None, delay=None): if delay: await asyncio.sleep(delay) - def assert_image_size(self, image_size, size): + def assert_image_size(self, image_size, size, screen=None): assert image_size == size diff --git a/iOS/tests_backend/probe.py b/iOS/tests_backend/probe.py index c7dc5764a7..b5e80b31ae 100644 --- a/iOS/tests_backend/probe.py +++ b/iOS/tests_backend/probe.py @@ -19,7 +19,7 @@ async def redraw(self, message=None, delay=None): # for at least one iteration. `runUntilDate:None` does this. NSRunLoop.currentRunLoop.runUntilDate(None) - def assert_image_size(self, image_size, size): + def assert_image_size(self, image_size, size, screen=None): # Retina displays render images at a higher resolution than their reported size. scale = int(UIScreen.mainScreen.scale) assert image_size == (size[0] * scale, size[1] * scale) diff --git a/testbed/tests/app/test_screens.py b/testbed/tests/app/test_screens.py index 55ea3ce740..e5a84c2661 100644 --- a/testbed/tests/app/test_screens.py +++ b/testbed/tests/app/test_screens.py @@ -61,4 +61,4 @@ async def test_as_image(app): await probe.redraw(f"Screenshot of {screen} has been taken in PIL format") # Check if returned image is of type `PIL.Image.Image`. assert isinstance(pil_screenshot, PILImage) - probe.assert_image_size(pil_screenshot.size, probe.screen.size) + probe.assert_image_size(pil_screenshot.size, probe.screen.size, probe.screen) diff --git a/winforms/tests_backend/probe.py b/winforms/tests_backend/probe.py index f6495899be..099f7973aa 100644 --- a/winforms/tests_backend/probe.py +++ b/winforms/tests_backend/probe.py @@ -54,5 +54,5 @@ async def type_character(self, char, *, shift=False, ctrl=False, alt=False): # background. SendKeys.SendWait(key_code) - def assert_image_size(self, image_size, size): + def assert_image_size(self, image_size, size, screen=None): assert image_size == (size[0] * self.scale_factor, size[1] * self.scale_factor) From 8847d4ea55a6a3dc6b427f7cc97344941b990904 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 3 Feb 2024 20:21:10 -0800 Subject: [PATCH 085/102] Empty commit for CI From 52cd9d23203488cc776567ee906986e12e3f67d1 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 3 Feb 2024 20:38:07 -0800 Subject: [PATCH 086/102] Fixed cocoa implementation --- testbed/tests/app/test_screens.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testbed/tests/app/test_screens.py b/testbed/tests/app/test_screens.py index e5a84c2661..2d7f64c093 100644 --- a/testbed/tests/app/test_screens.py +++ b/testbed/tests/app/test_screens.py @@ -54,6 +54,7 @@ async def test_as_image(app): probe.assert_image_size( toga_image_screenshot.size, probe.screen.size, + probe.screen, ) # Capture screenshot in PIL format From 9aaeddd085c19beedab35525b072263a0739f8d7 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 3 Feb 2024 23:44:20 -0800 Subject: [PATCH 087/102] Misc Fixes --- cocoa/src/toga_cocoa/widgets/canvas.py | 12 +++++++++--- cocoa/src/toga_cocoa/window.py | 11 ++++++++--- testbed/tests/app/test_screens.py | 4 +--- testbed/tests/test_window.py | 4 +++- testbed/tests/widgets/test_canvas.py | 2 +- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/cocoa/src/toga_cocoa/widgets/canvas.py b/cocoa/src/toga_cocoa/widgets/canvas.py index d8c8ca8305..6409c04c48 100644 --- a/cocoa/src/toga_cocoa/widgets/canvas.py +++ b/cocoa/src/toga_cocoa/widgets/canvas.py @@ -321,9 +321,15 @@ def write_text(self, text, x, y, font, baseline, **kwargs): ) def get_image_data(self): - bitmap = self.native.bitmapImageRepForCachingDisplayInRect(self.native.bounds) - bitmap.setSize(self.native.bounds.size) - self.native.cacheDisplayInRect(self.native.bounds, toBitmapImageRep=bitmap) + # Convert to native backing scale bounds. + native_bounds_backing = self.native.window.screen.convertRectToBacking( + self.native.bounds + ) + bitmap = self.native.bitmapImageRepForCachingDisplayInRect( + native_bounds_backing + ) + bitmap.setSize(native_bounds_backing.size) + self.native.cacheDisplayInRect(native_bounds_backing, toBitmapImageRep=bitmap) return nsdata_to_bytes( bitmap.representationUsingType( diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index a4e554ef86..4ffc624ba5 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -311,12 +311,17 @@ def get_current_screen(self): return ScreenImpl(self.native.screen) def get_image_data(self): - bitmap = self.container.native.bitmapImageRepForCachingDisplayInRect( + # Convert to native backing scale bounds. + native_bounds_backing = self.native.screen.convertRectToBacking( self.container.native.bounds ) - bitmap.setSize(self.container.native.bounds.size) + + bitmap = self.container.native.bitmapImageRepForCachingDisplayInRect( + native_bounds_backing + ) + bitmap.setSize(native_bounds_backing.size) self.container.native.cacheDisplayInRect( - self.container.native.bounds, toBitmapImageRep=bitmap + native_bounds_backing, toBitmapImageRep=bitmap ) data = bitmap.representationUsingType( NSBitmapImageFileType.PNG, diff --git a/testbed/tests/app/test_screens.py b/testbed/tests/app/test_screens.py index 2d7f64c093..94d2eca309 100644 --- a/testbed/tests/app/test_screens.py +++ b/testbed/tests/app/test_screens.py @@ -52,9 +52,7 @@ async def test_as_image(app): # Check if returned image is of type `toga.images.Image`. assert isinstance(toga_image_screenshot, TogaImage) probe.assert_image_size( - toga_image_screenshot.size, - probe.screen.size, - probe.screen, + toga_image_screenshot.size, probe.screen.size, probe.screen ) # Capture screenshot in PIL format diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index a6595edeec..782ac4f05a 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -526,7 +526,9 @@ async def test_as_image(main_window, main_window_probe): """The window can be captured as a screenshot""" screenshot = main_window.as_image() - main_window_probe.assert_image_size(screenshot.size, main_window_probe.content_size) + main_window_probe.assert_image_size( + screenshot.size, main_window_probe.content_size, screen=main_window.screen + ) ######################################################################################## diff --git a/testbed/tests/widgets/test_canvas.py b/testbed/tests/widgets/test_canvas.py index 07e4df0023..702bc0631c 100644 --- a/testbed/tests/widgets/test_canvas.py +++ b/testbed/tests/widgets/test_canvas.py @@ -231,7 +231,7 @@ async def test_image_data(canvas, probe): # Cloned image is the right size. The platform may do DPI scaling; # let the probe determine the correct scaled size. - probe.assert_image_size(image.size, (200, 200)) + probe.assert_image_size(image.size, (200, 200), screen=canvas.window.screen) def assert_reference(probe, reference, threshold=0.0): From 1537b95f85065a6053565191362f15bb775b929e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 5 Feb 2024 07:10:13 +0800 Subject: [PATCH 088/102] Minor cleanups making screen non-optional. --- android/tests_backend/probe.py | 2 +- cocoa/tests_backend/probe.py | 13 ++++--------- docs/reference/api/screens.rst | 1 - gtk/tests_backend/probe.py | 2 +- iOS/tests_backend/probe.py | 2 +- testbed/tests/app/test_screens.py | 14 ++++++++++---- testbed/tests/test_window.py | 4 +++- testbed/tests/widgets/test_canvas.py | 6 +++++- winforms/tests_backend/probe.py | 2 +- 9 files changed, 26 insertions(+), 20 deletions(-) diff --git a/android/tests_backend/probe.py b/android/tests_backend/probe.py index ce53b4ed94..b219b47b95 100644 --- a/android/tests_backend/probe.py +++ b/android/tests_backend/probe.py @@ -94,7 +94,7 @@ async def redraw(self, message=None, delay=0): print("Waiting for redraw" if message is None else message) await asyncio.sleep(delay) - def assert_image_size(self, image_size, size, screen=None): + def assert_image_size(self, image_size, size, screen): # Sizes are approximate because of scaling inconsistencies. assert image_size == ( approx(size[0] * self.scale_factor, abs=2), diff --git a/cocoa/tests_backend/probe.py b/cocoa/tests_backend/probe.py index 10a23a180f..671671f3cc 100644 --- a/cocoa/tests_backend/probe.py +++ b/cocoa/tests_backend/probe.py @@ -61,12 +61,7 @@ async def redraw(self, message=None, delay=None): # for at least one iteration. `runUntilDate:None` does this. NSRunLoop.currentRunLoop.runUntilDate(None) - def assert_image_size(self, image_size, size, screen=None): - if isinstance(screen, toga.screens.Screen): - # Screenshots are captured in native device resolution, not in CSS pixels. - scale = int(screen._impl.native.backingScaleFactor) - assert image_size == (size[0] * scale, size[1] * scale) - else: - # Cocoa reports image sizing in the natural screen coordinates, not the size of - # the backing store. - assert image_size == size + def assert_image_size(self, image_size, size, screen): + # Screenshots are captured in native device resolution, not in CSS pixels. + scale = int(screen._impl.native.backingScaleFactor) + assert image_size == (size[0] * scale, size[1] * scale) diff --git a/docs/reference/api/screens.rst b/docs/reference/api/screens.rst index b3f97d4728..57d9dcaa0f 100644 --- a/docs/reference/api/screens.rst +++ b/docs/reference/api/screens.rst @@ -25,7 +25,6 @@ the "primary" screen. Screen sizes and positions are given in CSS pixels. # Print the identifying name of the second screen print(my_app.screens[1].name) - Notes ----- diff --git a/gtk/tests_backend/probe.py b/gtk/tests_backend/probe.py index 484027d955..198d7b554a 100644 --- a/gtk/tests_backend/probe.py +++ b/gtk/tests_backend/probe.py @@ -22,5 +22,5 @@ async def redraw(self, message=None, delay=None): if delay: await asyncio.sleep(delay) - def assert_image_size(self, image_size, size, screen=None): + def assert_image_size(self, image_size, size, screen): assert image_size == size diff --git a/iOS/tests_backend/probe.py b/iOS/tests_backend/probe.py index b5e80b31ae..1b101dc6bd 100644 --- a/iOS/tests_backend/probe.py +++ b/iOS/tests_backend/probe.py @@ -19,7 +19,7 @@ async def redraw(self, message=None, delay=None): # for at least one iteration. `runUntilDate:None` does this. NSRunLoop.currentRunLoop.runUntilDate(None) - def assert_image_size(self, image_size, size, screen=None): + def assert_image_size(self, image_size, size, screen): # Retina displays render images at a higher resolution than their reported size. scale = int(UIScreen.mainScreen.scale) assert image_size == (size[0] * scale, size[1] * scale) diff --git a/testbed/tests/app/test_screens.py b/testbed/tests/app/test_screens.py index 94d2eca309..78ec227b10 100644 --- a/testbed/tests/app/test_screens.py +++ b/testbed/tests/app/test_screens.py @@ -47,12 +47,14 @@ async def test_as_image(app): probe = screen_probe(screen) # `get_screenshot()` should default to `toga.images.Image` as format. - toga_image_screenshot = probe.get_screenshot() + screenshot = probe.get_screenshot() await probe.redraw(f"Screenshot of {screen} has been taken") # Check if returned image is of type `toga.images.Image`. - assert isinstance(toga_image_screenshot, TogaImage) + assert isinstance(screenshot, TogaImage) probe.assert_image_size( - toga_image_screenshot.size, probe.screen.size, probe.screen + screenshot.size, + probe.screen.size, + screen=probe.screen, ) # Capture screenshot in PIL format @@ -60,4 +62,8 @@ async def test_as_image(app): await probe.redraw(f"Screenshot of {screen} has been taken in PIL format") # Check if returned image is of type `PIL.Image.Image`. assert isinstance(pil_screenshot, PILImage) - probe.assert_image_size(pil_screenshot.size, probe.screen.size, probe.screen) + probe.assert_image_size( + pil_screenshot.size, + probe.screen.size, + screen=probe.screen, + ) diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 782ac4f05a..5905bffd13 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -527,7 +527,9 @@ async def test_as_image(main_window, main_window_probe): screenshot = main_window.as_image() main_window_probe.assert_image_size( - screenshot.size, main_window_probe.content_size, screen=main_window.screen + screenshot.size, + main_window_probe.content_size, + screen=main_window.screen, ) diff --git a/testbed/tests/widgets/test_canvas.py b/testbed/tests/widgets/test_canvas.py index 702bc0631c..e351ed1364 100644 --- a/testbed/tests/widgets/test_canvas.py +++ b/testbed/tests/widgets/test_canvas.py @@ -231,7 +231,11 @@ async def test_image_data(canvas, probe): # Cloned image is the right size. The platform may do DPI scaling; # let the probe determine the correct scaled size. - probe.assert_image_size(image.size, (200, 200), screen=canvas.window.screen) + probe.assert_image_size( + image.size, + (200, 200), + screen=canvas.window.screen, + ) def assert_reference(probe, reference, threshold=0.0): diff --git a/winforms/tests_backend/probe.py b/winforms/tests_backend/probe.py index 099f7973aa..77bb06fd34 100644 --- a/winforms/tests_backend/probe.py +++ b/winforms/tests_backend/probe.py @@ -54,5 +54,5 @@ async def type_character(self, char, *, shift=False, ctrl=False, alt=False): # background. SendKeys.SendWait(key_code) - def assert_image_size(self, image_size, size, screen=None): + def assert_image_size(self, image_size, size, screen): assert image_size == (size[0] * self.scale_factor, size[1] * self.scale_factor) From 1012549320718eeec981a9c9e15ca081ac4b99e7 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 8 Feb 2024 02:32:33 -0800 Subject: [PATCH 089/102] Misc Fix --- cocoa/src/toga_cocoa/libs/core_graphics.py | 48 ++++++++++++++++---- cocoa/src/toga_cocoa/screens.py | 10 ++--- cocoa/src/toga_cocoa/widgets/canvas.py | 51 +++++++++++++++------- cocoa/tests_backend/widgets/canvas.py | 3 +- testbed/tests/widgets/test_canvas.py | 3 +- 5 files changed, 83 insertions(+), 32 deletions(-) diff --git a/cocoa/src/toga_cocoa/libs/core_graphics.py b/cocoa/src/toga_cocoa/libs/core_graphics.py index bcda2b8bc9..fad0ba2b8e 100644 --- a/cocoa/src/toga_cocoa/libs/core_graphics.py +++ b/cocoa/src/toga_cocoa/libs/core_graphics.py @@ -171,6 +171,36 @@ class CGAffineTransform(Structure): core_graphics.CGContextTranslateCTM.restype = c_void_p core_graphics.CGContextTranslateCTM.argtypes = [CGContextRef, CGFloat, CGFloat] +CGColorSpaceRef = c_void_p +register_preferred_encoding(b"^{CGColorSpaceRef=}", CGColorSpaceRef) + +# CGColorSpaceRef CGColorSpaceCreateDeviceRGB(void); +core_graphics.CGColorSpaceCreateDeviceRGB.restype = CGColorSpaceRef +core_graphics.CGColorSpaceCreateDeviceRGB.argtypes = None + +# void CGColorSpaceRelease(CGColorSpaceRef space); +core_graphics.CGColorSpaceRelease.restype = None +core_graphics.CGColorSpaceRelease.argtypes = [CGColorSpaceRef] + +# CGContextRef CGBitmapContextCreate( +# void *data, +# size_t width, +# size_t height, +# size_t bitsPerComponent, +# size_t bytesPerRow, +# CGColorSpaceRef space, +# uint32_t bitmapInfo +# ); +core_graphics.CGBitmapContextCreate.restype = CGContextRef +core_graphics.CGBitmapContextCreate.argtypes = [ + c_void_p, + c_size_t, + c_size_t, + c_size_t, + c_size_t, + CGColorSpaceRef, + c_uint32, +] ###################################################################### # CGEvent.h @@ -236,14 +266,14 @@ class CGEventRef(c_void_p): register_preferred_encoding(b"^{CGImage=}", CGImageRef) -# CGImageRef CGDisplayCreateImage(CGDirectDisplayID displayID, CGRect rect); -core_graphics.CGDisplayCreateImage.restype = CGImageRef -core_graphics.CGDisplayCreateImage.argtypes = [CGDirectDisplayID, CGRect] +# CGImageRef CGDisplayCreateImageForRect(CGDirectDisplayID displayID, CGRect rect); +core_graphics.CGDisplayCreateImageForRect.restype = CGImageRef +core_graphics.CGDisplayCreateImageForRect.argtypes = [CGDirectDisplayID, CGRect] -CGImageGetWidth = core_graphics.CGImageGetWidth -CGImageGetWidth.argtypes = [c_void_p] -CGImageGetWidth.restype = c_size_t +# size_t CGImageGetWidth(CGImageRef image); +core_graphics.CGImageGetWidth.argtypes = [c_void_p] +core_graphics.CGImageGetWidth.restype = c_size_t -CGImageGetHeight = core_graphics.CGImageGetHeight -CGImageGetHeight.argtypes = [c_void_p] -CGImageGetHeight.restype = c_size_t +# size_t CGImageGetHeight(CGImageRef image); +core_graphics.CGImageGetHeight.argtypes = [c_void_p] +core_graphics.CGImageGetHeight.restype = c_size_t diff --git a/cocoa/src/toga_cocoa/screens.py b/cocoa/src/toga_cocoa/screens.py index 47651d8028..5835531efb 100644 --- a/cocoa/src/toga_cocoa/screens.py +++ b/cocoa/src/toga_cocoa/screens.py @@ -1,7 +1,5 @@ from toga.screens import Screen as ScreenInterface from toga_cocoa.libs import ( - CGImageGetHeight, - CGImageGetWidth, NSImage, core_graphics, ) @@ -38,13 +36,15 @@ def get_image_data(self): cg_direct_display_id = device_description.objectForKey_( "NSScreenNumber" ).unsignedIntValue - - cg_image = core_graphics.CGDisplayCreateImage( + cg_image = core_graphics.CGDisplayCreateImageForRect( cg_direct_display_id, self.native.frame, ) # Get the size of the CGImage - size = CGImageGetWidth(cg_image), CGImageGetHeight(cg_image) + size = ( + core_graphics.CGImageGetWidth(cg_image), + core_graphics.CGImageGetHeight(cg_image), + ) # Create an NSImage from the CGImage ns_image = NSImage.alloc().initWithCGImage(cg_image, size=size) diff --git a/cocoa/src/toga_cocoa/widgets/canvas.py b/cocoa/src/toga_cocoa/widgets/canvas.py index 6409c04c48..bb2bcbf4dc 100644 --- a/cocoa/src/toga_cocoa/widgets/canvas.py +++ b/cocoa/src/toga_cocoa/widgets/canvas.py @@ -1,21 +1,20 @@ from math import ceil -from rubicon.objc import objc_method, objc_property +from rubicon.objc import CGPoint, CGRect, CGSize, objc_method, objc_property from travertino.size import at_least from toga.colors import BLACK, TRANSPARENT, color from toga.widgets.canvas import Baseline, FillRule from toga_cocoa.colors import native_color -from toga_cocoa.images import nsdata_to_bytes from toga_cocoa.libs import ( CGFloat, CGPathDrawingMode, CGRectMake, NSAttributedString, - NSBitmapImageFileType, NSFontAttributeName, NSForegroundColorAttributeName, NSGraphicsContext, + NSImage, NSMutableDictionary, NSPoint, NSRect, @@ -24,6 +23,7 @@ NSStrokeWidthAttributeName, NSView, core_graphics, + kCGImageAlphaPremultipliedLast, kCGPathEOFill, kCGPathFill, kCGPathStroke, @@ -321,22 +321,41 @@ def write_text(self, text, x, y, font, baseline, **kwargs): ) def get_image_data(self): - # Convert to native backing scale bounds. - native_bounds_backing = self.native.window.screen.convertRectToBacking( - self.native.bounds + + bitmap = self.native.bitmapImageRepForCachingDisplayInRect(self.native.bounds) + bitmap.setSize(self.native.bounds.size) + self.native.cacheDisplayInRect(self.native.bounds, toBitmapImageRep=bitmap) + + # Create a CGImage from the bitmap + cg_image = bitmap.CGImage + + backing_scale = self.interface.window.screen._impl.native.backingScaleFactor + target_size = CGSize( + core_graphics.CGImageGetWidth(cg_image) * backing_scale, + core_graphics.CGImageGetHeight(cg_image) * backing_scale, ) - bitmap = self.native.bitmapImageRepForCachingDisplayInRect( - native_bounds_backing + target_frame = CGRect(CGPoint(0, 0), target_size) + + color_space = core_graphics.CGColorSpaceCreateDeviceRGB() + context = core_graphics.CGBitmapContextCreate( + None, + int(target_size.width), + int(target_size.height), + 8, + 0, + color_space, + kCGImageAlphaPremultipliedLast, ) - bitmap.setSize(native_bounds_backing.size) - self.native.cacheDisplayInRect(native_bounds_backing, toBitmapImageRep=bitmap) + core_graphics.CGColorSpaceRelease(color_space) + core_graphics.CGContextScaleCTM(context, backing_scale, backing_scale) - return nsdata_to_bytes( - bitmap.representationUsingType( - NSBitmapImageFileType.PNG, - properties=None, - ) - ) + context.draw(cg_image, target_frame) + + new_cg_image = context.makeImage() + + # Create an NSImage from the CGImage + ns_image = NSImage.alloc().initWithCGImage(new_cg_image, size=target_size) + return ns_image # Rehint def rehint(self): diff --git a/cocoa/tests_backend/widgets/canvas.py b/cocoa/tests_backend/widgets/canvas.py index cd2472ef0a..900f313f0c 100644 --- a/cocoa/tests_backend/widgets/canvas.py +++ b/cocoa/tests_backend/widgets/canvas.py @@ -4,6 +4,7 @@ from rubicon.objc import NSPoint from toga.colors import TRANSPARENT +from toga.images import Image as TogaImage from toga_cocoa.libs import NSEventType, NSView from .base import SimpleProbe @@ -27,7 +28,7 @@ def reference_variant(self, reference): return reference def get_image(self): - image = Image.open(BytesIO(self.impl.get_image_data())) + image = Image.open(BytesIO(TogaImage(self.impl.get_image_data()).data)) try: # If the image has an ICC profile, convert it into sRGB colorspace. diff --git a/testbed/tests/widgets/test_canvas.py b/testbed/tests/widgets/test_canvas.py index e351ed1364..a6ea7d5163 100644 --- a/testbed/tests/widgets/test_canvas.py +++ b/testbed/tests/widgets/test_canvas.py @@ -242,7 +242,7 @@ def assert_reference(probe, reference, threshold=0.0): """Assert that the canvas currently matches a reference image, within an RMS threshold""" # Get the canvas image. image = probe.get_image() - scaled_image = image.resize((200, 200)) + scaled_image = image.resize((200, 200), Image.Resampling.LANCZOS) # Look for a platform-specific reference variant. reference_variant = probe.reference_variant(reference) @@ -275,6 +275,7 @@ def save(): if rmse > threshold: save() pytest.fail(f"Rendered image doesn't match reference (RMSE=={rmse})") + save() else: save() pytest.fail(f"Couldn't find {reference_variant!r} reference image") From e24462d9b08132c9d4a36c2f2e21b641b9dc0244 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 9 Feb 2024 06:28:17 +0800 Subject: [PATCH 090/102] Tweak CoreGraphics usage. --- cocoa/src/toga_cocoa/libs/core_graphics.py | 4 ++++ cocoa/src/toga_cocoa/widgets/canvas.py | 7 ++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cocoa/src/toga_cocoa/libs/core_graphics.py b/cocoa/src/toga_cocoa/libs/core_graphics.py index fad0ba2b8e..38ffd6fa53 100644 --- a/cocoa/src/toga_cocoa/libs/core_graphics.py +++ b/cocoa/src/toga_cocoa/libs/core_graphics.py @@ -270,6 +270,10 @@ class CGEventRef(c_void_p): core_graphics.CGDisplayCreateImageForRect.restype = CGImageRef core_graphics.CGDisplayCreateImageForRect.argtypes = [CGDirectDisplayID, CGRect] +# void CGContextDrawImage(CGContextRef c, CGRect rect, CGImageRef image); +core_graphics.CGContextDrawImage.restype = None +core_graphics.CGContextDrawImage.argtypes = [CGContextRef, CGRect, CGImageRef] + # size_t CGImageGetWidth(CGImageRef image); core_graphics.CGImageGetWidth.argtypes = [c_void_p] core_graphics.CGImageGetWidth.restype = c_size_t diff --git a/cocoa/src/toga_cocoa/widgets/canvas.py b/cocoa/src/toga_cocoa/widgets/canvas.py index bb2bcbf4dc..f4166e3c36 100644 --- a/cocoa/src/toga_cocoa/widgets/canvas.py +++ b/cocoa/src/toga_cocoa/widgets/canvas.py @@ -348,13 +348,10 @@ def get_image_data(self): ) core_graphics.CGColorSpaceRelease(color_space) core_graphics.CGContextScaleCTM(context, backing_scale, backing_scale) - - context.draw(cg_image, target_frame) - - new_cg_image = context.makeImage() + core_graphics.CGContextDrawImage(context, target_frame, cg_image) # Create an NSImage from the CGImage - ns_image = NSImage.alloc().initWithCGImage(new_cg_image, size=target_size) + ns_image = NSImage.alloc().initWithCGImage(cg_image, size=target_size) return ns_image # Rehint From 209457ee9f7c1e57217cb074f9922034476e2e1c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 8 Feb 2024 18:17:06 -0800 Subject: [PATCH 091/102] Fixed `canvas.as_image()` --- cocoa/src/toga_cocoa/libs/core_graphics.py | 33 +++++++++++++--------- cocoa/src/toga_cocoa/widgets/canvas.py | 15 +++++++--- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/cocoa/src/toga_cocoa/libs/core_graphics.py b/cocoa/src/toga_cocoa/libs/core_graphics.py index 38ffd6fa53..981bad0d3a 100644 --- a/cocoa/src/toga_cocoa/libs/core_graphics.py +++ b/cocoa/src/toga_cocoa/libs/core_graphics.py @@ -201,6 +201,14 @@ class CGAffineTransform(Structure): CGColorSpaceRef, c_uint32, ] + + +CGImageRef = c_void_p +register_preferred_encoding(b"^{CGImage=}", CGImageRef) + + +core_graphics.CGBitmapContextCreateImage.restype = CGImageRef +core_graphics.CGBitmapContextCreateImage.argtypes = [CGContextRef] ###################################################################### # CGEvent.h @@ -251,6 +259,18 @@ class CGEventRef(c_void_p): kCGBitmapByteOrder16Big = 3 << 12 kCGBitmapByteOrder32Big = 4 << 12 +# size_t CGImageGetWidth(CGImageRef image); +core_graphics.CGImageGetWidth.restype = c_size_t +core_graphics.CGImageGetWidth.argtypes = [CGImageRef] + +# size_t CGImageGetHeight(CGImageRef image); +core_graphics.CGImageGetHeight.restype = c_size_t +core_graphics.CGImageGetHeight.argtypes = [CGImageRef] + +# void CGImageRelease(CGImageRef image); +core_graphics.CGImageRelease.restype = None +core_graphics.CGImageRelease.argtypes = [CGImageRef] + ###################################################################### # CoreGraphics.h @@ -261,11 +281,6 @@ class CGEventRef(c_void_p): core_graphics.CGMainDisplayID.argtypes = None -CGImageRef = c_void_p - - -register_preferred_encoding(b"^{CGImage=}", CGImageRef) - # CGImageRef CGDisplayCreateImageForRect(CGDirectDisplayID displayID, CGRect rect); core_graphics.CGDisplayCreateImageForRect.restype = CGImageRef core_graphics.CGDisplayCreateImageForRect.argtypes = [CGDirectDisplayID, CGRect] @@ -273,11 +288,3 @@ class CGEventRef(c_void_p): # void CGContextDrawImage(CGContextRef c, CGRect rect, CGImageRef image); core_graphics.CGContextDrawImage.restype = None core_graphics.CGContextDrawImage.argtypes = [CGContextRef, CGRect, CGImageRef] - -# size_t CGImageGetWidth(CGImageRef image); -core_graphics.CGImageGetWidth.argtypes = [c_void_p] -core_graphics.CGImageGetWidth.restype = c_size_t - -# size_t CGImageGetHeight(CGImageRef image); -core_graphics.CGImageGetHeight.argtypes = [c_void_p] -core_graphics.CGImageGetHeight.restype = c_size_t diff --git a/cocoa/src/toga_cocoa/widgets/canvas.py b/cocoa/src/toga_cocoa/widgets/canvas.py index f4166e3c36..31e890fac5 100644 --- a/cocoa/src/toga_cocoa/widgets/canvas.py +++ b/cocoa/src/toga_cocoa/widgets/canvas.py @@ -326,7 +326,7 @@ def get_image_data(self): bitmap.setSize(self.native.bounds.size) self.native.cacheDisplayInRect(self.native.bounds, toBitmapImageRep=bitmap) - # Create a CGImage from the bitmap + # Get a reference to the CGImage from the bitmap cg_image = bitmap.CGImage backing_scale = self.interface.window.screen._impl.native.backingScaleFactor @@ -347,11 +347,18 @@ def get_image_data(self): kCGImageAlphaPremultipliedLast, ) core_graphics.CGColorSpaceRelease(color_space) - core_graphics.CGContextScaleCTM(context, backing_scale, backing_scale) core_graphics.CGContextDrawImage(context, target_frame, cg_image) - + new_cg_image = core_graphics.CGBitmapContextCreateImage(context) + # ------------------------------For Debugging---------------------------- + new_cg_image_size = ( + core_graphics.CGImageGetWidth(new_cg_image), + core_graphics.CGImageGetHeight(new_cg_image), + ) + print(f"New CGImage size:{new_cg_image_size[0]}x{new_cg_image_size[1]}") + # ---------------------------------------------------------------------- # Create an NSImage from the CGImage - ns_image = NSImage.alloc().initWithCGImage(cg_image, size=target_size) + ns_image = NSImage.alloc().initWithCGImage(new_cg_image, size=target_size) + core_graphics.CGImageRelease(new_cg_image) return ns_image # Rehint From e10ca336c7f23b0dc552ecedc2ca1443a560d629 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 9 Feb 2024 04:21:38 -0800 Subject: [PATCH 092/102] Corrected cocoa as_image() implementations --- cocoa/src/toga_cocoa/libs/core_graphics.py | 69 ++++------------------ cocoa/src/toga_cocoa/screens.py | 10 ++-- cocoa/src/toga_cocoa/widgets/canvas.py | 36 ++--------- cocoa/src/toga_cocoa/window.py | 33 ++++++----- 4 files changed, 44 insertions(+), 104 deletions(-) diff --git a/cocoa/src/toga_cocoa/libs/core_graphics.py b/cocoa/src/toga_cocoa/libs/core_graphics.py index 981bad0d3a..07e4af9084 100644 --- a/cocoa/src/toga_cocoa/libs/core_graphics.py +++ b/cocoa/src/toga_cocoa/libs/core_graphics.py @@ -171,44 +171,6 @@ class CGAffineTransform(Structure): core_graphics.CGContextTranslateCTM.restype = c_void_p core_graphics.CGContextTranslateCTM.argtypes = [CGContextRef, CGFloat, CGFloat] -CGColorSpaceRef = c_void_p -register_preferred_encoding(b"^{CGColorSpaceRef=}", CGColorSpaceRef) - -# CGColorSpaceRef CGColorSpaceCreateDeviceRGB(void); -core_graphics.CGColorSpaceCreateDeviceRGB.restype = CGColorSpaceRef -core_graphics.CGColorSpaceCreateDeviceRGB.argtypes = None - -# void CGColorSpaceRelease(CGColorSpaceRef space); -core_graphics.CGColorSpaceRelease.restype = None -core_graphics.CGColorSpaceRelease.argtypes = [CGColorSpaceRef] - -# CGContextRef CGBitmapContextCreate( -# void *data, -# size_t width, -# size_t height, -# size_t bitsPerComponent, -# size_t bytesPerRow, -# CGColorSpaceRef space, -# uint32_t bitmapInfo -# ); -core_graphics.CGBitmapContextCreate.restype = CGContextRef -core_graphics.CGBitmapContextCreate.argtypes = [ - c_void_p, - c_size_t, - c_size_t, - c_size_t, - c_size_t, - CGColorSpaceRef, - c_uint32, -] - - -CGImageRef = c_void_p -register_preferred_encoding(b"^{CGImage=}", CGImageRef) - - -core_graphics.CGBitmapContextCreateImage.restype = CGImageRef -core_graphics.CGBitmapContextCreateImage.argtypes = [CGContextRef] ###################################################################### # CGEvent.h @@ -259,18 +221,6 @@ class CGEventRef(c_void_p): kCGBitmapByteOrder16Big = 3 << 12 kCGBitmapByteOrder32Big = 4 << 12 -# size_t CGImageGetWidth(CGImageRef image); -core_graphics.CGImageGetWidth.restype = c_size_t -core_graphics.CGImageGetWidth.argtypes = [CGImageRef] - -# size_t CGImageGetHeight(CGImageRef image); -core_graphics.CGImageGetHeight.restype = c_size_t -core_graphics.CGImageGetHeight.argtypes = [CGImageRef] - -# void CGImageRelease(CGImageRef image); -core_graphics.CGImageRelease.restype = None -core_graphics.CGImageRelease.argtypes = [CGImageRef] - ###################################################################### # CoreGraphics.h @@ -281,10 +231,17 @@ class CGEventRef(c_void_p): core_graphics.CGMainDisplayID.argtypes = None -# CGImageRef CGDisplayCreateImageForRect(CGDirectDisplayID displayID, CGRect rect); -core_graphics.CGDisplayCreateImageForRect.restype = CGImageRef -core_graphics.CGDisplayCreateImageForRect.argtypes = [CGDirectDisplayID, CGRect] +CGImageRef = c_void_p + -# void CGContextDrawImage(CGContextRef c, CGRect rect, CGImageRef image); -core_graphics.CGContextDrawImage.restype = None -core_graphics.CGContextDrawImage.argtypes = [CGContextRef, CGRect, CGImageRef] +register_preferred_encoding(b"^{CGImage=}", CGImageRef) + +# CGImageRef CGDisplayCreateImage(CGDirectDisplayID displayID, CGRect rect); +core_graphics.CGDisplayCreateImage.restype = CGImageRef +core_graphics.CGDisplayCreateImage.argtypes = [CGDirectDisplayID, CGRect] + +core_graphics.CGImageGetWidth.argtypes = [CGImageRef] +core_graphics.CGImageGetWidth.restype = c_size_t + +core_graphics.CGImageGetHeight.argtypes = [CGImageRef] +core_graphics.CGImageGetHeight.restype = c_size_t diff --git a/cocoa/src/toga_cocoa/screens.py b/cocoa/src/toga_cocoa/screens.py index 5835531efb..f58c1954e4 100644 --- a/cocoa/src/toga_cocoa/screens.py +++ b/cocoa/src/toga_cocoa/screens.py @@ -1,3 +1,5 @@ +from rubicon.objc import CGSize + from toga.screens import Screen as ScreenInterface from toga_cocoa.libs import ( NSImage, @@ -36,16 +38,16 @@ def get_image_data(self): cg_direct_display_id = device_description.objectForKey_( "NSScreenNumber" ).unsignedIntValue - cg_image = core_graphics.CGDisplayCreateImageForRect( + + cg_image = core_graphics.CGDisplayCreateImage( cg_direct_display_id, self.native.frame, ) # Get the size of the CGImage - size = ( + target_size = CGSize( core_graphics.CGImageGetWidth(cg_image), core_graphics.CGImageGetHeight(cg_image), ) # Create an NSImage from the CGImage - ns_image = NSImage.alloc().initWithCGImage(cg_image, size=size) - + ns_image = NSImage.alloc().initWithCGImage(cg_image, size=target_size) return ns_image diff --git a/cocoa/src/toga_cocoa/widgets/canvas.py b/cocoa/src/toga_cocoa/widgets/canvas.py index 31e890fac5..e4c07bf303 100644 --- a/cocoa/src/toga_cocoa/widgets/canvas.py +++ b/cocoa/src/toga_cocoa/widgets/canvas.py @@ -1,6 +1,6 @@ from math import ceil -from rubicon.objc import CGPoint, CGRect, CGSize, objc_method, objc_property +from rubicon.objc import CGSize, objc_method, objc_property from travertino.size import at_least from toga.colors import BLACK, TRANSPARENT, color @@ -23,7 +23,6 @@ NSStrokeWidthAttributeName, NSView, core_graphics, - kCGImageAlphaPremultipliedLast, kCGPathEOFill, kCGPathFill, kCGPathStroke, @@ -323,42 +322,19 @@ def write_text(self, text, x, y, font, baseline, **kwargs): def get_image_data(self): bitmap = self.native.bitmapImageRepForCachingDisplayInRect(self.native.bounds) - bitmap.setSize(self.native.bounds.size) self.native.cacheDisplayInRect(self.native.bounds, toBitmapImageRep=bitmap) # Get a reference to the CGImage from the bitmap cg_image = bitmap.CGImage - backing_scale = self.interface.window.screen._impl.native.backingScaleFactor target_size = CGSize( - core_graphics.CGImageGetWidth(cg_image) * backing_scale, - core_graphics.CGImageGetHeight(cg_image) * backing_scale, + core_graphics.CGImageGetWidth(cg_image), + core_graphics.CGImageGetHeight(cg_image), ) - target_frame = CGRect(CGPoint(0, 0), target_size) - - color_space = core_graphics.CGColorSpaceCreateDeviceRGB() - context = core_graphics.CGBitmapContextCreate( - None, - int(target_size.width), - int(target_size.height), - 8, - 0, - color_space, - kCGImageAlphaPremultipliedLast, - ) - core_graphics.CGColorSpaceRelease(color_space) - core_graphics.CGContextDrawImage(context, target_frame, cg_image) - new_cg_image = core_graphics.CGBitmapContextCreateImage(context) # ------------------------------For Debugging---------------------------- - new_cg_image_size = ( - core_graphics.CGImageGetWidth(new_cg_image), - core_graphics.CGImageGetHeight(new_cg_image), - ) - print(f"New CGImage size:{new_cg_image_size[0]}x{new_cg_image_size[1]}") - # ---------------------------------------------------------------------- - # Create an NSImage from the CGImage - ns_image = NSImage.alloc().initWithCGImage(new_cg_image, size=target_size) - core_graphics.CGImageRelease(new_cg_image) + print(f"Canvas CGImage size: {target_size.width} x {target_size.height}") + # ----------------------------------------------------------------------- + ns_image = NSImage.alloc().initWithCGImage(cg_image, size=target_size) return ns_image # Rehint diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 4ffc624ba5..d47878a09f 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -1,10 +1,11 @@ +from rubicon.objc import CGSize + from toga.command import Command, Separator from toga_cocoa.container import Container -from toga_cocoa.images import nsdata_to_bytes from toga_cocoa.libs import ( SEL, NSBackingStoreBuffered, - NSBitmapImageFileType, + NSImage, NSMakeRect, NSMutableArray, NSPoint, @@ -14,6 +15,7 @@ NSToolbarItem, NSWindow, NSWindowStyleMask, + core_graphics, objc_method, objc_property, ) @@ -311,20 +313,23 @@ def get_current_screen(self): return ScreenImpl(self.native.screen) def get_image_data(self): - # Convert to native backing scale bounds. - native_bounds_backing = self.native.screen.convertRectToBacking( - self.container.native.bounds - ) - bitmap = self.container.native.bitmapImageRepForCachingDisplayInRect( - native_bounds_backing + self.container.native.bounds ) - bitmap.setSize(native_bounds_backing.size) self.container.native.cacheDisplayInRect( - native_bounds_backing, toBitmapImageRep=bitmap + self.container.native.bounds, toBitmapImageRep=bitmap ) - data = bitmap.representationUsingType( - NSBitmapImageFileType.PNG, - properties=None, + + # Get a reference to the CGImage from the bitmap + cg_image = bitmap.CGImage + + target_size = CGSize( + core_graphics.CGImageGetWidth(cg_image), + core_graphics.CGImageGetHeight(cg_image), ) - return nsdata_to_bytes(data) + + # ------------------------------For Debugging---------------------------- + print(f"Window CGImage size: {target_size.width} x {target_size.height}") + # ----------------------------------------------------------------------- + ns_image = NSImage.alloc().initWithCGImage(cg_image, size=target_size) + return ns_image From d12c7fa6ff7e04d9f4045890d42b74c44c568e17 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Fri, 9 Feb 2024 06:12:31 -0800 Subject: [PATCH 093/102] Misc Fix --- testbed/tests/widgets/test_canvas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testbed/tests/widgets/test_canvas.py b/testbed/tests/widgets/test_canvas.py index a6ea7d5163..2f56595d36 100644 --- a/testbed/tests/widgets/test_canvas.py +++ b/testbed/tests/widgets/test_canvas.py @@ -242,7 +242,7 @@ def assert_reference(probe, reference, threshold=0.0): """Assert that the canvas currently matches a reference image, within an RMS threshold""" # Get the canvas image. image = probe.get_image() - scaled_image = image.resize((200, 200), Image.Resampling.LANCZOS) + scaled_image = image.resize((200, 200)) # Look for a platform-specific reference variant. reference_variant = probe.reference_variant(reference) From b2a47a1b01ca3c11a2c37f7a7c2a4cef048cce49 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Fri, 9 Feb 2024 06:15:21 -0800 Subject: [PATCH 094/102] Misc Fix --- testbed/tests/widgets/test_canvas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testbed/tests/widgets/test_canvas.py b/testbed/tests/widgets/test_canvas.py index 2f56595d36..e351ed1364 100644 --- a/testbed/tests/widgets/test_canvas.py +++ b/testbed/tests/widgets/test_canvas.py @@ -275,7 +275,6 @@ def save(): if rmse > threshold: save() pytest.fail(f"Rendered image doesn't match reference (RMSE=={rmse})") - save() else: save() pytest.fail(f"Couldn't find {reference_variant!r} reference image") From b15eb9f14fde2f4fd5e3ada474f529db5290bf8a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 11 Feb 2024 09:00:13 +0800 Subject: [PATCH 095/102] Clarified macOS core_graphics declarations. --- cocoa/src/toga_cocoa/libs/core_graphics.py | 24 ++++++++++------------ 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/cocoa/src/toga_cocoa/libs/core_graphics.py b/cocoa/src/toga_cocoa/libs/core_graphics.py index 07e4af9084..ca75f37a11 100644 --- a/cocoa/src/toga_cocoa/libs/core_graphics.py +++ b/cocoa/src/toga_cocoa/libs/core_graphics.py @@ -202,6 +202,16 @@ class CGEventRef(c_void_p): ###################################################################### # CGImage.h + +CGImageRef = c_void_p +register_preferred_encoding(b"^{CGImage=}", CGImageRef) + +core_graphics.CGImageGetWidth.argtypes = [CGImageRef] +core_graphics.CGImageGetWidth.restype = c_size_t + +core_graphics.CGImageGetHeight.argtypes = [CGImageRef] +core_graphics.CGImageGetHeight.restype = c_size_t + kCGImageAlphaNone = 0 kCGImageAlphaPremultipliedLast = 1 kCGImageAlphaPremultipliedFirst = 2 @@ -222,7 +232,7 @@ class CGEventRef(c_void_p): kCGBitmapByteOrder32Big = 4 << 12 ###################################################################### -# CoreGraphics.h +# CGDirectDisplay.h CGDirectDisplayID = c_uint32 @@ -230,18 +240,6 @@ class CGEventRef(c_void_p): core_graphics.CGMainDisplayID.restype = CGDirectDisplayID core_graphics.CGMainDisplayID.argtypes = None - -CGImageRef = c_void_p - - -register_preferred_encoding(b"^{CGImage=}", CGImageRef) - # CGImageRef CGDisplayCreateImage(CGDirectDisplayID displayID, CGRect rect); core_graphics.CGDisplayCreateImage.restype = CGImageRef core_graphics.CGDisplayCreateImage.argtypes = [CGDirectDisplayID, CGRect] - -core_graphics.CGImageGetWidth.argtypes = [CGImageRef] -core_graphics.CGImageGetWidth.restype = c_size_t - -core_graphics.CGImageGetHeight.argtypes = [CGImageRef] -core_graphics.CGImageGetHeight.restype = c_size_t From 656efb52fafcab6374c1dbecfd2bd8f78920137f Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 11 Feb 2024 09:01:12 +0800 Subject: [PATCH 096/102] Removed image size debugging. --- cocoa/src/toga_cocoa/widgets/canvas.py | 3 --- cocoa/src/toga_cocoa/window.py | 4 ---- 2 files changed, 7 deletions(-) diff --git a/cocoa/src/toga_cocoa/widgets/canvas.py b/cocoa/src/toga_cocoa/widgets/canvas.py index e4c07bf303..a37ff954f7 100644 --- a/cocoa/src/toga_cocoa/widgets/canvas.py +++ b/cocoa/src/toga_cocoa/widgets/canvas.py @@ -331,9 +331,6 @@ def get_image_data(self): core_graphics.CGImageGetWidth(cg_image), core_graphics.CGImageGetHeight(cg_image), ) - # ------------------------------For Debugging---------------------------- - print(f"Canvas CGImage size: {target_size.width} x {target_size.height}") - # ----------------------------------------------------------------------- ns_image = NSImage.alloc().initWithCGImage(cg_image, size=target_size) return ns_image diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index d47878a09f..b10a63d784 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -327,9 +327,5 @@ def get_image_data(self): core_graphics.CGImageGetWidth(cg_image), core_graphics.CGImageGetHeight(cg_image), ) - - # ------------------------------For Debugging---------------------------- - print(f"Window CGImage size: {target_size.width} x {target_size.height}") - # ----------------------------------------------------------------------- ns_image = NSImage.alloc().initWithCGImage(cg_image, size=target_size) return ns_image From 4d36cd642830b5d9d4785f15b96b600a01d826da Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 11 Feb 2024 09:04:50 +0800 Subject: [PATCH 097/102] Add backwards compatibility note about macOS image capture changes. --- changes/1930.removal.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/1930.removal.rst diff --git a/changes/1930.removal.rst b/changes/1930.removal.rst new file mode 100644 index 0000000000..60539edbe9 --- /dev/null +++ b/changes/1930.removal.rst @@ -0,0 +1 @@ +The macOS implementations of ``Window.as_image()`` and ``Canvas.as_image()`` APIs now return images in native device resolution, not CSS pixel resolution. This will result in images that are double the previous size on Retina displays. From 5eecd21f46d7c7d83c465990ab06420200dc5c05 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 11 Feb 2024 09:08:35 +0800 Subject: [PATCH 098/102] Tweak docstrings for position. --- core/src/toga/app.py | 1 - core/src/toga/window.py | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index fd3d318c78..b114ba1cf5 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -34,7 +34,6 @@ if TYPE_CHECKING: from toga.icons import IconContent - # Make sure deprecation warnings are shown by default warnings.filterwarnings("default", category=DeprecationWarning) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index ca9df146b0..c9456ce9a7 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -328,7 +328,10 @@ def size(self, size: tuple[int, int]) -> None: @property def position(self) -> tuple[int, int]: """Absolute position of the window, as a ``(x, y)`` tuple coordinates, in - :ref:`CSS pixels `. The origin is at the top left corner.""" + :ref:`CSS pixels `. + + The origin is the top left corner of the primary screen. + """ absolute_origin = self._app.screens[0].origin absolute_window_position = self._impl.get_position() From 35f084740fdcea4f82668d6a098116753cc319e3 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 11 Feb 2024 09:20:47 +0800 Subject: [PATCH 099/102] Clean up test docstrings. --- testbed/tests/app/test_app.py | 1 - testbed/tests/test_window.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index 32e40b32ce..b4d331bbce 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -552,7 +552,6 @@ async def test_beep(app): app.beep() -# Test primary screen `origin` and `name` & `origin` uniqueness of other screens. async def test_screens(app, app_probe): """Screens must have unique origins and names, with the primary screen at (0,0).""" diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 5905bffd13..d85654a9f2 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -152,7 +152,6 @@ async def test_full_screen(main_window, main_window_probe): main_window.full_screen = False await main_window_probe.wait_for_window("Full screen is a no-op") - # Test the `origin`, `position` and `screen_position`. async def test_screen(main_window, main_window_probe): """The window can be relocated to another screen, using both absolute and relative screen positions.""" assert main_window.screen.origin == (0, 0) @@ -490,7 +489,6 @@ async def test_full_screen(second_window, second_window_probe): assert not second_window_probe.is_full_screen assert second_window_probe.content_size == initial_content_size - # Test the `position`, `screen_position` and `screen`. @pytest.mark.parametrize( "second_window_kwargs", [dict(title="Secondary Window", position=(200, 150))], From 777f272dbd2d28812d4dc55ff95e96cc6a3325d5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 11 Feb 2024 09:22:17 +0800 Subject: [PATCH 100/102] Ensure textual backend does scaling on screen sizes. --- textual/src/toga_textual/screens.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/textual/src/toga_textual/screens.py b/textual/src/toga_textual/screens.py index 21abb3489e..cde07341e5 100644 --- a/textual/src/toga_textual/screens.py +++ b/textual/src/toga_textual/screens.py @@ -1,7 +1,9 @@ from toga.screens import Screen as ScreenInterface +from .widgets.base import Scalable -class Screen: + +class Screen(Scalable): _instances = {} def __new__(cls, native): @@ -21,7 +23,10 @@ def get_origin(self): return (0, 0) def get_size(self): - return (self.native.size.width, self.native.size.height) + return ( + self.scale_out_horizontal(self.native.size.width), + self.scale_out_vertical(self.native.size.height), + ) def get_image_data(self): self.interface.factory.not_implemented("Screen.get_image_data()") From 16b33ae8450e0b57f77c315ff27031c2de2d16c3 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 11 Feb 2024 09:51:13 +0800 Subject: [PATCH 101/102] Simplify android scaling calculation. --- android/src/toga_android/screens.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/android/src/toga_android/screens.py b/android/src/toga_android/screens.py index 39dc07fa8e..a4d3dead0c 100644 --- a/android/src/toga_android/screens.py +++ b/android/src/toga_android/screens.py @@ -30,8 +30,9 @@ def get_origin(self): return (0, 0) def get_size(self): - return tuple( - map(self.scale_out, (self.native.getWidth(), self.native.getHeight())) + return ( + self.scale_out(self.native.getWidth()), + self.scale_out(self.native.getHeight()), ) def get_image_data(self): From b025d218326b828dc8e6488e549d0c584c8d90db Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 10 Feb 2024 18:32:23 -0800 Subject: [PATCH 102/102] Corrected windows screen name --- winforms/src/toga_winforms/screens.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/winforms/src/toga_winforms/screens.py b/winforms/src/toga_winforms/screens.py index ad919aaf50..7a0001e9e1 100644 --- a/winforms/src/toga_winforms/screens.py +++ b/winforms/src/toga_winforms/screens.py @@ -24,7 +24,10 @@ def __new__(cls, native): return instance def get_name(self): - return self.native.DeviceName + name = self.native.DeviceName + # WinForms Display naming convention is "\\.\DISPLAY1". Remove the + # non-text part to prevent any errors due to non-escaped characters. + return name.split("\\")[-1] def get_origin(self): return self.native.Bounds.X, self.native.Bounds.Y