From b04602dd64b0af10f50954615046734107552e83 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 21 Mar 2024 03:30:29 -0700 Subject: [PATCH 001/248] Added winforms backend --- core/src/toga/constants/__init__.py | 16 +++++++++++++- core/src/toga/window.py | 10 +++++++++ winforms/src/toga_winforms/window.py | 31 ++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/core/src/toga/constants/__init__.py b/core/src/toga/constants/__init__.py index 1f7f3cf47b..3910f4d8cd 100644 --- a/core/src/toga/constants/__init__.py +++ b/core/src/toga/constants/__init__.py @@ -1,4 +1,4 @@ -from enum import Enum, auto +from enum import Enum, auto, unique from travertino.constants import * # noqa: F401, F403 pragma: no cover @@ -61,3 +61,17 @@ def __str__(self): # CELLULAR = 0 # WIFI = 1 # HIGHEST = 2 + +########################################################################## +# Window States +########################################################################## + + +@unique +class WindowState(Enum): + """The possible window states of an app.""" + + NORMAL = auto() + MAXIMIZED = auto() + MINIMIZED = auto() + FULLSCREEN = auto() diff --git a/core/src/toga/window.py b/core/src/toga/window.py index e107307ce4..a88d75a633 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -18,6 +18,7 @@ ) from toga.command import Command, CommandSet +from toga.constants import WindowState from toga.handlers import AsyncResult, wrapped_handler from toga.images import Image from toga.platform import get_platform_factory @@ -449,6 +450,15 @@ def full_screen(self, is_full_screen: bool) -> None: self._is_full_screen = is_full_screen self._impl.set_full_screen(is_full_screen) + @property + def state(self) -> WindowState: + """The current state of the window.""" + return self._impl.get_window_state() + + @state.setter + def state(self, state: WindowState) -> None: + self._impl.set_window_state(state) + ###################################################################### # Window capabilities ###################################################################### diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 9407c80ef4..75ff1aba12 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -4,6 +4,7 @@ from System.IO import MemoryStream from toga.command import Separator +from toga.constants import WindowState from .container import Container from .libs.wrapper import WeakrefCallable @@ -225,6 +226,36 @@ def set_full_screen(self, is_full_screen): ) self.native.WindowState = WinForms.FormWindowState.Normal + def get_window_state(self): + window_state = self.native.WindowState + window_border_style = self.native.FormBorderStyle + + if window_border_style != getattr(WinForms.FormBorderStyle, "None"): + if window_state == WinForms.FormWindowState.Normal: + return WindowState.NORMAL + elif window_state == WinForms.FormWindowState.Maximized: + return WindowState.MAXIMIZED + elif window_state == WinForms.FormWindowState.Minimized: + return WindowState.MINIMIZED + else: + if window_state == WinForms.FormWindowState.Maximized: + return WindowState.FULLSCREEN + + def set_window_state(self, state): + if state == WindowState.FULLSCREEN: + self.interface.app.set_full_screen(self.interface) + else: + self.native.FormBorderStyle = getattr( + WinForms.FormBorderStyle, + "Sizable" if self.interface.resizable else "FixedSingle", + ) + if state == WindowState.NORMAL: + self.native.WindowState = WinForms.WindowState.Normal + elif state == WindowState.MAXIMIZED: + self.native.WindowState = WinForms.WindowState.Maximized + elif state == WindowState.MINIMIZED: + self.native.WindowState = WinForms.WindowState.Minimized + ###################################################################### # Window capabilities ###################################################################### From 69712166ac26e7eccf65eda40bd4a884eadc4279 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 21 Mar 2024 03:32:24 -0700 Subject: [PATCH 002/248] Added winforms backend --- winforms/src/toga_winforms/window.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 75ff1aba12..4e878cbcff 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -228,9 +228,8 @@ def set_full_screen(self, is_full_screen): def get_window_state(self): window_state = self.native.WindowState - window_border_style = self.native.FormBorderStyle - if window_border_style != getattr(WinForms.FormBorderStyle, "None"): + if self.native.FormBorderStyle != getattr(WinForms.FormBorderStyle, "None"): if window_state == WinForms.FormWindowState.Normal: return WindowState.NORMAL elif window_state == WinForms.FormWindowState.Maximized: From a39d22fbb4152e1f13e09f49495ad0411ffffc38 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 21 Mar 2024 03:49:46 -0700 Subject: [PATCH 003/248] Added gtk backend --- gtk/src/toga_gtk/window.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 0273f62424..5087c7f350 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -1,4 +1,5 @@ from toga.command import Separator +from toga.constants import WindowState from .container import TogaContainer from .libs import Gdk, Gtk @@ -11,6 +12,7 @@ def __init__(self, interface, title, position, size): self.interface._impl = self self._is_closing = False + self._window_state = WindowState.NORMAL self.layout = None @@ -18,6 +20,7 @@ def __init__(self, interface, title, position, size): self.native._impl = self self.native.connect("delete-event", self.gtk_delete_event) + self.native.connect("window-state-event", self.gtk_window_state_event) self.native.set_default_size(size[0], size[1]) @@ -57,6 +60,19 @@ def create(self): # Native event handlers ###################################################################### + def gtk_window_state_event(self, widget, event): + # Get the window state + instantaneous_state = event.new_window_state + + if instantaneous_state & Gdk.WindowState.MAXIMIZED: + self._window_state = WindowState.MAXIMIZED + elif instantaneous_state & Gdk.WindowState.ICONIFIED: + self._window_state = WindowState.MINIMIZED + elif instantaneous_state & Gdk.WindowState.FULLSCREEN: + self._window_state = WindowState.FULLSCREEN + else: + self._window_state = WindowState.NORMAL + def gtk_delete_event(self, widget, data): if self._is_closing: should_close = True @@ -201,6 +217,28 @@ def set_full_screen(self, is_full_screen): else: self.native.unfullscreen() + def get_window_state(self): + return self._window_state + + def set_window_state(self, state): + if state == WindowState.NORMAL: + current_state = self.get_window_state() + if current_state == WindowState.MAXIMIZED: + self.native.unmaximize() + elif current_state == WindowState.MINIMIZED: + self.native.deiconify() + elif current_state == WindowState.FULLSCREEN: + self.native.unfullscreen() + + elif state == WindowState.MAXIMIZED: + self.native.maximize() + + elif state == WindowState.MINIMIZED: + self.native.iconify() + + elif state == WindowState.FULLSCREEN: + self.interface.app.set_full_screen(self.interface) + ###################################################################### # Window capabilities ###################################################################### From 2f2a3f255409cfc716dc38027ed1edd1dff92835 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 21 Mar 2024 04:41:55 -0700 Subject: [PATCH 004/248] Added cocoa backend --- cocoa/src/toga_cocoa/window.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 0bc63d99b6..859f023e1f 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -1,6 +1,7 @@ from rubicon.objc import CGSize from toga.command import Command, Separator +from toga.constants import WindowState from toga_cocoa.container import Container from toga_cocoa.libs import ( SEL, @@ -344,6 +345,38 @@ def set_full_screen(self, is_full_screen): if is_full_screen != current_state: self.native.toggleFullScreen(self.native) + def get_window_state(self): + if self.native.styleMask() & NSWindow.NSWindowZoomed: + return WindowState.MAXIMIZED + elif self.native.isMiniaturized(): + return WindowState.MINIMIZED + elif self.native.styleMask() & NSWindow.NSFullScreenWindowMask: + return WindowState.FULLSCREEN + else: + return WindowState.NORMAL + + def set_window_state(self, state): + if state == WindowState.NORMAL: + current_state = self.get_window_state() + # If the window is maximized, restore it to its normal size + if current_state == WindowState.MAXIMIZED: + self.native.zoom(None) + # Deminiaturize the window to restore it to its previous state + elif current_state == WindowState.MINIMIZED: + self.native.deminiaturize() + # If the window is in full-screen mode, exit full-screen mode + elif current_state == WindowState.FULLSCREEN: + self.native.toggleFullScreen(None) + + elif state == WindowState.MAXIMIZED: + self.native.zoom() + + elif state == WindowState.MINIMIZED: + self.native.miniaturize() + + elif state == WindowState.FULLSCREEN: + self.interface.app.set_full_screen(self.interface) + ###################################################################### # Window capabilities ###################################################################### From d4e2cd60eed12892a44c92598bac4dd191758188 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 21 Mar 2024 04:54:59 -0700 Subject: [PATCH 005/248] Added window example --- examples/window/window/app.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/examples/window/window/app.py b/examples/window/window/app.py index a1050e5cb8..286ad8756c 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -3,7 +3,7 @@ from functools import partial import toga -from toga.constants import COLUMN, RIGHT +from toga.constants import COLUMN, RIGHT, WindowState from toga.style import Pack @@ -30,6 +30,15 @@ def do_small(self, widget, **kwargs): def do_large(self, widget, **kwargs): self.main_window.size = (1500, 1000) + def do_normal(self, widget, **kwargs): + self.main_window.state = WindowState.NORMAL + + def do_maximize(self, widget, **kwargs): + self.main_window.state = WindowState.MAXIMIZED + + def do_minimize(self, widget, **kwargs): + self.main_window.state = WindowState.MINIMIZED + def do_app_full_screen(self, widget, **kwargs): if self.is_full_screen: self.exit_full_screen() @@ -177,6 +186,15 @@ def startup(self): btn_do_large = toga.Button( "Become large", on_press=self.do_large, style=btn_style ) + btn_do_normal = toga.Button( + "Become normal", on_press=self.do_normal, style=btn_style + ) + btn_do_maximize = toga.Button( + "Become maximize", on_press=self.do_maximize, style=btn_style + ) + btn_do_minimize = toga.Button( + "Become minimize", on_press=self.do_minimize, style=btn_style + ) btn_do_app_full_screen = toga.Button( "Make app full screen", on_press=self.do_app_full_screen, style=btn_style ) @@ -253,6 +271,9 @@ def startup(self): btn_do_right_current_screen, btn_do_small, btn_do_large, + btn_do_normal, + btn_do_maximize, + btn_do_minimize, btn_do_app_full_screen, btn_do_window_full_screen, btn_do_title, From aa3de1ad3877f00a37df6881791ad50c44652c40 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 21 Mar 2024 05:01:01 -0700 Subject: [PATCH 006/248] Modified winforms backend --- winforms/src/toga_winforms/window.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 4e878cbcff..d12192bb03 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -249,11 +249,11 @@ def set_window_state(self, state): "Sizable" if self.interface.resizable else "FixedSingle", ) if state == WindowState.NORMAL: - self.native.WindowState = WinForms.WindowState.Normal + self.native.WindowState = WinForms.FormWindowState.Normal elif state == WindowState.MAXIMIZED: - self.native.WindowState = WinForms.WindowState.Maximized + self.native.WindowState = WinForms.FormWindowState.Maximized elif state == WindowState.MINIMIZED: - self.native.WindowState = WinForms.WindowState.Minimized + self.native.WindowState = WinForms.FormWindowState.Minimized ###################################################################### # Window capabilities From 2ba241f7a0588a0bb4a477eb929c82e80d48ec6e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 21 Mar 2024 10:09:45 -0700 Subject: [PATCH 007/248] Modified cocoa backend --- cocoa/src/toga_cocoa/window.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 859f023e1f..d9d0f6ae4f 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -346,35 +346,35 @@ def set_full_screen(self, is_full_screen): self.native.toggleFullScreen(self.native) def get_window_state(self): - if self.native.styleMask() & NSWindow.NSWindowZoomed: + if bool(self.native.isZoomed): return WindowState.MAXIMIZED - elif self.native.isMiniaturized(): + elif bool(self.native.isMiniaturized): return WindowState.MINIMIZED - elif self.native.styleMask() & NSWindow.NSFullScreenWindowMask: + elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): return WindowState.FULLSCREEN else: return WindowState.NORMAL def set_window_state(self, state): - if state == WindowState.NORMAL: - current_state = self.get_window_state() + current_state = self.get_window_state() + if state == WindowState.NORMAL and current_state != WindowState.NORMAL: # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: - self.native.zoom(None) + self.native.setIsZoomed(False) # Deminiaturize the window to restore it to its previous state elif current_state == WindowState.MINIMIZED: - self.native.deminiaturize() + self.native.setIsMiniaturized(False) # If the window is in full-screen mode, exit full-screen mode elif current_state == WindowState.FULLSCREEN: self.native.toggleFullScreen(None) - elif state == WindowState.MAXIMIZED: - self.native.zoom() + elif state == WindowState.MAXIMIZED and current_state != WindowState.MAXIMIZED: + self.native.setIsZoomed(True) - elif state == WindowState.MINIMIZED: - self.native.miniaturize() + elif state == WindowState.MINIMIZED and current_state != WindowState.MINIMIZED: + self.native.setIsMiniaturized(True) - elif state == WindowState.FULLSCREEN: + elif state == WindowState.FULLSCREEN and current_state != WindowState.FULLSCREEN: self.interface.app.set_full_screen(self.interface) ###################################################################### From ac7ee71d7b3a58ba4ebb797bf11aaac4125f1d3b Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 21 Mar 2024 10:12:25 -0700 Subject: [PATCH 008/248] Modified cocoa backend From da782ba4acc6b3a7d1daf93de01b38e508acc441 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 21 Mar 2024 10:13:54 -0700 Subject: [PATCH 009/248] Modified cocoa backend --- cocoa/src/toga_cocoa/window.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index d9d0f6ae4f..e05ce008b3 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -374,7 +374,9 @@ def set_window_state(self, state): elif state == WindowState.MINIMIZED and current_state != WindowState.MINIMIZED: self.native.setIsMiniaturized(True) - elif state == WindowState.FULLSCREEN and current_state != WindowState.FULLSCREEN: + elif ( + state == WindowState.FULLSCREEN and current_state != WindowState.FULLSCREEN + ): self.interface.app.set_full_screen(self.interface) ###################################################################### From 45d3befa6a6c2844e73bd2d44ec72b0e9cff8c5e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 22 Mar 2024 04:32:30 -0700 Subject: [PATCH 010/248] Added Full screen to backend --- cocoa/src/toga_cocoa/app.py | 17 ++++++----- cocoa/src/toga_cocoa/window.py | 17 ++++++----- core/src/toga/app.py | 56 +++++++++++++++++++++++++++++----- 3 files changed, 69 insertions(+), 21 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index d1587edaf3..582acd09fb 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -495,14 +495,16 @@ def set_current_window(self, window): # Full screen control ###################################################################### - def enter_full_screen(self, windows): + def enter_full_screen(self, screen_window_dict): opts = NSMutableDictionary.alloc().init() opts.setObject( NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" ) - for window, screen in zip(windows, NSScreen.screens): - window.content._impl.native.enterFullScreenMode(screen, withOptions=opts) + for screen, window in screen_window_dict.items(): + window.content._impl.native.enterFullScreenMode( + screen._impl.native, withOptions=opts + ) # Going full screen causes the window content to be re-homed # in a NSFullScreenWindow; teach the new parent window # about its Toga representations. @@ -510,15 +512,16 @@ def enter_full_screen(self, windows): window.content._impl.native.window.interface = window window.content.refresh() - def exit_full_screen(self, windows): + def exit_full_screen(self): opts = NSMutableDictionary.alloc().init() opts.setObject( NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" ) - for window in windows: - window.content._impl.native.exitFullScreenModeWithOptions(opts) - window.content.refresh() + for window in self.interface.windows: + if bool(window.content._impl.native.isInFullScreenMode()): + window.content._impl.native.exitFullScreenModeWithOptions(opts) + window.content.refresh() class DocumentApp(App): # pragma: no cover diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index e05ce008b3..3e9dc95f08 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -341,16 +341,16 @@ def get_visible(self): ###################################################################### def set_full_screen(self, is_full_screen): - current_state = bool(self.native.styleMask & NSWindowStyleMask.FullScreen) - if is_full_screen != current_state: - self.native.toggleFullScreen(self.native) + self.set_window_state(WindowState.FULLSCREEN) def get_window_state(self): if bool(self.native.isZoomed): return WindowState.MAXIMIZED elif bool(self.native.isMiniaturized): return WindowState.MINIMIZED - elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): + elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen) or bool( + self.interface.content._impl.native.isInFullScreenMode() + ): return WindowState.FULLSCREEN else: return WindowState.NORMAL @@ -364,9 +364,12 @@ def set_window_state(self, state): # Deminiaturize the window to restore it to its previous state elif current_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(False) - # If the window is in full-screen mode, exit full-screen mode - elif current_state == WindowState.FULLSCREEN: + # If the window is in window full-screen mode, exit full-screen mode + elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): self.native.toggleFullScreen(None) + # If the window is in app full-screen mode, exit full-screen mode + elif bool(self.interface.content._impl.native.isInFullScreenMode()): + self.interface.app._impl.exit_full_screen() elif state == WindowState.MAXIMIZED and current_state != WindowState.MAXIMIZED: self.native.setIsZoomed(True) @@ -377,7 +380,7 @@ def set_window_state(self, state): elif ( state == WindowState.FULLSCREEN and current_state != WindowState.FULLSCREEN ): - self.interface.app.set_full_screen(self.interface) + self.native.toggleFullScreen(self.native) ###################################################################### # Window capabilities diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 9999591a27..e342e2b91f 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -21,6 +21,7 @@ from weakref import WeakValueDictionary from toga.command import Command, CommandSet +from toga.constants import WindowState from toga.documents import Document from toga.handlers import wrapped_handler from toga.hardware.camera import Camera @@ -764,15 +765,34 @@ def current_window(self, window: Window): def exit_full_screen(self) -> None: """Exit full screen mode.""" if self.is_full_screen: - self._impl.exit_full_screen(self._full_screen_windows) - self._full_screen_windows = None + self._impl.exit_full_screen() @property def is_full_screen(self) -> bool: """Is the app currently in full screen mode?""" - return self._full_screen_windows is not None + return any(window.state == WindowState.FULLSCREEN for window in self.windows) - def set_full_screen(self, *windows: Window) -> None: + # def set_full_screen(self, *windows: Window) -> None: + # """Make one or more windows full screen. + + # Full screen is not the same as "maximized"; full screen mode is when all window + # borders and other window decorations are no longer visible. + + # :param windows: The list of windows to go full screen, in order of allocation to + # screens. If the number of windows exceeds the number of available displays, + # those windows will not be visible. If no windows are specified, the app will + # exit full screen mode. + # """ + # self.exit_full_screen() + # if windows: + # self._impl.enter_full_screen(windows) + # self._full_screen_windows = windows + + def set_full_screen( + self, + window_or_list_or_dict: Window | list[Window] | dict[Screen, Window] | None, + *additional_windows: Window | None, + ) -> None: """Make one or more windows full screen. Full screen is not the same as "maximized"; full screen mode is when all window @@ -784,9 +804,31 @@ def set_full_screen(self, *windows: Window) -> None: exit full screen mode. """ self.exit_full_screen() - if windows: - self._impl.enter_full_screen(windows) - self._full_screen_windows = windows + if self.windows is not None: + screen_window_dict = dict() + if isinstance(window_or_list_or_dict, Window): + screen_window_dict[self.screens[0]] = window_or_list_or_dict + if additional_windows is not None: + for index, (window, screen) in enumerate( + zip(additional_windows, self.screens[1:]) + ): + if index < len(self.screens) - 1: + screen_window_dict[screen] = window + else: + break + elif additional_windows is None: + if isinstance(window_or_list_or_dict, list): + for index, (window, screen) in enumerate( + zip(additional_windows, self.screens) + ): + if index < len(self.screens) - 1: + screen_window_dict[screen] = window + else: + break + elif isinstance(window_or_list_or_dict, dict): + screen_window_dict = window_or_list_or_dict + + self._impl.enter_full_screen(screen_window_dict) ###################################################################### # App events From faeb9dd0457dccf6bf7f7c0e09dd8546719b1358 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 22 Mar 2024 09:30:45 -0700 Subject: [PATCH 011/248] Modified Full Screen winforms backend --- cocoa/src/toga_cocoa/app.py | 3 +++ core/src/toga/app.py | 1 - winforms/src/toga_winforms/app.py | 13 ++++++++----- winforms/src/toga_winforms/window.py | 29 ++++++++++++++++++---------- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 582acd09fb..ca95b87110 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -34,6 +34,7 @@ NSOpenPanel, NSScreen, NSString, + NSWindowStyleMask, objc_method, objc_property, ) @@ -522,6 +523,8 @@ def exit_full_screen(self): if bool(window.content._impl.native.isInFullScreenMode()): window.content._impl.native.exitFullScreenModeWithOptions(opts) window.content.refresh() + elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): + self.native.toggleFullScreen(None) class DocumentApp(App): # pragma: no cover diff --git a/core/src/toga/app.py b/core/src/toga/app.py index e342e2b91f..35c27142f3 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -827,7 +827,6 @@ def set_full_screen( break elif isinstance(window_or_list_or_dict, dict): screen_window_dict = window_or_list_or_dict - self._impl.enter_full_screen(screen_window_dict) ###################################################################### diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index eb5f5b5d24..1bee29fb0e 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -14,6 +14,7 @@ import toga from toga import Key from toga.command import Separator +from toga.constants import WindowState from .keys import toga_to_winforms_key, toga_to_winforms_shortcut from .libs.proactor import WinformsProactorEventLoop @@ -372,13 +373,15 @@ def set_current_window(self, window): # Full screen control ###################################################################### - def enter_full_screen(self, windows): - for window in windows: + def enter_full_screen(self, screen_window_dict): + for screen, window in screen_window_dict.items(): + window.screen = screen window._impl.set_full_screen(True) - def exit_full_screen(self, windows): - for window in windows: - window._impl.set_full_screen(False) + def exit_full_screen(self): + for window in self.interface.windows: + if window.state == WindowState.FULLSCREEN: + window._impl.set_full_screen(False) class DocumentApp(App): # pragma: no cover diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index d12192bb03..e802048e3a 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -228,31 +228,40 @@ def set_full_screen(self, is_full_screen): def get_window_state(self): window_state = self.native.WindowState - - if self.native.FormBorderStyle != getattr(WinForms.FormBorderStyle, "None"): + if ( + self.native.FormBorderStyle == getattr(WinForms.FormBorderStyle, "None") + and window_state == WinForms.FormWindowState.Maximized + ): + return WindowState.FULLSCREEN + else: if window_state == WinForms.FormWindowState.Normal: return WindowState.NORMAL elif window_state == WinForms.FormWindowState.Maximized: return WindowState.MAXIMIZED elif window_state == WinForms.FormWindowState.Minimized: return WindowState.MINIMIZED - else: - if window_state == WinForms.FormWindowState.Maximized: - return WindowState.FULLSCREEN def set_window_state(self, state): - if state == WindowState.FULLSCREEN: - self.interface.app.set_full_screen(self.interface) + current_state = self.get_window_state() + if state == WindowState.FULLSCREEN and current_state != WindowState.FULLSCREEN: + self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") + self.native.WindowState = WinForms.FormWindowState.Maximized else: self.native.FormBorderStyle = getattr( WinForms.FormBorderStyle, "Sizable" if self.interface.resizable else "FixedSingle", ) - if state == WindowState.NORMAL: + if state == WindowState.NORMAL and current_state != WindowState.NORMAL: self.native.WindowState = WinForms.FormWindowState.Normal - elif state == WindowState.MAXIMIZED: + elif ( + state == WindowState.MAXIMIZED + and current_state != WindowState.MAXIMIZED + ): self.native.WindowState = WinForms.FormWindowState.Maximized - elif state == WindowState.MINIMIZED: + elif ( + state == WindowState.MINIMIZED + and current_state != WindowState.MINIMIZED + ): self.native.WindowState = WinForms.FormWindowState.Minimized ###################################################################### From eccccbd57a5ef7fa5a0b8b5b201229c1bd354e7b Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 23 Mar 2024 06:36:55 -0700 Subject: [PATCH 012/248] Corrected cocoa backend and core --- cocoa/src/toga_cocoa/app.py | 48 +++++----- cocoa/src/toga_cocoa/window.py | 58 +++++++++--- core/src/toga/app.py | 133 ++++++++++++++++++---------- core/src/toga/constants/__init__.py | 1 + core/src/toga/window.py | 13 ++- 5 files changed, 166 insertions(+), 87 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index ca95b87110..2a47a96ea5 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -9,6 +9,7 @@ import toga from toga.command import Separator +from toga.constants import WindowState from toga.handlers import NativeHandler from .keys import cocoa_key @@ -29,12 +30,10 @@ NSMenuItem, NSMutableArray, NSMutableDictionary, - NSNumber, NSObject, NSOpenPanel, NSScreen, NSString, - NSWindowStyleMask, objc_method, objc_property, ) @@ -493,38 +492,33 @@ def set_current_window(self, window): window._impl.native.makeKeyAndOrderFront(window._impl.native) ###################################################################### - # Full screen control + # Full screen/Presentation mode controls ###################################################################### + # ----------------------Future Deprecated methods---------------------- def enter_full_screen(self, screen_window_dict): - opts = NSMutableDictionary.alloc().init() - opts.setObject( - NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" - ) - - for screen, window in screen_window_dict.items(): - window.content._impl.native.enterFullScreenMode( - screen._impl.native, withOptions=opts - ) - # Going full screen causes the window content to be re-homed - # in a NSFullScreenWindow; teach the new parent window - # about its Toga representations. - window.content._impl.native.window._impl = window._impl - window.content._impl.native.window.interface = window - window.content.refresh() + self.enter_presentation_mode(screen_window_dict) def exit_full_screen(self): - opts = NSMutableDictionary.alloc().init() - opts.setObject( - NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" - ) + for window in self.interface.windows: + if ( + window.state == WindowState.FULLSCREEN + or window.state == WindowState.PRESENTATION + ): + window.state = WindowState.NORMAL + + # --------------------------------------------------------------------- + + def enter_presentation_mode(self, screen_window_dict): + for screen, window in screen_window_dict.items(): + window._impl._before_presentation_mode_screen = window.screen + window.screen = screen + window.state = WindowState.PRESENTATION + def exit_presentation_mode(self): for window in self.interface.windows: - if bool(window.content._impl.native.isInFullScreenMode()): - window.content._impl.native.exitFullScreenModeWithOptions(opts) - window.content.refresh() - elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): - self.native.toggleFullScreen(None) + if window.state == WindowState.PRESENTATION: + window.state = WindowState.NORMAL class DocumentApp(App): # pragma: no cover diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 3e9dc95f08..5477b9e769 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -9,6 +9,8 @@ NSImage, NSMakeRect, NSMutableArray, + NSMutableDictionary, + NSNumber, NSPoint, NSScreen, NSSize, @@ -340,18 +342,24 @@ def get_visible(self): # Window state ###################################################################### + # ----------------------Future Deprecated method---------------------- def set_full_screen(self, is_full_screen): - self.set_window_state(WindowState.FULLSCREEN) + if is_full_screen and (self.get_window_state() != WindowState.FULLSCREEN): + self.set_window_state(WindowState.FULLSCREEN) + elif not is_full_screen and (self.get_window_state() == WindowState.FULLSCREEN): + self.set_window_state(WindowState.NORMAL) + + # --------------------------------------------------------------------- def get_window_state(self): if bool(self.native.isZoomed): return WindowState.MAXIMIZED elif bool(self.native.isMiniaturized): return WindowState.MINIMIZED - elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen) or bool( - self.interface.content._impl.native.isInFullScreenMode() - ): + elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): return WindowState.FULLSCREEN + elif bool(self.interface.content._impl.native.isInFullScreenMode()): + return WindowState.PRESENTATION else: return WindowState.NORMAL @@ -364,23 +372,53 @@ def set_window_state(self, state): # Deminiaturize the window to restore it to its previous state elif current_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(False) - # If the window is in window full-screen mode, exit full-screen mode - elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): + # If the window is in full-screen mode, exit full-screen mode + elif current_state == WindowState.FULLSCREEN: self.native.toggleFullScreen(None) - # If the window is in app full-screen mode, exit full-screen mode - elif bool(self.interface.content._impl.native.isInFullScreenMode()): - self.interface.app._impl.exit_full_screen() - + # If the window is in presentation mode, exit presentation mode + elif current_state == WindowState.PRESENTATION: + opts = NSMutableDictionary.alloc().init() + opts.setObject( + NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" + ) + self.interface.content._impl.native.exitFullScreenModeWithOptions(opts) + self.interface.content.refresh() + self.interface.screen = ( + self.interface._impl._before_presentation_mode_screen + ) + # Set Window state to NORMAL before changing to other states as some states + # block changing window state without first exiting them. elif state == WindowState.MAXIMIZED and current_state != WindowState.MAXIMIZED: + self.set_window_state(WindowState.NORMAL) self.native.setIsZoomed(True) elif state == WindowState.MINIMIZED and current_state != WindowState.MINIMIZED: + self.set_window_state(WindowState.NORMAL) self.native.setIsMiniaturized(True) elif ( state == WindowState.FULLSCREEN and current_state != WindowState.FULLSCREEN ): + self.set_window_state(WindowState.NORMAL) self.native.toggleFullScreen(self.native) + elif ( + state == WindowState.PRESENTATION + and current_state != WindowState.PRESENTATION + ): + self.set_window_state(WindowState.NORMAL) + opts = NSMutableDictionary.alloc().init() + opts.setObject( + NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" + ) + self.interface.content._impl.native.enterFullScreenMode( + self.interface.screen._impl.native, withOptions=opts + ) + # Going presentation mode(full screen) causes the window content to be + # re-homed in a NSFullScreenWindow; teach the new parent window + # about its Toga representations. + self.interface.content._impl.native.window._impl = self + self.interface.content._impl.native.window.interface = self.interface + self.interface.content.refresh() ###################################################################### # Window capabilities diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 35c27142f3..46244207ee 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -762,37 +762,46 @@ def current_window(self, window: Window): # Full screen control ###################################################################### + # ----------------------Future Deprecated methods---------------------- def exit_full_screen(self) -> None: - """Exit full screen mode.""" + """Exit full screen mode. + + .. warning:: + `App.exit_full_screen()` method is deprecated and will be + removed in the future. Consider using `App.enter_presentation_mode()` + and `App.exit_presentation_mode()` methods instead. + """ + warnings.warn( + "`App.exit_full_screen()` method is deprecated and will be" + " removed in the future. Consider using `App.enter_presentation_mode()`" + " and `App.exit_presentation_mode()` methods instead.", + FutureWarning, + ) if self.is_full_screen: self._impl.exit_full_screen() @property def is_full_screen(self) -> bool: - """Is the app currently in full screen mode?""" - return any(window.state == WindowState.FULLSCREEN for window in self.windows) - - # def set_full_screen(self, *windows: Window) -> None: - # """Make one or more windows full screen. - - # Full screen is not the same as "maximized"; full screen mode is when all window - # borders and other window decorations are no longer visible. - - # :param windows: The list of windows to go full screen, in order of allocation to - # screens. If the number of windows exceeds the number of available displays, - # those windows will not be visible. If no windows are specified, the app will - # exit full screen mode. - # """ - # self.exit_full_screen() - # if windows: - # self._impl.enter_full_screen(windows) - # self._full_screen_windows = windows - - def set_full_screen( - self, - window_or_list_or_dict: Window | list[Window] | dict[Screen, Window] | None, - *additional_windows: Window | None, - ) -> None: + """Is the app currently in full screen mode? + + .. warning:: + `App.is_full_screen` property is deprecated and will be removed in the + future. Consider using `App.is_in_presentation_mode` property instead. + """ + warnings.warn( + "`App.is_full_screen` property is deprecated and will be removed in the" + " future. Consider using `App.is_in_presentation_mode` property instead.", + FutureWarning, + ) + return any( + ( + window.state == WindowState.FULLSCREEN + or window.state == WindowState.PRESENTATION + ) + for window in self.windows + ) + + def set_full_screen(self, *windows: Window) -> None: """Make one or more windows full screen. Full screen is not the same as "maximized"; full screen mode is when all window @@ -802,32 +811,60 @@ def set_full_screen( screens. If the number of windows exceeds the number of available displays, those windows will not be visible. If no windows are specified, the app will exit full screen mode. + + .. warning:: + `App.set_full_screen()` method is deprecated and will be + removed in the future. Consider using `App.enter_presentation_mode()` + and `App.exit_presentation_mode()` methods instead. """ - self.exit_full_screen() + warnings.warn( + "`App.set_full_screen()` method is deprecated and will be" + " removed in the future. Consider using `App.enter_presentation_mode()`" + " and `App.exit_presentation_mode()` methods instead.", + FutureWarning, + ) if self.windows is not None: + self.exit_full_screen() screen_window_dict = dict() - if isinstance(window_or_list_or_dict, Window): - screen_window_dict[self.screens[0]] = window_or_list_or_dict - if additional_windows is not None: - for index, (window, screen) in enumerate( - zip(additional_windows, self.screens[1:]) - ): - if index < len(self.screens) - 1: - screen_window_dict[screen] = window - else: - break - elif additional_windows is None: - if isinstance(window_or_list_or_dict, list): - for index, (window, screen) in enumerate( - zip(additional_windows, self.screens) - ): - if index < len(self.screens) - 1: - screen_window_dict[screen] = window - else: - break - elif isinstance(window_or_list_or_dict, dict): - screen_window_dict = window_or_list_or_dict - self._impl.enter_full_screen(screen_window_dict) + for window, screen in zip(windows, self.screens): + screen_window_dict[screen] = window + self.enter_presentation_mode(screen_window_dict) + else: + warnings.warn("App doesn't have any windows") + + # --------------------------------------------------------------------- + + @property + def is_in_presentation_mode(self) -> bool: + """Is the app currently in presentation mode?""" + return any(window.state == WindowState.PRESENTATION for window in self.windows) + + def enter_presentation_mode(self, window_or_list_or_dict): + """Enter into presentation mode with one or more windows on different screens. + + Presentation mode is not the same as Full Screen mode; full screen mode is when all window + borders and other window decorations are no longer visible. + + :param window_or_list_or_dict: A single window, a list of windows, a dictionary + mapping screens to windows, to go into full screen, in order of allocation to + screens. If the number of windows exceeds the number of available displays, + those windows will not be visible. + """ + screen_window_dict = dict() + if isinstance(window_or_list_or_dict, Window): + screen_window_dict[self.screens[0]] = window_or_list_or_dict + elif isinstance(window_or_list_or_dict, list): + for window, screen in zip(window_or_list_or_dict, self.screens): + screen_window_dict[screen] = window + elif isinstance(window_or_list_or_dict, dict): + screen_window_dict = window_or_list_or_dict + + self._impl.enter_full_screen(screen_window_dict) + + def exit_presentation_mode(self): + for window in self.windows: + if window.state == WindowState.PRESENTATION: + window.state = WindowState.NORMAL ###################################################################### # App events diff --git a/core/src/toga/constants/__init__.py b/core/src/toga/constants/__init__.py index 3910f4d8cd..e24f5e4882 100644 --- a/core/src/toga/constants/__init__.py +++ b/core/src/toga/constants/__init__.py @@ -75,3 +75,4 @@ class WindowState(Enum): MAXIMIZED = auto() MINIMIZED = auto() FULLSCREEN = auto() + PRESENTATION = auto() diff --git a/core/src/toga/window.py b/core/src/toga/window.py index a88d75a633..0b5b4add8a 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -443,11 +443,20 @@ def full_screen(self) -> bool: mode is a slideshow app in presentation mode - the only visible content is the slide. """ - return self._is_full_screen + warnings.warn( + "`Window.full_screen` property is deprecated and will be removed in" + " the future. Consider using the `Window.state` property instead.", + FutureWarning, + ) + return bool(self.state == WindowState.FULLSCREEN) @full_screen.setter def full_screen(self, is_full_screen: bool) -> None: - self._is_full_screen = is_full_screen + warnings.warn( + "`Window.full_screen` property is deprecated and will be removed in" + " the future. Consider using the `Window.state` property instead.", + FutureWarning, + ) self._impl.set_full_screen(is_full_screen) @property From 544acba5c99716d0a44ef3453cbf62879a236288 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 23 Mar 2024 07:50:46 -0700 Subject: [PATCH 013/248] Misc Fixes --- core/src/toga/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 46244207ee..05b6042635 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -861,7 +861,8 @@ def enter_presentation_mode(self, window_or_list_or_dict): self._impl.enter_full_screen(screen_window_dict) - def exit_presentation_mode(self): + def exit_presentation_mode(self) -> None: + """Exit full screen mode.""" for window in self.windows: if window.state == WindowState.PRESENTATION: window.state = WindowState.NORMAL From 901c7aef90b06b34f70bc7bb64a33d28d55d9f27 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 23 Mar 2024 15:22:18 -0700 Subject: [PATCH 014/248] Fixed cocoa backend and core --- cocoa/src/toga_cocoa/app.py | 16 +--------------- core/src/toga/app.py | 9 +++++++-- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 2a47a96ea5..1b75ce0618 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -492,23 +492,9 @@ def set_current_window(self, window): window._impl.native.makeKeyAndOrderFront(window._impl.native) ###################################################################### - # Full screen/Presentation mode controls + # Presentation mode controls ###################################################################### - # ----------------------Future Deprecated methods---------------------- - def enter_full_screen(self, screen_window_dict): - self.enter_presentation_mode(screen_window_dict) - - def exit_full_screen(self): - for window in self.interface.windows: - if ( - window.state == WindowState.FULLSCREEN - or window.state == WindowState.PRESENTATION - ): - window.state = WindowState.NORMAL - - # --------------------------------------------------------------------- - def enter_presentation_mode(self, screen_window_dict): for screen, window in screen_window_dict.items(): window._impl._before_presentation_mode_screen = window.screen diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 05b6042635..f8f3eb8b53 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -778,7 +778,12 @@ def exit_full_screen(self) -> None: FutureWarning, ) if self.is_full_screen: - self._impl.exit_full_screen() + for window in self.interface.windows: + if ( + window.state == WindowState.FULLSCREEN + or window.state == WindowState.PRESENTATION + ): + window.state = WindowState.NORMAL @property def is_full_screen(self) -> bool: @@ -859,7 +864,7 @@ def enter_presentation_mode(self, window_or_list_or_dict): elif isinstance(window_or_list_or_dict, dict): screen_window_dict = window_or_list_or_dict - self._impl.enter_full_screen(screen_window_dict) + self._impl.enter_presentation_mode(screen_window_dict) def exit_presentation_mode(self) -> None: """Exit full screen mode.""" From 6bfe761026f0fc56a9b65355f7cf560604275eb4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 23 Mar 2024 16:02:45 -0700 Subject: [PATCH 015/248] Fixed cocoa backend and core --- cocoa/src/toga_cocoa/window.py | 9 ---- core/src/toga/window.py | 13 ++++-- winforms/src/toga_winforms/window.py | 61 +++++++++++++++------------- 3 files changed, 42 insertions(+), 41 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 5477b9e769..dda25371d8 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -342,15 +342,6 @@ def get_visible(self): # Window state ###################################################################### - # ----------------------Future Deprecated method---------------------- - def set_full_screen(self, is_full_screen): - if is_full_screen and (self.get_window_state() != WindowState.FULLSCREEN): - self.set_window_state(WindowState.FULLSCREEN) - elif not is_full_screen and (self.get_window_state() == WindowState.FULLSCREEN): - self.set_window_state(WindowState.NORMAL) - - # --------------------------------------------------------------------- - def get_window_state(self): if bool(self.native.isZoomed): return WindowState.MAXIMIZED diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 0b5b4add8a..869571bfe5 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -442,10 +442,14 @@ def full_screen(self) -> bool: items may be visible on a maximized window. A good example of "full screen" mode is a slideshow app in presentation mode - the only visible content is the slide. + + .. warning:: + `Window.full_screen` property is deprecated and will be removed in the + future. Consider using `Window.state` property instead. """ warnings.warn( "`Window.full_screen` property is deprecated and will be removed in" - " the future. Consider using the `Window.state` property instead.", + " the future. Consider using `Window.state` property instead.", FutureWarning, ) return bool(self.state == WindowState.FULLSCREEN) @@ -454,10 +458,13 @@ def full_screen(self) -> bool: def full_screen(self, is_full_screen: bool) -> None: warnings.warn( "`Window.full_screen` property is deprecated and will be removed in" - " the future. Consider using the `Window.state` property instead.", + " the future. Consider using `Window.state` property instead.", FutureWarning, ) - self._impl.set_full_screen(is_full_screen) + if is_full_screen and (self.state != WindowState.FULLSCREEN): + self.set_window_state(WindowState.FULLSCREEN) + elif not is_full_screen and (self.state == WindowState.FULLSCREEN): + self.set_window_state(WindowState.NORMAL) @property def state(self) -> WindowState: diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index e802048e3a..fb1a815907 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -228,41 +228,44 @@ def set_full_screen(self, is_full_screen): def get_window_state(self): window_state = self.native.WindowState - if ( - self.native.FormBorderStyle == getattr(WinForms.FormBorderStyle, "None") - and window_state == WinForms.FormWindowState.Maximized - ): - return WindowState.FULLSCREEN - else: - if window_state == WinForms.FormWindowState.Normal: - return WindowState.NORMAL - elif window_state == WinForms.FormWindowState.Maximized: + if window_state == WinForms.FormWindowState.Maximized: + if self.native.FormBorderStyle == getattr(WinForms.FormBorderStyle, "None"): + return WindowState.FULLSCREEN + else: return WindowState.MAXIMIZED - elif window_state == WinForms.FormWindowState.Minimized: - return WindowState.MINIMIZED + elif window_state == WinForms.FormWindowState.Minimized: + return WindowState.MINIMIZED + elif window_state == WinForms.FormWindowState.Normal: + if getattr(self, "_presentation_window", None) is not None: + return WindowState.PRESENTATION + else: + return WindowState.NORMAL def set_window_state(self, state): current_state = self.get_window_state() - if state == WindowState.FULLSCREEN and current_state != WindowState.FULLSCREEN: + if state == WindowState.NORMAL and current_state != WindowState.NORMAL: + if current_state == WindowState.FULLSCREEN: + self.native.FormBorderStyle = getattr( + WinForms.FormBorderStyle, + "Sizable" if self.interface.resizable else "FixedSingle", + ) + elif current_state == WindowState.PRESENTATION: + pass + self.native.WindowState = WinForms.FormWindowState.Normal + elif state == WindowState.MAXIMIZED and current_state != WindowState.MAXIMIZED: + self.native.WindowState = WinForms.FormWindowState.Maximized + elif state == WindowState.MINIMIZED and current_state != WindowState.MINIMIZED: + self.native.WindowState = WinForms.FormWindowState.Minimized + elif ( + state == WindowState.FULLSCREEN and current_state != WindowState.FULLSCREEN + ): self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") self.native.WindowState = WinForms.FormWindowState.Maximized - else: - self.native.FormBorderStyle = getattr( - WinForms.FormBorderStyle, - "Sizable" if self.interface.resizable else "FixedSingle", - ) - if state == WindowState.NORMAL and current_state != WindowState.NORMAL: - self.native.WindowState = WinForms.FormWindowState.Normal - elif ( - state == WindowState.MAXIMIZED - and current_state != WindowState.MAXIMIZED - ): - self.native.WindowState = WinForms.FormWindowState.Maximized - elif ( - state == WindowState.MINIMIZED - and current_state != WindowState.MINIMIZED - ): - self.native.WindowState = WinForms.FormWindowState.Minimized + elif ( + state == WindowState.PRESENTATION + and current_state != WindowState.PRESENTATION + ): + pass ###################################################################### # Window capabilities From 598fbd0ea532be660b917a944215559741c3d4a1 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 23 Mar 2024 17:26:10 -0700 Subject: [PATCH 016/248] Corrected core --- core/src/toga/app.py | 2 +- core/src/toga/window.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index f8f3eb8b53..8eec939eb8 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -778,7 +778,7 @@ def exit_full_screen(self) -> None: FutureWarning, ) if self.is_full_screen: - for window in self.interface.windows: + for window in self.windows: if ( window.state == WindowState.FULLSCREEN or window.state == WindowState.PRESENTATION diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 869571bfe5..caaa989ae5 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -433,6 +433,7 @@ def visible(self, visible: bool) -> None: # Window state ###################################################################### + # ----------------------Future Deprecated methods---------------------- @property def full_screen(self) -> bool: """Is the window in full screen mode? @@ -462,9 +463,11 @@ def full_screen(self, is_full_screen: bool) -> None: FutureWarning, ) if is_full_screen and (self.state != WindowState.FULLSCREEN): - self.set_window_state(WindowState.FULLSCREEN) + self._impl.set_window_state(WindowState.FULLSCREEN) elif not is_full_screen and (self.state == WindowState.FULLSCREEN): - self.set_window_state(WindowState.NORMAL) + self._impl.set_window_state(WindowState.NORMAL) + + # --------------------------------------------------------------------- @property def state(self) -> WindowState: From 6e7fc638f0e692c9dfc68394e2464cc74c232191 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 24 Mar 2024 03:12:11 -0700 Subject: [PATCH 017/248] Fixed winforms backend --- winforms/src/toga_winforms/app.py | 11 +++++ winforms/src/toga_winforms/window.py | 71 +++++++++++++++++++--------- 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 1bee29fb0e..fdf2b158ae 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -383,6 +383,17 @@ def exit_full_screen(self): if window.state == WindowState.FULLSCREEN: window._impl.set_full_screen(False) + def enter_presentation_mode(self, screen_window_dict): + for screen, window in screen_window_dict.items(): + window._impl._before_presentation_mode_screen = window.screen + window.screen = screen + window.state = WindowState.PRESENTATION + + def exit_presentation_mode(self): + for window in self.interface.windows: + if window.state == WindowState.PRESENTATION: + window.state = WindowState.NORMAL + class DocumentApp(App): # pragma: no cover def create_app_commands(self): diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index fb1a815907..cde9a5ed1a 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -40,7 +40,7 @@ def __init__(self, interface, title, position, size): self.native.Resize += WeakrefCallable(self.winforms_Resize) self.resize_content() # Store initial size - self.set_full_screen(self.interface.full_screen) + # self.set_full_screen(self.interface.full_screen) ###################################################################### # Native event handlers @@ -215,30 +215,45 @@ def hide(self): # Window state ###################################################################### - def set_full_screen(self, is_full_screen): - if is_full_screen: + class _PresentationWindow: + def __init__(self, window_impl): + self.window_impl = window_impl + self.native = WinForms.Form() + self.original_window_size = self.window_impl.native.Size + window_screen = self.window_impl.interface.screen + self.native.Location = Point(*window_screen.origin) self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") self.native.WindowState = WinForms.FormWindowState.Maximized - else: - self.native.FormBorderStyle = getattr( - WinForms.FormBorderStyle, - "Sizable" if self.interface.resizable else "FixedSingle", - ) - self.native.WindowState = WinForms.FormWindowState.Normal + self.native.Controls.Add(self.window_impl.interface.content._impl.native) + self.window_impl.native.Size = Size(*window_screen.size) + self.window_impl.interface.content.refresh() + + def show(self): + self.native.Show() + + def close(self): + self.native.Controls.Remove(self.window_impl.interface.content._impl.native) + self.window_impl.native.Size = self.original_window_size + self.window_impl.interface.content = self.window_impl.interface.content + self.window_impl.resize_content() + self.window_impl.interface.content.refresh() + self.native.Close() def get_window_state(self): - window_state = self.native.WindowState - if window_state == WinForms.FormWindowState.Maximized: - if self.native.FormBorderStyle == getattr(WinForms.FormBorderStyle, "None"): - return WindowState.FULLSCREEN - else: - return WindowState.MAXIMIZED - elif window_state == WinForms.FormWindowState.Minimized: - return WindowState.MINIMIZED - elif window_state == WinForms.FormWindowState.Normal: - if getattr(self, "_presentation_window", None) is not None: - return WindowState.PRESENTATION - else: + if getattr(self, "_presentation_window", None) is not None: + return WindowState.PRESENTATION + else: + window_state = self.native.WindowState + if window_state == WinForms.FormWindowState.Maximized: + if self.native.FormBorderStyle == getattr( + WinForms.FormBorderStyle, "None" + ): + return WindowState.FULLSCREEN + else: + return WindowState.MAXIMIZED + elif window_state == WinForms.FormWindowState.Minimized: + return WindowState.MINIMIZED + elif window_state == WinForms.FormWindowState.Normal: return WindowState.NORMAL def set_window_state(self, state): @@ -250,22 +265,32 @@ def set_window_state(self, state): "Sizable" if self.interface.resizable else "FixedSingle", ) elif current_state == WindowState.PRESENTATION: - pass + self._presentation_window.close() + self._presentation_window = None + self.interface.screen = ( + self.interface._impl._before_presentation_mode_screen + ) + self.native.WindowState = WinForms.FormWindowState.Normal elif state == WindowState.MAXIMIZED and current_state != WindowState.MAXIMIZED: + self.set_window_state(WindowState.NORMAL) self.native.WindowState = WinForms.FormWindowState.Maximized elif state == WindowState.MINIMIZED and current_state != WindowState.MINIMIZED: + self.set_window_state(WindowState.NORMAL) self.native.WindowState = WinForms.FormWindowState.Minimized elif ( state == WindowState.FULLSCREEN and current_state != WindowState.FULLSCREEN ): + self.set_window_state(WindowState.NORMAL) self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") self.native.WindowState = WinForms.FormWindowState.Maximized elif ( state == WindowState.PRESENTATION and current_state != WindowState.PRESENTATION ): - pass + self.set_window_state(WindowState.NORMAL) + self._presentation_window = self._PresentationWindow(self) + self._presentation_window.show() ###################################################################### # Window capabilities From e5be1c7023fa60ec8ad3886fe7c2746efef025bf Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 24 Mar 2024 03:16:56 -0700 Subject: [PATCH 018/248] Misc Fixes --- winforms/src/toga_winforms/app.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index fdf2b158ae..7be930f6e0 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -370,19 +370,9 @@ def set_current_window(self, window): window._impl.native.Activate() ###################################################################### - # Full screen control + # Presentation mode controls ###################################################################### - def enter_full_screen(self, screen_window_dict): - for screen, window in screen_window_dict.items(): - window.screen = screen - window._impl.set_full_screen(True) - - def exit_full_screen(self): - for window in self.interface.windows: - if window.state == WindowState.FULLSCREEN: - window._impl.set_full_screen(False) - def enter_presentation_mode(self, screen_window_dict): for screen, window in screen_window_dict.items(): window._impl._before_presentation_mode_screen = window.screen From 401cd466caf3051b4434e430bd9fd304d42bd5d4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 24 Mar 2024 11:28:54 -0400 Subject: [PATCH 019/248] Fixed gtk backend --- gtk/src/toga_gtk/app.py | 16 +++++--- gtk/src/toga_gtk/window.py | 83 ++++++++++++++++++++++++++------------ 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 4b700ff02c..8e1a4138b2 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -9,6 +9,7 @@ import toga from toga import App as toga_App from toga.command import Command, Separator +from toga.constants import WindowState from .keys import gtk_accel from .libs import TOGA_DEFAULT_STYLES, Gdk, Gio, GLib, Gtk @@ -277,13 +278,16 @@ def set_current_window(self, window): # Full screen control ###################################################################### - def enter_full_screen(self, windows): - for window in windows: - window._impl.set_full_screen(True) + def enter_presentation_mode(self, screen_window_dict): + for screen, window in screen_window_dict.items(): + window._impl._before_presentation_mode_screen = window.screen + window.screen = screen + window.state = WindowState.PRESENTATION - def exit_full_screen(self, windows): - for window in windows: - window._impl.set_full_screen(False) + def exit_presentation_mode(self): + for window in self.interface.windows: + if window.state == WindowState.PRESENTATION: + window.state = WindowState.NORMAL class DocumentApp(App): # pragma: no cover diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 5087c7f350..1d318951f0 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -12,7 +12,6 @@ def __init__(self, interface, title, position, size): self.interface._impl = self self._is_closing = False - self._window_state = WindowState.NORMAL self.layout = None @@ -22,6 +21,8 @@ def __init__(self, interface, title, position, size): self.native.connect("delete-event", self.gtk_delete_event) self.native.connect("window-state-event", self.gtk_window_state_event) + self._window_state_flags = None + self.native.set_default_size(size[0], size[1]) self.set_title(title) @@ -61,17 +62,8 @@ def create(self): ###################################################################### def gtk_window_state_event(self, widget, event): - # Get the window state - instantaneous_state = event.new_window_state - - if instantaneous_state & Gdk.WindowState.MAXIMIZED: - self._window_state = WindowState.MAXIMIZED - elif instantaneous_state & Gdk.WindowState.ICONIFIED: - self._window_state = WindowState.MINIMIZED - elif instantaneous_state & Gdk.WindowState.FULLSCREEN: - self._window_state = WindowState.FULLSCREEN - else: - self._window_state = WindowState.NORMAL + # Get the window state flags + self._window_state_flags = event.new_window_state def gtk_delete_event(self, widget, data): if self._is_closing: @@ -211,33 +203,74 @@ def hide(self): # Window state ###################################################################### - def set_full_screen(self, is_full_screen): - if is_full_screen: + class _PresentationWindow: + def __init__(self, window_impl): + self.window_impl = window_impl + self.native = Gtk.Window() + self.window_impl.container.remove( + self.window_impl.interface.content._impl.native + ) + self.native.add(self.window_impl.interface.content._impl.native) self.native.fullscreen() - else: + + def show(self): + self.native.show() + + def close(self): self.native.unfullscreen() + self.native.remove(self.window_impl.interface.content._impl.native) + self.window_impl.container.add( + self.window_impl.interface.content._impl.native + ) + self.native.close() def get_window_state(self): - return self._window_state + if getattr(self, "_presentation_window", None) is not None: + return WindowState.PRESENTATION + else: + window_state = self._window_state_flags + if window_state & Gdk.WindowState.MAXIMIZED: + return WindowState.MAXIMIZED + elif window_state & Gdk.WindowState.ICONIFIED: + return WindowState.MINIMIZED + elif window_state & Gdk.WindowState.FULLSCREEN: + return WindowState.FULLSCREEN + else: + return WindowState.NORMAL def set_window_state(self, state): - if state == WindowState.NORMAL: - current_state = self.get_window_state() + current_state = self.get_window_state() + if state == WindowState.NORMAL and current_state != WindowState.NORMAL: if current_state == WindowState.MAXIMIZED: self.native.unmaximize() elif current_state == WindowState.MINIMIZED: self.native.deiconify() elif current_state == WindowState.FULLSCREEN: self.native.unfullscreen() - - elif state == WindowState.MAXIMIZED: + elif current_state == WindowState.PRESENTATION: + self._presentation_window.close() + self._presentation_window = None + self.interface.screen = ( + self.interface._impl._before_presentation_mode_screen + ) + elif state == WindowState.MAXIMIZED and current_state != WindowState.MAXIMIZED: + self.set_window_state(WindowState.NORMAL) self.native.maximize() - - elif state == WindowState.MINIMIZED: + elif state == WindowState.MINIMIZED and current_state != WindowState.MINIMIZED: + self.set_window_state(WindowState.NORMAL) self.native.iconify() - - elif state == WindowState.FULLSCREEN: - self.interface.app.set_full_screen(self.interface) + elif ( + state == WindowState.FULLSCREEN and current_state != WindowState.FULLSCREEN + ): + self.set_window_state(WindowState.NORMAL) + self.native.fullscreen() + elif ( + state == WindowState.PRESENTATION + and current_state != WindowState.PRESENTATION + ): + self.set_window_state(WindowState.NORMAL) + self._presentation_window = self._PresentationWindow(self) + self._presentation_window.show() ###################################################################### # Window capabilities From ef4f43a56b1fe523c96edfa5b757f4957f6afb0e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 24 Mar 2024 11:41:37 -0400 Subject: [PATCH 020/248] Added placeholders on android and iOS backends --- android/src/toga_android/app.py | 8 +++++--- android/src/toga_android/window.py | 9 +++++++-- iOS/src/toga_iOS/app.py | 6 +++--- iOS/src/toga_iOS/window.py | 6 +++++- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index a85a6ca7dd..a934450d4e 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -292,13 +292,15 @@ def set_current_window(self, window): pass ###################################################################### - # Full screen control + # Presentation mode controls ###################################################################### - def enter_full_screen(self, windows): + def enter_presentation_mode(self, screen_window_dict): + # No-op; mobile doesn't support full screen pass - def exit_full_screen(self, windows): + def exit_presentation_mode(self): + # No-op; mobile doesn't support full screen pass ###################################################################### diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 6fbf0a9ea9..a73d090be8 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -135,8 +135,13 @@ def get_visible(self): # Window state ###################################################################### - def set_full_screen(self, is_full_screen): - self.interface.factory.not_implemented("Window.set_full_screen()") + def get_window_state(self): + # Windows are always full screen + pass + + def set_window_state(self, state): + # Windows are always full screen + pass ###################################################################### # Window capabilities diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index b9e71ddc26..fcd3526043 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -143,13 +143,13 @@ def set_current_window(self, window): pass ###################################################################### - # Full screen control + # Presentation mode controls ###################################################################### - def enter_full_screen(self, windows): + def enter_presentation_mode(self, screen_window_dict): # No-op; mobile doesn't support full screen pass - def exit_full_screen(self, windows): + def exit_presentation_mode(self): # No-op; mobile doesn't support full screen pass diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index 7ab0129e6d..a4cfcb4c64 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -144,7 +144,11 @@ def hide(self): # Window state ###################################################################### - def set_full_screen(self, is_full_screen): + def get_window_state(self): + # Windows are always full screen + pass + + def set_window_state(self, state): # Windows are always full screen pass From 1eb067f8125c850c47942daf3cf60e2268be3361 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 24 Mar 2024 16:12:39 -0700 Subject: [PATCH 021/248] Added placeholders for web and textual backends --- textual/src/toga_textual/app.py | 8 +++++--- textual/src/toga_textual/window.py | 7 ++++++- web/src/toga_web/app.py | 12 +++++++----- web/src/toga_web/window.py | 9 +++++++-- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/textual/src/toga_textual/app.py b/textual/src/toga_textual/app.py index 9bd33dd859..d01add9d25 100644 --- a/textual/src/toga_textual/app.py +++ b/textual/src/toga_textual/app.py @@ -92,13 +92,15 @@ def set_current_window(self, window): self.native.title = window.get_title() ###################################################################### - # Full screen control + # Presentation mode controls ###################################################################### - def enter_full_screen(self, windows): + def enter_presentation_mode(self, screen_window_dict): + # Not implemented pass - def exit_full_screen(self, windows): + def exit_presentation_mode(self): + # Not implemented pass diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index 017aac5d8e..2128e2bf2c 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -202,7 +202,12 @@ def hide(self): # Window state ###################################################################### - def set_full_screen(self, is_full_screen): + def get_window_state(self): + # Not implemented + pass + + def set_window_state(self, state): + # Not implemented pass ###################################################################### diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index 9c1dac062d..c15bc55027 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -234,11 +234,13 @@ def set_current_window(self): self.interface.factory.not_implemented("App.set_current_window()") ###################################################################### - # Full screen control + # Presentation mode controls ###################################################################### - def enter_full_screen(self, windows): - self.interface.factory.not_implemented("App.enter_full_screen()") + def enter_presentation_mode(self, screen_window_dict): + # Not implemented + pass - def exit_full_screen(self, windows): - self.interface.factory.not_implemented("App.exit_full_screen()") + def exit_presentation_mode(self): + # Not implemented + pass diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index 29eca4532e..e9d2c7ffb3 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -112,8 +112,13 @@ def hide(self): # Window state ###################################################################### - def set_full_screen(self, is_full_screen): - self.interface.factory.not_implemented("Window.set_full_screen()") + def get_window_state(self): + # Not implemented + pass + + def set_window_state(self, state): + # Not implemented + pass ###################################################################### # Window capabilities From 6aaa06932c66e8c80aac9f3e1a5b2c80c3d123c6 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 24 Mar 2024 19:35:46 -0700 Subject: [PATCH 022/248] Fixed cocoa, gtk, winforms backends and core --- cocoa/src/toga_cocoa/app.py | 16 ---------------- cocoa/src/toga_cocoa/window.py | 24 +++++++++++------------- core/src/toga/app.py | 5 ++++- core/src/toga/window.py | 12 +++++++++++- core/tests/app/test_app.py | 12 ++++++++++++ core/tests/test_window.py | 20 ++++++++++++++++++++ dummy/src/toga_dummy/window.py | 8 ++++++++ gtk/src/toga_gtk/app.py | 16 ---------------- gtk/src/toga_gtk/window.py | 28 +++++++++++++++++----------- winforms/src/toga_winforms/app.py | 16 ---------------- winforms/src/toga_winforms/window.py | 21 ++++++++++----------- 11 files changed, 93 insertions(+), 85 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 1b75ce0618..9b603dd8b7 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -9,7 +9,6 @@ import toga from toga.command import Separator -from toga.constants import WindowState from toga.handlers import NativeHandler from .keys import cocoa_key @@ -491,21 +490,6 @@ def get_current_window(self): def set_current_window(self, window): window._impl.native.makeKeyAndOrderFront(window._impl.native) - ###################################################################### - # Presentation mode controls - ###################################################################### - - def enter_presentation_mode(self, screen_window_dict): - for screen, window in screen_window_dict.items(): - window._impl._before_presentation_mode_screen = window.screen - window.screen = screen - window.state = WindowState.PRESENTATION - - def exit_presentation_mode(self): - for window in self.interface.windows: - if window.state == WindowState.PRESENTATION: - window.state = WindowState.NORMAL - class DocumentApp(App): # pragma: no cover def create_app_commands(self): diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index dda25371d8..58a1f90cad 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -355,8 +355,8 @@ def get_window_state(self): return WindowState.NORMAL def set_window_state(self, state): - current_state = self.get_window_state() - if state == WindowState.NORMAL and current_state != WindowState.NORMAL: + if state == WindowState.NORMAL: + current_state = self.get_window_state() # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: self.native.setIsZoomed(False) @@ -377,25 +377,23 @@ def set_window_state(self, state): self.interface.screen = ( self.interface._impl._before_presentation_mode_screen ) - # Set Window state to NORMAL before changing to other states as some states - # block changing window state without first exiting them. - elif state == WindowState.MAXIMIZED and current_state != WindowState.MAXIMIZED: + + # Changing window states without reverting back to the NORMAL state will + # cause glitches, so revert to NORMAL state before switching to other states. + # Setting window state to NORMAL from the interface side also causes the same glitches. + elif state == WindowState.MAXIMIZED: self.set_window_state(WindowState.NORMAL) self.native.setIsZoomed(True) - elif state == WindowState.MINIMIZED and current_state != WindowState.MINIMIZED: + elif state == WindowState.MINIMIZED: self.set_window_state(WindowState.NORMAL) self.native.setIsMiniaturized(True) - elif ( - state == WindowState.FULLSCREEN and current_state != WindowState.FULLSCREEN - ): + elif state == WindowState.FULLSCREEN: self.set_window_state(WindowState.NORMAL) self.native.toggleFullScreen(self.native) - elif ( - state == WindowState.PRESENTATION - and current_state != WindowState.PRESENTATION - ): + + elif state == WindowState.PRESENTATION: self.set_window_state(WindowState.NORMAL) opts = NSMutableDictionary.alloc().init() opts.setObject( diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 8eec939eb8..00e7b82f38 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -864,7 +864,10 @@ def enter_presentation_mode(self, window_or_list_or_dict): elif isinstance(window_or_list_or_dict, dict): screen_window_dict = window_or_list_or_dict - self._impl.enter_presentation_mode(screen_window_dict) + for screen, window in screen_window_dict.items(): + window._impl._before_presentation_mode_screen = window.screen + window.screen = screen + window.state = WindowState.PRESENTATION def exit_presentation_mode(self) -> None: """Exit full screen mode.""" diff --git a/core/src/toga/window.py b/core/src/toga/window.py index caaa989ae5..59959c1a18 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -476,7 +476,17 @@ def state(self) -> WindowState: @state.setter def state(self, state: WindowState) -> None: - self._impl.set_window_state(state) + if state != self.state: + if state == WindowState.NORMAL: + self._impl.set_window_state(WindowState.NORMAL) + if state == WindowState.MAXIMIZED: + self._impl.set_window_state(WindowState.MAXIMIZED) + elif state == WindowState.MINIMIZED: + self._impl.set_window_state(WindowState.MINIMIZED) + elif state == WindowState.FULLSCREEN: + self._impl.set_window_state(WindowState.FULLSCREEN) + elif state == WindowState.PRESENTATION: + self._impl.set_window_state(WindowState.PRESENTATION) ###################################################################### # Window capabilities diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 6e73e4b2fd..c8ace8800d 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -425,6 +425,18 @@ def test_full_screen(event_loop): ) +def test_presentation_mode(event_loop): + """The app can be put into presentation mode.""" + # window1 = toga.Window() + # window2 = toga.Window() + app = toga.App(formal_name="Test App", app_id="org.example.test") + + assert not app.is_in_presentation_mode + + # If we're not in presentation mode, exiting presentation mode is a no-op + app.exit_presentation_mode() + + def test_set_empty_full_screen_window_list(event_loop): """Setting the full screen window list to [] is an explicit exit.""" app = toga.App(formal_name="Test App", app_id="org.example.test") diff --git a/core/tests/test_window.py b/core/tests/test_window.py index 250e2253c8..223c84cd37 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -4,6 +4,7 @@ import pytest import toga +from toga.constants import WindowState from toga_dummy.utils import ( assert_action_not_performed, assert_action_performed, @@ -270,6 +271,25 @@ def test_full_screen(window, app): assert_action_performed_with(window, "set full screen", full_screen=False) +def test_window_state(window): + """A window can have different states.""" + assert window.state == WindowState.NORMAL + + window.state = WindowState.MAXIMIZED + assert window.state == WindowState.MAXIMIZED + assert_action_performed_with( + window, "set window state", state=WindowState.MAXIMIZED + ) + + window.state = WindowState.FULLSCREEN + assert window.state == WindowState.FULLSCREEN + assert_action_performed_with( + window, + "set window state to WindowState.FULLSCREEN", + state=WindowState.FULLSCREEN, + ) + + def test_close_direct(window, app): """A window can be closed directly.""" on_close_handler = Mock(return_value=True) diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 2965d7c340..5618d2c32c 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -1,6 +1,7 @@ from pathlib import Path import toga_dummy +from toga.constants import WindowState from .screens import Screen as ScreenImpl from .utils import LoggedObject @@ -110,3 +111,10 @@ def simulate_close(self): def get_current_screen(self): # `window.screen` will return `Secondary Screen` return ScreenImpl(native=("Secondary Screen", (-1366, -768), (1366, 768))) + + def get_window_state(self): + return self._get_value("state", WindowState.NORMAL) + + def set_window_state(self, state): + self._action(f"set window state to {state}", state=state) + self._set_value("state", state) diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 8e1a4138b2..3a115fe629 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -9,7 +9,6 @@ import toga from toga import App as toga_App from toga.command import Command, Separator -from toga.constants import WindowState from .keys import gtk_accel from .libs import TOGA_DEFAULT_STYLES, Gdk, Gio, GLib, Gtk @@ -274,21 +273,6 @@ def get_current_window(self): def set_current_window(self, window): window._impl.native.present() - ###################################################################### - # Full screen control - ###################################################################### - - def enter_presentation_mode(self, screen_window_dict): - for screen, window in screen_window_dict.items(): - window._impl._before_presentation_mode_screen = window.screen - window.screen = screen - window.state = WindowState.PRESENTATION - - def exit_presentation_mode(self): - for window in self.interface.windows: - if window.state == WindowState.PRESENTATION: - window.state = WindowState.NORMAL - class DocumentApp(App): # pragma: no cover def create_app_commands(self): diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 1d318951f0..850c04f54b 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -239,35 +239,41 @@ def get_window_state(self): return WindowState.NORMAL def set_window_state(self, state): - current_state = self.get_window_state() - if state == WindowState.NORMAL and current_state != WindowState.NORMAL: + if state == WindowState.NORMAL: + current_state = self.get_window_state() + # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: self.native.unmaximize() + # Deminiaturize the window to restore it to its previous state elif current_state == WindowState.MINIMIZED: self.native.deiconify() + # If the window is in full-screen mode, exit full-screen mode elif current_state == WindowState.FULLSCREEN: self.native.unfullscreen() + # If the window is in presentation mode, exit presentation mode elif current_state == WindowState.PRESENTATION: self._presentation_window.close() self._presentation_window = None self.interface.screen = ( self.interface._impl._before_presentation_mode_screen ) - elif state == WindowState.MAXIMIZED and current_state != WindowState.MAXIMIZED: + + # Changing window states without reverting back to the NORMAL state will + # cause glitches, so revert to NORMAL state before switching to other states. + # Setting window state to NORMAL from the interface side also causes the same glitches. + elif state == WindowState.MAXIMIZED: self.set_window_state(WindowState.NORMAL) self.native.maximize() - elif state == WindowState.MINIMIZED and current_state != WindowState.MINIMIZED: + + elif state == WindowState.MINIMIZED: self.set_window_state(WindowState.NORMAL) self.native.iconify() - elif ( - state == WindowState.FULLSCREEN and current_state != WindowState.FULLSCREEN - ): + + elif state == WindowState.FULLSCREEN: self.set_window_state(WindowState.NORMAL) self.native.fullscreen() - elif ( - state == WindowState.PRESENTATION - and current_state != WindowState.PRESENTATION - ): + + elif state == WindowState.PRESENTATION: self.set_window_state(WindowState.NORMAL) self._presentation_window = self._PresentationWindow(self) self._presentation_window.show() diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 7be930f6e0..c9f28bce84 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -14,7 +14,6 @@ import toga from toga import Key from toga.command import Separator -from toga.constants import WindowState from .keys import toga_to_winforms_key, toga_to_winforms_shortcut from .libs.proactor import WinformsProactorEventLoop @@ -369,21 +368,6 @@ def get_current_window(self): def set_current_window(self, window): window._impl.native.Activate() - ###################################################################### - # Presentation mode controls - ###################################################################### - - def enter_presentation_mode(self, screen_window_dict): - for screen, window in screen_window_dict.items(): - window._impl._before_presentation_mode_screen = window.screen - window.screen = screen - window.state = WindowState.PRESENTATION - - def exit_presentation_mode(self): - for window in self.interface.windows: - if window.state == WindowState.PRESENTATION: - window.state = WindowState.NORMAL - class DocumentApp(App): # pragma: no cover def create_app_commands(self): diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index cde9a5ed1a..b4f103af79 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -257,8 +257,8 @@ def get_window_state(self): return WindowState.NORMAL def set_window_state(self, state): - current_state = self.get_window_state() - if state == WindowState.NORMAL and current_state != WindowState.NORMAL: + if state == WindowState.NORMAL: + current_state = self.get_window_state() if current_state == WindowState.FULLSCREEN: self.native.FormBorderStyle = getattr( WinForms.FormBorderStyle, @@ -272,22 +272,21 @@ def set_window_state(self, state): ) self.native.WindowState = WinForms.FormWindowState.Normal - elif state == WindowState.MAXIMIZED and current_state != WindowState.MAXIMIZED: + + # Changing window states without reverting back to the NORMAL state will + # cause glitches, so revert to NORMAL state before switching to other states. + # Setting window state to NORMAL from the interface side also causes the same glitches. + elif state == WindowState.MAXIMIZED: self.set_window_state(WindowState.NORMAL) self.native.WindowState = WinForms.FormWindowState.Maximized - elif state == WindowState.MINIMIZED and current_state != WindowState.MINIMIZED: + elif state == WindowState.MINIMIZED: self.set_window_state(WindowState.NORMAL) self.native.WindowState = WinForms.FormWindowState.Minimized - elif ( - state == WindowState.FULLSCREEN and current_state != WindowState.FULLSCREEN - ): + elif state == WindowState.FULLSCREEN: self.set_window_state(WindowState.NORMAL) self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") self.native.WindowState = WinForms.FormWindowState.Maximized - elif ( - state == WindowState.PRESENTATION - and current_state != WindowState.PRESENTATION - ): + elif state == WindowState.PRESENTATION: self.set_window_state(WindowState.NORMAL) self._presentation_window = self._PresentationWindow(self) self._presentation_window.show() From 1d9ff0af813fdc1f469066c584c56a7911f9ab73 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 25 Mar 2024 07:33:18 -0700 Subject: [PATCH 023/248] Added dummy backend and core tests --- core/src/toga/app.py | 56 ++++++-------- core/src/toga/window.py | 22 +++--- core/tests/app/test_app.py | 133 ++++++++++++++++++++++++++++----- core/tests/test_window.py | 22 +++++- dummy/src/toga_dummy/app.py | 6 -- dummy/src/toga_dummy/window.py | 3 - examples/window/window/app.py | 9 +++ 7 files changed, 176 insertions(+), 75 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 00e7b82f38..db982c1894 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -771,12 +771,12 @@ def exit_full_screen(self) -> None: removed in the future. Consider using `App.enter_presentation_mode()` and `App.exit_presentation_mode()` methods instead. """ - warnings.warn( - "`App.exit_full_screen()` method is deprecated and will be" - " removed in the future. Consider using `App.enter_presentation_mode()`" - " and `App.exit_presentation_mode()` methods instead.", - FutureWarning, - ) + # warnings.warn( + # "`App.exit_full_screen()` method is deprecated and will be" + # " removed in the future. Consider using `App.enter_presentation_mode()`" + # " and `App.exit_presentation_mode()` methods instead.", + # FutureWarning, + # ) if self.is_full_screen: for window in self.windows: if ( @@ -793,18 +793,12 @@ def is_full_screen(self) -> bool: `App.is_full_screen` property is deprecated and will be removed in the future. Consider using `App.is_in_presentation_mode` property instead. """ - warnings.warn( - "`App.is_full_screen` property is deprecated and will be removed in the" - " future. Consider using `App.is_in_presentation_mode` property instead.", - FutureWarning, - ) - return any( - ( - window.state == WindowState.FULLSCREEN - or window.state == WindowState.PRESENTATION - ) - for window in self.windows - ) + # warnings.warn( + # "`App.is_full_screen` property is deprecated and will be removed in the" + # " future. Consider using `App.is_in_presentation_mode` property instead.", + # FutureWarning, + # ) + return any(window.state == WindowState.PRESENTATION for window in self.windows) def set_full_screen(self, *windows: Window) -> None: """Make one or more windows full screen. @@ -822,12 +816,12 @@ def set_full_screen(self, *windows: Window) -> None: removed in the future. Consider using `App.enter_presentation_mode()` and `App.exit_presentation_mode()` methods instead. """ - warnings.warn( - "`App.set_full_screen()` method is deprecated and will be" - " removed in the future. Consider using `App.enter_presentation_mode()`" - " and `App.exit_presentation_mode()` methods instead.", - FutureWarning, - ) + # warnings.warn( + # "`App.set_full_screen()` method is deprecated and will be" + # " removed in the future. Consider using `App.enter_presentation_mode()`" + # " and `App.exit_presentation_mode()` methods instead.", + # FutureWarning, + # ) if self.windows is not None: self.exit_full_screen() screen_window_dict = dict() @@ -844,25 +838,23 @@ def is_in_presentation_mode(self) -> bool: """Is the app currently in presentation mode?""" return any(window.state == WindowState.PRESENTATION for window in self.windows) - def enter_presentation_mode(self, window_or_list_or_dict): + def enter_presentation_mode(self, window_list_or_screen_window_dict): """Enter into presentation mode with one or more windows on different screens. Presentation mode is not the same as Full Screen mode; full screen mode is when all window borders and other window decorations are no longer visible. - :param window_or_list_or_dict: A single window, a list of windows, a dictionary + :param window_list_or_screen_window_dict: A list of windows, a dictionary mapping screens to windows, to go into full screen, in order of allocation to screens. If the number of windows exceeds the number of available displays, those windows will not be visible. """ screen_window_dict = dict() - if isinstance(window_or_list_or_dict, Window): - screen_window_dict[self.screens[0]] = window_or_list_or_dict - elif isinstance(window_or_list_or_dict, list): - for window, screen in zip(window_or_list_or_dict, self.screens): + if isinstance(window_list_or_screen_window_dict, list): + for window, screen in zip(window_list_or_screen_window_dict, self.screens): screen_window_dict[screen] = window - elif isinstance(window_or_list_or_dict, dict): - screen_window_dict = window_or_list_or_dict + elif isinstance(window_list_or_screen_window_dict, dict): + screen_window_dict = window_list_or_screen_window_dict for screen, window in screen_window_dict.items(): window._impl._before_presentation_mode_screen = window.screen diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 59959c1a18..ecf15b3512 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -447,21 +447,21 @@ def full_screen(self) -> bool: .. warning:: `Window.full_screen` property is deprecated and will be removed in the future. Consider using `Window.state` property instead. - """ - warnings.warn( - "`Window.full_screen` property is deprecated and will be removed in" - " the future. Consider using `Window.state` property instead.", - FutureWarning, - ) + #""" + # warnings.warn( + # "`Window.full_screen` property is deprecated and will be removed in" + # " the future. Consider using `Window.state` property instead.", + # FutureWarning, + # ) return bool(self.state == WindowState.FULLSCREEN) @full_screen.setter def full_screen(self, is_full_screen: bool) -> None: - warnings.warn( - "`Window.full_screen` property is deprecated and will be removed in" - " the future. Consider using `Window.state` property instead.", - FutureWarning, - ) + # warnings.warn( + # "`Window.full_screen` property is deprecated and will be removed in" + # " the future. Consider using `Window.state` property instead.", + # FutureWarning, + # ) if is_full_screen and (self.state != WindowState.FULLSCREEN): self._impl.set_window_state(WindowState.FULLSCREEN) elif not is_full_screen and (self.state == WindowState.FULLSCREEN): diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index c8ace8800d..8a23dffdd8 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -8,6 +8,7 @@ import pytest import toga +from toga.constants import WindowState from toga_dummy.utils import ( assert_action_not_performed, assert_action_performed, @@ -393,48 +394,62 @@ def test_no_current_window(app): def test_full_screen(event_loop): """The app can be put into full screen mode.""" + app = toga.App(formal_name="Test App", app_id="org.example.test") window1 = toga.Window() window2 = toga.Window() - app = toga.App(formal_name="Test App", app_id="org.example.test") assert not app.is_full_screen # If we're not full screen, exiting full screen is a no-op app.exit_full_screen() - assert_action_not_performed(app, "exit_full_screen") + for window in app.windows: + assert_action_not_performed(window, "set window state to WindowState.NORMAL") + + # TODO: Check and keep track of the window assignment to screens + # This seems to be difficult currently, as the interface api: `App.enter_presentation_mode` + # both assigns screens to windows and also sets the state. # Enter full screen with 2 windows app.set_full_screen(window2, app.main_window) assert app.is_full_screen assert_action_performed_with( - app, "enter_full_screen", windows=(window2, app.main_window) + window2, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + assert_action_performed_with( + app.main_window, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, ) # Change the screens that are full screen app.set_full_screen(app.main_window, window1) assert app.is_full_screen assert_action_performed_with( - app, "enter_full_screen", windows=(app.main_window, window1) + app.main_window, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + assert_action_performed_with( + window1, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, ) # Exit full screen mode app.exit_full_screen() assert not app.is_full_screen assert_action_performed_with( - app, "exit_full_screen", windows=(app.main_window, window1) + app.main_window, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + assert_action_performed_with( + window1, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, ) - - -def test_presentation_mode(event_loop): - """The app can be put into presentation mode.""" - # window1 = toga.Window() - # window2 = toga.Window() - app = toga.App(formal_name="Test App", app_id="org.example.test") - - assert not app.is_in_presentation_mode - - # If we're not in presentation mode, exiting presentation mode is a no-op - app.exit_presentation_mode() def test_set_empty_full_screen_window_list(event_loop): @@ -448,12 +463,90 @@ def test_set_empty_full_screen_window_list(event_loop): # Change the screens that are full screen app.set_full_screen(window1, window2) assert app.is_full_screen - assert_action_performed_with(app, "enter_full_screen", windows=(window1, window2)) - + assert_action_performed_with( + window1, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + assert_action_performed_with( + window2, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) # Exit full screen mode by setting no windows full screen app.set_full_screen() assert not app.is_full_screen - assert_action_performed_with(app, "exit_full_screen", windows=(window1, window2)) + assert_action_performed_with( + window1, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + assert_action_performed_with( + window2, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + + +def test_presentation_mode(event_loop): + """The app can be put into presentation mode.""" + app = toga.App(formal_name="Test App", app_id="org.example.test") + window1 = toga.Window() + window2 = toga.Window() + + assert not app.is_in_presentation_mode + + # If we're not in presentation mode, exiting presentation mode is a no-op + app.exit_presentation_mode() + for window in app.windows: + assert_action_not_performed(window, "set window state to WindowState.NORMAL") + + # TODO: Check and keep track of the window assignment to screens + # This seems to be difficult currently, as the interface api: `App.enter_presentation_mode` + # both assigns screens to windows and also sets the state. + + # Enter presentation mode with 1 window: + app.enter_presentation_mode([window1]) + window1.state = WindowState.PRESENTATION + assert app.is_in_presentation_mode + assert_action_performed_with( + window1, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + # Exit presentation mode: + app.exit_presentation_mode() + assert_action_performed_with( + window1, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + + # Enter presentation mode with 2 window: + app.enter_presentation_mode({app.screens[1]: window1, app.screens[0]: window2}) + assert app.is_in_presentation_mode + assert_action_performed_with( + window1, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + assert_action_performed_with( + window2, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + # Exit presentation mode: + app.exit_presentation_mode() + assert_action_performed_with( + window1, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + assert_action_performed_with( + window2, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) def test_show_hide_cursor(app): diff --git a/core/tests/test_window.py b/core/tests/test_window.py index 223c84cd37..76d6576a70 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -264,11 +264,19 @@ def test_full_screen(window, app): window.full_screen = True assert window.full_screen - assert_action_performed_with(window, "set full screen", full_screen=True) + assert_action_performed_with( + window, + "set window state to WindowState.FULLSCREEN", + state=WindowState.FULLSCREEN, + ) window.full_screen = False assert not window.full_screen - assert_action_performed_with(window, "set full screen", full_screen=False) + assert_action_performed_with( + window, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) def test_window_state(window): @@ -278,7 +286,9 @@ def test_window_state(window): window.state = WindowState.MAXIMIZED assert window.state == WindowState.MAXIMIZED assert_action_performed_with( - window, "set window state", state=WindowState.MAXIMIZED + window, + "set window state to WindowState.MAXIMIZED", + state=WindowState.MAXIMIZED, ) window.state = WindowState.FULLSCREEN @@ -289,6 +299,12 @@ def test_window_state(window): state=WindowState.FULLSCREEN, ) + window.state = WindowState.NORMAL + assert window.state == WindowState.NORMAL + assert_action_performed_with( + window, "set window state to WindowState.NORMAL", state=WindowState.NORMAL + ) + def test_close_direct(window, app): """A window can be closed directly.""" diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index 7e95948625..e3c5dd644d 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -53,12 +53,6 @@ def set_current_window(self, window): self._action("set_current_window", window=window) self._set_value("current_window", window._impl) - def enter_full_screen(self, windows): - self._action("enter_full_screen", windows=windows) - - def exit_full_screen(self, windows): - self._action("exit_full_screen", windows=windows) - def show_cursor(self): self._action("show_cursor") diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 5618d2c32c..7d3481bc3c 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -102,9 +102,6 @@ def get_image_data(self): path = Path(toga_dummy.__file__).parent / "resources/screenshot.png" return path.read_bytes() - def set_full_screen(self, is_full_screen): - self._action("set full screen", full_screen=is_full_screen) - def simulate_close(self): self.interface.on_close() diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 286ad8756c..2979349863 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -48,6 +48,9 @@ def do_app_full_screen(self, widget, **kwargs): def do_window_full_screen(self, widget, **kwargs): self.main_window.full_screen = not self.main_window.full_screen + def do_enter_presentation_mode(self, widget, **kwargs): + self.enter_presentation_mode([self.main_window]) + def do_title(self, widget, **kwargs): self.main_window.title = f"Time is {datetime.now()}" @@ -203,6 +206,11 @@ def startup(self): on_press=self.do_window_full_screen, style=btn_style, ) + btn_do_enter_presentation_mode = toga.Button( + "Enter into presentation mode", + on_press=self.do_enter_presentation_mode, + style=btn_style, + ) btn_do_title = toga.Button( "Change title", on_press=self.do_title, style=btn_style ) @@ -276,6 +284,7 @@ def startup(self): btn_do_minimize, btn_do_app_full_screen, btn_do_window_full_screen, + btn_do_enter_presentation_mode, btn_do_title, btn_do_new_windows, btn_do_current_window_cycling, From 04f27c99ced949bab9cc1a02183d5db9aabb047e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 26 Mar 2024 04:40:06 -0700 Subject: [PATCH 024/248] Fixed winforms backend and core --- core/src/toga/app.py | 36 ++++++++++++++-------------- core/src/toga/window.py | 25 ++++++++++--------- winforms/src/toga_winforms/window.py | 6 ++++- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index db982c1894..f60b277b63 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -771,12 +771,12 @@ def exit_full_screen(self) -> None: removed in the future. Consider using `App.enter_presentation_mode()` and `App.exit_presentation_mode()` methods instead. """ - # warnings.warn( - # "`App.exit_full_screen()` method is deprecated and will be" - # " removed in the future. Consider using `App.enter_presentation_mode()`" - # " and `App.exit_presentation_mode()` methods instead.", - # FutureWarning, - # ) + warnings.warn( + "`App.exit_full_screen()` method is deprecated and will be" + " removed in the future. Consider using `App.enter_presentation_mode()`" + " and `App.exit_presentation_mode()` methods instead.", + DeprecationWarning, + ) if self.is_full_screen: for window in self.windows: if ( @@ -793,11 +793,11 @@ def is_full_screen(self) -> bool: `App.is_full_screen` property is deprecated and will be removed in the future. Consider using `App.is_in_presentation_mode` property instead. """ - # warnings.warn( - # "`App.is_full_screen` property is deprecated and will be removed in the" - # " future. Consider using `App.is_in_presentation_mode` property instead.", - # FutureWarning, - # ) + warnings.warn( + "`App.is_full_screen` property is deprecated and will be removed in the" + " future. Consider using `App.is_in_presentation_mode` property instead.", + DeprecationWarning, + ) return any(window.state == WindowState.PRESENTATION for window in self.windows) def set_full_screen(self, *windows: Window) -> None: @@ -816,12 +816,12 @@ def set_full_screen(self, *windows: Window) -> None: removed in the future. Consider using `App.enter_presentation_mode()` and `App.exit_presentation_mode()` methods instead. """ - # warnings.warn( - # "`App.set_full_screen()` method is deprecated and will be" - # " removed in the future. Consider using `App.enter_presentation_mode()`" - # " and `App.exit_presentation_mode()` methods instead.", - # FutureWarning, - # ) + warnings.warn( + "`App.set_full_screen()` method is deprecated and will be" + " removed in the future. Consider using `App.enter_presentation_mode()`" + " and `App.exit_presentation_mode()` methods instead.", + DeprecationWarning, + ) if self.windows is not None: self.exit_full_screen() screen_window_dict = dict() @@ -844,7 +844,7 @@ def enter_presentation_mode(self, window_list_or_screen_window_dict): Presentation mode is not the same as Full Screen mode; full screen mode is when all window borders and other window decorations are no longer visible. - :param window_list_or_screen_window_dict: A list of windows, a dictionary + :param window_list_or_screen_window_dict: A list of windows, or a dictionary mapping screens to windows, to go into full screen, in order of allocation to screens. If the number of windows exceeds the number of available displays, those windows will not be visible. diff --git a/core/src/toga/window.py b/core/src/toga/window.py index ecf15b3512..b529bb63b7 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -448,20 +448,20 @@ def full_screen(self) -> bool: `Window.full_screen` property is deprecated and will be removed in the future. Consider using `Window.state` property instead. #""" - # warnings.warn( - # "`Window.full_screen` property is deprecated and will be removed in" - # " the future. Consider using `Window.state` property instead.", - # FutureWarning, - # ) + warnings.warn( + "`Window.full_screen` property is deprecated and will be removed in" + " the future. Consider using `Window.state` property instead.", + DeprecationWarning, + ) return bool(self.state == WindowState.FULLSCREEN) @full_screen.setter def full_screen(self, is_full_screen: bool) -> None: - # warnings.warn( - # "`Window.full_screen` property is deprecated and will be removed in" - # " the future. Consider using `Window.state` property instead.", - # FutureWarning, - # ) + warnings.warn( + "`Window.full_screen` property is deprecated and will be removed in" + " the future. Consider using `Window.state` property instead.", + DeprecationWarning, + ) if is_full_screen and (self.state != WindowState.FULLSCREEN): self._impl.set_window_state(WindowState.FULLSCREEN) elif not is_full_screen and (self.state == WindowState.FULLSCREEN): @@ -472,7 +472,10 @@ def full_screen(self, is_full_screen: bool) -> None: @property def state(self) -> WindowState: """The current state of the window.""" - return self._impl.get_window_state() + if getattr(self, "_impl", None) is None: + return WindowState.NORMAL + else: + return self._impl.get_window_state() @state.setter def state(self, state: WindowState) -> None: diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index b4f103af79..21d7ee1033 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -40,7 +40,11 @@ def __init__(self, interface, title, position, size): self.native.Resize += WeakrefCallable(self.winforms_Resize) self.resize_content() # Store initial size - # self.set_full_screen(self.interface.full_screen) + # Set window border style based on whether window resizability is enabled or not. + self.native.FormBorderStyle = getattr( + WinForms.FormBorderStyle, + "Sizable" if self.interface.resizable else "FixedSingle", + ) ###################################################################### # Native event handlers From b17e6fd9c3a8c0992255e2852f3efed3056f98da Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 26 Mar 2024 06:34:22 -0700 Subject: [PATCH 025/248] Fixed core --- core/src/toga/window.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index b529bb63b7..c6e1fd02e1 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -482,14 +482,20 @@ def state(self, state: WindowState) -> None: if state != self.state: if state == WindowState.NORMAL: self._impl.set_window_state(WindowState.NORMAL) - if state == WindowState.MAXIMIZED: - self._impl.set_window_state(WindowState.MAXIMIZED) elif state == WindowState.MINIMIZED: self._impl.set_window_state(WindowState.MINIMIZED) - elif state == WindowState.FULLSCREEN: - self._impl.set_window_state(WindowState.FULLSCREEN) - elif state == WindowState.PRESENTATION: - self._impl.set_window_state(WindowState.PRESENTATION) + else: + if not self.resizable: + warnings.warn( + f"Cannot set window state to {state} of a non-resizable window. " + ) + else: + if state == WindowState.MAXIMIZED: + self._impl.set_window_state(WindowState.MAXIMIZED) + elif state == WindowState.FULLSCREEN: + self._impl.set_window_state(WindowState.FULLSCREEN) + elif state == WindowState.PRESENTATION: + self._impl.set_window_state(WindowState.PRESENTATION) ###################################################################### # Window capabilities From 6377420275f43df4d67569ac7992a8a5ed8d0cbf Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 26 Mar 2024 05:06:18 -0700 Subject: [PATCH 026/248] Fixed cocoa backend --- cocoa/src/toga_cocoa/window.py | 12 +++++++----- gtk/src/toga_gtk/window.py | 3 ++- winforms/src/toga_winforms/window.py | 3 ++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 58a1f90cad..5d18aeb349 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -344,11 +344,12 @@ def get_visible(self): def get_window_state(self): if bool(self.native.isZoomed): - return WindowState.MAXIMIZED + if bool(self.native.styleMask & NSWindowStyleMask.FullScreen): + return WindowState.FULLSCREEN + else: + return WindowState.MAXIMIZED elif bool(self.native.isMiniaturized): return WindowState.MINIMIZED - elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): - return WindowState.FULLSCREEN elif bool(self.interface.content._impl.native.isInFullScreenMode()): return WindowState.PRESENTATION else: @@ -365,7 +366,7 @@ def set_window_state(self, state): self.native.setIsMiniaturized(False) # If the window is in full-screen mode, exit full-screen mode elif current_state == WindowState.FULLSCREEN: - self.native.toggleFullScreen(None) + self.native.toggleFullScreen(self.native) # If the window is in presentation mode, exit presentation mode elif current_state == WindowState.PRESENTATION: opts = NSMutableDictionary.alloc().init() @@ -380,7 +381,8 @@ def set_window_state(self, state): # Changing window states without reverting back to the NORMAL state will # cause glitches, so revert to NORMAL state before switching to other states. - # Setting window state to NORMAL from the interface side also causes the same glitches. + # Setting window state to NORMAL from the interface side also causes the + # same glitches. Might be a race condition. elif state == WindowState.MAXIMIZED: self.set_window_state(WindowState.NORMAL) self.native.setIsZoomed(True) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 850c04f54b..aa0976f511 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -260,7 +260,8 @@ def set_window_state(self, state): # Changing window states without reverting back to the NORMAL state will # cause glitches, so revert to NORMAL state before switching to other states. - # Setting window state to NORMAL from the interface side also causes the same glitches. + # Setting window state to NORMAL from the interface side also causes the + # same glitches. Might be a race condition. elif state == WindowState.MAXIMIZED: self.set_window_state(WindowState.NORMAL) self.native.maximize() diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 21d7ee1033..94f96a2813 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -279,7 +279,8 @@ def set_window_state(self, state): # Changing window states without reverting back to the NORMAL state will # cause glitches, so revert to NORMAL state before switching to other states. - # Setting window state to NORMAL from the interface side also causes the same glitches. + # Setting window state to NORMAL from the interface side also causes the + # same glitches. Might be a race condition. elif state == WindowState.MAXIMIZED: self.set_window_state(WindowState.NORMAL) self.native.WindowState = WinForms.FormWindowState.Maximized From 8288be9c94feb49e5bc5f394bac00235bd2ce480 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 27 Mar 2024 00:38:41 -0700 Subject: [PATCH 027/248] Fixed presentation mode on cocoa --- cocoa/src/toga_cocoa/app.py | 31 ++++++++++++++++++++++++++++ cocoa/src/toga_cocoa/window.py | 37 +++++++--------------------------- core/src/toga/app.py | 9 ++------- 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 9b603dd8b7..5800ad27b6 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -29,6 +29,7 @@ NSMenuItem, NSMutableArray, NSMutableDictionary, + NSNumber, NSObject, NSOpenPanel, NSScreen, @@ -490,6 +491,36 @@ def get_current_window(self): def set_current_window(self, window): window._impl.native.makeKeyAndOrderFront(window._impl.native) + ###################################################################### + # Presentation mode controls + ###################################################################### + + def enter_presentation_mode(self, screen_window_dict): + opts = NSMutableDictionary.alloc().init() + opts.setObject( + NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" + ) + for screen, window in screen_window_dict.items(): + window.content._impl.native.enterFullScreenMode( + screen._impl.native, withOptions=opts + ) + # Going full screen causes the window content to be re-homed + # in a NSFullScreenWindow; teach the new parent window + # about its Toga representations. + window.content._impl.native.window._impl = window._impl + window.content._impl.native.window.interface = window + window.content.refresh() + + def exit_presentation_mode(self): + opts = NSMutableDictionary.alloc().init() + opts.setObject( + NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" + ) + for window in self.interface.windows: + if bool(window.content._impl.native.isInFullScreenMode()): + window.content._impl.native.exitFullScreenModeWithOptions(opts) + window.content.refresh() + class DocumentApp(App): # pragma: no cover def create_app_commands(self): diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 5d18aeb349..3854566fc7 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -9,8 +9,6 @@ NSImage, NSMakeRect, NSMutableArray, - NSMutableDictionary, - NSNumber, NSPoint, NSScreen, NSSize, @@ -346,8 +344,7 @@ def get_window_state(self): if bool(self.native.isZoomed): if bool(self.native.styleMask & NSWindowStyleMask.FullScreen): return WindowState.FULLSCREEN - else: - return WindowState.MAXIMIZED + return WindowState.MAXIMIZED elif bool(self.native.isMiniaturized): return WindowState.MINIMIZED elif bool(self.interface.content._impl.native.isInFullScreenMode()): @@ -369,20 +366,10 @@ def set_window_state(self, state): self.native.toggleFullScreen(self.native) # If the window is in presentation mode, exit presentation mode elif current_state == WindowState.PRESENTATION: - opts = NSMutableDictionary.alloc().init() - opts.setObject( - NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" - ) - self.interface.content._impl.native.exitFullScreenModeWithOptions(opts) - self.interface.content.refresh() - self.interface.screen = ( - self.interface._impl._before_presentation_mode_screen - ) - - # Changing window states without reverting back to the NORMAL state will - # cause glitches, so revert to NORMAL state before switching to other states. - # Setting window state to NORMAL from the interface side also causes the - # same glitches. Might be a race condition. + self.interface.app.exit_presentation_mode() + + # Set Window state to NORMAL before changing to other states as some states + # block changing window state without first exiting them. elif state == WindowState.MAXIMIZED: self.set_window_state(WindowState.NORMAL) self.native.setIsZoomed(True) @@ -397,19 +384,9 @@ def set_window_state(self, state): elif state == WindowState.PRESENTATION: self.set_window_state(WindowState.NORMAL) - opts = NSMutableDictionary.alloc().init() - opts.setObject( - NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" - ) - self.interface.content._impl.native.enterFullScreenMode( - self.interface.screen._impl.native, withOptions=opts + self.interface.app.enter_presentation_mode( + {self.interface.screen: self.interface} ) - # Going presentation mode(full screen) causes the window content to be - # re-homed in a NSFullScreenWindow; teach the new parent window - # about its Toga representations. - self.interface.content._impl.native.window._impl = self - self.interface.content._impl.native.window.interface = self.interface - self.interface.content.refresh() ###################################################################### # Window capabilities diff --git a/core/src/toga/app.py b/core/src/toga/app.py index f60b277b63..59b0521cce 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -856,16 +856,11 @@ def enter_presentation_mode(self, window_list_or_screen_window_dict): elif isinstance(window_list_or_screen_window_dict, dict): screen_window_dict = window_list_or_screen_window_dict - for screen, window in screen_window_dict.items(): - window._impl._before_presentation_mode_screen = window.screen - window.screen = screen - window.state = WindowState.PRESENTATION + self._impl.enter_presentation_mode(screen_window_dict) def exit_presentation_mode(self) -> None: """Exit full screen mode.""" - for window in self.windows: - if window.state == WindowState.PRESENTATION: - window.state = WindowState.NORMAL + self._impl.exit_presentation_mode() ###################################################################### # App events From 041f15b412ad02f34d7f2266560d63010cc9442b Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 27 Mar 2024 00:49:16 -0700 Subject: [PATCH 028/248] Misc Fixes --- cocoa/src/toga_cocoa/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 5800ad27b6..e7a00e5b05 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -9,6 +9,7 @@ import toga from toga.command import Separator +from toga.constants import WindowState from toga.handlers import NativeHandler from .keys import cocoa_key @@ -517,7 +518,7 @@ def exit_presentation_mode(self): NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" ) for window in self.interface.windows: - if bool(window.content._impl.native.isInFullScreenMode()): + if window.state == WindowState.PRESENTATION: window.content._impl.native.exitFullScreenModeWithOptions(opts) window.content.refresh() From 3591bb0348225bdd6fed7f04cb15956224b53010 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 27 Mar 2024 00:58:59 -0700 Subject: [PATCH 029/248] Fixed presentation mode on cocoa, gtk, winforms --- cocoa/src/toga_cocoa/window.py | 4 ++-- gtk/src/toga_gtk/app.py | 16 ++++++++++++++++ gtk/src/toga_gtk/window.py | 6 ++---- winforms/src/toga_winforms/app.py | 16 ++++++++++++++++ winforms/src/toga_winforms/window.py | 6 ++---- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 3854566fc7..7f7be28196 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -368,8 +368,8 @@ def set_window_state(self, state): elif current_state == WindowState.PRESENTATION: self.interface.app.exit_presentation_mode() - # Set Window state to NORMAL before changing to other states as some states - # block changing window state without first exiting them. + # Set Window state to NORMAL before changing to other states as + # some states block changing window state without first exiting them. elif state == WindowState.MAXIMIZED: self.set_window_state(WindowState.NORMAL) self.native.setIsZoomed(True) diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 3a115fe629..e5f2169beb 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -9,6 +9,7 @@ import toga from toga import App as toga_App from toga.command import Command, Separator +from toga.constants import WindowState from .keys import gtk_accel from .libs import TOGA_DEFAULT_STYLES, Gdk, Gio, GLib, Gtk @@ -273,6 +274,21 @@ def get_current_window(self): def set_current_window(self, window): window._impl.native.present() + ###################################################################### + # Presentation mode controls + ###################################################################### + + def enter_presentation_mode(self, screen_window_dict): + for screen, window in screen_window_dict.items(): + window._impl._before_presentation_mode_screen = window.screen + window.screen = screen + window.state = WindowState.PRESENTATION + + def exit_presentation_mode(self): + for window in self.interface.windows: + if window.state == WindowState.PRESENTATION: + window.state = WindowState.NORMAL + class DocumentApp(App): # pragma: no cover def create_app_commands(self): diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index aa0976f511..a268acd24e 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -258,10 +258,8 @@ def set_window_state(self, state): self.interface._impl._before_presentation_mode_screen ) - # Changing window states without reverting back to the NORMAL state will - # cause glitches, so revert to NORMAL state before switching to other states. - # Setting window state to NORMAL from the interface side also causes the - # same glitches. Might be a race condition. + # Set Window state to NORMAL before changing to other states as + # some states block changing window state without first exiting them. elif state == WindowState.MAXIMIZED: self.set_window_state(WindowState.NORMAL) self.native.maximize() diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index c9f28bce84..7be930f6e0 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -14,6 +14,7 @@ import toga from toga import Key from toga.command import Separator +from toga.constants import WindowState from .keys import toga_to_winforms_key, toga_to_winforms_shortcut from .libs.proactor import WinformsProactorEventLoop @@ -368,6 +369,21 @@ def get_current_window(self): def set_current_window(self, window): window._impl.native.Activate() + ###################################################################### + # Presentation mode controls + ###################################################################### + + def enter_presentation_mode(self, screen_window_dict): + for screen, window in screen_window_dict.items(): + window._impl._before_presentation_mode_screen = window.screen + window.screen = screen + window.state = WindowState.PRESENTATION + + def exit_presentation_mode(self): + for window in self.interface.windows: + if window.state == WindowState.PRESENTATION: + window.state = WindowState.NORMAL + class DocumentApp(App): # pragma: no cover def create_app_commands(self): diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 94f96a2813..9cfb9f65df 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -277,10 +277,8 @@ def set_window_state(self, state): self.native.WindowState = WinForms.FormWindowState.Normal - # Changing window states without reverting back to the NORMAL state will - # cause glitches, so revert to NORMAL state before switching to other states. - # Setting window state to NORMAL from the interface side also causes the - # same glitches. Might be a race condition. + # Set Window state to NORMAL before changing to other states as + # some states block changing window state without first exiting them. elif state == WindowState.MAXIMIZED: self.set_window_state(WindowState.NORMAL) self.native.WindowState = WinForms.FormWindowState.Maximized From 3b98fe790984849159e35da7952f6a08c29a016b Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 27 Mar 2024 02:14:39 -0700 Subject: [PATCH 030/248] Fixed presentation mode core tests --- core/src/toga/app.py | 46 ++++++------- core/src/toga/window.py | 20 +++--- core/tests/app/test_app.py | 126 ++++++++++++++---------------------- dummy/src/toga_dummy/app.py | 13 ++++ 4 files changed, 91 insertions(+), 114 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 59b0521cce..0ef8b5817b 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -771,19 +771,14 @@ def exit_full_screen(self) -> None: removed in the future. Consider using `App.enter_presentation_mode()` and `App.exit_presentation_mode()` methods instead. """ - warnings.warn( - "`App.exit_full_screen()` method is deprecated and will be" - " removed in the future. Consider using `App.enter_presentation_mode()`" - " and `App.exit_presentation_mode()` methods instead.", - DeprecationWarning, - ) + # warnings.warn( + # "`App.exit_full_screen()` method is deprecated and will be" + # " removed in the future. Consider using `App.enter_presentation_mode()`" + # " and `App.exit_presentation_mode()` methods instead.", + # DeprecationWarning, + # ) if self.is_full_screen: - for window in self.windows: - if ( - window.state == WindowState.FULLSCREEN - or window.state == WindowState.PRESENTATION - ): - window.state = WindowState.NORMAL + self._impl.exit_presentation_mode() @property def is_full_screen(self) -> bool: @@ -792,12 +787,12 @@ def is_full_screen(self) -> bool: .. warning:: `App.is_full_screen` property is deprecated and will be removed in the future. Consider using `App.is_in_presentation_mode` property instead. - """ - warnings.warn( - "`App.is_full_screen` property is deprecated and will be removed in the" - " future. Consider using `App.is_in_presentation_mode` property instead.", - DeprecationWarning, - ) + #""" + # warnings.warn( + # "`App.is_full_screen` property is deprecated and will be removed in the" + # " future. Consider using `App.is_in_presentation_mode` property instead.", + # DeprecationWarning, + # ) return any(window.state == WindowState.PRESENTATION for window in self.windows) def set_full_screen(self, *windows: Window) -> None: @@ -816,12 +811,12 @@ def set_full_screen(self, *windows: Window) -> None: removed in the future. Consider using `App.enter_presentation_mode()` and `App.exit_presentation_mode()` methods instead. """ - warnings.warn( - "`App.set_full_screen()` method is deprecated and will be" - " removed in the future. Consider using `App.enter_presentation_mode()`" - " and `App.exit_presentation_mode()` methods instead.", - DeprecationWarning, - ) + # warnings.warn( + # "`App.set_full_screen()` method is deprecated and will be" + # " removed in the future. Consider using `App.enter_presentation_mode()`" + # " and `App.exit_presentation_mode()` methods instead.", + # DeprecationWarning, + # ) if self.windows is not None: self.exit_full_screen() screen_window_dict = dict() @@ -860,7 +855,8 @@ def enter_presentation_mode(self, window_list_or_screen_window_dict): def exit_presentation_mode(self) -> None: """Exit full screen mode.""" - self._impl.exit_presentation_mode() + if self.is_in_presentation_mode: + self._impl.exit_presentation_mode() ###################################################################### # App events diff --git a/core/src/toga/window.py b/core/src/toga/window.py index c6e1fd02e1..adde00e0da 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -448,20 +448,20 @@ def full_screen(self) -> bool: `Window.full_screen` property is deprecated and will be removed in the future. Consider using `Window.state` property instead. #""" - warnings.warn( - "`Window.full_screen` property is deprecated and will be removed in" - " the future. Consider using `Window.state` property instead.", - DeprecationWarning, - ) + # warnings.warn( + # "`Window.full_screen` property is deprecated and will be removed in" + # " the future. Consider using `Window.state` property instead.", + # DeprecationWarning, + # ) return bool(self.state == WindowState.FULLSCREEN) @full_screen.setter def full_screen(self, is_full_screen: bool) -> None: - warnings.warn( - "`Window.full_screen` property is deprecated and will be removed in" - " the future. Consider using `Window.state` property instead.", - DeprecationWarning, - ) + # warnings.warn( + # "`Window.full_screen` property is deprecated and will be removed in" + # " the future. Consider using `Window.state` property instead.", + # DeprecationWarning, + # ) if is_full_screen and (self.state != WindowState.FULLSCREEN): self._impl.set_window_state(WindowState.FULLSCREEN) elif not is_full_screen and (self.state == WindowState.FULLSCREEN): diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 8a23dffdd8..56d705f98c 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -402,53 +402,33 @@ def test_full_screen(event_loop): # If we're not full screen, exiting full screen is a no-op app.exit_full_screen() - for window in app.windows: - assert_action_not_performed(window, "set window state to WindowState.NORMAL") - - # TODO: Check and keep track of the window assignment to screens - # This seems to be difficult currently, as the interface api: `App.enter_presentation_mode` - # both assigns screens to windows and also sets the state. + assert not app.is_full_screen + assert_action_not_performed(app, "exit presentation mode") # Enter full screen with 2 windows app.set_full_screen(window2, app.main_window) assert app.is_full_screen assert_action_performed_with( - window2, - "set window state to WindowState.PRESENTATION", - state=WindowState.PRESENTATION, - ) - assert_action_performed_with( - app.main_window, - "set window state to WindowState.PRESENTATION", - state=WindowState.PRESENTATION, + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window2, app.screens[1]: app.main_window}, ) # Change the screens that are full screen app.set_full_screen(app.main_window, window1) assert app.is_full_screen assert_action_performed_with( - app.main_window, - "set window state to WindowState.PRESENTATION", - state=WindowState.PRESENTATION, - ) - assert_action_performed_with( - window1, - "set window state to WindowState.PRESENTATION", - state=WindowState.PRESENTATION, + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window2, app.screens[1]: app.main_window}, ) # Exit full screen mode app.exit_full_screen() assert not app.is_full_screen - assert_action_performed_with( - app.main_window, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, - ) - assert_action_performed_with( - window1, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, + assert_action_performed( + app, + "exit presentation mode", ) @@ -464,27 +444,16 @@ def test_set_empty_full_screen_window_list(event_loop): app.set_full_screen(window1, window2) assert app.is_full_screen assert_action_performed_with( - window1, - "set window state to WindowState.PRESENTATION", - state=WindowState.PRESENTATION, - ) - assert_action_performed_with( - window2, - "set window state to WindowState.PRESENTATION", - state=WindowState.PRESENTATION, + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, ) # Exit full screen mode by setting no windows full screen app.set_full_screen() assert not app.is_full_screen - assert_action_performed_with( - window1, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, - ) - assert_action_performed_with( - window2, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, + assert_action_performed( + app, + "exit presentation mode", ) @@ -498,54 +467,53 @@ def test_presentation_mode(event_loop): # If we're not in presentation mode, exiting presentation mode is a no-op app.exit_presentation_mode() - for window in app.windows: - assert_action_not_performed(window, "set window state to WindowState.NORMAL") - - # TODO: Check and keep track of the window assignment to screens - # This seems to be difficult currently, as the interface api: `App.enter_presentation_mode` - # both assigns screens to windows and also sets the state. + assert_action_not_performed(app, "exit presentation mode") # Enter presentation mode with 1 window: - app.enter_presentation_mode([window1]) + app.enter_presentation_mode({app.screens[0]: window1}) window1.state = WindowState.PRESENTATION assert app.is_in_presentation_mode assert_action_performed_with( - window1, - "set window state to WindowState.PRESENTATION", - state=WindowState.PRESENTATION, + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window1}, ) # Exit presentation mode: app.exit_presentation_mode() - assert_action_performed_with( - window1, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, + assert_action_performed( + app, + "exit presentation mode", ) - # Enter presentation mode with 2 window: - app.enter_presentation_mode({app.screens[1]: window1, app.screens[0]: window2}) + # Enter presentation mode with 2 windows: + app.enter_presentation_mode([window1, window2]) assert app.is_in_presentation_mode assert_action_performed_with( - window1, - "set window state to WindowState.PRESENTATION", - state=WindowState.PRESENTATION, - ) - assert_action_performed_with( - window2, - "set window state to WindowState.PRESENTATION", - state=WindowState.PRESENTATION, + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, ) # Exit presentation mode: app.exit_presentation_mode() - assert_action_performed_with( - window1, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, + assert_action_performed( + app, + "exit presentation mode", ) + + # Entering presentation mode with 3 windows should drop the last window, + # as the app has only 2 screens: + app.enter_presentation_mode([app.main_window, window2, window1]) + assert app.is_in_presentation_mode assert_action_performed_with( - window2, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: app.main_window, app.screens[1]: window2}, + ) + # Exit presentation mode: + app.exit_presentation_mode() + assert_action_performed( + app, + "exit presentation mode", ) diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index e3c5dd644d..a73079186e 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -2,6 +2,8 @@ import sys from pathlib import Path +from toga.constants import WindowState + from .screens import Screen as ScreenImpl from .utils import LoggedObject from .window import Window @@ -89,6 +91,17 @@ def get_screens(self): ScreenImpl(native=("Secondary Screen", (-1366, -768), (1366, 768))), ] + def enter_presentation_mode(self, screen_window_dict): + self._action("enter presentation mode", screen_window_dict=screen_window_dict) + for screen, window in screen_window_dict.items(): + window.state = WindowState.PRESENTATION + + def exit_presentation_mode(self): + self._action("exit presentation mode") + for window in self.interface.windows: + if window.state == WindowState.PRESENTATION: + window.state = WindowState.NORMAL + class DocumentApp(App): def create(self): From 06e7f258c07e7b1e9f6932178f3f5dae219df783 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 27 Mar 2024 16:23:54 -0700 Subject: [PATCH 031/248] Added tests to testbed and backend probe methods on cocoa, gtk, winforms --- cocoa/src/toga_cocoa/window.py | 3 +- cocoa/tests_backend/window.py | 15 +++ gtk/src/toga_gtk/window.py | 8 +- gtk/tests_backend/window.py | 16 +++ testbed/tests/app/test_app.py | 67 +++++++++++++ testbed/tests/test_window.py | 162 +++++++++++++++++++++++++++++++ winforms/tests_backend/window.py | 18 ++++ 7 files changed, 284 insertions(+), 5 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 7f7be28196..3f7aa98fee 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -344,7 +344,8 @@ def get_window_state(self): if bool(self.native.isZoomed): if bool(self.native.styleMask & NSWindowStyleMask.FullScreen): return WindowState.FULLSCREEN - return WindowState.MAXIMIZED + else: + return WindowState.MAXIMIZED elif bool(self.native.isMiniaturized): return WindowState.MINIMIZED elif bool(self.interface.content._impl.native.isInFullScreenMode()): diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 9eeba9ff29..52351f9819 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -3,6 +3,7 @@ from rubicon.objc import objc_id, send_message from rubicon.objc.collections import ObjCListInstance +from toga.constants import WindowState from toga_cocoa.libs import ( NSURL, NSAlertFirstButtonReturn, @@ -49,6 +50,20 @@ def content_size(self): self.native.contentView.frame.size.height, ) + def is_window_state(self, state): + if bool(self.native.isZoomed): + if bool(self.native.styleMask & NSWindowStyleMask.FullScreen): + current_state = WindowState.FULLSCREEN + else: + current_state = WindowState.MAXIMIZED + elif bool(self.native.isMiniaturized): + current_state = WindowState.MINIMIZED + elif bool(self.interface.content._impl.native.isInFullScreenMode()): + current_state = WindowState.PRESENTATION + else: + current_state = WindowState.NORMAL + return bool(current_state == state) + @property def is_full_screen(self): return bool(self.native.styleMask & NSWindowStyleMask.FullScreen) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index a268acd24e..b137ac6855 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -228,12 +228,12 @@ def get_window_state(self): if getattr(self, "_presentation_window", None) is not None: return WindowState.PRESENTATION else: - window_state = self._window_state_flags - if window_state & Gdk.WindowState.MAXIMIZED: + window_state_flags = self._window_state_flags + if window_state_flags & Gdk.WindowState.MAXIMIZED: return WindowState.MAXIMIZED - elif window_state & Gdk.WindowState.ICONIFIED: + elif window_state_flags & Gdk.WindowState.ICONIFIED: return WindowState.MINIMIZED - elif window_state & Gdk.WindowState.FULLSCREEN: + elif window_state_flags & Gdk.WindowState.FULLSCREEN: return WindowState.FULLSCREEN else: return WindowState.NORMAL diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index a90b60f8ef..2edea34175 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -2,6 +2,7 @@ from pathlib import Path from unittest.mock import Mock +from toga.constants import WindowState from toga_gtk.libs import Gdk, Gtk from .probe import BaseProbe @@ -36,6 +37,21 @@ def content_size(self): content_allocation = self.impl.container.get_allocation() return (content_allocation.width, content_allocation.height) + def is_window_state(self, state): + if getattr(self.impl, "_presentation_window", None) is not None: + current_state = WindowState.PRESENTATION + else: + window_state_flags = self._window_state_flags + if window_state_flags & Gdk.WindowState.MAXIMIZED: + current_state = WindowState.MAXIMIZED + elif window_state_flags & Gdk.WindowState.ICONIFIED: + current_state = WindowState.MINIMIZED + elif window_state_flags & Gdk.WindowState.FULLSCREEN: + current_state = WindowState.FULLSCREEN + else: + current_state = WindowState.NORMAL + return bool(current_state == state) + @property def is_full_screen(self): return bool(self.native.get_window().get_state() & Gdk.WindowState.FULLSCREEN) diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index 067c6466cf..6915be47aa 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -4,6 +4,7 @@ import toga from toga.colors import CORNFLOWERBLUE, FIREBRICK, REBECCAPURPLE +from toga.constants import WindowState from toga.style.pack import Pack from ..test_window import window_probe @@ -287,6 +288,72 @@ async def test_full_screen(app, app_probe): window1.close() window2.close() + async def test_presentation_mode(app, app_probe): + """The app can enter into presentation mode""" + try: + window1 = toga.Window("Test Window 1", position=(150, 150), size=(200, 200)) + window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) + window2 = toga.Window("Test Window 2", position=(400, 150), size=(200, 200)) + window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + window1_probe = window_probe(app, window1) + window2_probe = window_probe(app, window2) + + window1.show() + window2.show() + await app_probe.redraw("Extra windows are visible") + + assert not app.is_in_presentation_mode + assert not window1_probe.is_window_state(WindowState.PRESENTATION) + assert not window2_probe.is_window_state(WindowState.PRESENTATION) + initial_content1_size = window1_probe.content_size + initial_content2_size = window2_probe.content_size + + # Enter presentation mode with window 2 via the app + app.enter_presentation_mode(window2) + await window2_probe.wait_for_window( + "Second extra window is in presentation mode" + ) + assert app.is_in_presentation_mode + + assert not window1_probe.is_window_state(WindowState.PRESENTATION) + assert window1_probe.content_size == initial_content1_size + + assert window2_probe.is_window_state(WindowState.PRESENTATION) + assert window2_probe.content_size[0] > 1000 + assert window2_probe.content_size[1] > 700 + # Exit presentation mode + app.exit_presentation_mode() + await window2_probe.wait_for_window( + "Second extra window is no longer in presentation mode" + ) + assert not app.is_in_presentation_mode + assert window2_probe.content_size == initial_content2_size + + # Enter presentation mode with window 1 via the app + app.enter_presentation_mode(window1) + await window1_probe.wait_for_window( + "First extra window is in presentation mode" + ) + assert app.is_in_presentation_mode + + assert not window2_probe.is_window_state(WindowState.PRESENTATION) + assert window2_probe.content_size == initial_content2_size + + assert window1_probe.is_window_state(WindowState.PRESENTATION) + assert window1_probe.content_size[0] > 1000 + assert window1_probe.content_size[1] > 700 + # Exit presentation mode + app.exit_presentation_mode() + await window1_probe.wait_for_window( + "First extra window is no longer in presentation mode" + ) + assert not app.is_in_presentation_mode + assert window1_probe.content_size == initial_content1_size + + finally: + window1.close() + window2.close() + async def test_show_hide_cursor(app, app_probe): """The app cursor can be hidden and shown""" assert app_probe.is_cursor_visible diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 19d5a2fd95..a7a956303e 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.constants import WindowState from toga.style.pack import COLUMN, Pack @@ -489,6 +490,167 @@ 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 + @pytest.mark.parametrize( + "second_window_kwargs", + [dict(title="Secondary Window", position=(200, 150))], + ) + async def test_window_state_minimized(second_window, second_window_probe): + """Window can have minimized window state""" + assert not second_window_probe.is_window_state(WindowState.MINIMIZED) + assert second_window_probe.is_minimizable + + second_window.state = WindowState.MINIMIZED + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is minimized", + ) + assert second_window_probe.is_window_state(WindowState.MINIMIZED) + + second_window.state = WindowState.MINIMIZED + await second_window_probe.wait_for_window("Secondary window is still minimized") + assert second_window_probe.is_window_state(WindowState.MINIMIZED) + + second_window.state = WindowState.NORMAL + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is not minimized", + ) + assert not second_window_probe.is_window_state(WindowState.MINIMIZED) + assert second_window_probe.is_minimizable + + second_window.state = WindowState.NORMAL + await second_window_probe.wait_for_window( + "Secondary window is still not minimized" + ) + assert not second_window_probe.is_window_state(WindowState.MINIMIZED) + + @pytest.mark.parametrize( + "second_window_kwargs", + [dict(title="Secondary Window", position=(200, 150))], + ) + async def test_window_state_maximized(second_window, second_window_probe): + """Window can have maximized window state""" + assert not second_window_probe.is_window_state(WindowState.MAXIMIZED) + assert second_window_probe.is_resizable + initial_content_size = second_window_probe.content_size + + second_window.state = WindowState.MAXIMIZED + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is maximized", + ) + assert second_window_probe.is_window_state(WindowState.MAXIMIZED) + assert second_window_probe.content_size[0] > initial_content_size[0] + assert second_window_probe.content_size[1] > initial_content_size[1] + + second_window.state = WindowState.MAXIMIZED + await second_window_probe.wait_for_window("Secondary window is still maximized") + assert second_window_probe.is_window_state(WindowState.MAXIMIZED) + assert second_window_probe.content_size[0] > initial_content_size[0] + assert second_window_probe.content_size[1] > initial_content_size[1] + + second_window.state = WindowState.NORMAL + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is not maximized", + ) + assert not second_window_probe.is_window_state(WindowState.MAXIMIZED) + assert second_window_probe.is_resizable + assert second_window_probe.content_size == initial_content_size + + second_window.state = WindowState.NORMAL + await second_window_probe.wait_for_window( + "Secondary window is still not maximized" + ) + assert not second_window_probe.is_window_state(WindowState.MAXIMIZED) + assert second_window_probe.content_size == initial_content_size + + @pytest.mark.parametrize( + "second_window_kwargs", + [dict(title="Secondary Window", position=(200, 150))], + ) + async def test_window_state_full_screen(second_window, second_window_probe): + """Window can have full screen window state""" + assert not second_window_probe.is_window_state(WindowState.FULLSCREEN) + assert second_window_probe.is_resizable + initial_content_size = second_window_probe.content_size + + second_window.state = WindowState.FULLSCREEN + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is full screen", + ) + assert second_window_probe.is_window_state(WindowState.FULLSCREEN) + assert second_window_probe.content_size[0] > initial_content_size[0] + assert second_window_probe.content_size[1] > initial_content_size[1] + + second_window.state = WindowState.FULLSCREEN + await second_window_probe.wait_for_window( + "Secondary window is still full screen" + ) + assert second_window_probe.is_window_state(WindowState.FULLSCREEN) + assert second_window_probe.content_size[0] > initial_content_size[0] + assert second_window_probe.content_size[1] > initial_content_size[1] + + second_window.state = WindowState.NORMAL + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is not full screen", + ) + assert not second_window_probe.is_window_state(WindowState.FULLSCREEN) + assert second_window_probe.is_resizable + assert second_window_probe.content_size == initial_content_size + + second_window.state = WindowState.NORMAL + await second_window_probe.wait_for_window( + "Secondary window is still not full screen" + ) + assert not second_window_probe.is_window_state(WindowState.FULLSCREEN) + assert second_window_probe.content_size == initial_content_size + + @pytest.mark.parametrize( + "second_window_kwargs", + [dict(title="Secondary Window", position=(200, 150))], + ) + async def test_window_state_presentation(second_window, second_window_probe): + """Window can have presentation window state""" + assert not second_window_probe.is_window_state(WindowState.PRESENTATION) + assert second_window_probe.is_resizable + initial_content_size = second_window_probe.content_size + + second_window.state = WindowState.PRESENTATION + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is in presentation mode", + ) + assert second_window_probe.is_window_state(WindowState.PRESENTATION) + assert second_window_probe.content_size[0] > initial_content_size[0] + assert second_window_probe.content_size[1] > initial_content_size[1] + + second_window.state = WindowState.PRESENTATION + await second_window_probe.wait_for_window( + "Secondary window is still in presentation mode" + ) + assert second_window_probe.is_window_state(WindowState.PRESENTATION) + assert second_window_probe.content_size[0] > initial_content_size[0] + assert second_window_probe.content_size[1] > initial_content_size[1] + + second_window.state = WindowState.NORMAL + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is not in presentation mode", + ) + assert not second_window_probe.is_window_state(WindowState.PRESENTATION) + assert second_window_probe.is_resizable + assert second_window_probe.content_size == initial_content_size + + second_window.state = WindowState.NORMAL + await second_window_probe.wait_for_window( + "Secondary window is still not in presentation mode" + ) + assert not second_window_probe.is_window_state(WindowState.PRESENTATION) + assert second_window_probe.content_size == initial_content_size + @pytest.mark.parametrize( "second_window_kwargs", [dict(title="Secondary Window", position=(200, 150))], diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index d7e8d7ac9c..e717a642cf 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -11,6 +11,8 @@ ToolStripSeparator, ) +from toga.constants import WindowState + from .probe import BaseProbe @@ -48,6 +50,22 @@ def content_size(self): ), ) + def is_window_state(self, state): + if getattr(self.impl, "_presentation_window", None) is not None: + current_state = WindowState.PRESENTATION + else: + window_state = self.native.WindowState + if window_state == FormWindowState.Maximized: + if self.native.FormBorderStyle == getattr(FormBorderStyle, "None"): + current_state = WindowState.FULLSCREEN + else: + current_state = WindowState.MAXIMIZED + elif window_state == FormWindowState.Minimized: + current_state = WindowState.MINIMIZED + elif window_state == FormWindowState.Normal: + current_state = WindowState.NORMAL + return bool(current_state == state) + @property def is_full_screen(self): return ( From 0cf8cf8db781a4c3758a834575138aee2d805fc8 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 29 Mar 2024 11:25:12 -0700 Subject: [PATCH 032/248] Fixed winforms backend and testbed tests --- cocoa/tests_backend/app.py | 4 +- core/src/toga/app.py | 6 +- gtk/src/toga_gtk/window.py | 16 +++-- gtk/tests_backend/app.py | 6 +- gtk/tests_backend/window.py | 2 +- testbed/tests/app/test_app.py | 55 ++++++++++++++- testbed/tests/test_window.py | 14 ++++ winforms/src/toga_winforms/window.py | 100 ++++++++++++--------------- winforms/tests_backend/app.py | 3 +- winforms/tests_backend/window.py | 29 ++++---- 10 files changed, 150 insertions(+), 85 deletions(-) diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index 7b63e6ec9a..da4e007005 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -2,6 +2,7 @@ from rubicon.objc import NSPoint, ObjCClass, objc_id, send_message +from toga.constants import WindowState from toga_cocoa.keys import cocoa_key, toga_key from toga_cocoa.libs import ( NSApplication, @@ -12,6 +13,7 @@ ) from .probe import BaseProbe +from .window import WindowProbe NSPanel = ObjCClass("NSPanel") @@ -50,7 +52,7 @@ def is_cursor_visible(self): return self.app._impl._cursor_visible def is_full_screen(self, window): - return window.content._impl.native.isInFullScreenMode() + return WindowProbe(self.app, window).is_window_state(WindowState.PRESENTATION) def content_size(self, window): return ( diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 0ef8b5817b..082d4aac99 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -833,7 +833,10 @@ def is_in_presentation_mode(self) -> bool: """Is the app currently in presentation mode?""" return any(window.state == WindowState.PRESENTATION for window in self.windows) - def enter_presentation_mode(self, window_list_or_screen_window_dict): + def enter_presentation_mode( + self, + window_list_or_screen_window_dict: list[Window] | dict[Screen, Window], + ): """Enter into presentation mode with one or more windows on different screens. Presentation mode is not the same as Full Screen mode; full screen mode is when all window @@ -850,7 +853,6 @@ def enter_presentation_mode(self, window_list_or_screen_window_dict): screen_window_dict[screen] = window elif isinstance(window_list_or_screen_window_dict, dict): screen_window_dict = window_list_or_screen_window_dict - self._impl.enter_presentation_mode(screen_window_dict) def exit_presentation_mode(self) -> None: diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index b137ac6855..81f1acedc0 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -207,13 +207,16 @@ class _PresentationWindow: def __init__(self, window_impl): self.window_impl = window_impl self.native = Gtk.Window() + self.native.fullscreen() + + def update_content(self): self.window_impl.container.remove( self.window_impl.interface.content._impl.native ) self.native.add(self.window_impl.interface.content._impl.native) - self.native.fullscreen() def show(self): + self.update_content() self.native.show() def close(self): @@ -254,10 +257,10 @@ def set_window_state(self, state): elif current_state == WindowState.PRESENTATION: self._presentation_window.close() self._presentation_window = None - self.interface.screen = ( - self.interface._impl._before_presentation_mode_screen - ) + self.interface.screen = self._before_presentation_mode_screen + self._before_presentation_mode_screen = None + self._is_in_presentation_mode = False # Set Window state to NORMAL before changing to other states as # some states block changing window state without first exiting them. elif state == WindowState.MAXIMIZED: @@ -274,8 +277,13 @@ def set_window_state(self, state): elif state == WindowState.PRESENTATION: self.set_window_state(WindowState.NORMAL) + if getattr(self, "_before_presentation_mode_screen", None) is None: + self._before_presentation_mode_screen = self.interface.screen self._presentation_window = self._PresentationWindow(self) self._presentation_window.show() + self._is_in_presentation_mode = True + else: # pragma: no cover + pass ###################################################################### # Window capabilities diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index 18b460b9b9..a8338581e7 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -2,10 +2,12 @@ import pytest +from toga.constants import WindowState from toga_gtk.keys import gtk_accel, toga_key from toga_gtk.libs import Gdk, Gtk from .probe import BaseProbe +from .window import WindowProbe class AppProbe(BaseProbe): @@ -38,9 +40,7 @@ def is_cursor_visible(self): pytest.skip("Cursor visibility not implemented on GTK") def is_full_screen(self, window): - return bool( - window._impl.native.get_window().get_state() & Gdk.WindowState.FULLSCREEN - ) + return WindowProbe(self.app, window).is_window_state(WindowState.PRESENTATION) def content_size(self, window): content_allocation = window._impl.container.get_allocation() diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index 2edea34175..f79cf00978 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -54,7 +54,7 @@ def is_window_state(self, state): @property def is_full_screen(self): - return bool(self.native.get_window().get_state() & Gdk.WindowState.FULLSCREEN) + return self.is_window_state(WindowState.FULLSCREEN) @property def is_resizable(self): diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index 6915be47aa..2a915341ab 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -288,13 +288,14 @@ async def test_full_screen(app, app_probe): window1.close() window2.close() - async def test_presentation_mode(app, app_probe): + async def test_presentation_mode(app, app_probe, main_window, main_window_probe): """The app can enter into presentation mode""" try: window1 = toga.Window("Test Window 1", position=(150, 150), size=(200, 200)) window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) window2 = toga.Window("Test Window 2", position=(400, 150), size=(200, 200)) window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + window2.toolbar.add(app.cmd1) window1_probe = window_probe(app, window1) window2_probe = window_probe(app, window2) @@ -303,13 +304,39 @@ async def test_presentation_mode(app, app_probe): await app_probe.redraw("Extra windows are visible") assert not app.is_in_presentation_mode + assert not main_window_probe.is_window_state(WindowState.PRESENTATION) assert not window1_probe.is_window_state(WindowState.PRESENTATION) + assert window2_probe.has_toolbar() assert not window2_probe.is_window_state(WindowState.PRESENTATION) + initial_content_main_window_size = main_window_probe.content_size initial_content1_size = window1_probe.content_size initial_content2_size = window2_probe.content_size + # Enter presentation mode with main window via the app + app.enter_presentation_mode([main_window]) + await main_window_probe.wait_for_window( + "Main window is in presentation mode" + ) + assert app.is_in_presentation_mode + + assert not window1_probe.is_window_state(WindowState.PRESENTATION) + assert window1_probe.content_size == initial_content1_size + assert not window2_probe.is_window_state(WindowState.PRESENTATION) + assert window2_probe.content_size == initial_content2_size + + assert main_window_probe.is_window_state(WindowState.PRESENTATION) + assert main_window_probe.content_size[0] > 1000 + assert main_window_probe.content_size[1] > 700 + # Exit presentation mode + app.exit_presentation_mode() + await main_window_probe.wait_for_window( + "Main window is no longer in presentation mode" + ) + assert not app.is_in_presentation_mode + assert main_window_probe.content_size == initial_content_main_window_size + # Enter presentation mode with window 2 via the app - app.enter_presentation_mode(window2) + app.enter_presentation_mode([window2]) await window2_probe.wait_for_window( "Second extra window is in presentation mode" ) @@ -330,7 +357,7 @@ async def test_presentation_mode(app, app_probe): assert window2_probe.content_size == initial_content2_size # Enter presentation mode with window 1 via the app - app.enter_presentation_mode(window1) + app.enter_presentation_mode([window1]) await window1_probe.wait_for_window( "First extra window is in presentation mode" ) @@ -350,6 +377,28 @@ async def test_presentation_mode(app, app_probe): assert not app.is_in_presentation_mode assert window1_probe.content_size == initial_content1_size + if len(app.screens) < 2: + # Enter presentation mode with 2 windows via the app, + # but the second window should be dropped. + app.enter_presentation_mode([window1, window2]) + await window1_probe.wait_for_window( + "First extra window is in presentation mode" + ) + assert app.is_in_presentation_mode + + assert not window2_probe.is_window_state(WindowState.PRESENTATION) + assert window2_probe.content_size == initial_content2_size + + assert window1_probe.is_window_state(WindowState.PRESENTATION) + assert window1_probe.content_size[0] > 1000 + assert window1_probe.content_size[1] > 700 + # Exit presentation mode + app.exit_presentation_mode() + await window1_probe.wait_for_window( + "First extra window is no longer in presentation mode" + ) + assert not app.is_in_presentation_mode + assert window1_probe.content_size == initial_content1_size finally: window1.close() window2.close() diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index a7a956303e..9b24f7f6f3 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -451,6 +451,8 @@ async def test_move_and_resize(second_window, second_window_probe): ) async def test_full_screen(second_window, second_window_probe): """Window can be made full screen""" + second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + assert not second_window_probe.is_full_screen assert second_window_probe.is_resizable initial_content_size = second_window_probe.content_size @@ -496,6 +498,9 @@ async def test_full_screen(second_window, second_window_probe): ) async def test_window_state_minimized(second_window, second_window_probe): """Window can have minimized window state""" + second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + + assert second_window_probe.is_window_state(WindowState.NORMAL) assert not second_window_probe.is_window_state(WindowState.MINIMIZED) assert second_window_probe.is_minimizable @@ -530,6 +535,9 @@ async def test_window_state_minimized(second_window, second_window_probe): ) async def test_window_state_maximized(second_window, second_window_probe): """Window can have maximized window state""" + second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + + assert second_window_probe.is_window_state(WindowState.NORMAL) assert not second_window_probe.is_window_state(WindowState.MAXIMIZED) assert second_window_probe.is_resizable initial_content_size = second_window_probe.content_size @@ -571,6 +579,9 @@ async def test_window_state_maximized(second_window, second_window_probe): ) async def test_window_state_full_screen(second_window, second_window_probe): """Window can have full screen window state""" + second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + + assert second_window_probe.is_window_state(WindowState.NORMAL) assert not second_window_probe.is_window_state(WindowState.FULLSCREEN) assert second_window_probe.is_resizable initial_content_size = second_window_probe.content_size @@ -614,6 +625,9 @@ async def test_window_state_full_screen(second_window, second_window_probe): ) async def test_window_state_presentation(second_window, second_window_probe): """Window can have presentation window state""" + second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + + assert second_window_probe.is_window_state(WindowState.NORMAL) assert not second_window_probe.is_window_state(WindowState.PRESENTATION) assert second_window_probe.is_resizable initial_content_size = second_window_probe.content_size diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 9cfb9f65df..ad384424c8 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -149,9 +149,9 @@ def _decor_height(self): def _top_bars_height(self): vertical_shift = 0 - if self.toolbar_native: + if self.toolbar_native and self.toolbar_native.Visible: vertical_shift += self.toolbar_native.Height - if self.native.MainMenuStrip: + if self.native.MainMenuStrip and self.native.MainMenuStrip.Visible: vertical_shift += self.native.MainMenuStrip.Height return vertical_shift @@ -219,62 +219,42 @@ def hide(self): # Window state ###################################################################### - class _PresentationWindow: - def __init__(self, window_impl): - self.window_impl = window_impl - self.native = WinForms.Form() - self.original_window_size = self.window_impl.native.Size - window_screen = self.window_impl.interface.screen - self.native.Location = Point(*window_screen.origin) - self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") - self.native.WindowState = WinForms.FormWindowState.Maximized - self.native.Controls.Add(self.window_impl.interface.content._impl.native) - self.window_impl.native.Size = Size(*window_screen.size) - self.window_impl.interface.content.refresh() - - def show(self): - self.native.Show() - - def close(self): - self.native.Controls.Remove(self.window_impl.interface.content._impl.native) - self.window_impl.native.Size = self.original_window_size - self.window_impl.interface.content = self.window_impl.interface.content - self.window_impl.resize_content() - self.window_impl.interface.content.refresh() - self.native.Close() - def get_window_state(self): - if getattr(self, "_presentation_window", None) is not None: - return WindowState.PRESENTATION - else: - window_state = self.native.WindowState - if window_state == WinForms.FormWindowState.Maximized: - if self.native.FormBorderStyle == getattr( - WinForms.FormBorderStyle, "None" - ): - return WindowState.FULLSCREEN + window_state = self.native.WindowState + if window_state == WinForms.FormWindowState.Maximized: + if self.native.FormBorderStyle == getattr(WinForms.FormBorderStyle, "None"): + # Use a shadow variable since a window without any app menu and toolbar + # would be indistinguishable from full screen mode. + if getattr(self, "_is_in_presentation_mode", False) is True: + return WindowState.PRESENTATION else: - return WindowState.MAXIMIZED - elif window_state == WinForms.FormWindowState.Minimized: - return WindowState.MINIMIZED - elif window_state == WinForms.FormWindowState.Normal: - return WindowState.NORMAL + return WindowState.FULLSCREEN + else: + return WindowState.MAXIMIZED + elif window_state == WinForms.FormWindowState.Minimized: + return WindowState.MINIMIZED + elif window_state == WinForms.FormWindowState.Normal: + return WindowState.NORMAL + else: # pragma: no cover + return def set_window_state(self, state): if state == WindowState.NORMAL: current_state = self.get_window_state() - if current_state == WindowState.FULLSCREEN: - self.native.FormBorderStyle = getattr( - WinForms.FormBorderStyle, - "Sizable" if self.interface.resizable else "FixedSingle", - ) - elif current_state == WindowState.PRESENTATION: - self._presentation_window.close() - self._presentation_window = None - self.interface.screen = ( - self.interface._impl._before_presentation_mode_screen - ) - + if current_state == WindowState.PRESENTATION: + if self.native.MainMenuStrip: + self.native.MainMenuStrip.Visible = True + if self.toolbar_native: + self.toolbar_native.Visible = True + + self.interface.screen = self._before_presentation_mode_screen + self._before_presentation_mode_screen = None + self._is_in_presentation_mode = False + + self.native.FormBorderStyle = getattr( + WinForms.FormBorderStyle, + "Sizable" if self.interface.resizable else "FixedSingle", + ) self.native.WindowState = WinForms.FormWindowState.Normal # Set Window state to NORMAL before changing to other states as @@ -282,17 +262,29 @@ def set_window_state(self, state): elif state == WindowState.MAXIMIZED: self.set_window_state(WindowState.NORMAL) self.native.WindowState = WinForms.FormWindowState.Maximized + elif state == WindowState.MINIMIZED: self.set_window_state(WindowState.NORMAL) self.native.WindowState = WinForms.FormWindowState.Minimized + elif state == WindowState.FULLSCREEN: self.set_window_state(WindowState.NORMAL) self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") self.native.WindowState = WinForms.FormWindowState.Maximized + elif state == WindowState.PRESENTATION: self.set_window_state(WindowState.NORMAL) - self._presentation_window = self._PresentationWindow(self) - self._presentation_window.show() + if getattr(self, "_before_presentation_mode_screen", None) is None: + self._before_presentation_mode_screen = self.interface.screen + if self.native.MainMenuStrip: + self.native.MainMenuStrip.Visible = False + if self.toolbar_native: + self.toolbar_native.Visible = False + self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") + self.native.WindowState = WinForms.FormWindowState.Maximized + self._is_in_presentation_mode = True + else: # pragma: no cover + pass ###################################################################### # Window capabilities diff --git a/winforms/tests_backend/app.py b/winforms/tests_backend/app.py index 202c649aff..418cf8128b 100644 --- a/winforms/tests_backend/app.py +++ b/winforms/tests_backend/app.py @@ -7,6 +7,7 @@ from System.Drawing import Point from System.Windows.Forms import Application, Cursor, ToolStripSeparator +from toga.constants import WindowState from toga_winforms.keys import toga_to_winforms_key, winforms_to_toga_key from .probe import BaseProbe @@ -98,7 +99,7 @@ class CURSORINFO(ctypes.Structure): return info.hCursor is not None def is_full_screen(self, window): - return WindowProbe(self.app, window).is_full_screen + return WindowProbe(self.app, window).is_window_state(WindowState.PRESENTATION) def content_size(self, window): return WindowProbe(self.app, window).content_size diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index e717a642cf..9b264b1d06 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -51,27 +51,24 @@ def content_size(self): ) def is_window_state(self, state): - if getattr(self.impl, "_presentation_window", None) is not None: - current_state = WindowState.PRESENTATION - else: - window_state = self.native.WindowState - if window_state == FormWindowState.Maximized: - if self.native.FormBorderStyle == getattr(FormBorderStyle, "None"): - current_state = WindowState.FULLSCREEN + window_state = self.native.WindowState + if window_state == FormWindowState.Maximized: + if self.native.FormBorderStyle == getattr(FormBorderStyle, "None"): + if getattr(self.impl, "_is_in_presentation_mode", False) is True: + current_state = WindowState.PRESENTATION else: - current_state = WindowState.MAXIMIZED - elif window_state == FormWindowState.Minimized: - current_state = WindowState.MINIMIZED - elif window_state == FormWindowState.Normal: - current_state = WindowState.NORMAL + current_state = WindowState.FULLSCREEN + else: + current_state = WindowState.MAXIMIZED + elif window_state == FormWindowState.Minimized: + current_state = WindowState.MINIMIZED + elif window_state == FormWindowState.Normal: + current_state = WindowState.NORMAL return bool(current_state == state) @property def is_full_screen(self): - return ( - self.native.FormBorderStyle == getattr(FormBorderStyle, "None") - and self.native.WindowState == FormWindowState.Maximized - ) + return self.is_window_state(WindowState.FULLSCREEN) @property def is_resizable(self): From d82c615283d996d99b29b02139183d33282113c5 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 30 Mar 2024 10:15:23 -0400 Subject: [PATCH 033/248] Corrected gtk backend and core --- core/src/toga/app.py | 4 +- core/src/toga/window.py | 33 +++++---- gtk/src/toga_gtk/window.py | 105 +++++++++++---------------- gtk/tests_backend/window.py | 24 +++--- testbed/tests/test_window.py | 6 +- winforms/src/toga_winforms/window.py | 2 +- 6 files changed, 82 insertions(+), 92 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 082d4aac99..80820a88a1 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -793,7 +793,7 @@ def is_full_screen(self) -> bool: # " future. Consider using `App.is_in_presentation_mode` property instead.", # DeprecationWarning, # ) - return any(window.state == WindowState.PRESENTATION for window in self.windows) + return self.is_in_presentation_mode def set_full_screen(self, *windows: Window) -> None: """Make one or more windows full screen. @@ -819,6 +819,8 @@ def set_full_screen(self, *windows: Window) -> None: # ) if self.windows is not None: self.exit_full_screen() + if windows is None: + return screen_window_dict = dict() for window, screen in zip(windows, self.screens): screen_window_dict[screen] = window diff --git a/core/src/toga/window.py b/core/src/toga/window.py index adde00e0da..4c48f508ca 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -479,23 +479,24 @@ def state(self) -> WindowState: @state.setter def state(self, state: WindowState) -> None: - if state != self.state: - if state == WindowState.NORMAL: - self._impl.set_window_state(WindowState.NORMAL) - elif state == WindowState.MINIMIZED: - self._impl.set_window_state(WindowState.MINIMIZED) + if isinstance(state, WindowState): + current_state = self._impl.get_window_state() + if current_state == state: + return + elif not self.resizable and state in [ + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ]: + warnings.warn( + f"Cannot set window state to {state} of a non-resizable window. " + ) else: - if not self.resizable: - warnings.warn( - f"Cannot set window state to {state} of a non-resizable window. " - ) - else: - if state == WindowState.MAXIMIZED: - self._impl.set_window_state(WindowState.MAXIMIZED) - elif state == WindowState.FULLSCREEN: - self._impl.set_window_state(WindowState.FULLSCREEN) - elif state == WindowState.PRESENTATION: - self._impl.set_window_state(WindowState.PRESENTATION) + self._impl.set_window_state(state) + else: + raise ValueError( + "Invalid type for state parameter. Expected WindowState type." + ) ###################################################################### # Window capabilities diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 81f1acedc0..ae305cc7bf 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -203,87 +203,70 @@ def hide(self): # Window state ###################################################################### - class _PresentationWindow: - def __init__(self, window_impl): - self.window_impl = window_impl - self.native = Gtk.Window() - self.native.fullscreen() - - def update_content(self): - self.window_impl.container.remove( - self.window_impl.interface.content._impl.native - ) - self.native.add(self.window_impl.interface.content._impl.native) - - def show(self): - self.update_content() - self.native.show() - - def close(self): - self.native.unfullscreen() - self.native.remove(self.window_impl.interface.content._impl.native) - self.window_impl.container.add( - self.window_impl.interface.content._impl.native - ) - self.native.close() - def get_window_state(self): - if getattr(self, "_presentation_window", None) is not None: - return WindowState.PRESENTATION - else: - window_state_flags = self._window_state_flags - if window_state_flags & Gdk.WindowState.MAXIMIZED: - return WindowState.MAXIMIZED - elif window_state_flags & Gdk.WindowState.ICONIFIED: - return WindowState.MINIMIZED - elif window_state_flags & Gdk.WindowState.FULLSCREEN: - return WindowState.FULLSCREEN + window_state_flags = self._window_state_flags + if window_state_flags & Gdk.WindowState.MAXIMIZED: + return WindowState.MAXIMIZED + elif window_state_flags & Gdk.WindowState.ICONIFIED: + return WindowState.MINIMIZED + elif window_state_flags & Gdk.WindowState.FULLSCREEN: + # Use a shadow variable since a window without any app menu and toolbar + # in presentation mode would be indistinguishable from full screen mode. + if getattr(self, "_is_in_presentation_mode", False) is True: + return WindowState.PRESENTATION else: - return WindowState.NORMAL + return WindowState.FULLSCREEN + else: + return WindowState.NORMAL def set_window_state(self, state): + current_state = self.get_window_state() if state == WindowState.NORMAL: - current_state = self.get_window_state() # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: self.native.unmaximize() # Deminiaturize the window to restore it to its previous state elif current_state == WindowState.MINIMIZED: - self.native.deiconify() + # deconify() doesn't work + self.native.present() # If the window is in full-screen mode, exit full-screen mode elif current_state == WindowState.FULLSCREEN: self.native.unfullscreen() # If the window is in presentation mode, exit presentation mode elif current_state == WindowState.PRESENTATION: - self._presentation_window.close() - self._presentation_window = None + if isinstance(self.native, Gtk.ApplicationWindow): + self.native.set_show_menubar(True) + if self.native_toolbar: + self.native_toolbar.set_visible(True) + self.native.unfullscreen() self.interface.screen = self._before_presentation_mode_screen self._before_presentation_mode_screen = None self._is_in_presentation_mode = False - # Set Window state to NORMAL before changing to other states as - # some states block changing window state without first exiting them. - elif state == WindowState.MAXIMIZED: - self.set_window_state(WindowState.NORMAL) - self.native.maximize() - - elif state == WindowState.MINIMIZED: - self.set_window_state(WindowState.NORMAL) - self.native.iconify() - - elif state == WindowState.FULLSCREEN: - self.set_window_state(WindowState.NORMAL) - self.native.fullscreen() - - elif state == WindowState.PRESENTATION: + else: + # Set Window state to NORMAL before changing to other states as + # some states block changing window state without first exiting them. self.set_window_state(WindowState.NORMAL) - if getattr(self, "_before_presentation_mode_screen", None) is None: - self._before_presentation_mode_screen = self.interface.screen - self._presentation_window = self._PresentationWindow(self) - self._presentation_window.show() - self._is_in_presentation_mode = True - else: # pragma: no cover - pass + if state == WindowState.MAXIMIZED: + self.native.maximize() + + elif state == WindowState.MINIMIZED: + self.native.iconify() + + elif state == WindowState.FULLSCREEN: + self.native.fullscreen() + + elif state == WindowState.PRESENTATION: + if getattr(self, "_before_presentation_mode_screen", None) is None: + self._before_presentation_mode_screen = self.interface.screen + if isinstance(self.native, Gtk.ApplicationWindow): + self.native.set_show_menubar(False) + if self.native_toolbar: + self.native_toolbar.set_visible(False) + self.native.fullscreen() + self._is_in_presentation_mode = True + else: # pragma: no cover + pass ###################################################################### # Window capabilities diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index f79cf00978..4557a8e295 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -38,18 +38,20 @@ def content_size(self): return (content_allocation.width, content_allocation.height) def is_window_state(self, state): - if getattr(self.impl, "_presentation_window", None) is not None: - current_state = WindowState.PRESENTATION - else: - window_state_flags = self._window_state_flags - if window_state_flags & Gdk.WindowState.MAXIMIZED: - current_state = WindowState.MAXIMIZED - elif window_state_flags & Gdk.WindowState.ICONIFIED: - current_state = WindowState.MINIMIZED - elif window_state_flags & Gdk.WindowState.FULLSCREEN: - current_state = WindowState.FULLSCREEN + window_state_flags = self.impl._window_state_flags + if window_state_flags & Gdk.WindowState.MAXIMIZED: + current_state = WindowState.MAXIMIZED + elif window_state_flags & Gdk.WindowState.ICONIFIED: + current_state = WindowState.MINIMIZED + elif window_state_flags & Gdk.WindowState.FULLSCREEN: + # Use a shadow variable since a window without any app menu and toolbar + # in presentation mode would be indistinguishable from full screen mode. + if getattr(self.impl, "_is_in_presentation_mode", False) is True: + current_state = WindowState.PRESENTATION else: - current_state = WindowState.NORMAL + current_state = WindowState.FULLSCREEN + else: + current_state = WindowState.NORMAL return bool(current_state == state) @property diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 9b24f7f6f3..18c134928e 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -502,7 +502,8 @@ async def test_window_state_minimized(second_window, second_window_probe): assert second_window_probe.is_window_state(WindowState.NORMAL) assert not second_window_probe.is_window_state(WindowState.MINIMIZED) - assert second_window_probe.is_minimizable + if second_window_probe.supports_minimizable: + assert second_window_probe.is_minimizable second_window.state = WindowState.MINIMIZED # A longer delay to allow for genie animations @@ -521,7 +522,8 @@ async def test_window_state_minimized(second_window, second_window_probe): "Secondary window is not minimized", ) assert not second_window_probe.is_window_state(WindowState.MINIMIZED) - assert second_window_probe.is_minimizable + if second_window_probe.supports_minimizable: + assert second_window_probe.is_minimizable second_window.state = WindowState.NORMAL await second_window_probe.wait_for_window( diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index ad384424c8..1e2be17188 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -224,7 +224,7 @@ def get_window_state(self): if window_state == WinForms.FormWindowState.Maximized: if self.native.FormBorderStyle == getattr(WinForms.FormBorderStyle, "None"): # Use a shadow variable since a window without any app menu and toolbar - # would be indistinguishable from full screen mode. + # in presentation mode would be indistinguishable from full screen mode. if getattr(self, "_is_in_presentation_mode", False) is True: return WindowState.PRESENTATION else: From 96ef3ed85da8a69e6ac4da4bfeb67528de65e5f2 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 30 Mar 2024 17:23:06 -0700 Subject: [PATCH 034/248] Fixed winforms backend and cocoa --- cocoa/src/toga_cocoa/window.py | 29 ++++++++--------- winforms/src/toga_winforms/window.py | 47 +++++++++++++--------------- 2 files changed, 35 insertions(+), 41 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 3f7aa98fee..0bc6802ade 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -368,26 +368,23 @@ def set_window_state(self, state): # If the window is in presentation mode, exit presentation mode elif current_state == WindowState.PRESENTATION: self.interface.app.exit_presentation_mode() - - # Set Window state to NORMAL before changing to other states as - # some states block changing window state without first exiting them. - elif state == WindowState.MAXIMIZED: + else: + # Set Window state to NORMAL before changing to other states as + # some states block changing window state without first exiting them. self.set_window_state(WindowState.NORMAL) - self.native.setIsZoomed(True) + if state == WindowState.MAXIMIZED: + self.native.setIsZoomed(True) - elif state == WindowState.MINIMIZED: - self.set_window_state(WindowState.NORMAL) - self.native.setIsMiniaturized(True) + elif state == WindowState.MINIMIZED: + self.native.setIsMiniaturized(True) - elif state == WindowState.FULLSCREEN: - self.set_window_state(WindowState.NORMAL) - self.native.toggleFullScreen(self.native) + elif state == WindowState.FULLSCREEN: + self.native.toggleFullScreen(self.native) - elif state == WindowState.PRESENTATION: - self.set_window_state(WindowState.NORMAL) - self.interface.app.enter_presentation_mode( - {self.interface.screen: self.interface} - ) + elif state == WindowState.PRESENTATION: + self.interface.app.enter_presentation_mode( + {self.interface.screen: self.interface} + ) ###################################################################### # Window capabilities diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 1e2be17188..df79c4eeea 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -256,35 +256,32 @@ def set_window_state(self, state): "Sizable" if self.interface.resizable else "FixedSingle", ) self.native.WindowState = WinForms.FormWindowState.Normal - - # Set Window state to NORMAL before changing to other states as - # some states block changing window state without first exiting them. - elif state == WindowState.MAXIMIZED: + else: + # Set Window state to NORMAL before changing to other states as + # some states block changing window state without first exiting them. self.set_window_state(WindowState.NORMAL) - self.native.WindowState = WinForms.FormWindowState.Maximized + if state == WindowState.MAXIMIZED: + self.native.WindowState = WinForms.FormWindowState.Maximized - elif state == WindowState.MINIMIZED: - self.set_window_state(WindowState.NORMAL) - self.native.WindowState = WinForms.FormWindowState.Minimized + elif state == WindowState.MINIMIZED: + self.native.WindowState = WinForms.FormWindowState.Minimized - elif state == WindowState.FULLSCREEN: - self.set_window_state(WindowState.NORMAL) - self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") - self.native.WindowState = WinForms.FormWindowState.Maximized + elif state == WindowState.FULLSCREEN: + self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") + self.native.WindowState = WinForms.FormWindowState.Maximized - elif state == WindowState.PRESENTATION: - self.set_window_state(WindowState.NORMAL) - if getattr(self, "_before_presentation_mode_screen", None) is None: - self._before_presentation_mode_screen = self.interface.screen - if self.native.MainMenuStrip: - self.native.MainMenuStrip.Visible = False - if self.toolbar_native: - self.toolbar_native.Visible = False - self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") - self.native.WindowState = WinForms.FormWindowState.Maximized - self._is_in_presentation_mode = True - else: # pragma: no cover - pass + elif state == WindowState.PRESENTATION: + if getattr(self, "_before_presentation_mode_screen", None) is None: + self._before_presentation_mode_screen = self.interface.screen + if self.native.MainMenuStrip: + self.native.MainMenuStrip.Visible = False + if self.toolbar_native: + self.toolbar_native.Visible = False + self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") + self.native.WindowState = WinForms.FormWindowState.Maximized + self._is_in_presentation_mode = True + else: # pragma: no cover + pass ###################################################################### # Window capabilities From 4657567237e701972d7da86be0becb9daf06eb31 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 31 Mar 2024 02:37:10 -0700 Subject: [PATCH 035/248] Fixed core tests --- core/tests/test_window.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/core/tests/test_window.py b/core/tests/test_window.py index 76d6576a70..02d47bd040 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -283,27 +283,23 @@ def test_window_state(window): """A window can have different states.""" assert window.state == WindowState.NORMAL - window.state = WindowState.MAXIMIZED - assert window.state == WindowState.MAXIMIZED - assert_action_performed_with( - window, - "set window state to WindowState.MAXIMIZED", - state=WindowState.MAXIMIZED, - ) - - window.state = WindowState.FULLSCREEN - assert window.state == WindowState.FULLSCREEN - assert_action_performed_with( - window, - "set window state to WindowState.FULLSCREEN", - state=WindowState.FULLSCREEN, - ) - - window.state = WindowState.NORMAL - assert window.state == WindowState.NORMAL - assert_action_performed_with( - window, "set window state to WindowState.NORMAL", state=WindowState.NORMAL - ) + for state in WindowState: + if state != WindowState.NORMAL: + window.state = state + assert window.state == state + assert_action_performed_with( + window, + f"set window state to {state}", + state=state, + ) + + window.state = WindowState.NORMAL + assert window.state == WindowState.NORMAL + assert_action_performed_with( + window, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) def test_close_direct(window, app): From 00491c8865ecb5030024373c9c4203a4e5df998d Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 31 Mar 2024 06:32:16 -0700 Subject: [PATCH 036/248] Fixed cocoa backend and tests --- cocoa/src/toga_cocoa/window.py | 21 ++++---- cocoa/tests_backend/window.py | 20 +++++--- examples/window/window/app.py | 88 +++++++++++++++++++++++++--------- testbed/tests/app/test_app.py | 69 +++++++++++++++----------- testbed/tests/test_window.py | 43 ++++++++++++----- 5 files changed, 163 insertions(+), 78 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 0bc6802ade..f7b6c00b7e 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -42,6 +42,10 @@ def windowDidResize_(self, notification) -> None: # Set the window to the new size self.interface.content.refresh() + @objc_method + def windowDidExitFullScreen_(self, notification) -> None: + pass + ###################################################################### # Toolbar delegate methods ###################################################################### @@ -341,21 +345,20 @@ def get_visible(self): ###################################################################### def get_window_state(self): - if bool(self.native.isZoomed): - if bool(self.native.styleMask & NSWindowStyleMask.FullScreen): - return WindowState.FULLSCREEN - else: - return WindowState.MAXIMIZED + if bool(self.interface.content._impl.native.isInFullScreenMode()): + return WindowState.PRESENTATION + elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): + return WindowState.FULLSCREEN + elif bool(self.native.isZoomed): + return WindowState.MAXIMIZED elif bool(self.native.isMiniaturized): return WindowState.MINIMIZED - elif bool(self.interface.content._impl.native.isInFullScreenMode()): - return WindowState.PRESENTATION else: return WindowState.NORMAL def set_window_state(self, state): + current_state = self.get_window_state() if state == WindowState.NORMAL: - current_state = self.get_window_state() # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: self.native.setIsZoomed(False) @@ -385,6 +388,8 @@ def set_window_state(self, state): self.interface.app.enter_presentation_mode( {self.interface.screen: self.interface} ) + else: # pragma: no cover + return ###################################################################### # Window capabilities diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 52351f9819..a167f9acf2 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -50,16 +50,22 @@ def content_size(self): self.native.contentView.frame.size.height, ) + @property + def presentation_content_size(self): + return ( + self.window.content._impl.native.frame.size.width, + self.window.content._impl.native.frame.size.height, + ) + def is_window_state(self, state): - if bool(self.native.isZoomed): - if bool(self.native.styleMask & NSWindowStyleMask.FullScreen): - current_state = WindowState.FULLSCREEN - else: - current_state = WindowState.MAXIMIZED + if bool(self.window.content._impl.native.isInFullScreenMode()): + current_state = WindowState.PRESENTATION + elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): + current_state = WindowState.FULLSCREEN + elif bool(self.native.isZoomed): + current_state = WindowState.MAXIMIZED elif bool(self.native.isMiniaturized): current_state = WindowState.MINIMIZED - elif bool(self.interface.content._impl.native.isInFullScreenMode()): - current_state = WindowState.PRESENTATION else: current_state = WindowState.NORMAL return bool(current_state == state) diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 2979349863..4e7f4085cf 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -30,14 +30,33 @@ def do_small(self, widget, **kwargs): def do_large(self, widget, **kwargs): self.main_window.size = (1500, 1000) - def do_normal(self, widget, **kwargs): + def do_current_window_state(self, widget, **kwargs): + self.label.text = f"Current state: {self.main_window.state}" + + def do_window_state_normal(self, widget, **kwargs): self.main_window.state = WindowState.NORMAL - def do_maximize(self, widget, **kwargs): + def do_window_state_maximize(self, widget, **kwargs): self.main_window.state = WindowState.MAXIMIZED - def do_minimize(self, widget, **kwargs): + def do_window_state_minimize(self, widget, **kwargs): self.main_window.state = WindowState.MINIMIZED + for i in range(5, 0, -1): + print(f"Back in {i}...") + yield 1 + self.main_window.state = WindowState.NORMAL + + def do_window_state_full_screen(self, widget, **kwargs): + self.main_window.state = WindowState.FULLSCREEN + + def do_window_state_presentation(self, widget, **kwargs): + self.main_window.state = WindowState.PRESENTATION + + def do_app_presentation_mode(self, widget, **kwargs): + if self.is_in_presentation_mode: + self.exit_presentation_mode() + else: + self.enter_presentation_mode([self.main_window]) def do_app_full_screen(self, widget, **kwargs): if self.is_full_screen: @@ -48,9 +67,6 @@ def do_app_full_screen(self, widget, **kwargs): def do_window_full_screen(self, widget, **kwargs): self.main_window.full_screen = not self.main_window.full_screen - def do_enter_presentation_mode(self, widget, **kwargs): - self.enter_presentation_mode([self.main_window]) - def do_title(self, widget, **kwargs): self.main_window.title = f"Time is {datetime.now()}" @@ -189,28 +205,51 @@ def startup(self): btn_do_large = toga.Button( "Become large", on_press=self.do_large, style=btn_style ) - btn_do_normal = toga.Button( - "Become normal", on_press=self.do_normal, style=btn_style + btn_do_current_window_state = toga.Button( + "Get current window state", + on_press=self.do_current_window_state, + style=btn_style, + ) + btn_do_window_state_normal = toga.Button( + "Make window state normal", + on_press=self.do_window_state_normal, + style=btn_style, + ) + btn_do_window_state_maximize = toga.Button( + "Make window state maximized", + on_press=self.do_window_state_maximize, + style=btn_style, + ) + btn_do_window_state_minimize = toga.Button( + "Make window state minimized", + on_press=self.do_window_state_minimize, + style=btn_style, + ) + btn_do_window_state_full_screen = toga.Button( + "Make window state full screen", + on_press=self.do_window_state_full_screen, + style=btn_style, ) - btn_do_maximize = toga.Button( - "Become maximize", on_press=self.do_maximize, style=btn_style + btn_do_window_state_presentation = toga.Button( + "Make window state presentation", + on_press=self.do_window_state_presentation, + style=btn_style, ) - btn_do_minimize = toga.Button( - "Become minimize", on_press=self.do_minimize, style=btn_style + btn_do_app_presentation_mode = toga.Button( + "Toggle app presentation mode", + on_press=self.do_app_presentation_mode, + style=btn_style, ) btn_do_app_full_screen = toga.Button( - "Make app full screen", on_press=self.do_app_full_screen, style=btn_style + "Make app full screen(legacy)", + on_press=self.do_app_full_screen, + style=btn_style, ) btn_do_window_full_screen = toga.Button( - "Make window full screen", + "Make window full screen(legacy)", on_press=self.do_window_full_screen, style=btn_style, ) - btn_do_enter_presentation_mode = toga.Button( - "Enter into presentation mode", - on_press=self.do_enter_presentation_mode, - style=btn_style, - ) btn_do_title = toga.Button( "Change title", on_press=self.do_title, style=btn_style ) @@ -279,12 +318,15 @@ def startup(self): btn_do_right_current_screen, btn_do_small, btn_do_large, - btn_do_normal, - btn_do_maximize, - btn_do_minimize, + btn_do_current_window_state, + btn_do_window_state_normal, + btn_do_window_state_maximize, + btn_do_window_state_minimize, + btn_do_window_state_full_screen, + btn_do_window_state_presentation, + btn_do_app_presentation_mode, btn_do_app_full_screen, btn_do_window_full_screen, - btn_do_enter_presentation_mode, btn_do_title, btn_do_new_windows, btn_do_current_window_cycling, diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index 2a915341ab..fb243d6374 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -308,97 +308,110 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) assert not window1_probe.is_window_state(WindowState.PRESENTATION) assert window2_probe.has_toolbar() assert not window2_probe.is_window_state(WindowState.PRESENTATION) - initial_content_main_window_size = main_window_probe.content_size - initial_content1_size = window1_probe.content_size - initial_content2_size = window2_probe.content_size + initial_content_main_window_size = ( + main_window_probe.presentation_content_size + ) + initial_content1_size = window1_probe.presentation_content_size + initial_content2_size = window2_probe.presentation_content_size # Enter presentation mode with main window via the app app.enter_presentation_mode([main_window]) await main_window_probe.wait_for_window( - "Main window is in presentation mode" + "Main window is in presentation mode", + full_screen=True, ) assert app.is_in_presentation_mode assert not window1_probe.is_window_state(WindowState.PRESENTATION) - assert window1_probe.content_size == initial_content1_size + assert window1_probe.presentation_content_size == initial_content1_size assert not window2_probe.is_window_state(WindowState.PRESENTATION) - assert window2_probe.content_size == initial_content2_size + assert window2_probe.presentation_content_size == initial_content2_size assert main_window_probe.is_window_state(WindowState.PRESENTATION) - assert main_window_probe.content_size[0] > 1000 - assert main_window_probe.content_size[1] > 700 + assert main_window_probe.presentation_content_size[0] > 1000 + assert main_window_probe.presentation_content_size[1] > 700 # Exit presentation mode app.exit_presentation_mode() await main_window_probe.wait_for_window( - "Main window is no longer in presentation mode" + "Main window is no longer in presentation mode", + full_screen=True, ) assert not app.is_in_presentation_mode - assert main_window_probe.content_size == initial_content_main_window_size + assert ( + main_window_probe.presentation_content_size + == initial_content_main_window_size + ) # Enter presentation mode with window 2 via the app app.enter_presentation_mode([window2]) await window2_probe.wait_for_window( - "Second extra window is in presentation mode" + "Second extra window is in presentation mode", + full_screen=True, ) assert app.is_in_presentation_mode assert not window1_probe.is_window_state(WindowState.PRESENTATION) - assert window1_probe.content_size == initial_content1_size + assert window1_probe.presentation_content_size == initial_content1_size assert window2_probe.is_window_state(WindowState.PRESENTATION) - assert window2_probe.content_size[0] > 1000 - assert window2_probe.content_size[1] > 700 + assert window2_probe.presentation_content_size[0] > 1000 + assert window2_probe.presentation_content_size[1] > 700 # Exit presentation mode app.exit_presentation_mode() await window2_probe.wait_for_window( - "Second extra window is no longer in presentation mode" + "Second extra window is no longer in presentation mode", + full_screen=True, ) assert not app.is_in_presentation_mode - assert window2_probe.content_size == initial_content2_size + assert window2_probe.presentation_content_size == initial_content2_size # Enter presentation mode with window 1 via the app app.enter_presentation_mode([window1]) await window1_probe.wait_for_window( - "First extra window is in presentation mode" + "First extra window is in presentation mode", + full_screen=True, ) assert app.is_in_presentation_mode assert not window2_probe.is_window_state(WindowState.PRESENTATION) - assert window2_probe.content_size == initial_content2_size + assert window2_probe.presentation_content_size == initial_content2_size assert window1_probe.is_window_state(WindowState.PRESENTATION) - assert window1_probe.content_size[0] > 1000 - assert window1_probe.content_size[1] > 700 + assert window1_probe.presentation_content_size[0] > 1000 + assert window1_probe.presentation_content_size[1] > 700 # Exit presentation mode app.exit_presentation_mode() await window1_probe.wait_for_window( - "First extra window is no longer in presentation mode" + "First extra window is no longer in presentation mode", + full_screen=True, ) assert not app.is_in_presentation_mode - assert window1_probe.content_size == initial_content1_size + assert window1_probe.presentation_content_size == initial_content1_size if len(app.screens) < 2: # Enter presentation mode with 2 windows via the app, # but the second window should be dropped. app.enter_presentation_mode([window1, window2]) await window1_probe.wait_for_window( - "First extra window is in presentation mode" + "First extra window is in presentation mode", + full_screen=True, ) assert app.is_in_presentation_mode assert not window2_probe.is_window_state(WindowState.PRESENTATION) - assert window2_probe.content_size == initial_content2_size + assert window2_probe.presentation_content_size == initial_content2_size assert window1_probe.is_window_state(WindowState.PRESENTATION) - assert window1_probe.content_size[0] > 1000 - assert window1_probe.content_size[1] > 700 + assert window1_probe.presentation_content_size[0] > 1000 + assert window1_probe.presentation_content_size[1] > 700 # Exit presentation mode app.exit_presentation_mode() await window1_probe.wait_for_window( - "First extra window is no longer in presentation mode" + "First extra window is no longer in presentation mode", + full_screen=True, ) assert not app.is_in_presentation_mode - assert window1_probe.content_size == initial_content1_size + assert window1_probe.presentation_content_size == initial_content1_size finally: window1.close() window2.close() diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 18c134928e..6571158310 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -509,6 +509,7 @@ async def test_window_state_minimized(second_window, second_window_probe): # A longer delay to allow for genie animations await second_window_probe.wait_for_window( "Secondary window is minimized", + minimize=True, ) assert second_window_probe.is_window_state(WindowState.MINIMIZED) @@ -520,6 +521,7 @@ async def test_window_state_minimized(second_window, second_window_probe): # A longer delay to allow for genie animations await second_window_probe.wait_for_window( "Secondary window is not minimized", + minimize=True, ) assert not second_window_probe.is_window_state(WindowState.MINIMIZED) if second_window_probe.supports_minimizable: @@ -527,7 +529,8 @@ async def test_window_state_minimized(second_window, second_window_probe): second_window.state = WindowState.NORMAL await second_window_probe.wait_for_window( - "Secondary window is still not minimized" + "Secondary window is still not minimized", + minimize=True, ) assert not second_window_probe.is_window_state(WindowState.MINIMIZED) @@ -592,6 +595,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): # A longer delay to allow for genie animations await second_window_probe.wait_for_window( "Secondary window is full screen", + full_screen=True, ) assert second_window_probe.is_window_state(WindowState.FULLSCREEN) assert second_window_probe.content_size[0] > initial_content_size[0] @@ -599,7 +603,8 @@ async def test_window_state_full_screen(second_window, second_window_probe): second_window.state = WindowState.FULLSCREEN await second_window_probe.wait_for_window( - "Secondary window is still full screen" + "Secondary window is still full screen", + full_screen=True, ) assert second_window_probe.is_window_state(WindowState.FULLSCREEN) assert second_window_probe.content_size[0] > initial_content_size[0] @@ -609,6 +614,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): # A longer delay to allow for genie animations await second_window_probe.wait_for_window( "Secondary window is not full screen", + full_screen=True, ) assert not second_window_probe.is_window_state(WindowState.FULLSCREEN) assert second_window_probe.is_resizable @@ -616,7 +622,8 @@ async def test_window_state_full_screen(second_window, second_window_probe): second_window.state = WindowState.NORMAL await second_window_probe.wait_for_window( - "Secondary window is still not full screen" + "Secondary window is still not full screen", + full_screen=True, ) assert not second_window_probe.is_window_state(WindowState.FULLSCREEN) assert second_window_probe.content_size == initial_content_size @@ -632,40 +639,52 @@ async def test_window_state_presentation(second_window, second_window_probe): assert second_window_probe.is_window_state(WindowState.NORMAL) assert not second_window_probe.is_window_state(WindowState.PRESENTATION) assert second_window_probe.is_resizable - initial_content_size = second_window_probe.content_size + initial_content_size = second_window_probe.presentation_content_size second_window.state = WindowState.PRESENTATION # A longer delay to allow for genie animations await second_window_probe.wait_for_window( "Secondary window is in presentation mode", + full_screen=True, ) assert second_window_probe.is_window_state(WindowState.PRESENTATION) - assert second_window_probe.content_size[0] > initial_content_size[0] - assert second_window_probe.content_size[1] > initial_content_size[1] + assert ( + second_window_probe.presentation_content_size[0] > initial_content_size[0] + ) + assert ( + second_window_probe.presentation_content_size[1] > initial_content_size[1] + ) second_window.state = WindowState.PRESENTATION await second_window_probe.wait_for_window( - "Secondary window is still in presentation mode" + "Secondary window is still in presentation mode", + full_screen=True, ) assert second_window_probe.is_window_state(WindowState.PRESENTATION) - assert second_window_probe.content_size[0] > initial_content_size[0] - assert second_window_probe.content_size[1] > initial_content_size[1] + assert ( + second_window_probe.presentation_content_size[0] > initial_content_size[0] + ) + assert ( + second_window_probe.presentation_content_size[1] > initial_content_size[1] + ) second_window.state = WindowState.NORMAL # A longer delay to allow for genie animations await second_window_probe.wait_for_window( "Secondary window is not in presentation mode", + full_screen=True, ) assert not second_window_probe.is_window_state(WindowState.PRESENTATION) assert second_window_probe.is_resizable - assert second_window_probe.content_size == initial_content_size + assert second_window_probe.presentation_content_size == initial_content_size second_window.state = WindowState.NORMAL await second_window_probe.wait_for_window( - "Secondary window is still not in presentation mode" + "Secondary window is still not in presentation mode", + full_screen=True, ) assert not second_window_probe.is_window_state(WindowState.PRESENTATION) - assert second_window_probe.content_size == initial_content_size + assert second_window_probe.presentation_content_size == initial_content_size @pytest.mark.parametrize( "second_window_kwargs", From 41c1dd72f4e92ee446c5ab253e7bed47b67dd55e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 31 Mar 2024 09:21:49 -0700 Subject: [PATCH 037/248] Fixed testbed tests --- cocoa/src/toga_cocoa/window.py | 4 +++- cocoa/tests_backend/app.py | 5 +---- cocoa/tests_backend/window.py | 8 +++++--- gtk/tests_backend/app.py | 3 +-- gtk/tests_backend/window.py | 6 +++++- winforms/tests_backend/app.py | 2 +- winforms/tests_backend/window.py | 6 +++++- 7 files changed, 21 insertions(+), 13 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index f7b6c00b7e..8e46c3e814 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -345,7 +345,9 @@ def get_visible(self): ###################################################################### def get_window_state(self): - if bool(self.interface.content._impl.native.isInFullScreenMode()): + if self.interface.content and bool( + self.interface.content._impl.native.isInFullScreenMode() + ): return WindowState.PRESENTATION elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): return WindowState.FULLSCREEN diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index da4e007005..203dfca5bb 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -55,10 +55,7 @@ def is_full_screen(self, window): return WindowProbe(self.app, window).is_window_state(WindowState.PRESENTATION) def content_size(self, window): - return ( - window.content._impl.native.frame.size.width, - window.content._impl.native.frame.size.height, - ) + return WindowProbe(self.app, window).presentation_content_size def _menu_item(self, path): main_menu = self.app._impl.native.mainMenu diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index a167f9acf2..5bff1c3afc 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -58,7 +58,9 @@ def presentation_content_size(self): ) def is_window_state(self, state): - if bool(self.window.content._impl.native.isInFullScreenMode()): + if self.window.content and bool( + self.window.content._impl.native.isInFullScreenMode() + ): current_state = WindowState.PRESENTATION elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): current_state = WindowState.FULLSCREEN @@ -72,7 +74,7 @@ def is_window_state(self, state): @property def is_full_screen(self): - return bool(self.native.styleMask & NSWindowStyleMask.FullScreen) + return self.is_window_state(WindowState.FULLSCREEN) @property def is_resizable(self): @@ -88,7 +90,7 @@ def is_minimizable(self): @property def is_minimized(self): - return bool(self.native.isMiniaturized) + return self.is_window_state(WindowState.MINIMIZED) def minimize(self): self.native.performMiniaturize(None) diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index a8338581e7..f1cca42955 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -43,8 +43,7 @@ def is_full_screen(self, window): return WindowProbe(self.app, window).is_window_state(WindowState.PRESENTATION) def content_size(self, window): - content_allocation = window._impl.container.get_allocation() - return (content_allocation.width, content_allocation.height) + return WindowProbe(self.app, window).presentation_content_size def _menu_item(self, path): main_menu = self.app._impl.native.get_menubar() diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index 4557a8e295..bd9f1d064a 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -37,6 +37,10 @@ def content_size(self): content_allocation = self.impl.container.get_allocation() return (content_allocation.width, content_allocation.height) + @property + def presentation_content_size(self): + return self.content_size + def is_window_state(self, state): window_state_flags = self.impl._window_state_flags if window_state_flags & Gdk.WindowState.MAXIMIZED: @@ -68,7 +72,7 @@ def is_closable(self): @property def is_minimized(self): - return bool(self.native.get_window().get_state() & Gdk.WindowState.ICONIFIED) + return self.is_window_state(WindowState.MINIMIZED) def minimize(self): self.native.iconify() diff --git a/winforms/tests_backend/app.py b/winforms/tests_backend/app.py index 418cf8128b..1f16c5080d 100644 --- a/winforms/tests_backend/app.py +++ b/winforms/tests_backend/app.py @@ -102,7 +102,7 @@ def is_full_screen(self, window): return WindowProbe(self.app, window).is_window_state(WindowState.PRESENTATION) def content_size(self, window): - return WindowProbe(self.app, window).content_size + return WindowProbe(self.app, window).presentation_content_size def _menu_item(self, path): item = self.main_window._impl.native.MainMenuStrip diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 9b264b1d06..b809bafdd1 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -50,6 +50,10 @@ def content_size(self): ), ) + @property + def presentation_content_size(self): + return self.content_size + def is_window_state(self, state): window_state = self.native.WindowState if window_state == FormWindowState.Maximized: @@ -80,7 +84,7 @@ def is_minimizable(self): @property def is_minimized(self): - return self.native.WindowState == FormWindowState.Minimized + return self.is_window_state(WindowState.MINIMIZED) def minimize(self): if self.native.MinimizeBox: From 60d5e43cc2b30ff78354460024262fc8453d1a1c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 31 Mar 2024 13:59:03 -0400 Subject: [PATCH 038/248] corrected testbed --- testbed/tests/app/test_app.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index fb243d6374..f658d09bfe 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -200,7 +200,8 @@ async def test_full_screen(app, app_probe): window1.show() window2.show() - await app_probe.redraw("Extra windows are visible") + # Add delay for gtk to show the windows + await app_probe.redraw("Extra windows are visible", delay=0.1) assert not app.is_full_screen assert not app_probe.is_full_screen(window1) @@ -301,7 +302,9 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) window1.show() window2.show() - await app_probe.redraw("Extra windows are visible") + + # Add delay for gtk to show the windows + await app_probe.redraw("Extra windows are visible", delay=0.1) assert not app.is_in_presentation_mode assert not main_window_probe.is_window_state(WindowState.PRESENTATION) From f497a1d3cc919bba35591f1011ab060c8986ad24 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 1 Apr 2024 04:47:39 -0700 Subject: [PATCH 039/248] Added android implementation --- android/src/toga_android/app.py | 12 ++++-- android/src/toga_android/window.py | 69 ++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index a934450d4e..858f392ee5 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -10,6 +10,7 @@ from org.beeware.android import IPythonApp, MainActivity from toga.command import Command, Group, Separator +from toga.constants import WindowState from .libs import events from .screens import Screen as ScreenImpl @@ -296,12 +297,15 @@ def set_current_window(self, window): ###################################################################### def enter_presentation_mode(self, screen_window_dict): - # No-op; mobile doesn't support full screen - pass + for screen, window in screen_window_dict.items(): + window._impl._before_presentation_mode_screen = window.screen + window.screen = screen + window.state = WindowState.PRESENTATION def exit_presentation_mode(self): - # No-op; mobile doesn't support full screen - pass + for window in self.interface.windows: + if window.state == WindowState.PRESENTATION: + window.state = WindowState.NORMAL ###################################################################### # Platform-specific APIs diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index a73d090be8..1f49be71c3 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -10,6 +10,8 @@ from java import dynamic_proxy from java.io import ByteArrayOutputStream +from toga.constants import WindowState + from .container import Container from .screens import Screen as ScreenImpl @@ -137,11 +139,72 @@ def get_visible(self): def get_window_state(self): # Windows are always full screen - pass + decor_view = self.app.native.getWindow().getDecorView() + system_ui_flags = decor_view.getSystemUiVisibility() + if ( + system_ui_flags + & ( + decor_view.SYSTEM_UI_FLAG_FULLSCREEN + | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | decor_view.SYSTEM_UI_FLAG_IMMERSIVE + ) + ) != 0: + if not self.app.native.getSupportActionBar().isShowing(): + return WindowState.PRESENTATION + else: + return WindowState.FULLSCREEN + return WindowState.NORMAL def set_window_state(self, state): - # Windows are always full screen - pass + current_state = self.get_window_state() + decor_view = self.app.native.getWindow().getDecorView() + # On Android Maximized state is same as the Normal state + if state in [WindowState.NORMAL, WindowState.MAXIMIZED]: + if current_state in [ + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ]: + decor_view.setSystemUiVisibility(0) + if current_state == WindowState.PRESENTATION: + self.app.native.getSupportActionBar().show() + self.interface.screen = self._before_presentation_mode_screen + self._before_presentation_mode_screen = None + + # Doesn't work consistently + # elif current_state == WindowState.MINIMIZED: + # # Get the context + # context = self.app.native.getApplicationContext() + # # Create the intent to bring the activity to the foreground + # new_intent = Intent(context, self.app.native.getClass()) + # # These 2 options work properly by resuming existing MainActivity instead of creating + # # a new instance of MainActivity, but they work only once: + # new_intent.addFlags( + # Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP + # ) + # new_intent.setFlags( + # Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP + # ) + # # This works every time but starts new instance of MainActivity + # new_intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + # self.app.native.startActivity(new_intent) + else: + # # This works but the issue lies in restoring to normal state + # if state == WindowState.MINIMIZED: + # self.app.native.moveTaskToBack(True) + + # Restore to normal state before switching states to avoid mixing states + # and prevent glitches. + self.set_window_state(WindowState.NORMAL) + if state in [WindowState.FULLSCREEN, WindowState.PRESENTATION]: + decor_view.setSystemUiVisibility( + decor_view.SYSTEM_UI_FLAG_FULLSCREEN + | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | decor_view.SYSTEM_UI_FLAG_IMMERSIVE + ) + if state == WindowState.PRESENTATION: + self.app.native.getSupportActionBar().hide() + if getattr(self, "_before_presentation_mode_screen", None) is None: + self._before_presentation_mode_screen = self.interface.screen ###################################################################### # Window capabilities From d34cb2fc5f49c9c0d81b497191cd750f1ff35c21 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 1 Apr 2024 05:08:48 -0700 Subject: [PATCH 040/248] Fixed core --- android/src/toga_android/window.py | 8 ++++---- core/src/toga/window.py | 23 +++++++++++------------ 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 1f49be71c3..392e0cc58f 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -159,11 +159,11 @@ def set_window_state(self, state): current_state = self.get_window_state() decor_view = self.app.native.getWindow().getDecorView() # On Android Maximized state is same as the Normal state - if state in [WindowState.NORMAL, WindowState.MAXIMIZED]: - if current_state in [ + if state in {WindowState.NORMAL, WindowState.MAXIMIZED}: + if current_state in { WindowState.FULLSCREEN, WindowState.PRESENTATION, - ]: + }: decor_view.setSystemUiVisibility(0) if current_state == WindowState.PRESENTATION: self.app.native.getSupportActionBar().show() @@ -195,7 +195,7 @@ def set_window_state(self, state): # Restore to normal state before switching states to avoid mixing states # and prevent glitches. self.set_window_state(WindowState.NORMAL) - if state in [WindowState.FULLSCREEN, WindowState.PRESENTATION]: + if state in {WindowState.FULLSCREEN, WindowState.PRESENTATION}: decor_view.setSystemUiVisibility( decor_view.SYSTEM_UI_FLAG_FULLSCREEN | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 4c48f508ca..64deb2529f 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -481,18 +481,17 @@ def state(self) -> WindowState: def state(self, state: WindowState) -> None: if isinstance(state, WindowState): current_state = self._impl.get_window_state() - if current_state == state: - return - elif not self.resizable and state in [ - WindowState.MAXIMIZED, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - ]: - warnings.warn( - f"Cannot set window state to {state} of a non-resizable window. " - ) - else: - self._impl.set_window_state(state) + if current_state != state: + if not self.resizable and state in { + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + }: + warnings.warn( + f"Cannot set window state to {state} of a non-resizable window. " + ) + else: + self._impl.set_window_state(state) else: raise ValueError( "Invalid type for state parameter. Expected WindowState type." From 4d790b23f2f8c948c19de5ef12d807dabf8a6706 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 1 Apr 2024 05:59:29 -0700 Subject: [PATCH 041/248] Fixed core --- core/src/toga/app.py | 62 +++++++++++++++++++++-------------------- core/src/toga/window.py | 22 +++++++-------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 80820a88a1..370b97f646 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -764,39 +764,43 @@ def current_window(self, window: Window): # ----------------------Future Deprecated methods---------------------- def exit_full_screen(self) -> None: - """Exit full screen mode. + """**DEPRECATED** – Use :any:`App.exit_presentation_mode()`. + + Exit full screen mode. - .. warning:: - `App.exit_full_screen()` method is deprecated and will be - removed in the future. Consider using `App.enter_presentation_mode()` - and `App.exit_presentation_mode()` methods instead. """ - # warnings.warn( - # "`App.exit_full_screen()` method is deprecated and will be" - # " removed in the future. Consider using `App.enter_presentation_mode()`" - # " and `App.exit_presentation_mode()` methods instead.", + # warn( + # ( + # "`App.exit_full_screen()` is deprecated. Use " + # "`App.exit_presentation_mode()` instead." + # ), # DeprecationWarning, + # stacklevel=2, # ) if self.is_full_screen: self._impl.exit_presentation_mode() @property def is_full_screen(self) -> bool: - """Is the app currently in full screen mode? - - .. warning:: - `App.is_full_screen` property is deprecated and will be removed in the - future. Consider using `App.is_in_presentation_mode` property instead. - #""" - # warnings.warn( - # "`App.is_full_screen` property is deprecated and will be removed in the" - # " future. Consider using `App.is_in_presentation_mode` property instead.", + """**DEPRECATED** – Use :any:`App.is_in_presentation_mode`. + + Is the app currently in full screen mode? + + """ + # warn( + # ( + # "`App.is_full_screen` is deprecated. Use " + # "`App.is_in_presentation_mode` instead." + # ), # DeprecationWarning, + # stacklevel=2, # ) return self.is_in_presentation_mode def set_full_screen(self, *windows: Window) -> None: - """Make one or more windows full screen. + """**DEPRECATED** – Use :any:`App.enter_presentation_mode()` and :any:`App.exit_presentation_mode()`. + + Make one or more windows full screen. Full screen is not the same as "maximized"; full screen mode is when all window borders and other window decorations are no longer visible. @@ -805,17 +809,15 @@ def set_full_screen(self, *windows: Window) -> None: screens. If the number of windows exceeds the number of available displays, those windows will not be visible. If no windows are specified, the app will exit full screen mode. - - .. warning:: - `App.set_full_screen()` method is deprecated and will be - removed in the future. Consider using `App.enter_presentation_mode()` - and `App.exit_presentation_mode()` methods instead. """ - # warnings.warn( - # "`App.set_full_screen()` method is deprecated and will be" - # " removed in the future. Consider using `App.enter_presentation_mode()`" - # " and `App.exit_presentation_mode()` methods instead.", + # warn( + # ( + # "`App.set_full_screen()` is deprecated. Use " + # "`App.enter_presentation_mode()` and " + # "`App.exit_presentation_mode()` instead." + # ), # DeprecationWarning, + # stacklevel=2, # ) if self.windows is not None: self.exit_full_screen() @@ -826,7 +828,7 @@ def set_full_screen(self, *windows: Window) -> None: screen_window_dict[screen] = window self.enter_presentation_mode(screen_window_dict) else: - warnings.warn("App doesn't have any windows") + warn("App doesn't have any windows") # --------------------------------------------------------------------- @@ -858,7 +860,7 @@ def enter_presentation_mode( self._impl.enter_presentation_mode(screen_window_dict) def exit_presentation_mode(self) -> None: - """Exit full screen mode.""" + """Exit presentation mode.""" if self.is_in_presentation_mode: self._impl.exit_presentation_mode() diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 64deb2529f..5bc0f79bec 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -436,31 +436,29 @@ def visible(self, visible: bool) -> None: # ----------------------Future Deprecated methods---------------------- @property def full_screen(self) -> bool: - """Is the window in full screen mode? + """**DEPRECATED** – Use :any:`Window.state`. + + Is the window in full screen mode? Full screen mode is *not* the same as "maximized". A full screen window has no title bar, toolbar or window controls; some or all of these items may be visible on a maximized window. A good example of "full screen" mode is a slideshow app in presentation mode - the only visible content is the slide. - - .. warning:: - `Window.full_screen` property is deprecated and will be removed in the - future. Consider using `Window.state` property instead. - #""" - # warnings.warn( - # "`Window.full_screen` property is deprecated and will be removed in" - # " the future. Consider using `Window.state` property instead.", + """ + # warn( + # ("`Window.full_screen` is deprecated. Use `Window.state` instead."), # DeprecationWarning, + # stacklevel=2, # ) return bool(self.state == WindowState.FULLSCREEN) @full_screen.setter def full_screen(self, is_full_screen: bool) -> None: - # warnings.warn( - # "`Window.full_screen` property is deprecated and will be removed in" - # " the future. Consider using `Window.state` property instead.", + # warn( + # ("`Window.full_screen` is deprecated. Use `Window.state` instead."), # DeprecationWarning, + # stacklevel=2, # ) if is_full_screen and (self.state != WindowState.FULLSCREEN): self._impl.set_window_state(WindowState.FULLSCREEN) From 18b0cbce5b529d37d5eed14bcc1ee46c30e73746 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 1 Apr 2024 19:11:08 -0700 Subject: [PATCH 042/248] Added stub implementations for iOS, textual, web --- iOS/src/toga_iOS/app.py | 6 ++---- iOS/src/toga_iOS/window.py | 8 ++++---- textual/src/toga_textual/app.py | 6 ++---- textual/src/toga_textual/window.py | 8 ++++---- web/src/toga_web/app.py | 6 ++---- web/src/toga_web/window.py | 8 ++++---- 6 files changed, 18 insertions(+), 24 deletions(-) diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index fcd3526043..d093292040 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -147,9 +147,7 @@ def set_current_window(self, window): ###################################################################### def enter_presentation_mode(self, screen_window_dict): - # No-op; mobile doesn't support full screen - pass + self.interface.factory.not_implemented("App.enter_presentation_mode()") def exit_presentation_mode(self): - # No-op; mobile doesn't support full screen - pass + self.interface.factory.not_implemented("App.exit_presentation_mode()") diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index a4cfcb4c64..27a023a880 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -6,6 +6,7 @@ objc_id, ) +from toga.constants import WindowState from toga_iOS.container import RootContainer from toga_iOS.images import nsdata_to_bytes from toga_iOS.libs import ( @@ -145,12 +146,11 @@ def hide(self): ###################################################################### def get_window_state(self): - # Windows are always full screen - pass + # Windows are always normal + return WindowState.NORMAL def set_window_state(self, state): - # Windows are always full screen - pass + self.interface.factory.not_implemented("Window.set_window_state()") ###################################################################### # Window capabilities diff --git a/textual/src/toga_textual/app.py b/textual/src/toga_textual/app.py index d01add9d25..07db808823 100644 --- a/textual/src/toga_textual/app.py +++ b/textual/src/toga_textual/app.py @@ -96,12 +96,10 @@ def set_current_window(self, window): ###################################################################### def enter_presentation_mode(self, screen_window_dict): - # Not implemented - pass + self.interface.factory.not_implemented("App.enter_presentation_mode()") def exit_presentation_mode(self): - # Not implemented - pass + self.interface.factory.not_implemented("App.exit_presentation_mode()") class DocumentApp(App): diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index 2128e2bf2c..3cb62fdb7b 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -5,6 +5,7 @@ from textual.screen import Screen as TextualScreen from textual.widget import Widget as TextualWidget from textual.widgets import Button as TextualButton +from toga.constants import WindowState from .container import Container from .screens import Screen as ScreenImpl @@ -203,12 +204,11 @@ def hide(self): ###################################################################### def get_window_state(self): - # Not implemented - pass + # Windows are always normal + return WindowState.NORMAL def set_window_state(self, state): - # Not implemented - pass + self.interface.factory.not_implemented("Window.set_window_state()") ###################################################################### # Window capabilities diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index c15bc55027..c33e0aedfb 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -238,9 +238,7 @@ def set_current_window(self): ###################################################################### def enter_presentation_mode(self, screen_window_dict): - # Not implemented - pass + self.interface.factory.not_implemented("App.enter_presentation_mode()") def exit_presentation_mode(self): - # Not implemented - pass + self.interface.factory.not_implemented("App.exit_presentation_mode()") diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index e9d2c7ffb3..0c8e0f7e91 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -1,3 +1,4 @@ +from toga.constants import WindowState from toga_web.libs import create_element, js from .screens import Screen as ScreenImpl @@ -113,12 +114,11 @@ def hide(self): ###################################################################### def get_window_state(self): - # Not implemented - pass + # Windows are always normal + return WindowState.NORMAL def set_window_state(self, state): - # Not implemented - pass + self.interface.factory.not_implemented("Window.set_window_state()") ###################################################################### # Window capabilities From b7eb1174d0d870f627103f9cbaa598d22b1d6ada Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 2 Apr 2024 06:43:18 -0700 Subject: [PATCH 043/248] Added mobile platform tests --- android/src/toga_android/app.py | 9 +--- android/src/toga_android/window.py | 4 -- android/tests_backend/window.py | 22 ++++++++ iOS/tests_backend/window.py | 3 ++ testbed/tests/app/test_app.py | 74 +++++++++++++++++++++++--- testbed/tests/test_window.py | 83 +++++++++++++++++++++++++++++- 6 files changed, 174 insertions(+), 21 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 858f392ee5..dbdf9611a7 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -297,15 +297,10 @@ def set_current_window(self, window): ###################################################################### def enter_presentation_mode(self, screen_window_dict): - for screen, window in screen_window_dict.items(): - window._impl._before_presentation_mode_screen = window.screen - window.screen = screen - window.state = WindowState.PRESENTATION + self.interface.main_window.state = WindowState.PRESENTATION def exit_presentation_mode(self): - for window in self.interface.windows: - if window.state == WindowState.PRESENTATION: - window.state = WindowState.NORMAL + self.interface.main_window.state = WindowState.NORMAL ###################################################################### # Platform-specific APIs diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 392e0cc58f..ec379891d9 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -167,8 +167,6 @@ def set_window_state(self, state): decor_view.setSystemUiVisibility(0) if current_state == WindowState.PRESENTATION: self.app.native.getSupportActionBar().show() - self.interface.screen = self._before_presentation_mode_screen - self._before_presentation_mode_screen = None # Doesn't work consistently # elif current_state == WindowState.MINIMIZED: @@ -203,8 +201,6 @@ def set_window_state(self, state): ) if state == WindowState.PRESENTATION: self.app.native.getSupportActionBar().hide() - if getattr(self, "_before_presentation_mode_screen", None) is None: - self._before_presentation_mode_screen = self.interface.screen ###################################################################### # Window capabilities diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index bc283f43a3..5630c0d45a 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.constants import WindowState + from .probe import BaseProbe @@ -19,6 +21,26 @@ def content_size(self): self.root_view.getHeight() / self.scale_factor, ) + def is_window_state(self, state): + # Windows are always full screen + decor_view = self.native.getWindow().getDecorView() + system_ui_flags = decor_view.getSystemUiVisibility() + if ( + system_ui_flags + & ( + decor_view.SYSTEM_UI_FLAG_FULLSCREEN + | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | decor_view.SYSTEM_UI_FLAG_IMMERSIVE + ) + ) != 0: + if not self.native.getSupportActionBar().isShowing(): + current_state = WindowState.PRESENTATION + else: + current_state = WindowState.FULLSCREEN + else: + current_state = WindowState.NORMAL + return bool(current_state == state) + async def close_info_dialog(self, dialog): dialog_view = self.get_dialog_view() self.assert_dialog_buttons(dialog_view, ["OK"]) diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py index 08f9a34295..699402175d 100644 --- a/iOS/tests_backend/window.py +++ b/iOS/tests_backend/window.py @@ -29,6 +29,9 @@ def content_size(self): ), ) + def is_window_state(self, state): + pytest.skip("Window states are not implemented on iOS") + async def close_info_dialog(self, dialog): self.native.rootViewController.dismissViewControllerAnimated( False, completion=None diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index f658d09bfe..768ea02c06 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -31,12 +31,70 @@ async def test_show_hide_cursor(app): app.show_cursor() app.hide_cursor() - async def test_full_screen(app): + async def test_full_screen(app, app_probe, main_window): """Window can be made full screen""" - # Invoke the methods to verify the endpoints exist. However, they're no-ops, - # so there's nothing to test. - app.set_full_screen(app.current_window) + # This test fails when run normally with: + # `briefcase run android -ur --test` + # But passes when run singly with: + # `briefcase run android -ur --test -- tests/app/test_app.py::test_full_screen` + app.set_full_screen(app.main_window) + await app_probe.redraw("App is in presentation mode") + assert app.is_full_screen + app.exit_full_screen() + await app_probe.redraw("App is not in presentation mode") + assert not app.is_full_screen + + async def test_presentation_mode(app, app_probe, main_window, main_window_probe): + """The app can enter into presentation mode""" + # This test fails when run normally with: + # `briefcase run android -ur --test` + # But passes when run singly with: + # `briefcase run android -ur --test -- tests/app/test_app.py::test_full_screen` + assert not app.is_in_presentation_mode + assert not main_window_probe.is_window_state(WindowState.PRESENTATION) + + # Enter presentation mode with main window via the app + app.enter_presentation_mode([main_window]) + await main_window_probe.wait_for_window( + "Main window is in presentation mode", full_screen=True + ) + + assert app.is_in_presentation_mode + assert not main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.is_window_state(WindowState.PRESENTATION) + + # Exit presentation mode + app.exit_presentation_mode() + await main_window_probe.wait_for_window( + "Main window is no longer in presentation mode", + full_screen=True, + ) + + assert not app.is_in_presentation_mode + assert not main_window_probe.is_window_state(WindowState.PRESENTATION) + assert main_window_probe.is_window_state(WindowState.NORMAL) + + # Enter presentation mode with a screen-window dict via the app + app.enter_presentation_mode({app.screens[0]: main_window}) + await main_window_probe.wait_for_window( + "Main window is in presentation mode", full_screen=True + ) + + assert app.is_in_presentation_mode + assert not main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.is_window_state(WindowState.PRESENTATION) + + # Exit presentation mode + app.exit_presentation_mode() + await main_window_probe.wait_for_window( + "Main window is no longer in presentation mode", + full_screen=True, + ) + + assert not app.is_in_presentation_mode + assert not main_window_probe.is_window_state(WindowState.PRESENTATION) + assert main_window_probe.is_window_state(WindowState.NORMAL) async def test_current_window(app, main_window, main_window_probe): """The current window can be retrieved""" @@ -368,8 +426,8 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) assert not app.is_in_presentation_mode assert window2_probe.presentation_content_size == initial_content2_size - # Enter presentation mode with window 1 via the app - app.enter_presentation_mode([window1]) + # Enter presentation mode with a screen-window1 dict via the app + app.enter_presentation_mode({app.screens[0]: window1}) await window1_probe.wait_for_window( "First extra window is in presentation mode", full_screen=True, @@ -392,8 +450,8 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) assert window1_probe.presentation_content_size == initial_content1_size if len(app.screens) < 2: - # Enter presentation mode with 2 windows via the app, - # but the second window should be dropped. + # Enter presentation mode with 2 windows via the app, but the + # second window should be dropped as there is only 1 screen. app.enter_presentation_mode([window1, window2]) await window1_probe.wait_for_window( "First extra window is in presentation mode", diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 6571158310..944e1d7af1 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -148,10 +148,89 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): async def test_full_screen(main_window, main_window_probe): """Window can be made full screen""" main_window.full_screen = True - await main_window_probe.wait_for_window("Full screen is a no-op") + await main_window_probe.wait_for_window("Main window is in full screen") main_window.full_screen = False - await main_window_probe.wait_for_window("Full screen is a no-op") + await main_window_probe.wait_for_window( + "Main window is no longer in full screen" + ) + + async def test_window_state_minimized(main_window, main_window_probe): + """Window can have minimized window state""" + # WindowState.MINIMIZED is unimplemented on both Android and iOS. + assert main_window_probe.is_window_state(WindowState.NORMAL) + assert not main_window_probe.is_window_state(WindowState.MINIMIZED) + main_window.state = WindowState.MINIMIZED + await main_window_probe.wait_for_window("WindowState.MINIMIZED is a no-op") + assert not main_window_probe.is_window_state(WindowState.MINIMIZED) + assert main_window_probe.is_window_state(WindowState.NORMAL) + + async def test_window_state_maximized(main_window, main_window_probe): + """Window can have maximized window state""" + # WindowState.MAXIMIZED is unimplemented on both Android and iOS. + assert main_window_probe.is_window_state(WindowState.NORMAL) + assert not main_window_probe.is_window_state(WindowState.MAXIMIZED) + main_window.state = WindowState.MAXIMIZED + await main_window_probe.wait_for_window("WindowState.MAXIMIZED is a no-op") + assert not main_window_probe.is_window_state(WindowState.MAXIMIZED) + assert main_window_probe.is_window_state(WindowState.NORMAL) + + async def test_window_state_full_screen(main_window, main_window_probe): + """Window can have full screen window state""" + # WindowState.FULLSCREEN is implemented on Android but not on iOS. + assert main_window_probe.is_window_state(WindowState.NORMAL) + assert not main_window_probe.is_window_state(WindowState.FULLSCREEN) + # Make main window full screen + main_window.state = WindowState.FULLSCREEN + await main_window_probe.wait_for_window("Main window is full screen") + assert main_window_probe.is_window_state(WindowState.FULLSCREEN) + assert not main_window_probe.is_window_state(WindowState.NORMAL) + + main_window.state = WindowState.FULLSCREEN + await main_window_probe.wait_for_window("Main window is still full screen") + assert main_window_probe.is_window_state(WindowState.FULLSCREEN) + assert not main_window_probe.is_window_state(WindowState.NORMAL) + # Exit full screen + main_window.state = WindowState.NORMAL + await main_window_probe.wait_for_window("Main window is not full screen") + assert not main_window_probe.is_window_state(WindowState.FULLSCREEN) + assert main_window_probe.is_window_state(WindowState.NORMAL) + + main_window.state = WindowState.NORMAL + await main_window_probe.wait_for_window("Main window is still not full screen") + assert not main_window_probe.is_window_state(WindowState.FULLSCREEN) + assert main_window_probe.is_window_state(WindowState.NORMAL) + + async def test_window_state_presentation(main_window, main_window_probe): + # WindowState.PRESENTATION is implemented on Android but not on iOS. + assert main_window_probe.is_window_state(WindowState.NORMAL) + assert not main_window_probe.is_window_state(WindowState.PRESENTATION) + # Enter presentation mode with main window + main_window.state = WindowState.PRESENTATION + await main_window_probe.wait_for_window("Main window is in presentation mode") + assert main_window_probe.is_window_state(WindowState.PRESENTATION) + assert not main_window_probe.is_window_state(WindowState.NORMAL) + + main_window.state = WindowState.PRESENTATION + await main_window_probe.wait_for_window( + "Main window is still in presentation mode" + ) + assert main_window_probe.is_window_state(WindowState.PRESENTATION) + assert not main_window_probe.is_window_state(WindowState.NORMAL) + # Exit presentation mode + main_window.state = WindowState.NORMAL + await main_window_probe.wait_for_window( + "Main window is not in presentation mode" + ) + assert not main_window_probe.is_window_state(WindowState.PRESENTATION) + assert main_window_probe.is_window_state(WindowState.NORMAL) + + main_window.state = WindowState.NORMAL + await main_window_probe.wait_for_window( + "Main window is still not in presentation mode" + ) + assert not main_window_probe.is_window_state(WindowState.PRESENTATION) + assert main_window_probe.is_window_state(WindowState.NORMAL) async def test_screen(main_window, main_window_probe): """The window can be relocated to another screen, using both absolute and relative screen positions.""" From cadc4cc1f22ae0f08938b1a3ef7210bddac3614d Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 2 Apr 2024 09:12:41 -0700 Subject: [PATCH 044/248] Added changelog --- changes/1857.feature.rsst | 1 + changes/1857.removal.rsst | 1 + cocoa/src/toga_cocoa/window.py | 5 +++++ core/src/toga/app.py | 14 ++++++-------- core/src/toga/window.py | 1 + testbed/tests/app/test_app.py | 12 ++++++++++++ 6 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 changes/1857.feature.rsst create mode 100644 changes/1857.removal.rsst diff --git a/changes/1857.feature.rsst b/changes/1857.feature.rsst new file mode 100644 index 0000000000..9b5bdb6a37 --- /dev/null +++ b/changes/1857.feature.rsst @@ -0,0 +1 @@ +Toga apps can now detect and set their window states including maximized, minimized, normal, full screen and presentation states. diff --git a/changes/1857.removal.rsst b/changes/1857.removal.rsst new file mode 100644 index 0000000000..932d6573de --- /dev/null +++ b/changes/1857.removal.rsst @@ -0,0 +1 @@ +The older full screen APIs on `toga.App` and `toga.Window` are now deprecated and are instead replaced by their suitable newer API counterparts. diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 8e46c3e814..e4708f8e8e 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -44,6 +44,7 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidExitFullScreen_(self, notification) -> None: + # This can be used to ensure that the window has completed exiting full screen. pass ###################################################################### @@ -369,6 +370,10 @@ def set_window_state(self, state): self.native.setIsMiniaturized(False) # If the window is in full-screen mode, exit full-screen mode elif current_state == WindowState.FULLSCREEN: + # This doesn't wait for completely exiting full screen mode. Hence, + # this will cause problems when rapid window state switching is done. + # We should wait until `windowDidExitFullScreen_` is notified and then + # return to user. self.native.toggleFullScreen(self.native) # If the window is in presentation mode, exit presentation mode elif current_state == WindowState.PRESENTATION: diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 370b97f646..beaabad6af 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -759,10 +759,11 @@ def current_window(self, window: Window): self._impl.set_current_window(window) ###################################################################### - # Full screen control + # Presentation mode controls ###################################################################### # ----------------------Future Deprecated methods---------------------- + # Warnings are disabled as old API tests are still in testbed and warnings will cause error. def exit_full_screen(self) -> None: """**DEPRECATED** – Use :any:`App.exit_presentation_mode()`. @@ -771,8 +772,7 @@ def exit_full_screen(self) -> None: """ # warn( # ( - # "`App.exit_full_screen()` is deprecated. Use " - # "`App.exit_presentation_mode()` instead." + # "`App.exit_full_screen()` is deprecated. Use `App.exit_presentation_mode()` instead." # ), # DeprecationWarning, # stacklevel=2, @@ -789,8 +789,7 @@ def is_full_screen(self) -> bool: """ # warn( # ( - # "`App.is_full_screen` is deprecated. Use " - # "`App.is_in_presentation_mode` instead." + # "`App.is_full_screen` is deprecated. Use `App.is_in_presentation_mode` instead." # ), # DeprecationWarning, # stacklevel=2, @@ -812,8 +811,7 @@ def set_full_screen(self, *windows: Window) -> None: """ # warn( # ( - # "`App.set_full_screen()` is deprecated. Use " - # "`App.enter_presentation_mode()` and " + # "`App.set_full_screen()` is deprecated. Use `App.enter_presentation_mode()` and " # "`App.exit_presentation_mode()` instead." # ), # DeprecationWarning, @@ -840,7 +838,7 @@ def is_in_presentation_mode(self) -> bool: def enter_presentation_mode( self, window_list_or_screen_window_dict: list[Window] | dict[Screen, Window], - ): + ) -> None: """Enter into presentation mode with one or more windows on different screens. Presentation mode is not the same as Full Screen mode; full screen mode is when all window diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 5bc0f79bec..535a0003b1 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -434,6 +434,7 @@ def visible(self, visible: bool) -> None: ###################################################################### # ----------------------Future Deprecated methods---------------------- + # Warnings are disabled as old API tests are still in testbed and warnings will cause error. @property def full_screen(self) -> bool: """**DEPRECATED** – Use :any:`Window.state`. diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index 768ea02c06..2fed20adde 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -37,6 +37,12 @@ async def test_full_screen(app, app_probe, main_window): # `briefcase run android -ur --test` # But passes when run singly with: # `briefcase run android -ur --test -- tests/app/test_app.py::test_full_screen` + + # The test seems to fail when any method or property of app is invoked but doesn't fail + # when window methods are invoked. For example, `app.set_full_screen(app.main_window)` + # will make the test to fail, but `main_window.state = WindowState.PRESENTATION` will + # not cause the test to fail. Even though both use the same API endpoint. + app.set_full_screen(app.main_window) await app_probe.redraw("App is in presentation mode") assert app.is_full_screen @@ -51,6 +57,12 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) # `briefcase run android -ur --test` # But passes when run singly with: # `briefcase run android -ur --test -- tests/app/test_app.py::test_full_screen` + + # The test seems to fail when any method or property of app is invoked but doesn't fail + # when window methods are invoked. For example, `app.set_full_screen(app.main_window)` + # will make the test to fail but `main_window.state = WindowState.PRESENTATION` will + # not cause the test to fail. Even though both use the same API endpoint. + assert not app.is_in_presentation_mode assert not main_window_probe.is_window_state(WindowState.PRESENTATION) From bf3511a5186cb1edc4b7c6bc80dcac74af1da6db Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 2 Apr 2024 18:17:32 -0700 Subject: [PATCH 045/248] Restart CI --- gtk/src/toga_gtk/window.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index ae305cc7bf..be616e8584 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -238,6 +238,8 @@ def set_window_state(self, state): self.native.set_show_menubar(True) if self.native_toolbar: self.native_toolbar.set_visible(True) + else: # pragma: no cover + pass self.native.unfullscreen() self.interface.screen = self._before_presentation_mode_screen @@ -263,6 +265,8 @@ def set_window_state(self, state): self.native.set_show_menubar(False) if self.native_toolbar: self.native_toolbar.set_visible(False) + else: # pragma: no cover + pass self.native.fullscreen() self._is_in_presentation_mode = True else: # pragma: no cover From c9a130781e39e51090460b368016870996f99cca Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 22 May 2024 05:12:43 -0700 Subject: [PATCH 046/248] Minor modifications --- testbed/tests/app/test_app.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index db0b61cbb4..71de230fb7 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -31,25 +31,12 @@ async def test_show_hide_cursor(app): app.show_cursor() app.hide_cursor() - async def test_full_screen(app, app_probe, main_window): + async def test_full_screen(app): """Window can be made full screen""" - # This test fails when run normally with: - # `briefcase run android -ur --test` - # But passes when run singly with: - # `briefcase run android -ur --test -- tests/app/test_app.py::test_full_screen` - - # The test seems to fail when any method or property of app is invoked but doesn't fail - # when window methods are invoked. For example, `app.set_full_screen(app.main_window)` - # will make the test to fail, but `main_window.state = WindowState.PRESENTATION` will - # not cause the test to fail. Even though both use the same API endpoint. - - app.set_full_screen(app.main_window) - await app_probe.redraw("App is in presentation mode") - assert app.is_full_screen - + # Invoke the methods to verify the endpoints exist. However, they're no-ops, + # so there's nothing to test. + app.set_full_screen(app.current_window) app.exit_full_screen() - await app_probe.redraw("App is not in presentation mode") - assert not app.is_full_screen async def test_presentation_mode(app, app_probe, main_window, main_window_probe): """The app can enter into presentation mode""" From ab9b7789aa73dbc59ea3098d1c3a1290cc8a47f7 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 22 May 2024 13:38:48 -0700 Subject: [PATCH 047/248] Minor modifications --- core/src/toga/app.py | 4 ++++ core/src/toga/window.py | 31 +++++++++++++------------- core/tests/app/test_app.py | 6 ++++-- core/tests/window/test_window.py | 37 +++++++++++++++++++++++++++----- 4 files changed, 56 insertions(+), 22 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index bc8e22f44b..6e581ea95f 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -868,12 +868,16 @@ def enter_presentation_mode( screen_window_dict[screen] = window elif isinstance(window_list_or_screen_window_dict, dict): screen_window_dict = window_list_or_screen_window_dict + else: + return self._impl.enter_presentation_mode(screen_window_dict) def exit_presentation_mode(self) -> None: """Exit presentation mode.""" if self.is_in_presentation_mode: self._impl.exit_presentation_mode() + else: + return ###################################################################### # App events diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 6a7d103b00..3af2a5488a 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -435,8 +435,7 @@ def visible(self, visible: bool) -> None: # Window state ###################################################################### - # ----------------------Future Deprecated methods---------------------- - # Warnings are disabled as old API tests are still in testbed and warnings will cause error. + # ------------------------ Deprecated methods------------------------- @property def full_screen(self) -> bool: """**DEPRECATED** – Use :any:`Window.state`. @@ -449,31 +448,33 @@ def full_screen(self) -> bool: mode is a slideshow app in presentation mode - the only visible content is the slide. """ - # warn( - # ("`Window.full_screen` is deprecated. Use `Window.state` instead."), - # DeprecationWarning, - # stacklevel=2, - # ) + warnings.warn( + ("`Window.full_screen` is deprecated. Use `Window.state` instead."), + DeprecationWarning, + stacklevel=2, + ) return bool(self.state == WindowState.FULLSCREEN) @full_screen.setter def full_screen(self, is_full_screen: bool) -> None: - # warn( - # ("`Window.full_screen` is deprecated. Use `Window.state` instead."), - # DeprecationWarning, - # stacklevel=2, - # ) + warnings.warn( + ("`Window.full_screen` is deprecated. Use `Window.state` instead."), + DeprecationWarning, + stacklevel=2, + ) if is_full_screen and (self.state != WindowState.FULLSCREEN): self._impl.set_window_state(WindowState.FULLSCREEN) elif not is_full_screen and (self.state == WindowState.FULLSCREEN): self._impl.set_window_state(WindowState.NORMAL) + else: # pragma: no cover + return # --------------------------------------------------------------------- @property def state(self) -> WindowState: """The current state of the window.""" - if getattr(self, "_impl", None) is None: + if getattr(self, "_impl", None) is None: # pragma: no cover return WindowState.NORMAL else: return self._impl.get_window_state() @@ -489,13 +490,13 @@ def state(self, state: WindowState) -> None: WindowState.PRESENTATION, }: warnings.warn( - f"Cannot set window state to {state} of a non-resizable window. " + f"Cannot set window state to {state} of a non-resizable window." ) else: self._impl.set_window_state(state) else: raise ValueError( - "Invalid type for state parameter. Expected WindowState type." + "Invalid type for state parameter. Expected WindowState enum type." ) ###################################################################### diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index b59bc95a08..8fd74d74bb 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -8,7 +8,6 @@ import pytest import toga -from toga.constants import WindowState from toga_dummy.utils import ( assert_action_not_performed, assert_action_performed, @@ -472,9 +471,12 @@ def test_presentation_mode(event_loop): app.exit_presentation_mode() assert_action_not_performed(app, "exit presentation mode") + # Entering presentation mode without any window is a no-op + app.enter_presentation_mode(None) + assert_action_not_performed(app, "enter presentation mode") + # Enter presentation mode with 1 window: app.enter_presentation_mode({app.screens[0]: window1}) - window1.state = WindowState.PRESENTATION assert app.is_in_presentation_mode assert_action_performed_with( app, diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index 7e4f928ec6..c62b9aca55 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -262,16 +262,19 @@ def test_full_screen(window, app): """A window can be set full screen.""" assert not window.full_screen - window.full_screen = True - assert window.full_screen + with pytest.deprecated_call(): + window.full_screen = True + with pytest.deprecated_call(): + assert window.full_screen assert_action_performed_with( window, "set window state to WindowState.FULLSCREEN", state=WindowState.FULLSCREEN, ) - - window.full_screen = False - assert not window.full_screen + with pytest.deprecated_call(): + window.full_screen = False + with pytest.deprecated_call(): + assert not window.full_screen assert_action_performed_with( window, "set window state to WindowState.NORMAL", @@ -301,6 +304,30 @@ def test_window_state(window): state=WindowState.NORMAL, ) + window_resizable_original_value = window.resizable + window.resizable = False + # Setting window state to any of the following when window is not resizable, is a no-op + # and should give a UserWarning. + for state in { + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + }: + with pytest.warns( + UserWarning, + match=f"Cannot set window state to {state} of a non-resizable window.", + ): + window.state = state + assert assert_action_not_performed(window, f"set window state to {state}") + window.resizable = window_resizable_original_value + + # Setting window state to any value other than a WindowState enum should raise a ValueError + with pytest.raises( + ValueError, + match="Invalid type for state parameter. Expected WindowState enum type.", + ): + window.state = None + def test_close_direct(window, app): """A window can be closed directly.""" From 0a90a12aa00e17d3bc424e62f79c06b013af9e47 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 22 May 2024 14:25:04 -0700 Subject: [PATCH 048/248] Minor modifications --- core/tests/window/test_window.py | 38 +++++++++++++++----------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index c62b9aca55..0a1a50b985 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -260,26 +260,23 @@ def test_visibility(window, app): def test_full_screen(window, app): """A window can be set full screen.""" - assert not window.full_screen - with pytest.deprecated_call(): + assert not window.full_screen window.full_screen = True - with pytest.deprecated_call(): assert window.full_screen - assert_action_performed_with( - window, - "set window state to WindowState.FULLSCREEN", - state=WindowState.FULLSCREEN, - ) - with pytest.deprecated_call(): + assert_action_performed_with( + window, + "set window state to WindowState.FULLSCREEN", + state=WindowState.FULLSCREEN, + ) + window.full_screen = False - with pytest.deprecated_call(): assert not window.full_screen - assert_action_performed_with( - window, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, - ) + assert_action_performed_with( + window, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) def test_window_state(window): @@ -304,8 +301,7 @@ def test_window_state(window): state=WindowState.NORMAL, ) - window_resizable_original_value = window.resizable - window.resizable = False + non_resizable_window = toga.Window(title="Non-Resizable Window", resizable=False) # Setting window state to any of the following when window is not resizable, is a no-op # and should give a UserWarning. for state in { @@ -317,9 +313,11 @@ def test_window_state(window): UserWarning, match=f"Cannot set window state to {state} of a non-resizable window.", ): - window.state = state - assert assert_action_not_performed(window, f"set window state to {state}") - window.resizable = window_resizable_original_value + non_resizable_window.state = state + assert_action_not_performed( + non_resizable_window, f"set window state to {state}" + ) + non_resizable_window.close() # Setting window state to any value other than a WindowState enum should raise a ValueError with pytest.raises( From 73721b33962718e82580aa77923fe73525d25644 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 22 May 2024 14:58:02 -0700 Subject: [PATCH 049/248] Minor modifications --- core/src/toga/window.py | 29 +++++++++++++++++++---------- core/tests/window/test_window.py | 31 +++++++++++++++---------------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 3af2a5488a..b4fe2f43c9 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -436,6 +436,7 @@ def visible(self, visible: bool) -> None: ###################################################################### # ------------------------ Deprecated methods------------------------- + # Warnings are disabled as old API tests are still in testbed and warnings will cause error. @property def full_screen(self) -> bool: """**DEPRECATED** – Use :any:`Window.state`. @@ -448,20 +449,20 @@ def full_screen(self) -> bool: mode is a slideshow app in presentation mode - the only visible content is the slide. """ - warnings.warn( - ("`Window.full_screen` is deprecated. Use `Window.state` instead."), - DeprecationWarning, - stacklevel=2, - ) + # warnings.warn( + # ("`Window.full_screen` is deprecated. Use `Window.state` instead."), + # DeprecationWarning, + # stacklevel=2, + # ) return bool(self.state == WindowState.FULLSCREEN) @full_screen.setter def full_screen(self, is_full_screen: bool) -> None: - warnings.warn( - ("`Window.full_screen` is deprecated. Use `Window.state` instead."), - DeprecationWarning, - stacklevel=2, - ) + # warnings.warn( + # ("`Window.full_screen` is deprecated. Use `Window.state` instead."), + # DeprecationWarning, + # stacklevel=2, + # ) if is_full_screen and (self.state != WindowState.FULLSCREEN): self._impl.set_window_state(WindowState.FULLSCREEN) elif not is_full_screen and (self.state == WindowState.FULLSCREEN): @@ -494,6 +495,14 @@ def state(self, state: WindowState) -> None: ) else: self._impl.set_window_state(state) + else: # pragma: no cover + # Marking this branch as no cover, since in core tests, setting the same + # state twice and then checking with assert_action_not_performed will still + # report that the window state setting action was performed. This is because + # the action was performed for the first time setting of window state, hence + # the action will still be in the EventLog and we cannot check if the action + # was done by the first call or the second call to the setter. + return else: raise ValueError( "Invalid type for state parameter. Expected WindowState enum type." diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index 0a1a50b985..2691199651 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -260,23 +260,22 @@ def test_visibility(window, app): def test_full_screen(window, app): """A window can be set full screen.""" - with pytest.deprecated_call(): - assert not window.full_screen - window.full_screen = True - assert window.full_screen - assert_action_performed_with( - window, - "set window state to WindowState.FULLSCREEN", - state=WindowState.FULLSCREEN, - ) + assert not window.full_screen + window.full_screen = True + assert window.full_screen + assert_action_performed_with( + window, + "set window state to WindowState.FULLSCREEN", + state=WindowState.FULLSCREEN, + ) - window.full_screen = False - assert not window.full_screen - assert_action_performed_with( - window, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, - ) + window.full_screen = False + assert not window.full_screen + assert_action_performed_with( + window, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) def test_window_state(window): From c809698eabc4ac5ba213e27fd3f7dda3821a2126 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 22 May 2024 22:58:51 -0700 Subject: [PATCH 050/248] Fixed bug on Window.close() --- changes/1857.bugfix.rst | 1 + changes/{1857.feature.rsst => 1857.feature.rst} | 0 changes/{1857.removal.rsst => 1857.removal.rst} | 0 cocoa/src/toga_cocoa/window.py | 6 ++++++ core/src/toga/app.py | 2 +- core/src/toga/window.py | 14 +++++++++----- gtk/src/toga_gtk/window.py | 6 ++++++ testbed/tests/app/test_app.py | 10 ---------- textual/src/toga_textual/window.py | 6 ++++++ winforms/src/toga_winforms/window.py | 6 ++++++ 10 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 changes/1857.bugfix.rst rename changes/{1857.feature.rsst => 1857.feature.rst} (100%) rename changes/{1857.removal.rsst => 1857.removal.rst} (100%) diff --git a/changes/1857.bugfix.rst b/changes/1857.bugfix.rst new file mode 100644 index 0000000000..69d1e45965 --- /dev/null +++ b/changes/1857.bugfix.rst @@ -0,0 +1 @@ +Calling `Window.close()` on platforms like Android and iOS now correctly performs a no-op and doesn't clear the window from the window registry. diff --git a/changes/1857.feature.rsst b/changes/1857.feature.rst similarity index 100% rename from changes/1857.feature.rsst rename to changes/1857.feature.rst diff --git a/changes/1857.removal.rsst b/changes/1857.removal.rst similarity index 100% rename from changes/1857.removal.rsst rename to changes/1857.removal.rst diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index e4708f8e8e..8c7e2d9671 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -211,8 +211,14 @@ def set_title(self, title): ###################################################################### def close(self): + if self.interface.content: + self.interface.content.window = None + self.interface.app.windows.discard(self.interface) + self.native.close() + self.interface._closed = True + def create_toolbar(self): # Purge any existing toolbar items self.purge_toolbar() diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 6e581ea95f..792394d75c 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -775,7 +775,7 @@ def current_window(self, window: Window): # Presentation mode controls ###################################################################### - # ----------------------Future Deprecated methods---------------------- + # ------------------------Deprecated methods-------------------------- # Warnings are disabled as old API tests are still in testbed and warnings will cause error. def exit_full_screen(self) -> None: """**DEPRECATED** – Use :any:`App.exit_presentation_mode()`. diff --git a/core/src/toga/window.py b/core/src/toga/window.py index b4fe2f43c9..1e2dfa9e80 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -273,11 +273,15 @@ def close(self) -> None: undefined, except for :attr:`closed` which can be used to check if the window was closed. """ - if self.content: - self.content.window = None - self.app.windows.discard(self) + # Since on some platforms close() is a no-op, we need to let each backend decide + # whether window discarding from the window registry and other cleanup operations + # should be performed or not. + # + # if self.content: + # self.content.window = None + # self.app.windows.discard(self) self._impl.close() - self._closed = True + # self._closed = True @property def closed(self) -> bool: @@ -435,7 +439,7 @@ def visible(self, visible: bool) -> None: # Window state ###################################################################### - # ------------------------ Deprecated methods------------------------- + # -------------------------Deprecated methods------------------------- # Warnings are disabled as old API tests are still in testbed and warnings will cause error. @property def full_screen(self) -> bool: diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 5fe73909e3..ab780429ca 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -93,9 +93,15 @@ def set_title(self, title): ###################################################################### def close(self): + if self.interface.content: + self.interface.content.window = None + self.interface.app.windows.discard(self.interface) + self._is_closing = True self.native.close() + self.interface._closed = True + def create_toolbar(self): # If there's an existing toolbar, hide it until we know we need it. if self.toolbar_items: diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index 71de230fb7..10c0d97395 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -40,16 +40,6 @@ async def test_full_screen(app): async def test_presentation_mode(app, app_probe, main_window, main_window_probe): """The app can enter into presentation mode""" - # This test fails when run normally with: - # `briefcase run android -ur --test` - # But passes when run singly with: - # `briefcase run android -ur --test -- tests/app/test_app.py::test_full_screen` - - # The test seems to fail when any method or property of app is invoked but doesn't fail - # when window methods are invoked. For example, `app.set_full_screen(app.main_window)` - # will make the test to fail but `main_window.state = WindowState.PRESENTATION` will - # not cause the test to fail. Even though both use the same API endpoint. - assert not app.is_in_presentation_mode assert not main_window_probe.is_window_state(WindowState.PRESENTATION) diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index 3cb62fdb7b..b394a5ee33 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -143,8 +143,14 @@ def set_title(self, title): ###################################################################### def close(self): + if self.interface.content: + self.interface.content.window = None + self.interface.app.windows.discard(self.interface) + self.native.dismiss(None) + self.interface._closed = True + def create_toolbar(self): pass diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 88c8de2dac..3ccf189b39 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -80,9 +80,15 @@ def set_title(self, title): ###################################################################### def close(self): + if self.interface.content: + self.interface.content.window = None + self.interface.app.windows.discard(self.interface) + self._is_closing = True self.native.Close() + self.interface._closed = True + def create_toolbar(self): if self.interface.toolbar: if self.toolbar_native: From 06b7f2ab0911303b6dc09603c6a8b9715f275ddf Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 22 May 2024 23:11:34 -0700 Subject: [PATCH 051/248] Fixed dummy backend --- dummy/src/toga_dummy/window.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 7d3481bc3c..bb72206e9e 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -94,9 +94,15 @@ def get_visible(self): return self._get_value("visible") def close(self): + if self.interface.content: + self.interface.content.window = None + self.interface.app.windows.discard(self.interface) + self._action("close") self._set_value("visible", False) + self.interface._closed = True + def get_image_data(self): self._action("get image data") path = Path(toga_dummy.__file__).parent / "resources/screenshot.png" From 11fea693241cd87ce5831819388544c93785a582 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 1 Jun 2024 01:56:59 -0700 Subject: [PATCH 052/248] Fixed tests --- android/src/toga_android/window.py | 11 ++++++---- cocoa/src/toga_cocoa/window.py | 2 ++ core/src/toga/app.py | 17 +++++++--------- core/src/toga/window.py | 15 +++----------- core/tests/app/test_app.py | 5 +++++ core/tests/window/test_window.py | 30 ++++++++++++++++++++++++++++ gtk/src/toga_gtk/window.py | 2 ++ winforms/src/toga_winforms/window.py | 4 ++++ 8 files changed, 60 insertions(+), 26 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index ec379891d9..294c2e95c8 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -153,6 +153,8 @@ def get_window_state(self): return WindowState.PRESENTATION else: return WindowState.FULLSCREEN + # elif getattr(self, "_is_minimized", False) is True: + # return WindowState.MINIMIZED return WindowState.NORMAL def set_window_state(self, state): @@ -185,11 +187,8 @@ def set_window_state(self, state): # # This works every time but starts new instance of MainActivity # new_intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) # self.app.native.startActivity(new_intent) + # self._is_minimized = False else: - # # This works but the issue lies in restoring to normal state - # if state == WindowState.MINIMIZED: - # self.app.native.moveTaskToBack(True) - # Restore to normal state before switching states to avoid mixing states # and prevent glitches. self.set_window_state(WindowState.NORMAL) @@ -201,6 +200,10 @@ def set_window_state(self, state): ) if state == WindowState.PRESENTATION: self.app.native.getSupportActionBar().hide() + # elif state == WindowState.MINIMIZED: + # # This works but the issue lies in restoring to normal state + # self.app.native.moveTaskToBack(True) + # self._is_minimized = True ###################################################################### # Window capabilities diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 8c7e2d9671..63f3a4501f 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -402,6 +402,8 @@ def set_window_state(self, state): {self.interface.screen: self.interface} ) else: # pragma: no cover + # Marking this as no cover, since the type of the state parameter + # value is checked on the interface. return ###################################################################### diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 792394d75c..eedc70998f 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -830,16 +830,13 @@ def set_full_screen(self, *windows: Window) -> None: # DeprecationWarning, # stacklevel=2, # ) - if self.windows is not None: - self.exit_full_screen() - if windows is None: - return - screen_window_dict = dict() - for window, screen in zip(windows, self.screens): - screen_window_dict[screen] = window - self.enter_presentation_mode(screen_window_dict) - else: - warn("App doesn't have any windows") + self.exit_full_screen() + if any(window is None for window in windows): + return + screen_window_dict = dict() + for window, screen in zip(windows, self.screens): + screen_window_dict[screen] = window + self.enter_presentation_mode(screen_window_dict) # --------------------------------------------------------------------- diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 1e2dfa9e80..4250763326 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -471,7 +471,7 @@ def full_screen(self, is_full_screen: bool) -> None: self._impl.set_window_state(WindowState.FULLSCREEN) elif not is_full_screen and (self.state == WindowState.FULLSCREEN): self._impl.set_window_state(WindowState.NORMAL) - else: # pragma: no cover + else: return # --------------------------------------------------------------------- @@ -479,10 +479,7 @@ def full_screen(self, is_full_screen: bool) -> None: @property def state(self) -> WindowState: """The current state of the window.""" - if getattr(self, "_impl", None) is None: # pragma: no cover - return WindowState.NORMAL - else: - return self._impl.get_window_state() + return self._impl.get_window_state() @state.setter def state(self, state: WindowState) -> None: @@ -499,13 +496,7 @@ def state(self, state: WindowState) -> None: ) else: self._impl.set_window_state(state) - else: # pragma: no cover - # Marking this branch as no cover, since in core tests, setting the same - # state twice and then checking with assert_action_not_performed will still - # report that the window state setting action was performed. This is because - # the action was performed for the first time setting of window state, hence - # the action will still be in the EventLog and we cannot check if the action - # was done by the first call or the second call to the setter. + else: return else: raise ValueError( diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 8fd74d74bb..8943f818fe 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -407,6 +407,11 @@ def test_full_screen(event_loop): assert not app.is_full_screen assert_action_not_performed(app, "exit presentation mode") + # Trying to enter full screen with no windows is a no-op + app.set_full_screen(None) + assert not app.is_full_screen + assert_action_not_performed(app, "enter presentation mode") + # Enter full screen with 2 windows app.set_full_screen(window2, app.main_window) assert app.is_full_screen diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index 2691199651..1168a10ad5 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -277,6 +277,16 @@ def test_full_screen(window, app): state=WindowState.NORMAL, ) + # Setting full screen to False when window is not in full screen, is a no-op + # + # We cannot assert that the action was not performed, since the same action + # was performed previously and would still be in EventLog. Therefore, we + # cannot check if the action was done by the first call or the second call. + # Hence, this is just to reach coverage. + assert not window.full_screen + window.full_screen = False + # assert_action_not_performed(window, "set window state to WindowState.NORMAL") + def test_window_state(window): """A window can have different states.""" @@ -292,6 +302,26 @@ def test_window_state(window): state=state, ) + # Setting the window state same as the current window state is a no-op. + # + # Here, setting the same state twice and then checking with assert_action_not_performed will still + # report that the window state setting action was performed. This is because + # the action was also performed for the first-time setting of the window state, + # hence the action will still be in the EventLog and we cannot check if the + # action was done by the first call or the second call to the setter. + # + # For example: For the test, we need to set window state to WindowState.MAXIMIZED and + # then again the window state needs to be set to WindowState.MAXIMIZED. + # But doing so will cause the above mentioned problem. + # Hence, this is just to reach coverage. + window.state = WindowState.MAXIMIZED + assert window.state == WindowState.MAXIMIZED + window.state = WindowState.MAXIMIZED + assert window.state == WindowState.MAXIMIZED + # assert_action_not_performed( + # window, "set window state to WindowState.MAXIMIZED" + # ) + window.state = WindowState.NORMAL assert window.state == WindowState.NORMAL assert_action_performed_with( diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index ab780429ca..b48dcc61f3 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -277,6 +277,8 @@ def set_window_state(self, state): self.native.fullscreen() self._is_in_presentation_mode = True else: # pragma: no cover + # Marking this as no cover, since the type of the state parameter + # value is checked on the interface. pass ###################################################################### diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 3ccf189b39..13d17e38f4 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -240,6 +240,8 @@ def get_window_state(self): elif window_state == WinForms.FormWindowState.Normal: return WindowState.NORMAL else: # pragma: no cover + # Marking this as no cover, since the above cases cover all the possible values + # of the FormWindowState enum type that can be returned by WinForms.WindowState. return def set_window_state(self, state): @@ -285,6 +287,8 @@ def set_window_state(self, state): self.native.WindowState = WinForms.FormWindowState.Maximized self._is_in_presentation_mode = True else: # pragma: no cover + # Marking this as no cover, since the type of the state parameter + # value is checked on the interface. pass ###################################################################### From 938504c0d52c9dade007edc8225a17b0e3a376ae Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 1 Jun 2024 06:47:59 -0400 Subject: [PATCH 053/248] Minor cleanup --- gtk/src/toga_gtk/window.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index b48dcc61f3..86f24560e3 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -243,10 +243,9 @@ def set_window_state(self, state): elif current_state == WindowState.PRESENTATION: if isinstance(self.native, Gtk.ApplicationWindow): self.native.set_show_menubar(True) - if self.native_toolbar: - self.native_toolbar.set_visible(True) - else: # pragma: no cover - pass + # self.native_toolbar is always set to Gtk.Toolbar(), so no need + # to check if self.native_toolbar exists. + self.native_toolbar.set_visible(True) self.native.unfullscreen() self.interface.screen = self._before_presentation_mode_screen @@ -270,10 +269,9 @@ def set_window_state(self, state): self._before_presentation_mode_screen = self.interface.screen if isinstance(self.native, Gtk.ApplicationWindow): self.native.set_show_menubar(False) - if self.native_toolbar: - self.native_toolbar.set_visible(False) - else: # pragma: no cover - pass + # self.native_toolbar is always set to Gtk.Toolbar(), so no need + # to check if self.native_toolbar exists. + self.native_toolbar.set_visible(False) self.native.fullscreen() self._is_in_presentation_mode = True else: # pragma: no cover From be8711362ed6a82131107f26479ca6be4197d3cd Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 1 Jun 2024 05:42:17 -0700 Subject: [PATCH 054/248] Code Cleanup --- android/src/toga_android/window.py | 3 --- cocoa/src/toga_cocoa/window.py | 3 --- core/src/toga/window.py | 10 +++++++++- gtk/src/toga_gtk/window.py | 3 --- winforms/src/toga_winforms/window.py | 3 --- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 294c2e95c8..2fc60bbe07 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -189,9 +189,6 @@ def set_window_state(self, state): # self.app.native.startActivity(new_intent) # self._is_minimized = False else: - # Restore to normal state before switching states to avoid mixing states - # and prevent glitches. - self.set_window_state(WindowState.NORMAL) if state in {WindowState.FULLSCREEN, WindowState.PRESENTATION}: decor_view.setSystemUiVisibility( decor_view.SYSTEM_UI_FLAG_FULLSCREEN diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 63f3a4501f..9072b856b9 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -385,9 +385,6 @@ def set_window_state(self, state): elif current_state == WindowState.PRESENTATION: self.interface.app.exit_presentation_mode() else: - # Set Window state to NORMAL before changing to other states as - # some states block changing window state without first exiting them. - self.set_window_state(WindowState.NORMAL) if state == WindowState.MAXIMIZED: self.native.setIsZoomed(True) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 4250763326..cb77317f8e 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -495,7 +495,15 @@ def state(self, state: WindowState) -> None: f"Cannot set window state to {state} of a non-resizable window." ) else: - self._impl.set_window_state(state) + # Set Window state to NORMAL before changing to other states as some + # states block changing window state without first exiting them or + # can even cause rendering glitches. + self.set_window_state(WindowState.NORMAL) + + if state != WindowState.NORMAL: + self._impl.set_window_state(state) + else: + return else: return else: diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index b48dcc61f3..60e0dc74b2 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -253,9 +253,6 @@ def set_window_state(self, state): self._before_presentation_mode_screen = None self._is_in_presentation_mode = False else: - # Set Window state to NORMAL before changing to other states as - # some states block changing window state without first exiting them. - self.set_window_state(WindowState.NORMAL) if state == WindowState.MAXIMIZED: self.native.maximize() diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 13d17e38f4..73df465235 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -263,9 +263,6 @@ def set_window_state(self, state): ) self.native.WindowState = WinForms.FormWindowState.Normal else: - # Set Window state to NORMAL before changing to other states as - # some states block changing window state without first exiting them. - self.set_window_state(WindowState.NORMAL) if state == WindowState.MAXIMIZED: self.native.WindowState = WinForms.FormWindowState.Maximized From 176e879aa002fdf65efc0d6f01cd23a1573d7f25 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 1 Jun 2024 05:47:37 -0700 Subject: [PATCH 055/248] Code Cleanup --- 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 cb77317f8e..1ded34822f 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -498,7 +498,7 @@ def state(self, state: WindowState) -> None: # Set Window state to NORMAL before changing to other states as some # states block changing window state without first exiting them or # can even cause rendering glitches. - self.set_window_state(WindowState.NORMAL) + self._impl.set_window_state(WindowState.NORMAL) if state != WindowState.NORMAL: self._impl.set_window_state(state) From d4bfdaa251ffccbe4a2aa6c033693dceeac0b1ed Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Sat, 1 Jun 2024 05:57:34 -0700 Subject: [PATCH 056/248] Restart CI for Linux Testbed failure From 38bf5c3ff159812c753c737df8b6cfe90b357e60 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 12 Jun 2024 09:54:28 -0700 Subject: [PATCH 057/248] Updated to latest main branch --- cocoa/src/toga_cocoa/window.py | 1 - dummy/src/toga_dummy/window.py | 1 - iOS/src/toga_iOS/window.py | 1 - textual/src/toga_textual/window.py | 3 +-- web/src/toga_web/window.py | 1 - 5 files changed, 1 insertion(+), 6 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 0405f633a1..8bbcda0bcc 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -3,7 +3,6 @@ from toga.command import Command, Separator from toga.constants import WindowState from toga.types import Position, Size - from toga_cocoa.container import Container from toga_cocoa.libs import ( SEL, diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index a334a7325b..cda9974a36 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -1,7 +1,6 @@ from pathlib import Path import toga_dummy - from toga.constants import WindowState from toga.types import Size diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index 702193ebed..986eda2d40 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -8,7 +8,6 @@ from toga.constants import WindowState from toga.types import Position, Size - from toga_iOS.container import RootContainer from toga_iOS.images import nsdata_to_bytes from toga_iOS.libs import ( diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index 11ade10af1..d8c09eaa66 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -5,9 +5,8 @@ from textual.screen import Screen as TextualScreen from textual.widget import Widget as TextualWidget from textual.widgets import Button as TextualButton - -from toga.constants import WindowState from toga import Position, Size +from toga.constants import WindowState from .container import Container from .screens import Screen as ScreenImpl diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index 5b540b8bb3..da9a1f2078 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -1,6 +1,5 @@ from toga.constants import WindowState from toga.types import Position, Size - from toga_web.libs import create_element, js from .screens import Screen as ScreenImpl From b710eaf0e4ac0d5b6170aaf3fff6b2af30f85a2f Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 16 Jun 2024 07:27:25 -0700 Subject: [PATCH 058/248] Parameterized test on core --- core/tests/window/test_window.py | 135 +++++++++++++++++++------------ 1 file changed, 84 insertions(+), 51 deletions(-) diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index 0fb4044607..8193bfd75d 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -319,66 +319,99 @@ def test_full_screen(window, app): # assert_action_not_performed(window, "set window state to WindowState.NORMAL") -def test_window_state(window): +@pytest.mark.parametrize( + "state", + [ + WindowState.MAXIMIZED, + WindowState.MINIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ], +) +def test_window_state(window, state): """A window can have different states.""" assert window.state == WindowState.NORMAL - for state in WindowState: - if state != WindowState.NORMAL: - window.state = state - assert window.state == state - assert_action_performed_with( - window, - f"set window state to {state}", - state=state, - ) - - # Setting the window state same as the current window state is a no-op. - # - # Here, setting the same state twice and then checking with assert_action_not_performed will still - # report that the window state setting action was performed. This is because - # the action was also performed for the first-time setting of the window state, - # hence the action will still be in the EventLog and we cannot check if the - # action was done by the first call or the second call to the setter. - # - # For example: For the test, we need to set window state to WindowState.MAXIMIZED and - # then again the window state needs to be set to WindowState.MAXIMIZED. - # But doing so will cause the above mentioned problem. - # Hence, this is just to reach coverage. - window.state = WindowState.MAXIMIZED - assert window.state == WindowState.MAXIMIZED - window.state = WindowState.MAXIMIZED - assert window.state == WindowState.MAXIMIZED - # assert_action_not_performed( - # window, "set window state to WindowState.MAXIMIZED" - # ) - - window.state = WindowState.NORMAL - assert window.state == WindowState.NORMAL - assert_action_performed_with( - window, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, - ) + window.state = state + assert window.state == state + assert_action_performed_with( + window, + f"set window state to {state}", + state=state, + ) - non_resizable_window = toga.Window(title="Non-Resizable Window", resizable=False) - # Setting window state to any of the following when window is not resizable, is a no-op - # and should give a UserWarning. - for state in { + window.state = WindowState.NORMAL + assert window.state == WindowState.NORMAL + assert_action_performed_with( + window, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + + +@pytest.mark.parametrize( + "state", + [ + WindowState.MAXIMIZED, + WindowState.MINIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ], +) +def test_window_state_same_as_previous(window, state): + # Setting the window state same as the current window state is a no-op. + # + # Here, setting the same state twice and then checking with assert_action_not_performed + # will still report that the window state setting action was performed. This is because + # the action was also performed for the first-time setting of the window state, + # hence the action will still be in the EventLog and we cannot check if the + # action was done by the first call or the second call to the setter. + # + # For example: For the test, we need to set window state to WindowState.MAXIMIZED and + # then again the window state needs to be set to WindowState.MAXIMIZED. + # But doing so will cause the above mentioned problem. + # Hence, this is just to reach coverage. + window.state = state + assert window.state == state + window.state = state + assert window.state == state + # assert_action_not_performed( + # window, "set window state to WindowState.MAXIMIZED" + # ) + + window.state = WindowState.NORMAL + assert window.state == WindowState.NORMAL + assert_action_performed_with( + window, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + + +@pytest.mark.parametrize( + "state", + [ + # Setting window state to any of the following when window is + # not resizable, is a no-op and should give a UserWarning. WindowState.MAXIMIZED, WindowState.FULLSCREEN, WindowState.PRESENTATION, - }: - with pytest.warns( - UserWarning, - match=f"Cannot set window state to {state} of a non-resizable window.", - ): - non_resizable_window.state = state - assert_action_not_performed( - non_resizable_window, f"set window state to {state}" - ) + ], +) +def test_non_resizable_window_state(state): + non_resizable_window = toga.Window(title="Non-Resizable Window", resizable=False) + with pytest.warns( + UserWarning, + match=f"Cannot set window state to {state} of a non-resizable window.", + ): + non_resizable_window.state = state + assert_action_not_performed( + non_resizable_window, f"set window state to {state}" + ) non_resizable_window.close() + +def test_window_state_invalid_value(window): # Setting window state to any value other than a WindowState enum should raise a ValueError with pytest.raises( ValueError, From 0c7900b721bcd7c4547cbdf40f7a45e2075ecf27 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 16 Jun 2024 07:46:22 -0700 Subject: [PATCH 059/248] Updating to latest main branch --- core/src/toga/app.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index d481004464..e12a1a4662 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -858,7 +858,8 @@ def set_full_screen(self, *windows: Window) -> None: Make one or more windows full screen. Full screen is not the same as "maximized"; full screen mode is when all window - borders and other window decorations are no longer visible. + borders and other window decorations are no longer visible, but the toolbars and + app menu are visible. :param windows: The list of windows to go full screen, in order of allocation to screens. If the number of windows exceeds the number of available displays, @@ -894,11 +895,11 @@ def enter_presentation_mode( ) -> None: """Enter into presentation mode with one or more windows on different screens. - Presentation mode is not the same as Full Screen mode; full screen mode is when all window - borders and other window decorations are no longer visible. + Presentation mode is not the same as "Full Screen" mode; presentation mode is when + window borders, other window decorations, app menu and toolbars are no longer visible. :param window_list_or_screen_window_dict: A list of windows, or a dictionary - mapping screens to windows, to go into full screen, in order of allocation to + mapping screens to windows, to go into presentation, in order of allocation to screens. If the number of windows exceeds the number of available displays, those windows will not be visible. """ From 97a6c6645e5935bc3f03c04ab12cbf96643df4e5 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 16 Jun 2024 07:57:32 -0700 Subject: [PATCH 060/248] Changed attribute to is_presentation_mode --- core/src/toga/app.py | 10 +++++----- core/tests/app/test_app.py | 8 ++++---- examples/window/window/app.py | 2 +- gtk/src/toga_gtk/window.py | 6 +++--- gtk/tests_backend/window.py | 2 +- testbed/tests/app/test_app.py | 28 ++++++++++++++-------------- winforms/src/toga_winforms/window.py | 6 +++--- winforms/tests_backend/window.py | 2 +- 8 files changed, 32 insertions(+), 32 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index e12a1a4662..abc17cb655 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -838,19 +838,19 @@ def exit_full_screen(self) -> None: @property def is_full_screen(self) -> bool: - """**DEPRECATED** – Use :any:`App.is_in_presentation_mode`. + """**DEPRECATED** – Use :any:`App.is_presentation_mode`. Is the app currently in full screen mode? """ # warn( # ( - # "`App.is_full_screen` is deprecated. Use `App.is_in_presentation_mode` instead." + # "`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead." # ), # DeprecationWarning, # stacklevel=2, # ) - return self.is_in_presentation_mode + return self.is_presentation_mode def set_full_screen(self, *windows: Window) -> None: """**DEPRECATED** – Use :any:`App.enter_presentation_mode()` and :any:`App.exit_presentation_mode()`. @@ -885,7 +885,7 @@ def set_full_screen(self, *windows: Window) -> None: # --------------------------------------------------------------------- @property - def is_in_presentation_mode(self) -> bool: + def is_presentation_mode(self) -> bool: """Is the app currently in presentation mode?""" return any(window.state == WindowState.PRESENTATION for window in self.windows) @@ -915,7 +915,7 @@ def enter_presentation_mode( def exit_presentation_mode(self) -> None: """Exit presentation mode.""" - if self.is_in_presentation_mode: + if self.is_presentation_mode: self._impl.exit_presentation_mode() else: return diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index cc094c1bab..d3fcb304e6 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -475,7 +475,7 @@ def test_presentation_mode(event_loop): window1 = toga.Window() window2 = toga.Window() - assert not app.is_in_presentation_mode + assert not app.is_presentation_mode # If we're not in presentation mode, exiting presentation mode is a no-op app.exit_presentation_mode() @@ -487,7 +487,7 @@ def test_presentation_mode(event_loop): # Enter presentation mode with 1 window: app.enter_presentation_mode({app.screens[0]: window1}) - assert app.is_in_presentation_mode + assert app.is_presentation_mode assert_action_performed_with( app, "enter presentation mode", @@ -502,7 +502,7 @@ def test_presentation_mode(event_loop): # Enter presentation mode with 2 windows: app.enter_presentation_mode([window1, window2]) - assert app.is_in_presentation_mode + assert app.is_presentation_mode assert_action_performed_with( app, "enter presentation mode", @@ -518,7 +518,7 @@ def test_presentation_mode(event_loop): # Entering presentation mode with 3 windows should drop the last window, # as the app has only 2 screens: app.enter_presentation_mode([app.main_window, window2, window1]) - assert app.is_in_presentation_mode + assert app.is_presentation_mode assert_action_performed_with( app, "enter presentation mode", diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 4e7f4085cf..2f5f0064ec 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -53,7 +53,7 @@ def do_window_state_presentation(self, widget, **kwargs): self.main_window.state = WindowState.PRESENTATION def do_app_presentation_mode(self, widget, **kwargs): - if self.is_in_presentation_mode: + if self.is_presentation_mode: self.exit_presentation_mode() else: self.enter_presentation_mode([self.main_window]) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 51d619279d..05a48228ef 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -228,7 +228,7 @@ def get_window_state(self): elif window_state_flags & Gdk.WindowState.FULLSCREEN: # Use a shadow variable since a window without any app menu and toolbar # in presentation mode would be indistinguishable from full screen mode. - if getattr(self, "_is_in_presentation_mode", False) is True: + if getattr(self, "_is_presentation_mode", False) is True: return WindowState.PRESENTATION else: return WindowState.FULLSCREEN @@ -259,7 +259,7 @@ def set_window_state(self, state): self.interface.screen = self._before_presentation_mode_screen self._before_presentation_mode_screen = None - self._is_in_presentation_mode = False + self._is_presentation_mode = False else: if state == WindowState.MAXIMIZED: self.native.maximize() @@ -279,7 +279,7 @@ def set_window_state(self, state): # to check if self.native_toolbar exists. self.native_toolbar.set_visible(False) self.native.fullscreen() - self._is_in_presentation_mode = True + self._is_presentation_mode = True else: # pragma: no cover # Marking this as no cover, since the type of the state parameter # value is checked on the interface. diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index 07e00fa66e..e7094ccba5 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -50,7 +50,7 @@ def is_window_state(self, state): elif window_state_flags & Gdk.WindowState.FULLSCREEN: # Use a shadow variable since a window without any app menu and toolbar # in presentation mode would be indistinguishable from full screen mode. - if getattr(self.impl, "_is_in_presentation_mode", False) is True: + if getattr(self.impl, "_is_presentation_mode", False) is True: current_state = WindowState.PRESENTATION else: current_state = WindowState.FULLSCREEN diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index 8db3054741..831826b4b4 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -40,7 +40,7 @@ async def test_full_screen(app): async def test_presentation_mode(app, app_probe, main_window, main_window_probe): """The app can enter into presentation mode""" - assert not app.is_in_presentation_mode + assert not app.is_presentation_mode assert not main_window_probe.is_window_state(WindowState.PRESENTATION) # Enter presentation mode with main window via the app @@ -49,7 +49,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) "Main window is in presentation mode", full_screen=True ) - assert app.is_in_presentation_mode + assert app.is_presentation_mode assert not main_window_probe.is_window_state(WindowState.NORMAL) assert main_window_probe.is_window_state(WindowState.PRESENTATION) @@ -60,7 +60,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) full_screen=True, ) - assert not app.is_in_presentation_mode + assert not app.is_presentation_mode assert not main_window_probe.is_window_state(WindowState.PRESENTATION) assert main_window_probe.is_window_state(WindowState.NORMAL) @@ -70,7 +70,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) "Main window is in presentation mode", full_screen=True ) - assert app.is_in_presentation_mode + assert app.is_presentation_mode assert not main_window_probe.is_window_state(WindowState.NORMAL) assert main_window_probe.is_window_state(WindowState.PRESENTATION) @@ -81,7 +81,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) full_screen=True, ) - assert not app.is_in_presentation_mode + assert not app.is_presentation_mode assert not main_window_probe.is_window_state(WindowState.PRESENTATION) assert main_window_probe.is_window_state(WindowState.NORMAL) @@ -362,7 +362,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) # Add delay for gtk to show the windows await app_probe.redraw("Extra windows are visible", delay=0.1) - assert not app.is_in_presentation_mode + assert not app.is_presentation_mode assert not main_window_probe.is_window_state(WindowState.PRESENTATION) assert not window1_probe.is_window_state(WindowState.PRESENTATION) assert window2_probe.has_toolbar() @@ -379,7 +379,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) "Main window is in presentation mode", full_screen=True, ) - assert app.is_in_presentation_mode + assert app.is_presentation_mode assert not window1_probe.is_window_state(WindowState.PRESENTATION) assert window1_probe.presentation_content_size == initial_content1_size @@ -395,7 +395,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) "Main window is no longer in presentation mode", full_screen=True, ) - assert not app.is_in_presentation_mode + assert not app.is_presentation_mode assert ( main_window_probe.presentation_content_size == initial_content_main_window_size @@ -407,7 +407,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) "Second extra window is in presentation mode", full_screen=True, ) - assert app.is_in_presentation_mode + assert app.is_presentation_mode assert not window1_probe.is_window_state(WindowState.PRESENTATION) assert window1_probe.presentation_content_size == initial_content1_size @@ -421,7 +421,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) "Second extra window is no longer in presentation mode", full_screen=True, ) - assert not app.is_in_presentation_mode + assert not app.is_presentation_mode assert window2_probe.presentation_content_size == initial_content2_size # Enter presentation mode with a screen-window1 dict via the app @@ -430,7 +430,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) "First extra window is in presentation mode", full_screen=True, ) - assert app.is_in_presentation_mode + assert app.is_presentation_mode assert not window2_probe.is_window_state(WindowState.PRESENTATION) assert window2_probe.presentation_content_size == initial_content2_size @@ -444,7 +444,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) "First extra window is no longer in presentation mode", full_screen=True, ) - assert not app.is_in_presentation_mode + assert not app.is_presentation_mode assert window1_probe.presentation_content_size == initial_content1_size if len(app.screens) < 2: @@ -455,7 +455,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) "First extra window is in presentation mode", full_screen=True, ) - assert app.is_in_presentation_mode + assert app.is_presentation_mode assert not window2_probe.is_window_state(WindowState.PRESENTATION) assert window2_probe.presentation_content_size == initial_content2_size @@ -469,7 +469,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) "First extra window is no longer in presentation mode", full_screen=True, ) - assert not app.is_in_presentation_mode + assert not app.is_presentation_mode assert window1_probe.presentation_content_size == initial_content1_size finally: window1.close() diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 19c0024d55..ceec0c78cc 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -239,7 +239,7 @@ def get_window_state(self): if self.native.FormBorderStyle == getattr(WinForms.FormBorderStyle, "None"): # Use a shadow variable since a window without any app menu and toolbar # in presentation mode would be indistinguishable from full screen mode. - if getattr(self, "_is_in_presentation_mode", False) is True: + if getattr(self, "_is_presentation_mode", False) is True: return WindowState.PRESENTATION else: return WindowState.FULLSCREEN @@ -265,7 +265,7 @@ def set_window_state(self, state): self.interface.screen = self._before_presentation_mode_screen self._before_presentation_mode_screen = None - self._is_in_presentation_mode = False + self._is_presentation_mode = False self.native.FormBorderStyle = getattr( WinForms.FormBorderStyle, @@ -292,7 +292,7 @@ def set_window_state(self, state): self.toolbar_native.Visible = False self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") self.native.WindowState = WinForms.FormWindowState.Maximized - self._is_in_presentation_mode = True + self._is_presentation_mode = True else: # pragma: no cover # Marking this as no cover, since the type of the state parameter # value is checked on the interface. diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 387ed0372e..a9b0b20f16 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -58,7 +58,7 @@ def is_window_state(self, state): window_state = self.native.WindowState if window_state == FormWindowState.Maximized: if self.native.FormBorderStyle == getattr(FormBorderStyle, "None"): - if getattr(self.impl, "_is_in_presentation_mode", False) is True: + if getattr(self.impl, "_is_presentation_mode", False) is True: current_state = WindowState.PRESENTATION else: current_state = WindowState.FULLSCREEN From ba281945bc822a82b154694ef2309e3923c5c5e1 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 16 Jun 2024 08:00:07 -0700 Subject: [PATCH 061/248] Misc cleanup --- cocoa/src/toga_cocoa/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 157777c0c2..76aa1cbc15 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -11,7 +11,7 @@ from toga.app import overridden from toga.command import Command, Separator from toga.constants import WindowState -from toga.handlers import NativeHandler +from toga.handlers import NativeHandler, simple_handler from .keys import cocoa_key from .libs import ( From c7320ce96565a38127d71c9016abfb8cb4178d0c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 16 Jun 2024 08:14:17 -0700 Subject: [PATCH 062/248] Name cleanups --- core/src/toga/app.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index abc17cb655..fe0c8ecdb3 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -858,8 +858,7 @@ def set_full_screen(self, *windows: Window) -> None: Make one or more windows full screen. Full screen is not the same as "maximized"; full screen mode is when all window - borders and other window decorations are no longer visible, but the toolbars and - app menu are visible. + borders and other window decorations are no longer visible. :param windows: The list of windows to go full screen, in order of allocation to screens. If the number of windows exceeds the number of available displays, @@ -874,13 +873,10 @@ def set_full_screen(self, *windows: Window) -> None: # DeprecationWarning, # stacklevel=2, # ) - self.exit_full_screen() + self.exit_presentation_mode() if any(window is None for window in windows): return - screen_window_dict = dict() - for window, screen in zip(windows, self.screens): - screen_window_dict[screen] = window - self.enter_presentation_mode(screen_window_dict) + self.enter_presentation_mode([*windows]) # --------------------------------------------------------------------- @@ -891,24 +887,24 @@ def is_presentation_mode(self) -> bool: def enter_presentation_mode( self, - window_list_or_screen_window_dict: list[Window] | dict[Screen, Window], + windows: list[Window] | dict[Screen, Window], ) -> None: """Enter into presentation mode with one or more windows on different screens. Presentation mode is not the same as "Full Screen" mode; presentation mode is when window borders, other window decorations, app menu and toolbars are no longer visible. - :param window_list_or_screen_window_dict: A list of windows, or a dictionary + :param windows: A list of windows, or a dictionary mapping screens to windows, to go into presentation, in order of allocation to screens. If the number of windows exceeds the number of available displays, those windows will not be visible. """ screen_window_dict = dict() - if isinstance(window_list_or_screen_window_dict, list): - for window, screen in zip(window_list_or_screen_window_dict, self.screens): + if isinstance(windows, list): + for window, screen in zip(windows, self.screens): screen_window_dict[screen] = window - elif isinstance(window_list_or_screen_window_dict, dict): - screen_window_dict = window_list_or_screen_window_dict + elif isinstance(windows, dict): + screen_window_dict = windows else: return self._impl.enter_presentation_mode(screen_window_dict) From aed525149953693f6339b0b4d4c6138cbf981a32 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 17 Jun 2024 03:57:47 -0700 Subject: [PATCH 063/248] Modified as per review --- core/src/toga/app.py | 72 +++++------ core/src/toga/constants/__init__.py | 38 +++++- core/src/toga/window.py | 30 ++--- core/tests/app/test_app.py | 183 ++++++++++++++++++++++++---- core/tests/window/test_window.py | 43 +++++-- 5 files changed, 278 insertions(+), 88 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index fe0c8ecdb3..b61529591b 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -819,21 +819,20 @@ def current_window(self, window: Window) -> None: ###################################################################### # ------------------------Deprecated methods-------------------------- - # Warnings are disabled as old API tests are still in testbed and warnings will cause error. def exit_full_screen(self) -> None: """**DEPRECATED** – Use :any:`App.exit_presentation_mode()`. Exit full screen mode. """ - # warn( - # ( - # "`App.exit_full_screen()` is deprecated. Use `App.exit_presentation_mode()` instead." - # ), - # DeprecationWarning, - # stacklevel=2, - # ) - if self.is_full_screen: + warn( + ( + "`App.exit_full_screen()` is deprecated. Use `App.exit_presentation_mode()` instead." + ), + DeprecationWarning, + stacklevel=2, + ) + if self.is_presentation_mode: self._impl.exit_presentation_mode() @property @@ -843,13 +842,13 @@ def is_full_screen(self) -> bool: Is the app currently in full screen mode? """ - # warn( - # ( - # "`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead." - # ), - # DeprecationWarning, - # stacklevel=2, - # ) + warn( + ( + "`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead." + ), + DeprecationWarning, + stacklevel=2, + ) return self.is_presentation_mode def set_full_screen(self, *windows: Window) -> None: @@ -865,18 +864,18 @@ def set_full_screen(self, *windows: Window) -> None: those windows will not be visible. If no windows are specified, the app will exit full screen mode. """ - # warn( - # ( - # "`App.set_full_screen()` is deprecated. Use `App.enter_presentation_mode()` and " - # "`App.exit_presentation_mode()` instead." - # ), - # DeprecationWarning, - # stacklevel=2, - # ) + warn( + ( + "`App.set_full_screen()` is deprecated. Use `App.enter_presentation_mode()` instead." + ), + DeprecationWarning, + stacklevel=2, + ) self.exit_presentation_mode() - if any(window is None for window in windows): + if not windows: return - self.enter_presentation_mode([*windows]) + else: + self.enter_presentation_mode([*windows]) # --------------------------------------------------------------------- @@ -899,15 +898,20 @@ def enter_presentation_mode( screens. If the number of windows exceeds the number of available displays, those windows will not be visible. """ - screen_window_dict = dict() - if isinstance(windows, list): - for window, screen in zip(windows, self.screens): - screen_window_dict[screen] = window - elif isinstance(windows, dict): - screen_window_dict = windows - else: + if not windows: return - self._impl.enter_presentation_mode(screen_window_dict) + else: + screen_window_dict = dict() + if isinstance(windows, list): + for window, screen in zip(windows, self.screens): + screen_window_dict[screen] = window + elif isinstance(windows, dict): + screen_window_dict = windows + else: + raise TypeError( + "Invalid type for windows parameter. Expected list or dict type." + ) + self._impl.enter_presentation_mode(screen_window_dict) def exit_presentation_mode(self) -> None: """Exit presentation mode.""" diff --git a/core/src/toga/constants/__init__.py b/core/src/toga/constants/__init__.py index 24d8f7ad77..7c9ba2b088 100644 --- a/core/src/toga/constants/__init__.py +++ b/core/src/toga/constants/__init__.py @@ -1,4 +1,4 @@ -from enum import Enum, auto, unique +from enum import Enum, auto from travertino.constants import * # noqa: F401, F403 pragma: no cover @@ -70,12 +70,42 @@ def __str__(self) -> str: ########################################################################## -@unique class WindowState(Enum): """The possible window states of an app.""" - NORMAL = auto() MAXIMIZED = auto() - MINIMIZED = auto() + """``MAXIMIZED`` state is when the window title bar and window chrome, + along with app menu and toolbars remain **visible**.""" + FULLSCREEN = auto() + """``FULLSCREEN`` state is when the window title bar and window chrome + remain **hidden**; But app menu and toolbars remain **visible**.""" + PRESENTATION = auto() + """``PRESENTATION`` state is when the window title bar, window chrome, + app menu and toolbars all remain **hidden**. + + A good example of "full screen" mode is a slideshow app in presentation + mode - the only visible content is the slide.""" + + MINIMIZED = auto() + """``MINIMIZED`` state is: + + For Desktop Platforms: + When the window is in the form of an icon or preview image and is placed in the: + * Taskbar - For Windows + * Dock - For macOS + * Area analogous to Taskbar or Dock - For Linux - Depending upon the DE + + For Mobile Platforms: + When the App is in the background + + """ + NORMAL = auto() + """``NORMAL`` state is when the window/app is not in any of the above window states. + + On Mobile Platforms(Like on Android) - Once the app is in minimized/background state, then + it is currently not possible to bring the app to foreground by setting window state to + ``NORMAL``. This is because of the design decisions imposed by the native mobile platforms. + (i.e., to prevent apps from becoming intrusively foreground against the user's wishes.) + """ diff --git a/core/src/toga/window.py b/core/src/toga/window.py index ee37261709..47382dda64 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -448,34 +448,30 @@ def visible(self, visible: bool) -> None: # Window state ###################################################################### - # -------------------------Deprecated methods------------------------- - # Warnings are disabled as old API tests are still in testbed and warnings will cause error. + # -------------------------Deprecated properties------------------------- @property def full_screen(self) -> bool: """**DEPRECATED** – Use :any:`Window.state`. Is the window in full screen mode? - Full screen mode is *not* the same as "maximized". A full screen window - has no title bar, toolbar or window controls; some or all of these - items may be visible on a maximized window. A good example of "full screen" - mode is a slideshow app in presentation mode - the only visible content is - the slide. + Full screen mode is *not* the same as "maximized". A full screen window has + no title bar or window chrome; But app menu and toolbars will remain visible. """ - # warnings.warn( - # ("`Window.full_screen` is deprecated. Use `Window.state` instead."), - # DeprecationWarning, - # stacklevel=2, - # ) + warnings.warn( + ("`Window.full_screen` is deprecated. Use `Window.state` instead."), + DeprecationWarning, + stacklevel=2, + ) return bool(self.state == WindowState.FULLSCREEN) @full_screen.setter def full_screen(self, is_full_screen: bool) -> None: - # warnings.warn( - # ("`Window.full_screen` is deprecated. Use `Window.state` instead."), - # DeprecationWarning, - # stacklevel=2, - # ) + warnings.warn( + ("`Window.full_screen` is deprecated. Use `Window.state` instead."), + DeprecationWarning, + stacklevel=2, + ) if is_full_screen and (self.state != WindowState.FULLSCREEN): self._impl.set_window_state(WindowState.FULLSCREEN) elif not is_full_screen and (self.state == WindowState.FULLSCREEN): diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index d3fcb304e6..4fc02fe8bb 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -405,21 +405,50 @@ def test_full_screen(event_loop): window1 = toga.Window() window2 = toga.Window() - assert not app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen # If we're not full screen, exiting full screen is a no-op - app.exit_full_screen() - assert not app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", + ): + app.exit_full_screen() + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen assert_action_not_performed(app, "exit presentation mode") # Trying to enter full screen with no windows is a no-op - app.set_full_screen(None) - assert not app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen() + + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen assert_action_not_performed(app, "enter presentation mode") # Enter full screen with 2 windows - app.set_full_screen(window2, app.main_window) - assert app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen(window2, app.main_window) + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert app.is_full_screen assert_action_performed_with( app, "enter presentation mode", @@ -427,8 +456,16 @@ def test_full_screen(event_loop): ) # Change the screens that are full screen - app.set_full_screen(app.main_window, window1) - assert app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen(app.main_window, window1) + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert app.is_full_screen assert_action_performed_with( app, "enter presentation mode", @@ -436,8 +473,16 @@ def test_full_screen(event_loop): ) # Exit full screen mode - app.exit_full_screen() - assert not app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", + ): + app.exit_full_screen() + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen assert_action_performed( app, "exit presentation mode", @@ -450,42 +495,97 @@ def test_set_empty_full_screen_window_list(event_loop): window1 = toga.Window() window2 = toga.Window() - assert not app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen # Change the screens that are full screen - app.set_full_screen(window1, window2) - assert app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen(window1, window2) + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert app.is_full_screen assert_action_performed_with( app, "enter presentation mode", screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, ) # Exit full screen mode by setting no windows full screen - app.set_full_screen() - assert not app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen() + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen assert_action_performed( app, "exit presentation mode", ) -def test_presentation_mode(event_loop): - """The app can be put into presentation mode.""" +def test_presentation_mode_with_windows_list(event_loop): + """The app can enter presentation mode with a windows list.""" app = toga.App(formal_name="Test App", app_id="org.example.test") window1 = toga.Window() window2 = toga.Window() assert not app.is_presentation_mode - # If we're not in presentation mode, exiting presentation mode is a no-op + # Entering presentation mode with an empty windows list, is a no-op: + app.enter_presentation_mode([]) + assert not app.is_presentation_mode + assert_action_not_performed(app, "enter presentation mode") + + # Enter presentation mode with 1 window: + app.enter_presentation_mode([window1]) + assert app.is_presentation_mode + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window1}, + ) + + # Enter presentation mode with 2 windows: + app.enter_presentation_mode([window1, window2]) + assert app.is_presentation_mode + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, + ) + # Exit presentation mode: app.exit_presentation_mode() - assert_action_not_performed(app, "exit presentation mode") + assert_action_performed( + app, + "exit presentation mode", + ) + - # Entering presentation mode without any window is a no-op - app.enter_presentation_mode(None) +def test_presentation_mode_with_screen_windows_dict(event_loop): + """The app can enter presentation mode with a screen-window paired dict.""" + app = toga.App(formal_name="Test App", app_id="org.example.test") + window1 = toga.Window() + window2 = toga.Window() + + assert not app.is_presentation_mode + + # Entering presentation mode with an empty dict, is a no-op: + app.enter_presentation_mode({}) + assert not app.is_presentation_mode assert_action_not_performed(app, "enter presentation mode") - # Enter presentation mode with 1 window: + # Enter presentation mode with an 1 element screen-window dict: app.enter_presentation_mode({app.screens[0]: window1}) assert app.is_presentation_mode assert_action_performed_with( @@ -500,8 +600,8 @@ def test_presentation_mode(event_loop): "exit presentation mode", ) - # Enter presentation mode with 2 windows: - app.enter_presentation_mode([window1, window2]) + # Enter presentation mode with a 2 elements screen-window dict: + app.enter_presentation_mode({app.screens[0]: window1, app.screens[1]: window2}) assert app.is_presentation_mode assert_action_performed_with( app, @@ -515,6 +615,15 @@ def test_presentation_mode(event_loop): "exit presentation mode", ) + +def test_presentation_mode_with_excess_windows_list(event_loop): + """Entering presentation mode limits windows to available displays.""" + app = toga.App(formal_name="Test App", app_id="org.example.test") + window1 = toga.Window() + window2 = toga.Window() + + assert not app.is_presentation_mode + # Entering presentation mode with 3 windows should drop the last window, # as the app has only 2 screens: app.enter_presentation_mode([app.main_window, window2, window1]) @@ -532,6 +641,30 @@ def test_presentation_mode(event_loop): ) +def test_presentation_mode_no_op(event_loop): + """Entering presentation mode with invalid conditions is a no-op.""" + app = toga.App(formal_name="Test App", app_id="org.example.test") + + assert not app.is_presentation_mode + + # If we're not in presentation mode, exiting presentation mode is a no-op. + app.exit_presentation_mode() + assert_action_not_performed(app, "exit presentation mode") + + # Entering presentation mode without any window is a no-op. + with pytest.raises(TypeError): + app.enter_presentation_mode() + assert_action_not_performed(app, "enter presentation mode") + + # Entering presentation mode without proper type of parameter is a no-op. + with pytest.raises( + TypeError, + match="Invalid type for windows parameter. Expected list or dict type.", + ): + app.enter_presentation_mode(app.main_window) + assert_action_not_performed(app, "enter presentation mode") + + def test_show_hide_cursor(app): """The app cursor can be shown and hidden.""" app.hide_cursor() diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index c646f533fd..07ce9f2b72 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -334,17 +334,36 @@ def test_visibility(window, app): def test_full_screen(window, app): """A window can be set full screen.""" - assert not window.full_screen - window.full_screen = True - assert window.full_screen + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + assert not window.full_screen + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + window.full_screen = True + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + assert window.full_screen assert_action_performed_with( window, "set window state to WindowState.FULLSCREEN", state=WindowState.FULLSCREEN, ) - - window.full_screen = False - assert not window.full_screen + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + window.full_screen = False + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + assert not window.full_screen assert_action_performed_with( window, "set window state to WindowState.NORMAL", @@ -357,8 +376,16 @@ def test_full_screen(window, app): # was performed previously and would still be in EventLog. Therefore, we # cannot check if the action was done by the first call or the second call. # Hence, this is just to reach coverage. - assert not window.full_screen - window.full_screen = False + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + assert not window.full_screen + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + window.full_screen = False # assert_action_not_performed(window, "set window state to WindowState.NORMAL") From a76bf9bbdd1ed60562d036d15fbd0707749ffd3e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 17 Jun 2024 06:21:05 -0700 Subject: [PATCH 064/248] Added _PLATFORM_ALLOWS_CLOSE attribute on required backends --- android/src/toga_android/window.py | 1 + cocoa/src/toga_cocoa/window.py | 6 ------ core/src/toga/window.py | 15 ++++++--------- gtk/src/toga_gtk/window.py | 6 ------ iOS/src/toga_iOS/window.py | 1 + textual/src/toga_textual/window.py | 6 ------ web/src/toga_web/window.py | 2 ++ winforms/src/toga_winforms/window.py | 6 ------ 8 files changed, 10 insertions(+), 33 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 6630b0297e..9e1db4465a 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -33,6 +33,7 @@ def onGlobalLayout(self): class Window(Container): _is_main_window = False + _PLATFORM_ALLOWS_CLOSE = False def __init__(self, interface, title, position, size): super().__init__() diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 054b6730f0..fc8739273d 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -213,14 +213,8 @@ def set_title(self, title): ###################################################################### def close(self): - if self.interface.content: - self.interface.content.window = None - self.interface.app.windows.discard(self.interface) - self.native.close() - self.interface._closed = True - def create_toolbar(self): # Purge any existing toolbar items self.purge_toolbar() diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 47382dda64..4c3d3ca1d2 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -299,15 +299,12 @@ def close(self) -> None: undefined, except for :attr:`closed` which can be used to check if the window was closed. """ - # Since on some platforms close() is a no-op, we need to let each backend decide - # whether window discarding from the window registry and other cleanup operations - # should be performed or not. - # - # if self.content: - # self.content.window = None - # self.app.windows.discard(self) - self._impl.close() - # self._closed = True + if getattr(self._impl, "_PLATFORM_ALLOWS_CLOSE", True): + if self.content: + self.content.window = None + self.app.windows.discard(self) + self._impl.close() + self._closed = True @property def closed(self) -> bool: diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 05a48228ef..e779731c93 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -102,15 +102,9 @@ def set_title(self, title): ###################################################################### def close(self): - if self.interface.content: - self.interface.content.window = None - self.interface.app.windows.discard(self.interface) - self._is_closing = True self.native.close() - self.interface._closed = True - def create_toolbar(self): # If there's an existing toolbar, hide it until we know we need it. if self.toolbar_items: diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index 986eda2d40..bb78cd601c 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -26,6 +26,7 @@ class Window: _is_main_window = False + _PLATFORM_ALLOWS_CLOSE = False def __init__(self, interface, title, position, size): self.interface = interface diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index d8c09eaa66..61fafb90e4 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -144,14 +144,8 @@ def set_title(self, title): ###################################################################### def close(self): - if self.interface.content: - self.interface.content.window = None - self.interface.app.windows.discard(self.interface) - self.native.dismiss(None) - self.interface._closed = True - def create_toolbar(self): pass diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index da9a1f2078..8452f95816 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -6,6 +6,8 @@ class Window: + _PLATFORM_ALLOWS_CLOSE = False + def __init__(self, interface, title, position, size): self.interface = interface self.interface._impl = self diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index ceec0c78cc..50734551c8 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -91,15 +91,9 @@ def set_title(self, title): ###################################################################### def close(self): - if self.interface.content: - self.interface.content.window = None - self.interface.app.windows.discard(self.interface) - self._is_closing = True self.native.Close() - self.interface._closed = True - def create_toolbar(self): if self.interface.toolbar: if self.toolbar_native: From f0d5d80dd039fc37d0fbd74595f8853ae556ee94 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 17 Jun 2024 06:36:33 -0700 Subject: [PATCH 065/248] Handled deprecation warning on testbed tests --- testbed/tests/app/test_app.py | 79 ++++++++++++++++++++++++----- testbed/tests/window/test_window.py | 36 ++++++++++--- 2 files changed, 95 insertions(+), 20 deletions(-) diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index 831826b4b4..68ca86a548 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -35,8 +35,16 @@ async def test_full_screen(app): """Window can be made full screen""" # Invoke the methods to verify the endpoints exist. However, they're no-ops, # so there's nothing to test. - app.set_full_screen(app.current_window) - app.exit_full_screen() + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen(app.current_window) + with pytest.warns( + DeprecationWarning, + match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", + ): + app.exit_full_screen() async def test_presentation_mode(app, app_probe, main_window, main_window_probe): """The app can enter into presentation mode""" @@ -258,20 +266,31 @@ async def test_full_screen(app, app_probe): window2.show() # Add delay for gtk to show the windows await app_probe.redraw("Extra windows are visible", delay=0.1) - - assert not app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen assert not app_probe.is_full_screen(window1) assert not app_probe.is_full_screen(window2) initial_content1_size = app_probe.content_size(window1) initial_content2_size = app_probe.content_size(window2) # Make window 2 full screen via the app - app.set_full_screen(window2) + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen(window2) await window2_probe.wait_for_window( "Second extra window is full screen", full_screen=True, ) - assert app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert app.is_full_screen assert not app_probe.is_full_screen(window1) assert app_probe.content_size(window1) == initial_content1_size @@ -281,12 +300,20 @@ async def test_full_screen(app, app_probe): assert app_probe.content_size(window2)[1] > 700 # Make window 1 full screen via the app, window 2 no longer full screen - app.set_full_screen(window1) + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen(window1) await window1_probe.wait_for_window( "First extra window is full screen", full_screen=True, ) - assert app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert app.is_full_screen assert app_probe.is_full_screen(window1) assert app_probe.content_size(window1)[0] > 1000 @@ -296,13 +323,21 @@ async def test_full_screen(app, app_probe): assert app_probe.content_size(window2) == initial_content2_size # Exit full screen - app.exit_full_screen() + with pytest.warns( + DeprecationWarning, + match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", + ): + app.exit_full_screen() await window1_probe.wait_for_window( "No longer full screen", full_screen=True, ) - assert not app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen assert not app_probe.is_full_screen(window1) assert app_probe.content_size(window1) == initial_content1_size @@ -311,13 +346,21 @@ async def test_full_screen(app, app_probe): assert app_probe.content_size(window2) == initial_content2_size # Go full screen again on window 1 - app.set_full_screen(window1) + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen(window1) # A longer delay to allow for genie animations await window1_probe.wait_for_window( "First extra window is full screen", full_screen=True, ) - assert app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert app.is_full_screen assert app_probe.is_full_screen(window1) assert app_probe.content_size(window1)[0] > 1000 @@ -327,13 +370,21 @@ async def test_full_screen(app, app_probe): assert app_probe.content_size(window2) == initial_content2_size # Exit full screen by passing no windows - app.set_full_screen() + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen() await window1_probe.wait_for_window( "No longer full screen", full_screen=True, ) - assert not app.is_full_screen + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen assert not app_probe.is_full_screen(window1) assert app_probe.content_size(window1) == initial_content1_size diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index c8df083677..69c86d1d21 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -143,10 +143,18 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): async def test_full_screen(main_window, main_window_probe): """Window can be made full screen""" - main_window.full_screen = True + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + main_window.full_screen = True await main_window_probe.wait_for_window("Main window is in full screen") - main_window.full_screen = False + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + main_window.full_screen = False await main_window_probe.wait_for_window( "Main window is no longer in full screen" ) @@ -556,7 +564,11 @@ async def test_full_screen(second_window, second_window_probe): assert second_window_probe.is_resizable initial_content_size = second_window_probe.content_size - second_window.full_screen = True + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + second_window.full_screen = True # A longer delay to allow for genie animations await second_window_probe.wait_for_window( "Secondary window is full screen", @@ -566,7 +578,11 @@ async def test_full_screen(second_window, second_window_probe): assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] - second_window.full_screen = True + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + second_window.full_screen = True await second_window_probe.wait_for_window( "Secondary window is still full screen" ) @@ -574,7 +590,11 @@ async def test_full_screen(second_window, second_window_probe): assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] - second_window.full_screen = False + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + second_window.full_screen = False # A longer delay to allow for genie animations await second_window_probe.wait_for_window( "Secondary window is not full screen", @@ -584,7 +604,11 @@ async def test_full_screen(second_window, second_window_probe): assert second_window_probe.is_resizable assert second_window_probe.content_size == initial_content_size - second_window.full_screen = False + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + second_window.full_screen = False await second_window_probe.wait_for_window( "Secondary window is still not full screen" ) From e416f20a635d766cdf35e9cc22eab3af6a0ae4fc Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 17 Jun 2024 07:01:15 -0700 Subject: [PATCH 066/248] Removed explicit closing from dummy backend --- dummy/src/toga_dummy/window.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 7fbbe943ce..e353197b8d 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -96,15 +96,9 @@ def get_visible(self): return self._get_value("visible") def close(self): - if self.interface.content: - self.interface.content.window = None - self.interface.app.windows.discard(self.interface) - self._action("close") self._set_value("visible", False) - self.interface._closed = True - def get_image_data(self): self._action("get image data") path = Path(toga_dummy.__file__).parent / "resources/screenshot.png" From 1fa8cdef0363fe7e7691d6c55ad202c337573451 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 17 Jun 2024 07:22:08 -0700 Subject: [PATCH 067/248] 100% coverage on core --- core/tests/window/test_window.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index 07ce9f2b72..6d55e7e4f6 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -510,6 +510,31 @@ def test_close_direct(window, app): on_close_handler.assert_not_called() +def test_close_platform_disallowed(window, app): + """A window cannot be closed directly on some platforms.""" + # Explicitly set to indicate that platform disallows direct close. + window._impl._PLATFORM_ALLOWS_CLOSE = False + + on_close_handler = Mock(return_value=True) + window.on_close = on_close_handler + + window.show() + assert window.app == app + assert window in app.windows + + # Close the window directly + window.close() + + # Window has *not* been closed, and the close handler has *not* been invoked. + assert not window.closed + assert window.app == app + assert window in app.windows + assert_action_not_performed(window, "close") + on_close_handler.assert_not_called() + + del window._impl._PLATFORM_ALLOWS_CLOSE + + def test_close_no_handler(window, app): """A window without a close handler can be closed.""" window.show() From 6350b5f7fca3666496a49b4942abf973fb3dc982 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 17 Jun 2024 07:44:24 -0700 Subject: [PATCH 068/248] 100% coverage on Android & iOS --- android/src/toga_android/window.py | 2 +- iOS/src/toga_iOS/window.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 9e1db4465a..f5f00503a0 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -61,7 +61,7 @@ def set_title(self, title): ###################################################################### def close(self): - pass + pass # pragma: no cover def create_toolbar(self): self.app.native.invalidateOptionsMenu() diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index bb78cd601c..e9583fa3bc 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -74,7 +74,7 @@ def set_title(self, title): ###################################################################### def close(self): - pass + pass # pragma: no cover def create_toolbar(self): pass # pragma: no cover From d091955b73eb9afd4d431a13b466b15fb31e635e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 17 Jun 2024 08:02:48 -0700 Subject: [PATCH 069/248] Restart CI for windows failure From 564c5ed2fe7c3b069d62df491e5c7746d3720731 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 22 Jun 2024 18:48:22 -0700 Subject: [PATCH 070/248] Updated to latest `main` branch --- core/tests/window/test_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index a824d130b9..01f5d1d3de 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -457,8 +457,8 @@ def test_non_resizable_window_state(state): non_resizable_window.close() -def test_window_state_invalid_value(window): - # Setting window state to any value other than a WindowState enum should raise a ValueError +def test_window_state_invalid_parameter(window): + # Setting window state to any value other than a WindowState enum should raise a ValueError. with pytest.raises( ValueError, match="Invalid type for state parameter. Expected WindowState enum type.", From e36e59629abf766a016b19a0c899f4c4c1217eb8 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Sat, 22 Jun 2024 18:55:21 -0700 Subject: [PATCH 071/248] Delete changes/1857.bugfix.rst --- changes/1857.bugfix.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changes/1857.bugfix.rst diff --git a/changes/1857.bugfix.rst b/changes/1857.bugfix.rst deleted file mode 100644 index 69d1e45965..0000000000 --- a/changes/1857.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Calling `Window.close()` on platforms like Android and iOS now correctly performs a no-op and doesn't clear the window from the window registry. From 17564a8efe501eabd6cf11119ebbdd23a6194a85 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 26 Jun 2024 02:49:11 -0700 Subject: [PATCH 072/248] Fixed testbed failures --- core/tests/app/test_app.py | 2 +- testbed/tests/app/test_app.py | 275 +++++++++++++++++---------- testbed/tests/window/test_window.py | 27 ++- winforms/src/toga_winforms/window.py | 8 +- 4 files changed, 203 insertions(+), 109 deletions(-) diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 9de9695421..59caf91051 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -581,7 +581,7 @@ def test_presentation_mode_with_windows_list(event_loop): ) -def test_presentation_mode_with_screen_windows_dict(event_loop): +def test_presentation_mode_with_screen_window_dict(event_loop): """The app can enter presentation mode with a screen-window paired dict.""" app = toga.App(formal_name="Test App", app_id="org.example.test") window1 = toga.Window() diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index b452723988..3489a36971 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -1,3 +1,4 @@ +import random from unittest.mock import Mock import pytest @@ -429,135 +430,213 @@ async def test_full_screen(app, app_probe): window1.close() window2.close() - async def test_presentation_mode(app, app_probe, main_window, main_window_probe): - """The app can enter into presentation mode""" + async def test_presentation_mode_with_main_window( + app, app_probe, main_window, main_window_probe + ): + """The app can enter presentation mode with just the main window.""" + # This test is required since the toolbar is present only on the main window. try: - window1 = toga.Window("Test Window 1", position=(150, 150), size=(200, 200)) - window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) - window2 = toga.Window("Test Window 2", position=(400, 150), size=(200, 200)) - window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - window2.toolbar.add(app.cmd1) - window1_probe = window_probe(app, window1) - window2_probe = window_probe(app, window2) - - window1.show() - window2.show() - - # Add delay for gtk to show the windows - await app_probe.redraw("Extra windows are visible", delay=0.1) - - assert not app.is_presentation_mode - assert not main_window_probe.is_window_state(WindowState.PRESENTATION) - assert not window1_probe.is_window_state(WindowState.PRESENTATION) - assert window2_probe.has_toolbar() - assert not window2_probe.is_window_state(WindowState.PRESENTATION) - initial_content_main_window_size = ( + main_window.toolbar.add(app.cmd1) + main_window_initial_content_size = ( main_window_probe.presentation_content_size ) - initial_content1_size = window1_probe.presentation_content_size - initial_content2_size = window2_probe.presentation_content_size + extra_window = toga.Window( + "Extra Test Window", position=(150, 150), size=(200, 200) + ) + extra_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + extra_window.show() + extra_window_probe = window_probe(app, extra_window) + extra_window_initial_content_size = ( + extra_window_probe.presentation_content_size + ) + + # Add delay for gtk to show the windows + await app_probe.redraw("Extra windows is visible", delay=0.1) - # Enter presentation mode with main window via the app + # Enter presentation mode with only main window app.enter_presentation_mode([main_window]) - await main_window_probe.wait_for_window( - "Main window is in presentation mode", - full_screen=True, - ) + # Add delay for gtk to show the windows + await app_probe.redraw("App is in presentation mode", delay=0.1) assert app.is_presentation_mode - assert not window1_probe.is_window_state(WindowState.PRESENTATION) - assert window1_probe.presentation_content_size == initial_content1_size - assert not window2_probe.is_window_state(WindowState.PRESENTATION) - assert window2_probe.presentation_content_size == initial_content2_size + # Extra window should not be in presentation mode. + assert not extra_window_probe.is_window_state( + WindowState.PRESENTATION + ), "Extra Window:" + assert ( + extra_window_probe.presentation_content_size + == extra_window_initial_content_size + ), "Extra Window" + # Main Window should be in presentation mode. assert main_window_probe.is_window_state(WindowState.PRESENTATION) assert main_window_probe.presentation_content_size[0] > 1000 assert main_window_probe.presentation_content_size[1] > 700 + # Exit presentation mode app.exit_presentation_mode() - await main_window_probe.wait_for_window( - "Main window is no longer in presentation mode", - full_screen=True, - ) + await app_probe.redraw("App is no longer in presentation mode", delay=0.1) assert not app.is_presentation_mode + assert ( main_window_probe.presentation_content_size - == initial_content_main_window_size + == main_window_initial_content_size ) - # Enter presentation mode with window 2 via the app - app.enter_presentation_mode([window2]) - await window2_probe.wait_for_window( - "Second extra window is in presentation mode", - full_screen=True, - ) - assert app.is_presentation_mode + finally: + main_window.toolbar.clear() + extra_window.close() + + async def test_presentation_mode_with_screen_window_dict(app, app_probe): + """The app can enter presentation mode with a screen-window paired dict.""" + try: + window_information_list = list() + windows_list = list() + for i in range(len(app.screens)): + window = toga.Window( + title=f"Test Window {i}", + position=(150 + (10 * i), 150 + (10 * i)), + size=(200, 200), + ) + r = random.randint(0, 255) + g = random.randint(0, 255) + b = random.randint(0, 255) + window.content = toga.Box( + style=Pack(background_color=f"#{r:02X}{g:02X}{b:02X}") + ) + window.show() + # Add delay for gtk to show the windows + await app_probe.redraw(f"Test Window {i} is visible", delay=0.1) + + window_information = dict() + window_information["window"] = window + window_information["window_probe"] = window_probe(app, window) + window_information["initial_content_size"] = window_information[ + "window_probe" + ].presentation_content_size + + window_information_list.append(window_information) + windows_list.append(window) + + screen_window_dict = dict() + for window, screen in zip(windows_list, app.screens): + screen_window_dict[screen] = window + + # Enter presentation mode with a screen-window dict via the app + app.enter_presentation_mode(screen_window_dict) + # Add delay for gtk to show the windows + await app_probe.redraw("App is in presentation mode", delay=0.1) - assert not window1_probe.is_window_state(WindowState.PRESENTATION) - assert window1_probe.presentation_content_size == initial_content1_size + assert app.is_presentation_mode + for window_information in window_information_list: + assert window_information["window_probe"].is_window_state( + WindowState.PRESENTATION + ), f"{window_information['window'].title}:" + assert ( + window_information["window_probe"].presentation_content_size[0] + > 1000 + ), f"{window_information['window'].title}:" + assert ( + window_information["window_probe"].presentation_content_size[1] + > 700 + ), f"{window_information['window'].title}:" - assert window2_probe.is_window_state(WindowState.PRESENTATION) - assert window2_probe.presentation_content_size[0] > 1000 - assert window2_probe.presentation_content_size[1] > 700 # Exit presentation mode app.exit_presentation_mode() - await window2_probe.wait_for_window( - "Second extra window is no longer in presentation mode", - full_screen=True, - ) + await app_probe.redraw("App is no longer in presentation mode", delay=0.1) + assert not app.is_presentation_mode - assert window2_probe.presentation_content_size == initial_content2_size + for window_information in window_information_list: + assert ( + window_information["window_probe"].presentation_content_size + == window_information["initial_content_size"] + ), f"{window_information['window'].title}:" - # Enter presentation mode with a screen-window1 dict via the app - app.enter_presentation_mode({app.screens[0]: window1}) - await window1_probe.wait_for_window( - "First extra window is in presentation mode", - full_screen=True, - ) + finally: + for window in windows_list: + window.close() + + async def test_presentation_mode_with_excess_windows_list(app, app_probe): + """Entering presentation mode limits windows to available displays.""" + try: + window_information_list = list() + excess_windows_list = list() + for i in range(len(app.screens) + 1): + window = toga.Window( + title=f"Test Window {i}", + position=(150 + (10 * i), 150 + (10 * i)), + size=(200, 200), + ) + r = random.randint(0, 255) + g = random.randint(0, 255) + b = random.randint(0, 255) + window.content = toga.Box( + style=Pack(background_color=f"#{r:02X}{g:02X}{b:02X}") + ) + window.show() + # Add delay for gtk to show the windows + await app_probe.redraw(f"Test Window {i} is visible", delay=0.1) + + window_information = dict() + window_information["window"] = window + window_information["window_probe"] = window_probe(app, window) + window_information["initial_content_size"] = window_information[ + "window_probe" + ].presentation_content_size + + window_information_list.append(window_information) + excess_windows_list.append(window) + + last_window = window_information_list[-1]["window"] + last_window_probe = window_information_list[-1]["window_probe"] + last_window_initial_content_size = window_information_list[-1][ + "initial_content_size" + ] + + # Enter presentation mode with excess windows via the app, but + # the last window should be dropped as there are less screens. + app.enter_presentation_mode(excess_windows_list) + # Add delay for gtk to show the windows + await app_probe.redraw("App is in presentation mode", delay=0.1) assert app.is_presentation_mode - assert not window2_probe.is_window_state(WindowState.PRESENTATION) - assert window2_probe.presentation_content_size == initial_content2_size + # Last window should not be in presentation mode. + assert not last_window_probe.is_window_state( + WindowState.PRESENTATION + ), f"Last Window({last_window.title}):" + assert ( + last_window_probe.presentation_content_size + == last_window_initial_content_size + ), f"Last Window({last_window.title}):" + + # All other windows should be in presentation mode. + for window_information in window_information_list[:-1]: + assert window_information["window_probe"].is_window_state( + WindowState.PRESENTATION + ), f"{window_information['window'].title}:" + assert ( + window_information["window_probe"].presentation_content_size[0] + > 1000 + ), f"{window_information['window'].title}:" + assert ( + window_information["window_probe"].presentation_content_size[1] + > 700 + ), f"{window_information['window'].title}:" - assert window1_probe.is_window_state(WindowState.PRESENTATION) - assert window1_probe.presentation_content_size[0] > 1000 - assert window1_probe.presentation_content_size[1] > 700 # Exit presentation mode app.exit_presentation_mode() - await window1_probe.wait_for_window( - "First extra window is no longer in presentation mode", - full_screen=True, - ) + await app_probe.redraw("App is no longer in presentation mode", delay=0.1) assert not app.is_presentation_mode - assert window1_probe.presentation_content_size == initial_content1_size - - if len(app.screens) < 2: - # Enter presentation mode with 2 windows via the app, but the - # second window should be dropped as there is only 1 screen. - app.enter_presentation_mode([window1, window2]) - await window1_probe.wait_for_window( - "First extra window is in presentation mode", - full_screen=True, - ) - assert app.is_presentation_mode - - assert not window2_probe.is_window_state(WindowState.PRESENTATION) - assert window2_probe.presentation_content_size == initial_content2_size - - assert window1_probe.is_window_state(WindowState.PRESENTATION) - assert window1_probe.presentation_content_size[0] > 1000 - assert window1_probe.presentation_content_size[1] > 700 - # Exit presentation mode - app.exit_presentation_mode() - await window1_probe.wait_for_window( - "First extra window is no longer in presentation mode", - full_screen=True, - ) - assert not app.is_presentation_mode - assert window1_probe.presentation_content_size == initial_content1_size + + for window_information in window_information_list: + assert ( + window_information["window_probe"].presentation_content_size + == window_information["initial_content_size"] + ), f"{window_information['window'].title}:" + finally: - window1.close() - window2.close() + for window in excess_windows_list: + window.close() async def test_show_hide_cursor(app, app_probe): """The app cursor can be hidden and shown""" diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 4237e6059c..c827a446ac 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -716,8 +716,13 @@ async def test_window_state_minimized(second_window, second_window_probe): assert not second_window_probe.is_window_state(WindowState.MINIMIZED) @pytest.mark.parametrize( - "second_window_kwargs", - [dict(title="Secondary Window", position=(200, 150))], + "second_window_class, second_window_kwargs", + [ + ( + toga.Window, + dict(title="Secondary Window", position=(200, 150)), + ) + ], ) async def test_window_state_maximized(second_window, second_window_probe): """Window can have maximized window state""" @@ -760,8 +765,13 @@ async def test_window_state_maximized(second_window, second_window_probe): assert second_window_probe.content_size == initial_content_size @pytest.mark.parametrize( - "second_window_kwargs", - [dict(title="Secondary Window", position=(200, 150))], + "second_window_class, second_window_kwargs", + [ + ( + toga.Window, + dict(title="Secondary Window", position=(200, 150)), + ) + ], ) async def test_window_state_full_screen(second_window, second_window_probe): """Window can have full screen window state""" @@ -810,8 +820,13 @@ async def test_window_state_full_screen(second_window, second_window_probe): assert second_window_probe.content_size == initial_content_size @pytest.mark.parametrize( - "second_window_kwargs", - [dict(title="Secondary Window", position=(200, 150))], + "second_window_class, second_window_kwargs", + [ + ( + toga.Window, + dict(title="Secondary Window", position=(200, 150)), + ) + ], ) async def test_window_state_presentation(second_window, second_window_probe): """Window can have presentation window state""" diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index af39414907..5b48932128 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -213,7 +213,7 @@ def set_window_state(self, state): if current_state == WindowState.PRESENTATION: if self.native.MainMenuStrip: self.native.MainMenuStrip.Visible = True - if self.toolbar_native: + if getattr(self, "toolbar_native", None): self.toolbar_native.Visible = True self.interface.screen = self._before_presentation_mode_screen @@ -241,7 +241,7 @@ def set_window_state(self, state): self._before_presentation_mode_screen = self.interface.screen if self.native.MainMenuStrip: self.native.MainMenuStrip.Visible = False - if self.toolbar_native: + if getattr(self, "toolbar_native", None): self.toolbar_native.Visible = False self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") self.native.WindowState = WinForms.FormWindowState.Maximized @@ -278,9 +278,9 @@ def create(self): def _top_bars_height(self): vertical_shift = 0 - if self.toolbar_native: + if self.toolbar_native and self.toolbar_native.Visible: vertical_shift += self.toolbar_native.Height - if self.native.MainMenuStrip: + if self.native.MainMenuStrip and self.native.MainMenuStrip.Visible: vertical_shift += self.native.MainMenuStrip.Height return vertical_shift From 51cea9354b1861da44e6ac8ba5df5e24b40948b8 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Wed, 26 Jun 2024 02:52:13 -0700 Subject: [PATCH 073/248] Restart CI for pre-commit failure From feb78a8681fa644e7f9ec573f73ee6eacbc1197b Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 26 Jun 2024 03:08:27 -0700 Subject: [PATCH 074/248] =?UTF-8?q?=C2=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- testbed/tests/window/test_window.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index c827a446ac..565d3015d2 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -883,8 +883,13 @@ async def test_window_state_presentation(second_window, second_window_probe): assert second_window_probe.presentation_content_size == initial_content_size @pytest.mark.parametrize( - "second_window_kwargs", - [dict(title="Secondary Window", position=(200, 150))], + "second_window_class, second_window_kwargs", + [ + ( + toga.Window, + 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.""" From 8286b860c10e81bb6c44cff702b44eb09427bc03 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 26 Jun 2024 04:42:03 -0700 Subject: [PATCH 075/248] =?UTF-8?q?=C2=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gtk/src/toga_gtk/window.py | 10 ++++------ testbed/tests/app/test_app.py | 2 ++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 309979801f..84a1c9e17f 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -184,9 +184,8 @@ def set_window_state(self, state): elif current_state == WindowState.PRESENTATION: if isinstance(self.native, Gtk.ApplicationWindow): self.native.set_show_menubar(True) - # self.native_toolbar is always set to Gtk.Toolbar(), so no need - # to check if self.native_toolbar exists. - self.native_toolbar.set_visible(True) + if getattr(self, "native_toolbar", None): + self.native_toolbar.set_visible(True) self.native.unfullscreen() self.interface.screen = self._before_presentation_mode_screen @@ -207,9 +206,8 @@ def set_window_state(self, state): self._before_presentation_mode_screen = self.interface.screen if isinstance(self.native, Gtk.ApplicationWindow): self.native.set_show_menubar(False) - # self.native_toolbar is always set to Gtk.Toolbar(), so no need - # to check if self.native_toolbar exists. - self.native_toolbar.set_visible(False) + if getattr(self, "native_toolbar", None): + self.native_toolbar.set_visible(False) self.native.fullscreen() self._is_presentation_mode = True else: # pragma: no cover diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index 3489a36971..de18d74ece 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -437,6 +437,7 @@ async def test_presentation_mode_with_main_window( # This test is required since the toolbar is present only on the main window. try: main_window.toolbar.add(app.cmd1) + main_window.show() main_window_initial_content_size = ( main_window_probe.presentation_content_size ) @@ -528,6 +529,7 @@ async def test_presentation_mode_with_screen_window_dict(app, app_probe): await app_probe.redraw("App is in presentation mode", delay=0.1) assert app.is_presentation_mode + # All the windows should be in presentation mode. for window_information in window_information_list: assert window_information["window_probe"].is_window_state( WindowState.PRESENTATION From 16d5758cd6360d724d559b1f942aa489ff90d33b Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 26 Jun 2024 05:05:31 -0700 Subject: [PATCH 076/248] Fix testbed for macOS --- testbed/tests/app/test_app.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index de18d74ece..3e8eaa2cad 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -437,23 +437,21 @@ async def test_presentation_mode_with_main_window( # This test is required since the toolbar is present only on the main window. try: main_window.toolbar.add(app.cmd1) - main_window.show() - main_window_initial_content_size = ( - main_window_probe.presentation_content_size - ) extra_window = toga.Window( "Extra Test Window", position=(150, 150), size=(200, 200) ) extra_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) extra_window.show() extra_window_probe = window_probe(app, extra_window) + # Add delay for gtk to show the windows + await app_probe.redraw("Extra window is visible", delay=0.1) + + main_window_initial_content_size = ( + main_window_probe.presentation_content_size + ) extra_window_initial_content_size = ( extra_window_probe.presentation_content_size ) - - # Add delay for gtk to show the windows - await app_probe.redraw("Extra windows is visible", delay=0.1) - # Enter presentation mode with only main window app.enter_presentation_mode([main_window]) # Add delay for gtk to show the windows From 5b59b1481dd86f91d15518b9f3d472c93848bb30 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 26 Jun 2024 08:59:18 -0400 Subject: [PATCH 077/248] Wayland test --- testbed/tests/app/test_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index 3e8eaa2cad..58346538a4 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -444,7 +444,7 @@ async def test_presentation_mode_with_main_window( extra_window.show() extra_window_probe = window_probe(app, extra_window) # Add delay for gtk to show the windows - await app_probe.redraw("Extra window is visible", delay=0.1) + await app_probe.redraw("Extra window is visible", delay=0.5) main_window_initial_content_size = ( main_window_probe.presentation_content_size @@ -474,7 +474,7 @@ async def test_presentation_mode_with_main_window( # Exit presentation mode app.exit_presentation_mode() - await app_probe.redraw("App is no longer in presentation mode", delay=0.1) + await app_probe.redraw("App is no longer in presentation mode", delay=0.5) assert not app.is_presentation_mode assert ( From 85dd20239c8abb972c12f0cb2b2e9117ae35ae45 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 26 Jun 2024 09:17:03 -0400 Subject: [PATCH 078/248] Fix testbed for wayland --- testbed/tests/app/test_app.py | 14 +++++++------- testbed/tests/window/test_window.py | 5 +++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index 58346538a4..1a07d113c3 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -455,7 +455,7 @@ async def test_presentation_mode_with_main_window( # Enter presentation mode with only main window app.enter_presentation_mode([main_window]) # Add delay for gtk to show the windows - await app_probe.redraw("App is in presentation mode", delay=0.1) + await app_probe.redraw("App is in presentation mode", delay=0.5) assert app.is_presentation_mode # Extra window should not be in presentation mode. @@ -505,7 +505,7 @@ async def test_presentation_mode_with_screen_window_dict(app, app_probe): ) window.show() # Add delay for gtk to show the windows - await app_probe.redraw(f"Test Window {i} is visible", delay=0.1) + await app_probe.redraw(f"Test Window {i} is visible", delay=0.5) window_information = dict() window_information["window"] = window @@ -524,7 +524,7 @@ async def test_presentation_mode_with_screen_window_dict(app, app_probe): # Enter presentation mode with a screen-window dict via the app app.enter_presentation_mode(screen_window_dict) # Add delay for gtk to show the windows - await app_probe.redraw("App is in presentation mode", delay=0.1) + await app_probe.redraw("App is in presentation mode", delay=0.5) assert app.is_presentation_mode # All the windows should be in presentation mode. @@ -543,7 +543,7 @@ async def test_presentation_mode_with_screen_window_dict(app, app_probe): # Exit presentation mode app.exit_presentation_mode() - await app_probe.redraw("App is no longer in presentation mode", delay=0.1) + await app_probe.redraw("App is no longer in presentation mode", delay=0.5) assert not app.is_presentation_mode for window_information in window_information_list: @@ -575,7 +575,7 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): ) window.show() # Add delay for gtk to show the windows - await app_probe.redraw(f"Test Window {i} is visible", delay=0.1) + await app_probe.redraw(f"Test Window {i} is visible", delay=0.5) window_information = dict() window_information["window"] = window @@ -597,7 +597,7 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): # the last window should be dropped as there are less screens. app.enter_presentation_mode(excess_windows_list) # Add delay for gtk to show the windows - await app_probe.redraw("App is in presentation mode", delay=0.1) + await app_probe.redraw("App is in presentation mode", delay=0.5) assert app.is_presentation_mode # Last window should not be in presentation mode. @@ -625,7 +625,7 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): # Exit presentation mode app.exit_presentation_mode() - await app_probe.redraw("App is no longer in presentation mode", delay=0.1) + await app_probe.redraw("App is no longer in presentation mode", delay=0.5) assert not app.is_presentation_mode for window_information in window_information_list: diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 565d3015d2..8711b72bcc 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -679,6 +679,11 @@ async def test_full_screen(second_window, second_window_probe): ) async def test_window_state_minimized(second_window, second_window_probe): """Window can have minimized window state""" + if not second_window_probe.supports_minimize: + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) assert second_window_probe.is_window_state(WindowState.NORMAL) From 552c0fc9f6f3f19eb76a1a25db441007acf4bf0f Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 26 Jun 2024 09:25:33 -0400 Subject: [PATCH 079/248] Fix testbed for wayland --- gtk/src/toga_gtk/window.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 84a1c9e17f..7b7c9e7ed3 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -156,7 +156,7 @@ def get_window_state(self): if window_state_flags & Gdk.WindowState.MAXIMIZED: return WindowState.MAXIMIZED elif window_state_flags & Gdk.WindowState.ICONIFIED: - return WindowState.MINIMIZED + return WindowState.MINIMIZED # pragma: no-cover-if-linux-wayland elif window_state_flags & Gdk.WindowState.FULLSCREEN: # Use a shadow variable since a window without any app menu and toolbar # in presentation mode would be indistinguishable from full screen mode. @@ -176,7 +176,7 @@ def set_window_state(self, state): # Deminiaturize the window to restore it to its previous state elif current_state == WindowState.MINIMIZED: # deconify() doesn't work - self.native.present() + self.native.present() # pragma: no-cover-if-linux-wayland # If the window is in full-screen mode, exit full-screen mode elif current_state == WindowState.FULLSCREEN: self.native.unfullscreen() @@ -196,7 +196,7 @@ def set_window_state(self, state): self.native.maximize() elif state == WindowState.MINIMIZED: - self.native.iconify() + self.native.iconify() # pragma: no-cover-if-linux-wayland elif state == WindowState.FULLSCREEN: self.native.fullscreen() From 56edec172d390e2951d7899fce6c5a8fa689cf71 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:34:04 -0400 Subject: [PATCH 080/248] Restart CI for macOS From 934b6b13943dcf73fefa85f532a9f40289852802 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 27 Jun 2024 08:13:59 -0700 Subject: [PATCH 081/248] Updated to latest main branch --- testbed/tests/app/test_mobile.py | 124 ++++++++++++++++--------------- 1 file changed, 63 insertions(+), 61 deletions(-) diff --git a/testbed/tests/app/test_mobile.py b/testbed/tests/app/test_mobile.py index a4da9040b3..66f29198b7 100644 --- a/testbed/tests/app/test_mobile.py +++ b/testbed/tests/app/test_mobile.py @@ -17,67 +17,69 @@ async def test_show_hide_cursor(app): app.show_cursor() app.hide_cursor() - async def test_full_screen(app): - """Window can be made full screen""" - # Invoke the methods to verify the endpoints exist. However, they're no-ops, - # so there's nothing to test. - with pytest.warns( - DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", - ): - app.set_full_screen(app.current_window) - with pytest.warns( - DeprecationWarning, - match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", - ): - app.exit_full_screen() - - async def test_presentation_mode(app, app_probe, main_window, main_window_probe): - """The app can enter into presentation mode""" - assert not app.is_presentation_mode - assert not main_window_probe.is_window_state(WindowState.PRESENTATION) - - # Enter presentation mode with main window via the app - app.enter_presentation_mode([main_window]) - await main_window_probe.wait_for_window( - "Main window is in presentation mode", full_screen=True - ) - - assert app.is_presentation_mode - assert not main_window_probe.is_window_state(WindowState.NORMAL) - assert main_window_probe.is_window_state(WindowState.PRESENTATION) - - # Exit presentation mode - app.exit_presentation_mode() - await main_window_probe.wait_for_window( - "Main window is no longer in presentation mode", - full_screen=True, - ) - - assert not app.is_presentation_mode - assert not main_window_probe.is_window_state(WindowState.PRESENTATION) - assert main_window_probe.is_window_state(WindowState.NORMAL) - - # Enter presentation mode with a screen-window dict via the app - app.enter_presentation_mode({app.screens[0]: main_window}) - await main_window_probe.wait_for_window( - "Main window is in presentation mode", full_screen=True - ) - - assert app.is_presentation_mode - assert not main_window_probe.is_window_state(WindowState.NORMAL) - assert main_window_probe.is_window_state(WindowState.PRESENTATION) - - # Exit presentation mode - app.exit_presentation_mode() - await main_window_probe.wait_for_window( - "Main window is no longer in presentation mode", - full_screen=True, - ) - - assert not app.is_presentation_mode - assert not main_window_probe.is_window_state(WindowState.PRESENTATION) - assert main_window_probe.is_window_state(WindowState.NORMAL) + +async def test_full_screen(app): + """Window can be made full screen""" + # Invoke the methods to verify the endpoints exist. However, they're no-ops, + # so there's nothing to test. + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen(app.current_window) + with pytest.warns( + DeprecationWarning, + match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", + ): + app.exit_full_screen() + + +async def test_presentation_mode(app, app_probe, main_window, main_window_probe): + """The app can enter into presentation mode""" + assert not app.is_presentation_mode + assert not main_window_probe.is_window_state(WindowState.PRESENTATION) + + # Enter presentation mode with main window via the app + app.enter_presentation_mode([main_window]) + await main_window_probe.wait_for_window( + "Main window is in presentation mode", full_screen=True + ) + + assert app.is_presentation_mode + assert not main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.is_window_state(WindowState.PRESENTATION) + + # Exit presentation mode + app.exit_presentation_mode() + await main_window_probe.wait_for_window( + "Main window is no longer in presentation mode", + full_screen=True, + ) + + assert not app.is_presentation_mode + assert not main_window_probe.is_window_state(WindowState.PRESENTATION) + assert main_window_probe.is_window_state(WindowState.NORMAL) + + # Enter presentation mode with a screen-window dict via the app + app.enter_presentation_mode({app.screens[0]: main_window}) + await main_window_probe.wait_for_window( + "Main window is in presentation mode", full_screen=True + ) + + assert app.is_presentation_mode + assert not main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.is_window_state(WindowState.PRESENTATION) + + # Exit presentation mode + app.exit_presentation_mode() + await main_window_probe.wait_for_window( + "Main window is no longer in presentation mode", + full_screen=True, + ) + + assert not app.is_presentation_mode + assert not main_window_probe.is_window_state(WindowState.PRESENTATION) + assert main_window_probe.is_window_state(WindowState.NORMAL) async def test_current_window(app, main_window, main_window_probe): From 426e8b92efe1b53a5add962d6182fe6ef614b35a Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 29 Jun 2024 02:23:35 -0700 Subject: [PATCH 082/248] Updated to latest main branch --- core/src/toga/app.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index af19a9d0fe..ee01c2083a 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -13,11 +13,8 @@ from weakref import WeakValueDictionary from toga.command import CommandSet - from toga.constants import WindowState - from toga.handlers import simple_handler, wrapped_handler - from toga.hardware.camera import Camera from toga.hardware.location import Location from toga.icons import Icon From 4d83d19a5377b90cab12b826a69ae003bbfb767d Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Wed, 3 Jul 2024 23:33:58 -0700 Subject: [PATCH 083/248] Update changes/1857.removal.rst Co-authored-by: Russell Keith-Magee --- changes/1857.removal.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/1857.removal.rst b/changes/1857.removal.rst index 932d6573de..567067bc83 100644 --- a/changes/1857.removal.rst +++ b/changes/1857.removal.rst @@ -1 +1 @@ -The older full screen APIs on `toga.App` and `toga.Window` are now deprecated and are instead replaced by their suitable newer API counterparts. +"Full screen mode" on an app has been renamed "Presentation mode" to avoid the ambiguity with "full screen mode" on a window. The ``toga.App.enter_full_screen`` and ``toga.App.exit_full_screen`` APIs have been renamed ``toga.App.enter_presentation_mode`` and ``toga.App.exit_presentation_mode``, respectively. ``` From 067e988ca72a35999298865333c2482e06d6a7c8 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Wed, 3 Jul 2024 23:34:46 -0700 Subject: [PATCH 084/248] Update core/src/toga/constants/__init__.py Co-authored-by: Russell Keith-Magee --- core/src/toga/constants/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/toga/constants/__init__.py b/core/src/toga/constants/__init__.py index 7c9ba2b088..805c47b96e 100644 --- a/core/src/toga/constants/__init__.py +++ b/core/src/toga/constants/__init__.py @@ -74,8 +74,7 @@ class WindowState(Enum): """The possible window states of an app.""" MAXIMIZED = auto() - """``MAXIMIZED`` state is when the window title bar and window chrome, - along with app menu and toolbars remain **visible**.""" + """The window is the largest size it can be on the screen with title bar and window chrome still visible.""" FULLSCREEN = auto() """``FULLSCREEN`` state is when the window title bar and window chrome From 3c84a806062687406c436b11964e189fbceeb0ab Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Wed, 3 Jul 2024 23:43:39 -0700 Subject: [PATCH 085/248] Update core/src/toga/app.py Co-authored-by: Russell Keith-Magee --- core/src/toga/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index ee01c2083a..4a173c7224 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -870,8 +870,8 @@ def enter_presentation_mode( elif isinstance(windows, dict): screen_window_dict = windows else: - raise TypeError( - "Invalid type for windows parameter. Expected list or dict type." + raise ValueError( + "Presentation layout should be a list of windows, or a dict mapping windows to screens." ) self._impl.enter_presentation_mode(screen_window_dict) From a276f22d0f0242f844f7a1c58b3f7ca8399943c2 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 4 Jul 2024 03:55:02 -0700 Subject: [PATCH 086/248] Various changes as per review: 1 --- core/src/toga/app.py | 107 +++---- core/src/toga/constants/__init__.py | 55 ++-- core/src/toga/window.py | 105 +++---- core/tests/app/test_app.py | 417 ++++++++++++++------------- core/tests/window/test_window.py | 9 - gtk/src/toga_gtk/window.py | 8 +- testbed/tests/app/test_desktop.py | 146 ---------- testbed/tests/app/test_mobile.py | 16 - testbed/tests/window/test_window.py | 68 ----- web/src/toga_web/window.py | 2 - winforms/src/toga_winforms/window.py | 13 +- 11 files changed, 339 insertions(+), 607 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 4a173c7224..ad58c3f454 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -780,67 +780,6 @@ def current_window(self, window: Window) -> None: # Presentation mode controls ###################################################################### - # ------------------------Deprecated methods-------------------------- - def exit_full_screen(self) -> None: - """**DEPRECATED** – Use :any:`App.exit_presentation_mode()`. - - Exit full screen mode. - - """ - warnings.warn( - ( - "`App.exit_full_screen()` is deprecated. Use `App.exit_presentation_mode()` instead." - ), - DeprecationWarning, - stacklevel=2, - ) - if self.is_presentation_mode: - self._impl.exit_presentation_mode() - - @property - def is_full_screen(self) -> bool: - """**DEPRECATED** – Use :any:`App.is_presentation_mode`. - - Is the app currently in full screen mode? - - """ - warnings.warn( - ( - "`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead." - ), - DeprecationWarning, - stacklevel=2, - ) - return self.is_presentation_mode - - def set_full_screen(self, *windows: Window) -> None: - """**DEPRECATED** – Use :any:`App.enter_presentation_mode()` and :any:`App.exit_presentation_mode()`. - - Make one or more windows full screen. - - Full screen is not the same as "maximized"; full screen mode is when all window - borders and other window decorations are no longer visible. - - :param windows: The list of windows to go full screen, in order of allocation to - screens. If the number of windows exceeds the number of available displays, - those windows will not be visible. If no windows are specified, the app will - exit full screen mode. - """ - warnings.warn( - ( - "`App.set_full_screen()` is deprecated. Use `App.enter_presentation_mode()` instead." - ), - DeprecationWarning, - stacklevel=2, - ) - self.exit_presentation_mode() - if not windows: - return - else: - self.enter_presentation_mode([*windows]) - - # --------------------------------------------------------------------- - @property def is_presentation_mode(self) -> bool: """Is the app currently in presentation mode?""" @@ -859,6 +798,9 @@ def enter_presentation_mode( mapping screens to windows, to go into presentation, in order of allocation to screens. If the number of windows exceeds the number of available displays, those windows will not be visible. + + :raises ValueError: If the presentation layout supplied is not a list of windows or + or a dict mapping windows to screens. """ if not windows: return @@ -944,6 +886,49 @@ def add_background_task(self, handler: BackgroundTask) -> None: self.loop.call_soon_threadsafe(wrapped_handler(self, handler)) + ###################################################################### + # 2024-07: Backwards compatibility + ###################################################################### + + def exit_full_screen(self) -> None: + """**DEPRECATED** – Use :any:`App.exit_presentation_mode()`.""" + warnings.warn( + ( + "`App.exit_full_screen()` is deprecated. Use `App.exit_presentation_mode()` instead." + ), + DeprecationWarning, + stacklevel=2, + ) + if self.is_presentation_mode: + self._impl.exit_presentation_mode() + + @property + def is_full_screen(self) -> bool: + """**DEPRECATED** – Use :any:`App.is_presentation_mode`.""" + warnings.warn( + ( + "`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead." + ), + DeprecationWarning, + stacklevel=2, + ) + return self.is_presentation_mode + + def set_full_screen(self, *windows: Window) -> None: + """**DEPRECATED** – Use :any:`App.enter_presentation_mode()` and :any:`App.exit_presentation_mode()`.""" + warnings.warn( + ( + "`App.set_full_screen()` is deprecated. Use `App.enter_presentation_mode()` instead." + ), + DeprecationWarning, + stacklevel=2, + ) + self.exit_presentation_mode() + if not windows: + return + else: + self.enter_presentation_mode([*windows]) + ###################################################################### # End backwards compatibility ###################################################################### diff --git a/core/src/toga/constants/__init__.py b/core/src/toga/constants/__init__.py index 805c47b96e..e020fa0256 100644 --- a/core/src/toga/constants/__init__.py +++ b/core/src/toga/constants/__init__.py @@ -73,38 +73,35 @@ def __str__(self) -> str: class WindowState(Enum): """The possible window states of an app.""" - MAXIMIZED = auto() - """The window is the largest size it can be on the screen with title bar and window chrome still visible.""" - - FULLSCREEN = auto() - """``FULLSCREEN`` state is when the window title bar and window chrome - remain **hidden**; But app menu and toolbars remain **visible**.""" - - PRESENTATION = auto() - """``PRESENTATION`` state is when the window title bar, window chrome, - app menu and toolbars all remain **hidden**. - - A good example of "full screen" mode is a slideshow app in presentation - mode - the only visible content is the slide.""" - - MINIMIZED = auto() - """``MINIMIZED`` state is: + NORMAL = 0 + """``NORMAL`` state is when the window/app is not in any of the other window states. + + On Mobile Platforms(Like on Android) - Once the app is in minimized/background + state, then it is currently not possible to bring the app to foreground by setting + window state to ``NORMAL``. This is because of the design decisions imposed by the + native mobile platforms.(i.e., to prevent apps from becoming intrusively foreground + against the user's wishes.) + """ - For Desktop Platforms: - When the window is in the form of an icon or preview image and is placed in the: - * Taskbar - For Windows - * Dock - For macOS - * Area analogous to Taskbar or Dock - For Linux - Depending upon the DE + MINIMIZED = 1 + """``MINIMIZED`` state is when the window isn't currently visible, although it will + appear in any operating system's list of active windows. + """ - For Mobile Platforms: - When the App is in the background + MAXIMIZED = 2 + """The window is the largest size it can be on the screen with title bar and window + chrome still visible. + """ + FULLSCREEN = 3 + """``FULLSCREEN`` state is when the window title bar and window chrome remain + **hidden**; But app menu and toolbars remain **visible**. """ - NORMAL = auto() - """``NORMAL`` state is when the window/app is not in any of the above window states. - On Mobile Platforms(Like on Android) - Once the app is in minimized/background state, then - it is currently not possible to bring the app to foreground by setting window state to - ``NORMAL``. This is because of the design decisions imposed by the native mobile platforms. - (i.e., to prevent apps from becoming intrusively foreground against the user's wishes.) + PRESENTATION = 4 + """``PRESENTATION`` state is when the window title bar, window chrome, app menu + and toolbars all remain **hidden**. + + A good example is a slideshow app in presentation mode - the only visible content + is the slide. """ diff --git a/core/src/toga/window.py b/core/src/toga/window.py index b0160251db..0a6bdd067a 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -459,39 +459,6 @@ def visible(self, visible: bool) -> None: # Window state ###################################################################### - # -------------------------Deprecated properties------------------------- - @property - def full_screen(self) -> bool: - """**DEPRECATED** – Use :any:`Window.state`. - - Is the window in full screen mode? - - Full screen mode is *not* the same as "maximized". A full screen window has - no title bar or window chrome; But app menu and toolbars will remain visible. - """ - warnings.warn( - ("`Window.full_screen` is deprecated. Use `Window.state` instead."), - DeprecationWarning, - stacklevel=2, - ) - return bool(self.state == WindowState.FULLSCREEN) - - @full_screen.setter - def full_screen(self, is_full_screen: bool) -> None: - warnings.warn( - ("`Window.full_screen` is deprecated. Use `Window.state` instead."), - DeprecationWarning, - stacklevel=2, - ) - if is_full_screen and (self.state != WindowState.FULLSCREEN): - self._impl.set_window_state(WindowState.FULLSCREEN) - elif not is_full_screen and (self.state == WindowState.FULLSCREEN): - self._impl.set_window_state(WindowState.NORMAL) - else: - return - - # --------------------------------------------------------------------- - @property def state(self) -> WindowState: """The current state of the window.""" @@ -499,33 +466,24 @@ def state(self) -> WindowState: @state.setter def state(self, state: WindowState) -> None: - if isinstance(state, WindowState): - current_state = self._impl.get_window_state() - if current_state != state: - if not self.resizable and state in { - WindowState.MAXIMIZED, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - }: - warnings.warn( - f"Cannot set window state to {state} of a non-resizable window." - ) - else: - # Set Window state to NORMAL before changing to other states as some - # states block changing window state without first exiting them or - # can even cause rendering glitches. - self._impl.set_window_state(WindowState.NORMAL) - - if state != WindowState.NORMAL: - self._impl.set_window_state(state) - else: - return + current_state = self._impl.get_window_state() + if current_state != state: + if not self.resizable and state in { + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + }: + warnings.warn( + f"Cannot set window state to {state} of a non-resizable window." + ) else: - return - else: - raise ValueError( - "Invalid type for state parameter. Expected WindowState enum type." - ) + # Set Window state to NORMAL before changing to other states as some + # states block changing window state without first exiting them or + # can even cause rendering glitches. + self._impl.set_window_state(WindowState.NORMAL) + + if state != WindowState.NORMAL: + self._impl.set_window_state(state) ###################################################################### # Window capabilities @@ -864,6 +822,35 @@ def closeable(self) -> bool: # End Backwards compatibility ###################################################################### + ###################################################################### + # 2024-07: Backwards compatibility + ###################################################################### + @property + def full_screen(self) -> bool: + """**DEPRECATED** – Use :any:`Window.state`.""" + warnings.warn( + ("`Window.full_screen` is deprecated. Use `Window.state` instead."), + DeprecationWarning, + ) + return bool(self.state == WindowState.FULLSCREEN) + + @full_screen.setter + def full_screen(self, is_full_screen: bool) -> None: + warnings.warn( + ("`Window.full_screen` is deprecated. Use `Window.state` instead."), + DeprecationWarning, + ) + if is_full_screen and (self.state != WindowState.FULLSCREEN): + self._impl.set_window_state(WindowState.FULLSCREEN) + elif not is_full_screen and (self.state == WindowState.FULLSCREEN): + self._impl.set_window_state(WindowState.NORMAL) + else: + return + + ###################################################################### + # End Backwards compatibility + ###################################################################### + class MainWindow(Window): _WINDOW_CLASS = "MainWindow" diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 47c76521b1..9eeead3267 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -465,221 +465,84 @@ def startup(self): BadMainWindowApp(formal_name="Test App", app_id="org.example.test") -def test_full_screen(event_loop): - """The app can be put into full screen mode.""" - app = toga.App(formal_name="Test App", app_id="org.example.test") - window1 = toga.Window() - window2 = toga.Window() - - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert not app.is_full_screen - - # If we're not full screen, exiting full screen is a no-op - with pytest.warns( - DeprecationWarning, - match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", - ): - app.exit_full_screen() - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert not app.is_full_screen - assert_action_not_performed(app, "exit presentation mode") - - # Trying to enter full screen with no windows is a no-op - with pytest.warns( - DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", - ): - app.set_full_screen() - - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert not app.is_full_screen - assert_action_not_performed(app, "enter presentation mode") - - # Enter full screen with 2 windows - with pytest.warns( - DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", - ): - app.set_full_screen(window2, app.main_window) - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert app.is_full_screen - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window2, app.screens[1]: app.main_window}, - ) - - # Change the screens that are full screen - with pytest.warns( - DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", - ): - app.set_full_screen(app.main_window, window1) - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert app.is_full_screen - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window2, app.screens[1]: app.main_window}, - ) - - # Exit full screen mode - with pytest.warns( - DeprecationWarning, - match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", - ): - app.exit_full_screen() - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert not app.is_full_screen - assert_action_performed( - app, - "exit presentation mode", - ) - - -def test_set_empty_full_screen_window_list(event_loop): - """Setting the full screen window list to [] is an explicit exit.""" - app = toga.App(formal_name="Test App", app_id="org.example.test") - window1 = toga.Window() - window2 = toga.Window() - - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert not app.is_full_screen - - # Change the screens that are full screen - with pytest.warns( - DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", - ): - app.set_full_screen(window1, window2) - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert app.is_full_screen - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, - ) - # Exit full screen mode by setting no windows full screen - with pytest.warns( - DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", - ): - app.set_full_screen() - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert not app.is_full_screen - assert_action_performed( - app, - "exit presentation mode", - ) - - -def test_presentation_mode_with_windows_list(event_loop): +@pytest.mark.parametrize( + "app, windows_list", + [ + (toga.App(formal_name="Test App", app_id="org.example.test"), []), + (toga.App(formal_name="Test App", app_id="org.example.test"), [toga.Window()]), + ( + toga.App(formal_name="Test App", app_id="org.example.test"), + [toga.Window(), toga.Window()], + ), + ], +) +def test_presentation_mode_with_windows_list(event_loop, app, windows_list): """The app can enter presentation mode with a windows list.""" - app = toga.App(formal_name="Test App", app_id="org.example.test") - window1 = toga.Window() - window2 = toga.Window() - - assert not app.is_presentation_mode - - # Entering presentation mode with an empty windows list, is a no-op: - app.enter_presentation_mode([]) assert not app.is_presentation_mode - assert_action_not_performed(app, "enter presentation mode") + app.enter_presentation_mode(windows_list) - # Enter presentation mode with 1 window: - app.enter_presentation_mode([window1]) - assert app.is_presentation_mode - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window1}, - ) + if not windows_list: + # Entering presentation mode with an empty windows list, is a no-op: + assert not app.is_presentation_mode + assert_action_not_performed(app, "enter presentation mode") + else: + assert app.is_presentation_mode + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={ + app.screens[i]: window for i, window in enumerate(windows_list) + }, + ) - # Enter presentation mode with 2 windows: - app.enter_presentation_mode([window1, window2]) - assert app.is_presentation_mode - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, - ) - # Exit presentation mode: - app.exit_presentation_mode() - assert_action_performed( - app, - "exit presentation mode", - ) + # Exit presentation mode: + app.exit_presentation_mode() + assert not app.is_presentation_mode + assert_action_performed( + app, + "exit presentation mode", + ) -def test_presentation_mode_with_screen_window_dict(event_loop): +@pytest.mark.parametrize( + "app, screen_window_dict", + [ + (toga.App(formal_name="Test App", app_id="org.example.test"), {}), + ( + toga.App(formal_name="Test App", app_id="org.example.test"), + {toga.App.app.screens[0]: toga.Window()}, + ), + ( + toga.App(formal_name="Test App", app_id="org.example.test"), + { + toga.App.app.screens[0]: toga.Window(), + toga.App.app.screens[1]: toga.Window(), + }, + ), + ], +) +def test_presentation_mode_with_screen_window_dict(event_loop, app, screen_window_dict): """The app can enter presentation mode with a screen-window paired dict.""" - app = toga.App(formal_name="Test App", app_id="org.example.test") - window1 = toga.Window() - window2 = toga.Window() - assert not app.is_presentation_mode + app.enter_presentation_mode(screen_window_dict) - # Entering presentation mode with an empty dict, is a no-op: - app.enter_presentation_mode({}) - assert not app.is_presentation_mode - assert_action_not_performed(app, "enter presentation mode") - - # Enter presentation mode with an 1 element screen-window dict: - app.enter_presentation_mode({app.screens[0]: window1}) - assert app.is_presentation_mode - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window1}, - ) - # Exit presentation mode: - app.exit_presentation_mode() - assert_action_performed( - app, - "exit presentation mode", - ) + if not screen_window_dict: + # Entering presentation mode with an empty screen-window dict, is a no-op: + assert not app.is_presentation_mode + assert_action_not_performed(app, "enter presentation mode") + else: + assert app.is_presentation_mode + assert_action_performed_with( + app, "enter presentation mode", screen_window_dict=screen_window_dict + ) - # Enter presentation mode with a 2 elements screen-window dict: - app.enter_presentation_mode({app.screens[0]: window1, app.screens[1]: window2}) - assert app.is_presentation_mode - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, - ) - # Exit presentation mode: - app.exit_presentation_mode() - assert_action_performed( - app, - "exit presentation mode", - ) + # Exit presentation mode: + app.exit_presentation_mode() + assert not app.is_presentation_mode + assert_action_performed( + app, + "exit presentation mode", + ) def test_presentation_mode_with_excess_windows_list(event_loop): @@ -699,8 +562,10 @@ def test_presentation_mode_with_excess_windows_list(event_loop): "enter presentation mode", screen_window_dict={app.screens[0]: app.main_window, app.screens[1]: window2}, ) + # Exit presentation mode: app.exit_presentation_mode() + assert not app.is_presentation_mode assert_action_performed( app, "exit presentation mode", @@ -716,19 +581,22 @@ def test_presentation_mode_no_op(event_loop): # If we're not in presentation mode, exiting presentation mode is a no-op. app.exit_presentation_mode() assert_action_not_performed(app, "exit presentation mode") + assert not app.is_presentation_mode # Entering presentation mode without any window is a no-op. with pytest.raises(TypeError): app.enter_presentation_mode() assert_action_not_performed(app, "enter presentation mode") + assert not app.is_presentation_mode # Entering presentation mode without proper type of parameter is a no-op. with pytest.raises( - TypeError, - match="Invalid type for windows parameter. Expected list or dict type.", + ValueError, + match="Presentation layout should be a list of windows, or a dict mapping windows to screens.", ): app.enter_presentation_mode(app.main_window) assert_action_not_performed(app, "enter presentation mode") + assert not app.is_presentation_mode def test_show_hide_cursor(app): @@ -1049,3 +917,138 @@ async def waiter(): # Once the loop has executed, the background task should have executed as well. canary.assert_called_once() + + +def test_deprecated_full_screen(event_loop): + """The app can be put into full screen mode using the deprecated API.""" + app = toga.App(formal_name="Test App", app_id="org.example.test") + window1 = toga.Window() + window2 = toga.Window() + + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen + + # If we're not full screen, exiting full screen is a no-op + with pytest.warns( + DeprecationWarning, + match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", + ): + app.exit_full_screen() + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen + assert_action_not_performed(app, "exit presentation mode") + + # Trying to enter full screen with no windows is a no-op + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen() + + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen + assert_action_not_performed(app, "enter presentation mode") + + # Enter full screen with 2 windows + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen(window2, app.main_window) + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert app.is_full_screen + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window2, app.screens[1]: app.main_window}, + ) + + # Change the screens that are full screen + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen(app.main_window, window1) + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert app.is_full_screen + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window2, app.screens[1]: app.main_window}, + ) + + # Exit full screen mode + with pytest.warns( + DeprecationWarning, + match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", + ): + app.exit_full_screen() + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen + assert_action_performed( + app, + "exit presentation mode", + ) + + +def test_deprecated_set_empty_full_screen_window_list(event_loop): + """Setting the full screen window list to [] is an explicit exit.""" + app = toga.App(formal_name="Test App", app_id="org.example.test") + window1 = toga.Window() + window2 = toga.Window() + + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen + + # Change the screens that are full screen + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen(window1, window2) + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert app.is_full_screen + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, + ) + # Exit full screen mode by setting no windows full screen + with pytest.warns( + DeprecationWarning, + match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + ): + app.set_full_screen() + with pytest.warns( + DeprecationWarning, + match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + ): + assert not app.is_full_screen + assert_action_performed( + app, + "exit presentation mode", + ) diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index b717be8f4f..f2486dd0f8 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -452,15 +452,6 @@ def test_non_resizable_window_state(state): non_resizable_window.close() -def test_window_state_invalid_parameter(window): - # Setting window state to any value other than a WindowState enum should raise a ValueError. - with pytest.raises( - ValueError, - match="Invalid type for state parameter. Expected WindowState enum type.", - ): - window.state = None - - def test_close_direct(window, app): """A window can be closed directly.""" on_close_handler = Mock(return_value=True) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 7b7c9e7ed3..fd04cdb2e1 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -32,6 +32,7 @@ def __init__(self, interface, title, position, size): self.native.connect("window-state-event", self.gtk_window_state_event) self._window_state_flags = None + self._is_presentation_mode = False self.native.set_default_size(size[0], size[1]) @@ -160,7 +161,7 @@ def get_window_state(self): elif window_state_flags & Gdk.WindowState.FULLSCREEN: # Use a shadow variable since a window without any app menu and toolbar # in presentation mode would be indistinguishable from full screen mode. - if getattr(self, "_is_presentation_mode", False) is True: + if self._is_presentation_mode: return WindowState.PRESENTATION else: return WindowState.FULLSCREEN @@ -189,7 +190,7 @@ def set_window_state(self, state): self.native.unfullscreen() self.interface.screen = self._before_presentation_mode_screen - self._before_presentation_mode_screen = None + del self._before_presentation_mode_screen self._is_presentation_mode = False else: if state == WindowState.MAXIMIZED: @@ -202,8 +203,7 @@ def set_window_state(self, state): self.native.fullscreen() elif state == WindowState.PRESENTATION: - if getattr(self, "_before_presentation_mode_screen", None) is None: - self._before_presentation_mode_screen = self.interface.screen + self._before_presentation_mode_screen = self.interface.screen if isinstance(self.native, Gtk.ApplicationWindow): self.native.set_show_menubar(False) if getattr(self, "native_toolbar", None): diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index f633ddb358..d5c92a6c15 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -182,152 +182,6 @@ async def test_menu_minimize(app, app_probe): window1.close() -async def test_full_screen(app, app_probe): - """Window can be made full screen""" - window1 = toga.Window("Test Window 1", position=(150, 150), size=(200, 200)) - window2 = toga.Window("Test Window 2", position=(400, 150), size=(200, 200)) - - try: - window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) - window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - window1_probe = window_probe(app, window1) - window2_probe = window_probe(app, window2) - - window1.show() - window2.show() - # Add delay for gtk to show the windows - await app_probe.redraw("Extra windows are visible", delay=0.1) - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert not app.is_full_screen - assert not app_probe.is_full_screen(window1) - assert not app_probe.is_full_screen(window2) - initial_content1_size = app_probe.content_size(window1) - initial_content2_size = app_probe.content_size(window2) - - # Make window 2 full screen via the app - with pytest.warns( - DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", - ): - app.set_full_screen(window2) - await window2_probe.wait_for_window( - "Second extra window is full screen", - full_screen=True, - ) - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert app.is_full_screen - - assert not app_probe.is_full_screen(window1) - assert app_probe.content_size(window1) == initial_content1_size - - assert app_probe.is_full_screen(window2) - assert app_probe.content_size(window2)[0] > 1000 - assert app_probe.content_size(window2)[1] > 700 - - # Make window 1 full screen via the app, window 2 no longer full screen - with pytest.warns( - DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", - ): - app.set_full_screen(window1) - await window1_probe.wait_for_window( - "First extra window is full screen", - full_screen=True, - ) - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert app.is_full_screen - - assert app_probe.is_full_screen(window1) - assert app_probe.content_size(window1)[0] > 1000 - assert app_probe.content_size(window1)[1] > 700 - - assert not app_probe.is_full_screen(window2) - assert app_probe.content_size(window2) == initial_content2_size - - # Exit full screen - with pytest.warns( - DeprecationWarning, - match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", - ): - app.exit_full_screen() - await window1_probe.wait_for_window( - "No longer full screen", - full_screen=True, - ) - - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert not app.is_full_screen - - assert not app_probe.is_full_screen(window1) - assert app_probe.content_size(window1) == initial_content1_size - - assert not app_probe.is_full_screen(window2) - assert app_probe.content_size(window2) == initial_content2_size - - # Go full screen again on window 1 - with pytest.warns( - DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", - ): - app.set_full_screen(window1) - # A longer delay to allow for genie animations - await window1_probe.wait_for_window( - "First extra window is full screen", - full_screen=True, - ) - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert app.is_full_screen - - assert app_probe.is_full_screen(window1) - assert app_probe.content_size(window1)[0] > 1000 - assert app_probe.content_size(window1)[1] > 700 - - assert not app_probe.is_full_screen(window2) - assert app_probe.content_size(window2) == initial_content2_size - - # Exit full screen by passing no windows - with pytest.warns( - DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", - ): - app.set_full_screen() - - await window1_probe.wait_for_window( - "No longer full screen", - full_screen=True, - ) - with pytest.warns( - DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", - ): - assert not app.is_full_screen - - assert not app_probe.is_full_screen(window1) - assert app_probe.content_size(window1) == initial_content1_size - - assert not app_probe.is_full_screen(window2) - assert app_probe.content_size(window2) == initial_content2_size - - finally: - window1.close() - window2.close() - - async def test_presentation_mode_with_main_window( app, app_probe, main_window, main_window_probe ): diff --git a/testbed/tests/app/test_mobile.py b/testbed/tests/app/test_mobile.py index 66f29198b7..821e1a902b 100644 --- a/testbed/tests/app/test_mobile.py +++ b/testbed/tests/app/test_mobile.py @@ -18,22 +18,6 @@ async def test_show_hide_cursor(app): app.hide_cursor() -async def test_full_screen(app): - """Window can be made full screen""" - # Invoke the methods to verify the endpoints exist. However, they're no-ops, - # so there's nothing to test. - with pytest.warns( - DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", - ): - app.set_full_screen(app.current_window) - with pytest.warns( - DeprecationWarning, - match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", - ): - app.exit_full_screen() - - async def test_presentation_mode(app, app_probe, main_window, main_window_probe): """The app can enter into presentation mode""" assert not app.is_presentation_mode diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 8711b72bcc..b29272b033 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -600,74 +600,6 @@ async def test_move_and_resize(second_window, second_window_probe): assert second_window.size == (250 + extra_width, 210 + extra_height) assert second_window_probe.content_size == (250, 210) - @pytest.mark.parametrize( - "second_window_class, second_window_kwargs", - [ - ( - toga.Window, - dict(title="Secondary Window", position=(200, 150)), - ) - ], - ) - async def test_full_screen(second_window, second_window_probe): - """Window can be made full screen""" - second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - - assert not second_window_probe.is_full_screen - assert second_window_probe.is_resizable - initial_content_size = second_window_probe.content_size - - with pytest.warns( - DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", - ): - second_window.full_screen = True - # A longer delay to allow for genie animations - await second_window_probe.wait_for_window( - "Secondary window is full screen", - full_screen=True, - ) - assert second_window_probe.is_full_screen - assert second_window_probe.content_size[0] > initial_content_size[0] - assert second_window_probe.content_size[1] > initial_content_size[1] - - with pytest.warns( - DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", - ): - second_window.full_screen = True - await second_window_probe.wait_for_window( - "Secondary window is still full screen" - ) - assert second_window_probe.is_full_screen - assert second_window_probe.content_size[0] > initial_content_size[0] - assert second_window_probe.content_size[1] > initial_content_size[1] - - with pytest.warns( - DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", - ): - second_window.full_screen = False - # A longer delay to allow for genie animations - await second_window_probe.wait_for_window( - "Secondary window is not full screen", - full_screen=True, - ) - assert not second_window_probe.is_full_screen - assert second_window_probe.is_resizable - assert second_window_probe.content_size == initial_content_size - - with pytest.warns( - DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", - ): - second_window.full_screen = False - await second_window_probe.wait_for_window( - "Secondary window is still not full screen" - ) - assert not second_window_probe.is_full_screen - assert second_window_probe.content_size == initial_content_size - @pytest.mark.parametrize( "second_window_class, second_window_kwargs", [ diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index 933454b1ba..712ce71872 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -7,8 +7,6 @@ class Window: - _PLATFORM_ALLOWS_CLOSE = False - def __init__(self, interface, title, position, size): self.interface = interface self.interface._impl = self diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 5b48932128..d624534b27 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -36,6 +36,10 @@ def __init__(self, interface, title, position, size): self.native.MinimizeBox = self.interface.minimizable self.native.MaximizeBox = self.interface.resizable + # Use a shadow variable since a window without any app menu and toolbar + # in presentation mode would be indistinguishable from full screen mode. + self._is_presentation_mode = False + self.set_title(title) self.set_size(size) # Winforms does window cascading by default; use that behavior, rather than @@ -190,9 +194,7 @@ def get_window_state(self): window_state = self.native.WindowState if window_state == WinForms.FormWindowState.Maximized: if self.native.FormBorderStyle == getattr(WinForms.FormBorderStyle, "None"): - # Use a shadow variable since a window without any app menu and toolbar - # in presentation mode would be indistinguishable from full screen mode. - if getattr(self, "_is_presentation_mode", False) is True: + if self._is_presentation_mode: return WindowState.PRESENTATION else: return WindowState.FULLSCREEN @@ -217,7 +219,7 @@ def set_window_state(self, state): self.toolbar_native.Visible = True self.interface.screen = self._before_presentation_mode_screen - self._before_presentation_mode_screen = None + del self._before_presentation_mode_screen self._is_presentation_mode = False self.native.FormBorderStyle = getattr( @@ -237,8 +239,7 @@ def set_window_state(self, state): self.native.WindowState = WinForms.FormWindowState.Maximized elif state == WindowState.PRESENTATION: - if getattr(self, "_before_presentation_mode_screen", None) is None: - self._before_presentation_mode_screen = self.interface.screen + self._before_presentation_mode_screen = self.interface.screen if self.native.MainMenuStrip: self.native.MainMenuStrip.Visible = False if getattr(self, "toolbar_native", None): From 78cdd3b42400bb7dd1d48dc2ca98781ccd470dc2 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 4 Jul 2024 05:08:20 -0700 Subject: [PATCH 087/248] Various changes as per review: 2 --- android/tests_backend/window.py | 4 +- cocoa/tests_backend/app.py | 4 +- cocoa/tests_backend/window.py | 8 +-- gtk/tests_backend/app.py | 4 +- gtk/tests_backend/window.py | 8 +-- iOS/tests_backend/window.py | 2 +- testbed/tests/app/test_desktop.py | 20 +++--- testbed/tests/app/test_mobile.py | 18 ++--- testbed/tests/window/test_window.py | 104 ++++++++++++++-------------- winforms/tests_backend/app.py | 4 +- winforms/tests_backend/window.py | 8 +-- 11 files changed, 96 insertions(+), 88 deletions(-) diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index e300efd6b2..3bbfa39362 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -22,7 +22,7 @@ def content_size(self): self.root_view.getHeight() / self.scale_factor, ) - def is_window_state(self, state): + def get_window_state(self, state): # Windows are always full screen decor_view = self.native.getWindow().getDecorView() system_ui_flags = decor_view.getSystemUiVisibility() @@ -40,7 +40,7 @@ def is_window_state(self, state): current_state = WindowState.FULLSCREEN else: current_state = WindowState.NORMAL - return bool(current_state == state) + return current_state def _native_menu(self): return self.native.findViewById(appcompat_R.id.action_bar).getMenu() diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index 977398e847..d39c3ece1c 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -58,7 +58,9 @@ def is_cursor_visible(self): return self.app._impl._cursor_visible def is_full_screen(self, window): - return WindowProbe(self.app, window).is_window_state(WindowState.PRESENTATION) + return bool( + WindowProbe(self.app, window).get_window_state() == WindowState.PRESENTATION + ) def content_size(self, window): return WindowProbe(self.app, window).presentation_content_size diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index e483ce05b3..aebf9bdce7 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -46,7 +46,7 @@ def presentation_content_size(self): self.window.content._impl.native.frame.size.height, ) - def is_window_state(self, state): + def get_window_state(self, state): if self.window.content and bool( self.window.content._impl.native.isInFullScreenMode() ): @@ -59,11 +59,11 @@ def is_window_state(self, state): current_state = WindowState.MINIMIZED else: current_state = WindowState.NORMAL - return bool(current_state == state) + return current_state @property def is_full_screen(self): - return self.is_window_state(WindowState.FULLSCREEN) + return bool(self.get_window_state() == WindowState.FULLSCREEN) @property def is_resizable(self): @@ -79,7 +79,7 @@ def is_minimizable(self): @property def is_minimized(self): - return self.is_window_state(WindowState.MINIMIZED) + return bool(self.get_window_state() == WindowState.MINIMIZED) def minimize(self): self.native.performMiniaturize(None) diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index 0d27f2ebf1..8f28ab4d16 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -47,7 +47,9 @@ def is_cursor_visible(self): pytest.skip("Cursor visibility not implemented on GTK") def is_full_screen(self, window): - return WindowProbe(self.app, window).is_window_state(WindowState.PRESENTATION) + return bool( + WindowProbe(self.app, window).get_window_state() == WindowState.PRESENTATION + ) def content_size(self, window): return WindowProbe(self.app, window).presentation_content_size diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index 9e21f17f23..c0c4965c9e 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -41,7 +41,7 @@ def content_size(self): def presentation_content_size(self): return self.content_size - def is_window_state(self, state): + def get_window_state(self, state): window_state_flags = self.impl._window_state_flags if window_state_flags & Gdk.WindowState.MAXIMIZED: current_state = WindowState.MAXIMIZED @@ -56,11 +56,11 @@ def is_window_state(self, state): current_state = WindowState.FULLSCREEN else: current_state = WindowState.NORMAL - return bool(current_state == state) + return current_state @property def is_full_screen(self): - return self.is_window_state(WindowState.FULLSCREEN) + return bool(self.get_window_state() == WindowState.FULLSCREEN) @property def is_resizable(self): @@ -72,7 +72,7 @@ def is_closable(self): @property def is_minimized(self): - return self.is_window_state(WindowState.MINIMIZED) + return bool(self.get_window_state() == WindowState.MINIMIZED) def minimize(self): self.native.iconify() diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py index d79cadd7d2..669a486d30 100644 --- a/iOS/tests_backend/window.py +++ b/iOS/tests_backend/window.py @@ -30,7 +30,7 @@ def content_size(self): ), ) - def is_window_state(self, state): + def get_window_state(self, state): pytest.skip("Window states are not implemented on iOS") def has_toolbar(self): diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index d5c92a6c15..81a06dfb1a 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -207,8 +207,8 @@ async def test_presentation_mode_with_main_window( assert app.is_presentation_mode # Extra window should not be in presentation mode. - assert not extra_window_probe.is_window_state( - WindowState.PRESENTATION + assert ( + extra_window_probe.get_window_state() != WindowState.PRESENTATION ), "Extra Window:" assert ( extra_window_probe.presentation_content_size @@ -216,7 +216,7 @@ async def test_presentation_mode_with_main_window( ), "Extra Window" # Main Window should be in presentation mode. - assert main_window_probe.is_window_state(WindowState.PRESENTATION) + assert main_window_probe.get_window_state() == WindowState.PRESENTATION assert main_window_probe.presentation_content_size[0] > 1000 assert main_window_probe.presentation_content_size[1] > 700 @@ -278,8 +278,9 @@ async def test_presentation_mode_with_screen_window_dict(app, app_probe): assert app.is_presentation_mode # All the windows should be in presentation mode. for window_information in window_information_list: - assert window_information["window_probe"].is_window_state( - WindowState.PRESENTATION + assert ( + window_information["window_probe"].get_window_state() + == WindowState.PRESENTATION ), f"{window_information['window'].title}:" assert ( window_information["window_probe"].presentation_content_size[0] > 1000 @@ -349,8 +350,8 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): assert app.is_presentation_mode # Last window should not be in presentation mode. - assert not last_window_probe.is_window_state( - WindowState.PRESENTATION + assert ( + last_window_probe.get_window_state() != WindowState.PRESENTATION ), f"Last Window({last_window.title}):" assert ( last_window_probe.presentation_content_size @@ -359,8 +360,9 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): # All other windows should be in presentation mode. for window_information in window_information_list[:-1]: - assert window_information["window_probe"].is_window_state( - WindowState.PRESENTATION + assert ( + window_information["window_probe"].get_window_state() + == WindowState.PRESENTATION ), f"{window_information['window'].title}:" assert ( window_information["window_probe"].presentation_content_size[0] > 1000 diff --git a/testbed/tests/app/test_mobile.py b/testbed/tests/app/test_mobile.py index 821e1a902b..5fced58df1 100644 --- a/testbed/tests/app/test_mobile.py +++ b/testbed/tests/app/test_mobile.py @@ -21,7 +21,7 @@ async def test_show_hide_cursor(app): async def test_presentation_mode(app, app_probe, main_window, main_window_probe): """The app can enter into presentation mode""" assert not app.is_presentation_mode - assert not main_window_probe.is_window_state(WindowState.PRESENTATION) + assert main_window_probe.get_window_state() != WindowState.PRESENTATION # Enter presentation mode with main window via the app app.enter_presentation_mode([main_window]) @@ -30,8 +30,8 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ) assert app.is_presentation_mode - assert not main_window_probe.is_window_state(WindowState.NORMAL) - assert main_window_probe.is_window_state(WindowState.PRESENTATION) + assert main_window_probe.get_window_state() != WindowState.NORMAL + assert main_window_probe.get_window_state() == WindowState.PRESENTATION # Exit presentation mode app.exit_presentation_mode() @@ -41,8 +41,8 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ) assert not app.is_presentation_mode - assert not main_window_probe.is_window_state(WindowState.PRESENTATION) - assert main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.get_window_state() != WindowState.PRESENTATION + assert main_window_probe.get_window_state() == WindowState.NORMAL # Enter presentation mode with a screen-window dict via the app app.enter_presentation_mode({app.screens[0]: main_window}) @@ -51,8 +51,8 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ) assert app.is_presentation_mode - assert not main_window_probe.is_window_state(WindowState.NORMAL) - assert main_window_probe.is_window_state(WindowState.PRESENTATION) + assert main_window_probe.get_window_state() != WindowState.NORMAL + assert main_window_probe.get_window_state() == WindowState.PRESENTATION # Exit presentation mode app.exit_presentation_mode() @@ -62,8 +62,8 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ) assert not app.is_presentation_mode - assert not main_window_probe.is_window_state(WindowState.PRESENTATION) - assert main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.get_window_state() != WindowState.PRESENTATION + assert main_window_probe.get_window_state() == WindowState.NORMAL async def test_current_window(app, main_window, main_window_probe): diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index b29272b033..7c898e6016 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -164,79 +164,79 @@ async def test_full_screen(main_window, main_window_probe): async def test_window_state_minimized(main_window, main_window_probe): """Window can have minimized window state""" # WindowState.MINIMIZED is unimplemented on both Android and iOS. - assert main_window_probe.is_window_state(WindowState.NORMAL) - assert not main_window_probe.is_window_state(WindowState.MINIMIZED) + assert main_window_probe.get_window_state() == WindowState.NORMAL + assert main_window_probe.get_window_state() != WindowState.MINIMIZED main_window.state = WindowState.MINIMIZED await main_window_probe.wait_for_window("WindowState.MINIMIZED is a no-op") - assert not main_window_probe.is_window_state(WindowState.MINIMIZED) - assert main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.get_window_state() != WindowState.MINIMIZED + assert main_window_probe.get_window_state() == WindowState.NORMAL async def test_window_state_maximized(main_window, main_window_probe): """Window can have maximized window state""" # WindowState.MAXIMIZED is unimplemented on both Android and iOS. - assert main_window_probe.is_window_state(WindowState.NORMAL) - assert not main_window_probe.is_window_state(WindowState.MAXIMIZED) + assert main_window_probe.get_window_state() == WindowState.NORMAL + assert main_window_probe.get_window_state() != WindowState.MAXIMIZED main_window.state = WindowState.MAXIMIZED await main_window_probe.wait_for_window("WindowState.MAXIMIZED is a no-op") - assert not main_window_probe.is_window_state(WindowState.MAXIMIZED) - assert main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.get_window_state() != WindowState.MAXIMIZED + assert main_window_probe.get_window_state() == WindowState.NORMAL async def test_window_state_full_screen(main_window, main_window_probe): """Window can have full screen window state""" # WindowState.FULLSCREEN is implemented on Android but not on iOS. - assert main_window_probe.is_window_state(WindowState.NORMAL) - assert not main_window_probe.is_window_state(WindowState.FULLSCREEN) + assert main_window_probe.get_window_state() == WindowState.NORMAL + assert main_window_probe.get_window_state() != WindowState.FULLSCREEN # Make main window full screen main_window.state = WindowState.FULLSCREEN await main_window_probe.wait_for_window("Main window is full screen") - assert main_window_probe.is_window_state(WindowState.FULLSCREEN) - assert not main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.get_window_state() == WindowState.FULLSCREEN + assert main_window_probe.get_window_state() != WindowState.NORMAL main_window.state = WindowState.FULLSCREEN await main_window_probe.wait_for_window("Main window is still full screen") - assert main_window_probe.is_window_state(WindowState.FULLSCREEN) - assert not main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.get_window_state() == WindowState.FULLSCREEN + assert main_window_probe.get_window_state() != WindowState.NORMAL # Exit full screen main_window.state = WindowState.NORMAL await main_window_probe.wait_for_window("Main window is not full screen") - assert not main_window_probe.is_window_state(WindowState.FULLSCREEN) - assert main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.get_window_state() != WindowState.FULLSCREEN + assert main_window_probe.get_window_state() == WindowState.NORMAL main_window.state = WindowState.NORMAL await main_window_probe.wait_for_window("Main window is still not full screen") - assert not main_window_probe.is_window_state(WindowState.FULLSCREEN) - assert main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.get_window_state() != WindowState.FULLSCREEN + assert main_window_probe.get_window_state() == WindowState.NORMAL async def test_window_state_presentation(main_window, main_window_probe): # WindowState.PRESENTATION is implemented on Android but not on iOS. - assert main_window_probe.is_window_state(WindowState.NORMAL) - assert not main_window_probe.is_window_state(WindowState.PRESENTATION) + assert main_window_probe.get_window_state() == WindowState.NORMAL + assert main_window_probe.get_window_state() != WindowState.PRESENTATION # Enter presentation mode with main window main_window.state = WindowState.PRESENTATION await main_window_probe.wait_for_window("Main window is in presentation mode") - assert main_window_probe.is_window_state(WindowState.PRESENTATION) - assert not main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.get_window_state() == WindowState.PRESENTATION + assert main_window_probe.get_window_state() != WindowState.NORMAL main_window.state = WindowState.PRESENTATION await main_window_probe.wait_for_window( "Main window is still in presentation mode" ) - assert main_window_probe.is_window_state(WindowState.PRESENTATION) - assert not main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.get_window_state() == WindowState.PRESENTATION + assert main_window_probe.get_window_state() != WindowState.NORMAL # Exit presentation mode main_window.state = WindowState.NORMAL await main_window_probe.wait_for_window( "Main window is not in presentation mode" ) - assert not main_window_probe.is_window_state(WindowState.PRESENTATION) - assert main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.get_window_state() != WindowState.PRESENTATION + assert main_window_probe.get_window_state() == WindowState.NORMAL main_window.state = WindowState.NORMAL await main_window_probe.wait_for_window( "Main window is still not in presentation mode" ) - assert not main_window_probe.is_window_state(WindowState.PRESENTATION) - assert main_window_probe.is_window_state(WindowState.NORMAL) + assert main_window_probe.get_window_state() != WindowState.PRESENTATION + assert main_window_probe.get_window_state() == WindowState.NORMAL async def test_screen(main_window, main_window_probe): """The window can be relocated to another screen, using both absolute and relative screen positions.""" @@ -618,8 +618,8 @@ async def test_window_state_minimized(second_window, second_window_probe): second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - assert second_window_probe.is_window_state(WindowState.NORMAL) - assert not second_window_probe.is_window_state(WindowState.MINIMIZED) + assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window_probe.get_window_state() != WindowState.MINIMIZED if second_window_probe.supports_minimizable: assert second_window_probe.is_minimizable @@ -629,11 +629,11 @@ async def test_window_state_minimized(second_window, second_window_probe): "Secondary window is minimized", minimize=True, ) - assert second_window_probe.is_window_state(WindowState.MINIMIZED) + assert second_window_probe.get_window_state() == WindowState.MINIMIZED second_window.state = WindowState.MINIMIZED await second_window_probe.wait_for_window("Secondary window is still minimized") - assert second_window_probe.is_window_state(WindowState.MINIMIZED) + assert second_window_probe.get_window_state() == WindowState.MINIMIZED second_window.state = WindowState.NORMAL # A longer delay to allow for genie animations @@ -641,7 +641,7 @@ async def test_window_state_minimized(second_window, second_window_probe): "Secondary window is not minimized", minimize=True, ) - assert not second_window_probe.is_window_state(WindowState.MINIMIZED) + assert second_window_probe.get_window_state() != WindowState.MINIMIZED if second_window_probe.supports_minimizable: assert second_window_probe.is_minimizable @@ -650,7 +650,7 @@ async def test_window_state_minimized(second_window, second_window_probe): "Secondary window is still not minimized", minimize=True, ) - assert not second_window_probe.is_window_state(WindowState.MINIMIZED) + assert second_window_probe.get_window_state() != WindowState.MINIMIZED @pytest.mark.parametrize( "second_window_class, second_window_kwargs", @@ -665,8 +665,8 @@ async def test_window_state_maximized(second_window, second_window_probe): """Window can have maximized window state""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - assert second_window_probe.is_window_state(WindowState.NORMAL) - assert not second_window_probe.is_window_state(WindowState.MAXIMIZED) + assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window_probe.get_window_state() != WindowState.MAXIMIZED assert second_window_probe.is_resizable initial_content_size = second_window_probe.content_size @@ -675,13 +675,13 @@ async def test_window_state_maximized(second_window, second_window_probe): await second_window_probe.wait_for_window( "Secondary window is maximized", ) - assert second_window_probe.is_window_state(WindowState.MAXIMIZED) + assert second_window_probe.get_window_state() == WindowState.MAXIMIZED assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] second_window.state = WindowState.MAXIMIZED await second_window_probe.wait_for_window("Secondary window is still maximized") - assert second_window_probe.is_window_state(WindowState.MAXIMIZED) + assert second_window_probe.get_window_state() == WindowState.MAXIMIZED assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] @@ -690,7 +690,7 @@ async def test_window_state_maximized(second_window, second_window_probe): await second_window_probe.wait_for_window( "Secondary window is not maximized", ) - assert not second_window_probe.is_window_state(WindowState.MAXIMIZED) + assert second_window_probe.get_window_state() != WindowState.MAXIMIZED assert second_window_probe.is_resizable assert second_window_probe.content_size == initial_content_size @@ -698,7 +698,7 @@ async def test_window_state_maximized(second_window, second_window_probe): await second_window_probe.wait_for_window( "Secondary window is still not maximized" ) - assert not second_window_probe.is_window_state(WindowState.MAXIMIZED) + assert second_window_probe.get_window_state() != WindowState.MAXIMIZED assert second_window_probe.content_size == initial_content_size @pytest.mark.parametrize( @@ -714,8 +714,8 @@ async def test_window_state_full_screen(second_window, second_window_probe): """Window can have full screen window state""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - assert second_window_probe.is_window_state(WindowState.NORMAL) - assert not second_window_probe.is_window_state(WindowState.FULLSCREEN) + assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window_probe.get_window_state() != WindowState.FULLSCREEN assert second_window_probe.is_resizable initial_content_size = second_window_probe.content_size @@ -725,7 +725,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): "Secondary window is full screen", full_screen=True, ) - assert second_window_probe.is_window_state(WindowState.FULLSCREEN) + assert second_window_probe.get_window_state() == WindowState.FULLSCREEN assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] @@ -734,7 +734,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): "Secondary window is still full screen", full_screen=True, ) - assert second_window_probe.is_window_state(WindowState.FULLSCREEN) + assert second_window_probe.get_window_state() == WindowState.FULLSCREEN assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] @@ -744,7 +744,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): "Secondary window is not full screen", full_screen=True, ) - assert not second_window_probe.is_window_state(WindowState.FULLSCREEN) + assert second_window_probe.get_window_state() != WindowState.FULLSCREEN assert second_window_probe.is_resizable assert second_window_probe.content_size == initial_content_size @@ -753,7 +753,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): "Secondary window is still not full screen", full_screen=True, ) - assert not second_window_probe.is_window_state(WindowState.FULLSCREEN) + assert second_window_probe.get_window_state() != WindowState.FULLSCREEN assert second_window_probe.content_size == initial_content_size @pytest.mark.parametrize( @@ -769,8 +769,8 @@ async def test_window_state_presentation(second_window, second_window_probe): """Window can have presentation window state""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - assert second_window_probe.is_window_state(WindowState.NORMAL) - assert not second_window_probe.is_window_state(WindowState.PRESENTATION) + assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window_probe.get_window_state() != WindowState.PRESENTATION assert second_window_probe.is_resizable initial_content_size = second_window_probe.presentation_content_size @@ -780,7 +780,7 @@ async def test_window_state_presentation(second_window, second_window_probe): "Secondary window is in presentation mode", full_screen=True, ) - assert second_window_probe.is_window_state(WindowState.PRESENTATION) + assert second_window_probe.get_window_state() == WindowState.PRESENTATION assert ( second_window_probe.presentation_content_size[0] > initial_content_size[0] ) @@ -793,7 +793,7 @@ async def test_window_state_presentation(second_window, second_window_probe): "Secondary window is still in presentation mode", full_screen=True, ) - assert second_window_probe.is_window_state(WindowState.PRESENTATION) + assert second_window_probe.get_window_state() == WindowState.PRESENTATION assert ( second_window_probe.presentation_content_size[0] > initial_content_size[0] ) @@ -807,7 +807,7 @@ async def test_window_state_presentation(second_window, second_window_probe): "Secondary window is not in presentation mode", full_screen=True, ) - assert not second_window_probe.is_window_state(WindowState.PRESENTATION) + assert second_window_probe.get_window_state() != WindowState.PRESENTATION assert second_window_probe.is_resizable assert second_window_probe.presentation_content_size == initial_content_size @@ -816,7 +816,7 @@ async def test_window_state_presentation(second_window, second_window_probe): "Secondary window is still not in presentation mode", full_screen=True, ) - assert not second_window_probe.is_window_state(WindowState.PRESENTATION) + assert second_window_probe.get_window_state() != WindowState.PRESENTATION assert second_window_probe.presentation_content_size == initial_content_size @pytest.mark.parametrize( diff --git a/winforms/tests_backend/app.py b/winforms/tests_backend/app.py index 6333a8533d..00f2562cd7 100644 --- a/winforms/tests_backend/app.py +++ b/winforms/tests_backend/app.py @@ -103,7 +103,9 @@ class CURSORINFO(ctypes.Structure): return info.hCursor is not None def is_full_screen(self, window): - return WindowProbe(self.app, window).is_window_state(WindowState.PRESENTATION) + return bool( + WindowProbe(self.app, window).get_window_state() == WindowState.PRESENTATION + ) def content_size(self, window): return WindowProbe(self.app, window).presentation_content_size diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 8753ddf0b7..8af45c063e 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -53,7 +53,7 @@ def content_size(self): def presentation_content_size(self): return self.content_size - def is_window_state(self, state): + def get_window_state(self, state): window_state = self.native.WindowState if window_state == FormWindowState.Maximized: if self.native.FormBorderStyle == getattr(FormBorderStyle, "None"): @@ -67,11 +67,11 @@ def is_window_state(self, state): current_state = WindowState.MINIMIZED elif window_state == FormWindowState.Normal: current_state = WindowState.NORMAL - return bool(current_state == state) + return current_state @property def is_full_screen(self): - return self.is_window_state(WindowState.FULLSCREEN) + return bool(self.get_window_state() == WindowState.FULLSCREEN) @property def is_resizable(self): @@ -83,7 +83,7 @@ def is_minimizable(self): @property def is_minimized(self): - return self.is_window_state(WindowState.MINIMIZED) + return bool(self.get_window_state() == WindowState.MINIMIZED) def minimize(self): if self.native.MinimizeBox: From b9e87c5f53bda84b27c9015009ee1e7527dfb71c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 4 Jul 2024 05:16:31 -0700 Subject: [PATCH 088/248] Various changes as per review: 3 --- cocoa/tests_backend/app.py | 6 ------ cocoa/tests_backend/window.py | 4 ---- core/src/toga/window.py | 1 - gtk/tests_backend/app.py | 6 ------ gtk/tests_backend/window.py | 4 ---- winforms/tests_backend/app.py | 6 ------ winforms/tests_backend/window.py | 4 ---- 7 files changed, 31 deletions(-) diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index d39c3ece1c..6d1d358aad 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -4,7 +4,6 @@ from rubicon.objc import NSPoint, ObjCClass, objc_id, send_message import toga -from toga.constants import WindowState from toga_cocoa.keys import cocoa_key, toga_key from toga_cocoa.libs import ( NSApplication, @@ -57,11 +56,6 @@ def is_cursor_visible(self): # fall back to the implementation's proxy variable. return self.app._impl._cursor_visible - def is_full_screen(self, window): - return bool( - WindowProbe(self.app, window).get_window_state() == WindowState.PRESENTATION - ) - def content_size(self, window): return WindowProbe(self.app, window).presentation_content_size diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index aebf9bdce7..d98864937c 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -61,10 +61,6 @@ def get_window_state(self, state): current_state = WindowState.NORMAL return current_state - @property - def is_full_screen(self): - return bool(self.get_window_state() == WindowState.FULLSCREEN) - @property def is_resizable(self): return bool(self.native.styleMask & NSWindowStyleMask.Resizable) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 0a6bdd067a..9cea4d0e88 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -195,7 +195,6 @@ def __init__( self._id = str(id if id else identifier(self)) self._impl: Any = None self._content: Widget | None = None - self._is_full_screen = False self._closed = False self._resizable = resizable diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index 8f28ab4d16..f818aceadf 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -4,7 +4,6 @@ import pytest import toga -from toga.constants import WindowState from toga_gtk.keys import gtk_accel, toga_key from toga_gtk.libs import Gdk, Gtk @@ -46,11 +45,6 @@ def logs_path(self): def is_cursor_visible(self): pytest.skip("Cursor visibility not implemented on GTK") - def is_full_screen(self, window): - return bool( - WindowProbe(self.app, window).get_window_state() == WindowState.PRESENTATION - ) - def content_size(self, window): return WindowProbe(self.app, window).presentation_content_size diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index c0c4965c9e..f92270f9ca 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -58,10 +58,6 @@ def get_window_state(self, state): current_state = WindowState.NORMAL return current_state - @property - def is_full_screen(self): - return bool(self.get_window_state() == WindowState.FULLSCREEN) - @property def is_resizable(self): return self.native.get_resizable() diff --git a/winforms/tests_backend/app.py b/winforms/tests_backend/app.py index 00f2562cd7..c97b2851e3 100644 --- a/winforms/tests_backend/app.py +++ b/winforms/tests_backend/app.py @@ -9,7 +9,6 @@ from System.Windows.Forms import Application, Cursor, ToolStripSeparator import toga -from toga.constants import WindowState from toga_winforms.keys import toga_to_winforms_key, winforms_to_toga_key from .dialogs import DialogsMixin @@ -102,11 +101,6 @@ class CURSORINFO(ctypes.Structure): # input through touch or pen instead of the mouse"). hCursor is more reliable. return info.hCursor is not None - def is_full_screen(self, window): - return bool( - WindowProbe(self.app, window).get_window_state() == WindowState.PRESENTATION - ) - def content_size(self, window): return WindowProbe(self.app, window).presentation_content_size diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 8af45c063e..ad30726b21 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -69,10 +69,6 @@ def get_window_state(self, state): current_state = WindowState.NORMAL return current_state - @property - def is_full_screen(self): - return bool(self.get_window_state() == WindowState.FULLSCREEN) - @property def is_resizable(self): return self.native.FormBorderStyle == FormBorderStyle.Sizable From 06d5a9ee788f9e4a855fa1976ffe0d83f7bc01d6 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 4 Jul 2024 06:36:43 -0700 Subject: [PATCH 089/248] Various changes as per review: 4 --- android/src/toga_android/window.py | 34 ++++++++++++++++++++---------- android/tests_backend/window.py | 2 +- cocoa/tests_backend/window.py | 2 +- gtk/src/toga_gtk/window.py | 4 ++-- gtk/tests_backend/window.py | 2 +- iOS/tests_backend/window.py | 2 +- winforms/tests_backend/window.py | 2 +- 7 files changed, 30 insertions(+), 18 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 5c1bf501ff..24ab7cad99 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -32,11 +32,16 @@ def onGlobalLayout(self): class Window(Container): + _actionbar_shown_by_default = False + def __init__(self, interface, title, position, size): super().__init__() self.interface = interface self.interface._impl = self self._initial_title = title + # Use a shadow variable since the presence of ActionBar is not + # a reliable indicator for confirmation of presentation mode. + self._is_presentation_mode = False ###################################################################### # Window properties @@ -142,15 +147,12 @@ def get_window_state(self): # Windows are always full screen decor_view = self.app.native.getWindow().getDecorView() system_ui_flags = decor_view.getSystemUiVisibility() - if ( - system_ui_flags - & ( - decor_view.SYSTEM_UI_FLAG_FULLSCREEN - | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | decor_view.SYSTEM_UI_FLAG_IMMERSIVE - ) - ) != 0: - if not self.app.native.getSupportActionBar().isShowing(): + if system_ui_flags & ( + decor_view.SYSTEM_UI_FLAG_FULLSCREEN + | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | decor_view.SYSTEM_UI_FLAG_IMMERSIVE + ): + if self._is_presentation_mode: return WindowState.PRESENTATION else: return WindowState.FULLSCREEN @@ -169,7 +171,11 @@ def set_window_state(self, state): }: decor_view.setSystemUiVisibility(0) if current_state == WindowState.PRESENTATION: - self.app.native.getSupportActionBar().show() + # Marking this as no branch, since the testbed can't create a simple + # window, so we can't test the other branch. + if self._actionbar_shown_by_default: # pragma: no branch + self.app.native.getSupportActionBar().show() + self._is_presentation_mode = False # Doesn't work consistently # elif current_state == WindowState.MINIMIZED: @@ -197,7 +203,11 @@ def set_window_state(self, state): | decor_view.SYSTEM_UI_FLAG_IMMERSIVE ) if state == WindowState.PRESENTATION: - self.app.native.getSupportActionBar().hide() + # Marking this as no branch, since the testbed can't create a simple + # window, so we can't test the other branch. + if self._actionbar_shown_by_default: # pragma: no branch + self.app.native.getSupportActionBar().hide() + self._is_presentation_mode = True # elif state == WindowState.MINIMIZED: # # This works but the issue lies in restoring to normal state # self.app.native.moveTaskToBack(True) @@ -223,6 +233,8 @@ def get_image_data(self): class MainWindow(Window): + _actionbar_shown_by_default = True + def configure_titlebar(self): # Display the titlebar on a MainWindow. pass diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index 3bbfa39362..28a06b6752 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -22,7 +22,7 @@ def content_size(self): self.root_view.getHeight() / self.scale_factor, ) - def get_window_state(self, state): + def get_window_state(self): # Windows are always full screen decor_view = self.native.getWindow().getDecorView() system_ui_flags = decor_view.getSystemUiVisibility() diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index d98864937c..517f426649 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -46,7 +46,7 @@ def presentation_content_size(self): self.window.content._impl.native.frame.size.height, ) - def get_window_state(self, state): + def get_window_state(self): if self.window.content and bool( self.window.content._impl.native.isInFullScreenMode() ): diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index fd04cdb2e1..cefb73b4d0 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -32,6 +32,8 @@ def __init__(self, interface, title, position, size): self.native.connect("window-state-event", self.gtk_window_state_event) self._window_state_flags = None + # Use a shadow variable since a window without any app menu and toolbar + # in presentation mode would be indistinguishable from full screen mode. self._is_presentation_mode = False self.native.set_default_size(size[0], size[1]) @@ -159,8 +161,6 @@ def get_window_state(self): elif window_state_flags & Gdk.WindowState.ICONIFIED: return WindowState.MINIMIZED # pragma: no-cover-if-linux-wayland elif window_state_flags & Gdk.WindowState.FULLSCREEN: - # Use a shadow variable since a window without any app menu and toolbar - # in presentation mode would be indistinguishable from full screen mode. if self._is_presentation_mode: return WindowState.PRESENTATION else: diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index f92270f9ca..00e5b32e67 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -41,7 +41,7 @@ def content_size(self): def presentation_content_size(self): return self.content_size - def get_window_state(self, state): + def get_window_state(self): window_state_flags = self.impl._window_state_flags if window_state_flags & Gdk.WindowState.MAXIMIZED: current_state = WindowState.MAXIMIZED diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py index 669a486d30..0877ab0fe4 100644 --- a/iOS/tests_backend/window.py +++ b/iOS/tests_backend/window.py @@ -30,7 +30,7 @@ def content_size(self): ), ) - def get_window_state(self, state): + def get_window_state(self): pytest.skip("Window states are not implemented on iOS") def has_toolbar(self): diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index ad30726b21..f68ecdf01d 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -53,7 +53,7 @@ def content_size(self): def presentation_content_size(self): return self.content_size - def get_window_state(self, state): + def get_window_state(self): window_state = self.native.WindowState if window_state == FormWindowState.Maximized: if self.native.FormBorderStyle == getattr(FormBorderStyle, "None"): From b0c37fe4df3cd088d5666843ecba795940ca4a1c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 4 Jul 2024 11:30:15 -0700 Subject: [PATCH 090/248] Various changes as per review: 5 --- android/src/toga_android/window.py | 27 ++------------------------- core/src/toga/app.py | 2 +- core/src/toga/constants/__init__.py | 13 +++++-------- 3 files changed, 8 insertions(+), 34 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 24ab7cad99..cfdc09a63d 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -32,6 +32,7 @@ def onGlobalLayout(self): class Window(Container): + # ActionBar is always hidden on Window. _actionbar_shown_by_default = False def __init__(self, interface, title, position, size): @@ -156,8 +157,6 @@ def get_window_state(self): return WindowState.PRESENTATION else: return WindowState.FULLSCREEN - # elif getattr(self, "_is_minimized", False) is True: - # return WindowState.MINIMIZED return WindowState.NORMAL def set_window_state(self, state): @@ -176,25 +175,6 @@ def set_window_state(self, state): if self._actionbar_shown_by_default: # pragma: no branch self.app.native.getSupportActionBar().show() self._is_presentation_mode = False - - # Doesn't work consistently - # elif current_state == WindowState.MINIMIZED: - # # Get the context - # context = self.app.native.getApplicationContext() - # # Create the intent to bring the activity to the foreground - # new_intent = Intent(context, self.app.native.getClass()) - # # These 2 options work properly by resuming existing MainActivity instead of creating - # # a new instance of MainActivity, but they work only once: - # new_intent.addFlags( - # Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP - # ) - # new_intent.setFlags( - # Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP - # ) - # # This works every time but starts new instance of MainActivity - # new_intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - # self.app.native.startActivity(new_intent) - # self._is_minimized = False else: if state in {WindowState.FULLSCREEN, WindowState.PRESENTATION}: decor_view.setSystemUiVisibility( @@ -208,10 +188,6 @@ def set_window_state(self, state): if self._actionbar_shown_by_default: # pragma: no branch self.app.native.getSupportActionBar().hide() self._is_presentation_mode = True - # elif state == WindowState.MINIMIZED: - # # This works but the issue lies in restoring to normal state - # self.app.native.moveTaskToBack(True) - # self._is_minimized = True ###################################################################### # Window capabilities @@ -233,6 +209,7 @@ def get_image_data(self): class MainWindow(Window): + # ActionBar is always hidden on MainWindow. _actionbar_shown_by_default = True def configure_titlebar(self): diff --git a/core/src/toga/app.py b/core/src/toga/app.py index ad58c3f454..d66aa2f206 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -800,7 +800,7 @@ def enter_presentation_mode( those windows will not be visible. :raises ValueError: If the presentation layout supplied is not a list of windows or - or a dict mapping windows to screens. + or a dict mapping windows to screens. """ if not windows: return diff --git a/core/src/toga/constants/__init__.py b/core/src/toga/constants/__init__.py index e020fa0256..70994c82b3 100644 --- a/core/src/toga/constants/__init__.py +++ b/core/src/toga/constants/__init__.py @@ -74,14 +74,8 @@ class WindowState(Enum): """The possible window states of an app.""" NORMAL = 0 - """``NORMAL`` state is when the window/app is not in any of the other window states. - - On Mobile Platforms(Like on Android) - Once the app is in minimized/background - state, then it is currently not possible to bring the app to foreground by setting - window state to ``NORMAL``. This is because of the design decisions imposed by the - native mobile platforms.(i.e., to prevent apps from becoming intrusively foreground - against the user's wishes.) - """ + """The ``NORMAL`` state represents the default state of the window or app when it is + not in any other specific window state.""" MINIMIZED = 1 """``MINIMIZED`` state is when the window isn't currently visible, although it will @@ -91,6 +85,9 @@ class WindowState(Enum): MAXIMIZED = 2 """The window is the largest size it can be on the screen with title bar and window chrome still visible. + + On Mobile Platforms(Like on Android) - The ``MAXIMIZED`` state is the same as the + ``NORMAL`` state. """ FULLSCREEN = 3 From 09ee4284e46a53f37ae3063f88ab1e64bb90856a Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 4 Jul 2024 12:53:17 -0700 Subject: [PATCH 091/248] Various changes as per review: 6 --- android/src/toga_android/window.py | 1 + cocoa/src/toga_cocoa/window.py | 64 +++++++++++++++++++--------- core/src/toga/window.py | 8 +--- gtk/src/toga_gtk/window.py | 53 +++++++++++++---------- winforms/src/toga_winforms/window.py | 60 +++++++++++++++----------- 5 files changed, 111 insertions(+), 75 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index cfdc09a63d..a116296ad6 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -176,6 +176,7 @@ def set_window_state(self, state): self.app.native.getSupportActionBar().show() self._is_presentation_mode = False else: + self.set_window_state(WindowState.NORMAL) if state in {WindowState.FULLSCREEN, WindowState.PRESENTATION}: decor_view.setSystemUiVisibility( decor_view.SYSTEM_UI_FLAG_FULLSCREEN diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index c1f5d8a300..8b47c36177 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -50,10 +50,20 @@ def windowDidResize_(self, notification) -> None: # Set the window to the new size self.interface.content.refresh() + @objc_method + def windowDidDeminiaturize_(self, notification) -> None: + # Complete any pending window state transition. + if getattr(self.impl, "_pending_window_state_transition", None) is not None: + self.impl.set_window_state(self.impl._pending_window_state_transition) + del self.impl._pending_window_state_transition + @objc_method def windowDidExitFullScreen_(self, notification) -> None: # This can be used to ensure that the window has completed exiting full screen. - pass + # Complete any pending window state transition. + if getattr(self.impl, "_pending_window_state_transition", None) is not None: + self.impl.set_window_state(self.impl._pending_window_state_transition) + del self.impl._pending_window_state_transition ###################################################################### # Toolbar delegate methods @@ -310,13 +320,40 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() - if state == WindowState.NORMAL: + if current_state != WindowState.NORMAL and state != WindowState.NORMAL: + # Set Window state to NORMAL before changing to other states as some + # states block changing window state without first exiting them or + # can even cause rendering glitches. + self._pending_window_state_transition = state + self.set_window_state(WindowState.NORMAL) + + elif state == WindowState.MAXIMIZED: + self.native.setIsZoomed(True) + + elif state == WindowState.MINIMIZED: + self.native.setIsMiniaturized(True) + + elif state == WindowState.FULLSCREEN: + self.native.toggleFullScreen(self.native) + + elif state == WindowState.PRESENTATION: + self.interface.app.enter_presentation_mode( + {self.interface.screen: self.interface} + ) + # WindowState.NORMAL case: + else: # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: self.native.setIsZoomed(False) + # Complete any pending window state transition. + if getattr(self, "_pending_window_state_transition", None) is not None: + self.set_window_state(self._pending_window_state_transition) + del self._pending_window_state_transition + # Deminiaturize the window to restore it to its previous state elif current_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(False) + # If the window is in full-screen mode, exit full-screen mode elif current_state == WindowState.FULLSCREEN: # This doesn't wait for completely exiting full screen mode. Hence, @@ -324,27 +361,14 @@ def set_window_state(self, state): # We should wait until `windowDidExitFullScreen_` is notified and then # return to user. self.native.toggleFullScreen(self.native) + # If the window is in presentation mode, exit presentation mode elif current_state == WindowState.PRESENTATION: self.interface.app.exit_presentation_mode() - else: - if state == WindowState.MAXIMIZED: - self.native.setIsZoomed(True) - - elif state == WindowState.MINIMIZED: - self.native.setIsMiniaturized(True) - - elif state == WindowState.FULLSCREEN: - self.native.toggleFullScreen(self.native) - - elif state == WindowState.PRESENTATION: - self.interface.app.enter_presentation_mode( - {self.interface.screen: self.interface} - ) - else: # pragma: no cover - # Marking this as no cover, since the type of the state parameter - # value is checked on the interface. - return + # Complete any pending window state transition. + if getattr(self, "_pending_window_state_transition", None) is not None: + self.set_window_state(self._pending_window_state_transition) + del self._pending_window_state_transition ###################################################################### # Window capabilities diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 9cea4d0e88..2814283034 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -476,13 +476,7 @@ def state(self, state: WindowState) -> None: f"Cannot set window state to {state} of a non-resizable window." ) else: - # Set Window state to NORMAL before changing to other states as some - # states block changing window state without first exiting them or - # can even cause rendering glitches. - self._impl.set_window_state(WindowState.NORMAL) - - if state != WindowState.NORMAL: - self._impl.set_window_state(state) + self._impl.set_window_state(state) ###################################################################### # Window capabilities diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index cefb73b4d0..b99bf4d64e 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -170,7 +170,33 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() - if state == WindowState.NORMAL: + if current_state != WindowState.NORMAL and state != WindowState.NORMAL: + # Set Window state to NORMAL before changing to other states as some + # states block changing window state without first exiting them or + # can even cause rendering glitches. + self._pending_window_state_transition = state + self.set_window_state(WindowState.NORMAL) + + elif state == WindowState.MAXIMIZED: + self.native.maximize() + + elif state == WindowState.MINIMIZED: + self.native.iconify() # pragma: no-cover-if-linux-wayland + + elif state == WindowState.FULLSCREEN: + self.native.fullscreen() + + elif state == WindowState.PRESENTATION: + self._before_presentation_mode_screen = self.interface.screen + if isinstance(self.native, Gtk.ApplicationWindow): + self.native.set_show_menubar(False) + if getattr(self, "native_toolbar", None): + self.native_toolbar.set_visible(False) + self.native.fullscreen() + self._is_presentation_mode = True + + # WindowState.NORMAL case: + else: # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: self.native.unmaximize() @@ -192,28 +218,11 @@ def set_window_state(self, state): self.interface.screen = self._before_presentation_mode_screen del self._before_presentation_mode_screen self._is_presentation_mode = False - else: - if state == WindowState.MAXIMIZED: - self.native.maximize() - - elif state == WindowState.MINIMIZED: - self.native.iconify() # pragma: no-cover-if-linux-wayland - elif state == WindowState.FULLSCREEN: - self.native.fullscreen() - - elif state == WindowState.PRESENTATION: - self._before_presentation_mode_screen = self.interface.screen - if isinstance(self.native, Gtk.ApplicationWindow): - self.native.set_show_menubar(False) - if getattr(self, "native_toolbar", None): - self.native_toolbar.set_visible(False) - self.native.fullscreen() - self._is_presentation_mode = True - else: # pragma: no cover - # Marking this as no cover, since the type of the state parameter - # value is checked on the interface. - pass + # Complete any pending window state transition. + if getattr(self, "_pending_window_state_transition", None) is not None: + self.set_window_state(self._pending_window_state_transition) + del self._pending_window_state_transition ###################################################################### # Window capabilities diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index d624534b27..419bc28ff8 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -210,8 +210,36 @@ def get_window_state(self): return def set_window_state(self, state): - if state == WindowState.NORMAL: - current_state = self.get_window_state() + current_state = self.get_window_state() + if current_state != WindowState.NORMAL and state != WindowState.NORMAL: + # Set Window state to NORMAL before changing to other states as some + # states block changing window state without first exiting them or + # can even cause rendering glitches. + self._pending_window_state_transition = state + self.set_window_state(WindowState.NORMAL) + + elif state == WindowState.MAXIMIZED: + self.native.WindowState = WinForms.FormWindowState.Maximized + + elif state == WindowState.MINIMIZED: + self.native.WindowState = WinForms.FormWindowState.Minimized + + elif state == WindowState.FULLSCREEN: + self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") + self.native.WindowState = WinForms.FormWindowState.Maximized + + elif state == WindowState.PRESENTATION: + self._before_presentation_mode_screen = self.interface.screen + if self.native.MainMenuStrip: + self.native.MainMenuStrip.Visible = False + if getattr(self, "toolbar_native", None): + self.toolbar_native.Visible = False + self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") + self.native.WindowState = WinForms.FormWindowState.Maximized + self._is_presentation_mode = True + + # WindowState.NORMAL case: + else: if current_state == WindowState.PRESENTATION: if self.native.MainMenuStrip: self.native.MainMenuStrip.Visible = True @@ -227,30 +255,10 @@ def set_window_state(self, state): "Sizable" if self.interface.resizable else "FixedSingle", ) self.native.WindowState = WinForms.FormWindowState.Normal - else: - if state == WindowState.MAXIMIZED: - self.native.WindowState = WinForms.FormWindowState.Maximized - - elif state == WindowState.MINIMIZED: - self.native.WindowState = WinForms.FormWindowState.Minimized - - elif state == WindowState.FULLSCREEN: - self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") - self.native.WindowState = WinForms.FormWindowState.Maximized - - elif state == WindowState.PRESENTATION: - self._before_presentation_mode_screen = self.interface.screen - if self.native.MainMenuStrip: - self.native.MainMenuStrip.Visible = False - if getattr(self, "toolbar_native", None): - self.toolbar_native.Visible = False - self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") - self.native.WindowState = WinForms.FormWindowState.Maximized - self._is_presentation_mode = True - else: # pragma: no cover - # Marking this as no cover, since the type of the state parameter - # value is checked on the interface. - pass + # Complete any pending window state transition. + if getattr(self, "_pending_window_state_transition", None) is not None: + self.set_window_state(self._pending_window_state_transition) + del self._pending_window_state_transition ###################################################################### # Window capabilities From 1bd1f39bd24b00262a1a6f82b39ccf6dea0beb58 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 5 Jul 2024 06:15:28 -0700 Subject: [PATCH 092/248] Various changes as per review: 7 --- cocoa/src/toga_cocoa/window.py | 9 ++- core/tests/window/test_window.py | 114 +++++++++++++++---------------- 2 files changed, 61 insertions(+), 62 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 8b47c36177..8292d59fb0 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -345,10 +345,6 @@ def set_window_state(self, state): # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: self.native.setIsZoomed(False) - # Complete any pending window state transition. - if getattr(self, "_pending_window_state_transition", None) is not None: - self.set_window_state(self._pending_window_state_transition) - del self._pending_window_state_transition # Deminiaturize the window to restore it to its previous state elif current_state == WindowState.MINIMIZED: @@ -365,7 +361,10 @@ def set_window_state(self, state): # If the window is in presentation mode, exit presentation mode elif current_state == WindowState.PRESENTATION: self.interface.app.exit_presentation_mode() - # Complete any pending window state transition. + + # Complete any pending window state transition. This operation is performed on the + # window delegate notifications for MINIMIZED and FULLSCREEN. Hence, exclude them here. + if current_state in {WindowState.MAXIMIZED, WindowState.PRESENTATION}: if getattr(self, "_pending_window_state_transition", None) is not None: self.set_window_state(self._pending_window_state_transition) del self._pending_window_state_transition diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index f2486dd0f8..d7adc3a40e 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -303,63 +303,6 @@ def test_visibility(window, app): assert not window.visible -def test_full_screen(window, app): - """A window can be set full screen.""" - with pytest.warns( - DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", - ): - assert not window.full_screen - with pytest.warns( - DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", - ): - window.full_screen = True - with pytest.warns( - DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", - ): - assert window.full_screen - assert_action_performed_with( - window, - "set window state to WindowState.FULLSCREEN", - state=WindowState.FULLSCREEN, - ) - with pytest.warns( - DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", - ): - window.full_screen = False - with pytest.warns( - DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", - ): - assert not window.full_screen - assert_action_performed_with( - window, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, - ) - - # Setting full screen to False when window is not in full screen, is a no-op - # - # We cannot assert that the action was not performed, since the same action - # was performed previously and would still be in EventLog. Therefore, we - # cannot check if the action was done by the first call or the second call. - # Hence, this is just to reach coverage. - with pytest.warns( - DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", - ): - assert not window.full_screen - with pytest.warns( - DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", - ): - window.full_screen = False - # assert_action_not_performed(window, "set window state to WindowState.NORMAL") - - @pytest.mark.parametrize( "state", [ @@ -1296,3 +1239,60 @@ def test_deprecated_names_closeable(): match=r"Window.closeable has been renamed Window.closable", ): assert window.closeable + + +def test_deprecated_full_screen(window, app): + """A window can be set full screen using the deprecated API.""" + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + assert not window.full_screen + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + window.full_screen = True + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + assert window.full_screen + assert_action_performed_with( + window, + "set window state to WindowState.FULLSCREEN", + state=WindowState.FULLSCREEN, + ) + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + window.full_screen = False + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + assert not window.full_screen + assert_action_performed_with( + window, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + + # Setting full screen to False when window is not in full screen, is a no-op + # + # We cannot assert that the action was not performed, since the same action + # was performed previously and would still be in EventLog. Therefore, we + # cannot check if the action was done by the first call or the second call. + # Hence, this is just to reach coverage. + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + assert not window.full_screen + with pytest.warns( + DeprecationWarning, + match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + ): + window.full_screen = False + # assert_action_not_performed(window, "set window state to WindowState.NORMAL") From 2b6d347f2858a04208affb769186162243c3be3b Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 5 Jul 2024 07:55:02 -0700 Subject: [PATCH 093/248] Added new tests as per review: 8 --- cocoa/src/toga_cocoa/window.py | 8 +- gtk/src/toga_gtk/window.py | 8 +- testbed/tests/app/test_desktop.py | 63 ++------------ testbed/tests/window/test_window.py | 120 +++++++++++++++++---------- winforms/src/toga_winforms/window.py | 8 +- 5 files changed, 103 insertions(+), 104 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 8292d59fb0..9a517eb8e2 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -320,7 +320,13 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() - if current_state != WindowState.NORMAL and state != WindowState.NORMAL: + if self.interface.app.is_presentation_mode and state != WindowState.NORMAL: + # Exit app presentation mode before changing to other states as it + # can cause rendering glitches. + self.interface.app.exit_presentation_mode() + self.set_window_state(state) + + elif current_state != WindowState.NORMAL and state != WindowState.NORMAL: # Set Window state to NORMAL before changing to other states as some # states block changing window state without first exiting them or # can even cause rendering glitches. diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index b99bf4d64e..fcdc841066 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -170,7 +170,13 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() - if current_state != WindowState.NORMAL and state != WindowState.NORMAL: + if self.interface.app.is_presentation_mode and state != WindowState.NORMAL: + # Exit app presentation mode before changing to other states as it + # can cause rendering glitches. + self.interface.app.exit_presentation_mode() + self.set_window_state(state) + + elif current_state != WindowState.NORMAL and state != WindowState.NORMAL: # Set Window state to NORMAL before changing to other states as some # states block changing window state without first exiting them or # can even cause rendering glitches. diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 81a06dfb1a..14baf916fc 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -182,66 +182,13 @@ async def test_menu_minimize(app, app_probe): window1.close() -async def test_presentation_mode_with_main_window( - app, app_probe, main_window, main_window_probe -): - """The app can enter presentation mode with just the main window.""" - # This test is required since the toolbar is present only on the main window. - try: - main_window.toolbar.add(app.cmd1) - extra_window = toga.Window( - "Extra Test Window", position=(150, 150), size=(200, 200) - ) - extra_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - extra_window.show() - extra_window_probe = window_probe(app, extra_window) - # Add delay for gtk to show the windows - await app_probe.redraw("Extra window is visible", delay=0.5) - - main_window_initial_content_size = main_window_probe.presentation_content_size - extra_window_initial_content_size = extra_window_probe.presentation_content_size - # Enter presentation mode with only main window - app.enter_presentation_mode([main_window]) - # Add delay for gtk to show the windows - await app_probe.redraw("App is in presentation mode", delay=0.5) - assert app.is_presentation_mode - - # Extra window should not be in presentation mode. - assert ( - extra_window_probe.get_window_state() != WindowState.PRESENTATION - ), "Extra Window:" - assert ( - extra_window_probe.presentation_content_size - == extra_window_initial_content_size - ), "Extra Window" - - # Main Window should be in presentation mode. - assert main_window_probe.get_window_state() == WindowState.PRESENTATION - assert main_window_probe.presentation_content_size[0] > 1000 - assert main_window_probe.presentation_content_size[1] > 700 - - # Exit presentation mode - app.exit_presentation_mode() - await app_probe.redraw("App is no longer in presentation mode", delay=0.5) - assert not app.is_presentation_mode - - assert ( - main_window_probe.presentation_content_size - == main_window_initial_content_size - ) - - finally: - main_window.toolbar.clear() - extra_window.close() - - -async def test_presentation_mode_with_screen_window_dict(app, app_probe): - """The app can enter presentation mode with a screen-window paired dict.""" +async def test_presentation_mode(app, app_probe): + """The app can enter presentation mode.""" try: window_information_list = list() windows_list = list() for i in range(len(app.screens)): - window = toga.Window( + window = toga.MainWindow( title=f"Test Window {i}", position=(150 + (10 * i), 150 + (10 * i)), size=(200, 200), @@ -252,6 +199,7 @@ async def test_presentation_mode_with_screen_window_dict(app, app_probe): window.content = toga.Box( style=Pack(background_color=f"#{r:02X}{g:02X}{b:02X}") ) + window.toolbar.add(app.cmd1) window.show() # Add delay for gtk to show the windows await app_probe.redraw(f"Test Window {i} is visible", delay=0.5) @@ -311,7 +259,7 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): window_information_list = list() excess_windows_list = list() for i in range(len(app.screens) + 1): - window = toga.Window( + window = toga.MainWindow( title=f"Test Window {i}", position=(150 + (10 * i), 150 + (10 * i)), size=(200, 200), @@ -322,6 +270,7 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): window.content = toga.Box( style=Pack(background_color=f"#{r:02X}{g:02X}{b:02X}") ) + window.toolbar.add(app.cmd1) window.show() # Add delay for gtk to show the windows await app_probe.redraw(f"Test Window {i} is visible", delay=0.5) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 7c898e6016..e58e56a865 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -143,27 +143,8 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): finally: main_window.content = orig_content - async def test_full_screen(main_window, main_window_probe): - """Window can be made full screen""" - with pytest.warns( - DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", - ): - main_window.full_screen = True - await main_window_probe.wait_for_window("Main window is in full screen") - - with pytest.warns( - DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", - ): - main_window.full_screen = False - await main_window_probe.wait_for_window( - "Main window is no longer in full screen" - ) - async def test_window_state_minimized(main_window, main_window_probe): """Window can have minimized window state""" - # WindowState.MINIMIZED is unimplemented on both Android and iOS. assert main_window_probe.get_window_state() == WindowState.NORMAL assert main_window_probe.get_window_state() != WindowState.MINIMIZED main_window.state = WindowState.MINIMIZED @@ -173,7 +154,6 @@ async def test_window_state_minimized(main_window, main_window_probe): async def test_window_state_maximized(main_window, main_window_probe): """Window can have maximized window state""" - # WindowState.MAXIMIZED is unimplemented on both Android and iOS. assert main_window_probe.get_window_state() == WindowState.NORMAL assert main_window_probe.get_window_state() != WindowState.MAXIMIZED main_window.state = WindowState.MAXIMIZED @@ -183,7 +163,6 @@ async def test_window_state_maximized(main_window, main_window_probe): async def test_window_state_full_screen(main_window, main_window_probe): """Window can have full screen window state""" - # WindowState.FULLSCREEN is implemented on Android but not on iOS. assert main_window_probe.get_window_state() == WindowState.NORMAL assert main_window_probe.get_window_state() != WindowState.FULLSCREEN # Make main window full screen @@ -626,8 +605,7 @@ async def test_window_state_minimized(second_window, second_window_probe): second_window.state = WindowState.MINIMIZED # A longer delay to allow for genie animations await second_window_probe.wait_for_window( - "Secondary window is minimized", - minimize=True, + "Secondary window is minimized", minimize=True ) assert second_window_probe.get_window_state() == WindowState.MINIMIZED @@ -638,8 +616,7 @@ async def test_window_state_minimized(second_window, second_window_probe): second_window.state = WindowState.NORMAL # A longer delay to allow for genie animations await second_window_probe.wait_for_window( - "Secondary window is not minimized", - minimize=True, + "Secondary window is not minimized", minimize=True ) assert second_window_probe.get_window_state() != WindowState.MINIMIZED if second_window_probe.supports_minimizable: @@ -647,8 +624,7 @@ async def test_window_state_minimized(second_window, second_window_probe): second_window.state = WindowState.NORMAL await second_window_probe.wait_for_window( - "Secondary window is still not minimized", - minimize=True, + "Secondary window is still not minimized", minimize=True ) assert second_window_probe.get_window_state() != WindowState.MINIMIZED @@ -722,8 +698,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): second_window.state = WindowState.FULLSCREEN # A longer delay to allow for genie animations await second_window_probe.wait_for_window( - "Secondary window is full screen", - full_screen=True, + "Secondary window is full screen", full_screen=True ) assert second_window_probe.get_window_state() == WindowState.FULLSCREEN assert second_window_probe.content_size[0] > initial_content_size[0] @@ -731,8 +706,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): second_window.state = WindowState.FULLSCREEN await second_window_probe.wait_for_window( - "Secondary window is still full screen", - full_screen=True, + "Secondary window is still full screen", full_screen=True ) assert second_window_probe.get_window_state() == WindowState.FULLSCREEN assert second_window_probe.content_size[0] > initial_content_size[0] @@ -741,8 +715,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): second_window.state = WindowState.NORMAL # A longer delay to allow for genie animations await second_window_probe.wait_for_window( - "Secondary window is not full screen", - full_screen=True, + "Secondary window is not full screen", full_screen=True ) assert second_window_probe.get_window_state() != WindowState.FULLSCREEN assert second_window_probe.is_resizable @@ -750,8 +723,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): second_window.state = WindowState.NORMAL await second_window_probe.wait_for_window( - "Secondary window is still not full screen", - full_screen=True, + "Secondary window is still not full screen", full_screen=True ) assert second_window_probe.get_window_state() != WindowState.FULLSCREEN assert second_window_probe.content_size == initial_content_size @@ -777,8 +749,7 @@ async def test_window_state_presentation(second_window, second_window_probe): second_window.state = WindowState.PRESENTATION # A longer delay to allow for genie animations await second_window_probe.wait_for_window( - "Secondary window is in presentation mode", - full_screen=True, + "Secondary window is in presentation mode", full_screen=True ) assert second_window_probe.get_window_state() == WindowState.PRESENTATION assert ( @@ -790,8 +761,7 @@ async def test_window_state_presentation(second_window, second_window_probe): second_window.state = WindowState.PRESENTATION await second_window_probe.wait_for_window( - "Secondary window is still in presentation mode", - full_screen=True, + "Secondary window is still in presentation mode", full_screen=True ) assert second_window_probe.get_window_state() == WindowState.PRESENTATION assert ( @@ -804,8 +774,7 @@ async def test_window_state_presentation(second_window, second_window_probe): second_window.state = WindowState.NORMAL # A longer delay to allow for genie animations await second_window_probe.wait_for_window( - "Secondary window is not in presentation mode", - full_screen=True, + "Secondary window is not in presentation mode", full_screen=True ) assert second_window_probe.get_window_state() != WindowState.PRESENTATION assert second_window_probe.is_resizable @@ -813,8 +782,7 @@ async def test_window_state_presentation(second_window, second_window_probe): second_window.state = WindowState.NORMAL await second_window_probe.wait_for_window( - "Secondary window is still not in presentation mode", - full_screen=True, + "Secondary window is still not in presentation mode", full_screen=True ) assert second_window_probe.get_window_state() != WindowState.PRESENTATION assert second_window_probe.presentation_content_size == initial_content_size @@ -823,11 +791,75 @@ async def test_window_state_presentation(second_window, second_window_probe): "second_window_class, second_window_kwargs", [ ( - toga.Window, + toga.MainWindow, dict(title="Secondary Window", position=(200, 150)), ) ], ) + @pytest.mark.parametrize( + "initial_state, final_state", + [ + # Direct switch from MINIMIZED: + (WindowState.MINIMIZED, WindowState.MAXIMIZED), + (WindowState.MINIMIZED, WindowState.FULLSCREEN), + (WindowState.MINIMIZED, WindowState.PRESENTATION), + # Direct switch from MAXIMIZED: + (WindowState.MAXIMIZED, WindowState.MINIMIZED), + (WindowState.MAXIMIZED, WindowState.FULLSCREEN), + (WindowState.MAXIMIZED, WindowState.PRESENTATION), + # Direct switch from FULLSCREEN: + (WindowState.FULLSCREEN, WindowState.MINIMIZED), + (WindowState.FULLSCREEN, WindowState.MAXIMIZED), + (WindowState.FULLSCREEN, WindowState.PRESENTATION), + # Direct switch from PRESENTATION: + (WindowState.PRESENTATION, WindowState.MINIMIZED), + (WindowState.PRESENTATION, WindowState.MAXIMIZED), + (WindowState.PRESENTATION, WindowState.FULLSCREEN), + ], + ) + async def test_window_state_direct_change( + app, initial_state, final_state, second_window, second_window_probe + ): + second_window.toolbar.add(app.cmd1) + second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + + assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window_probe.get_window_state() != initial_state + assert second_window_probe.get_window_state() != final_state + + # Set to initial state + second_window.state = initial_state + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + f"Secondary window is in {initial_state}", full_screen=True + ) + + assert second_window_probe.get_window_state() != WindowState.NORMAL + assert second_window_probe.get_window_state() == initial_state + assert second_window_probe.get_window_state() != final_state + + # Set to final state + second_window.state = final_state + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + f"Secondary window is in {final_state}", full_screen=True + ) + + assert second_window_probe.get_window_state() != WindowState.NORMAL + assert second_window_probe.get_window_state() == final_state + assert second_window_probe.get_window_state() != initial_state + + # Set to normal state + second_window.state = WindowState.NORMAL + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is in WindowState.NORMAL", full_screen=True + ) + + assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window_probe.get_window_state() != initial_state + assert second_window_probe.get_window_state() != final_state + async def test_screen(second_window, second_window_probe): """The window can be relocated to another screen, using both absolute and relative screen positions.""" diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 419bc28ff8..99715024a7 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -211,7 +211,13 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() - if current_state != WindowState.NORMAL and state != WindowState.NORMAL: + if self.interface.app.is_presentation_mode and state != WindowState.NORMAL: + # Exit app presentation mode before changing to other states as it + # can cause rendering glitches. + self.interface.app.exit_presentation_mode() + self.set_window_state(state) + + elif current_state != WindowState.NORMAL and state != WindowState.NORMAL: # Set Window state to NORMAL before changing to other states as some # states block changing window state without first exiting them or # can even cause rendering glitches. From 6b3e88391da04c2ca6d81e9a499fbe8681625c4e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 6 Jul 2024 07:53:32 -0700 Subject: [PATCH 094/248] Added tests as per review: 9 --- cocoa/src/toga_cocoa/app.py | 6 +-- core/src/toga/constants/__init__.py | 6 ++- core/tests/app/test_app.py | 79 +++++++++++++++++++++++++++ dummy/src/toga_dummy/window.py | 3 ++ testbed/tests/app/test_desktop.py | 83 +++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 4 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 94b72ff083..6ac8169c1d 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -504,9 +504,9 @@ def enter_presentation_mode(self, screen_window_dict): window.content._impl.native.enterFullScreenMode( screen._impl.native, withOptions=opts ) - # Going full screen causes the window content to be re-homed - # in a NSFullScreenWindow; teach the new parent window - # about its Toga representations. + # Going presentation mode causes the window content to be re-homed + # in a NSFullScreenWindow; teach the new parent window about its + # Toga representations. window.content._impl.native.window._impl = window._impl window.content._impl.native.window.interface = window window.content.refresh() diff --git a/core/src/toga/constants/__init__.py b/core/src/toga/constants/__init__.py index 70994c82b3..594e9d8e34 100644 --- a/core/src/toga/constants/__init__.py +++ b/core/src/toga/constants/__init__.py @@ -71,7 +71,11 @@ def __str__(self) -> str: class WindowState(Enum): - """The possible window states of an app.""" + """The possible window states of an app. + + Note: Changing window state while the app is in presentation mode will cause + the app to exit presentation mode, and the new window state will be set. + """ NORMAL = 0 """The ``NORMAL`` state represents the default state of the window or app when it is diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 9eeead3267..3a18fc1019 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -9,6 +9,7 @@ import pytest import toga +from toga.constants import WindowState from toga_dummy.utils import ( EventLog, assert_action_not_performed, @@ -599,6 +600,84 @@ def test_presentation_mode_no_op(event_loop): assert not app.is_presentation_mode +@pytest.mark.parametrize( + "new_window_state", + [ + WindowState.MINIMIZED, + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + ], +) +def test_presentation_mode_exit_on_window_state_change(event_loop, new_window_state): + """Changing window state exits presentation mode and sets the new state.""" + app = toga.App(formal_name="Test App", app_id="org.example.test") + extra_window = toga.Window() + + # Enter presentation mode + app.enter_presentation_mode([app.main_window]) + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: app.main_window}, + ) + + assert app.is_presentation_mode + assert app.main_window.state == WindowState.PRESENTATION + assert extra_window.state != WindowState.PRESENTATION + + # Changing window state of main_window should make the app exit presentation mode. + app.main_window.state = new_window_state + assert_action_performed_with( + app.main_window, + f"set window state to {new_window_state}", + state=new_window_state, + ) + + assert not app.is_presentation_mode + assert_action_performed( + app, + "exit presentation mode", + ) + assert app.main_window.state != WindowState.PRESENTATION + assert app.main_window.state == new_window_state + + # Reset window states + app.main_window.state = WindowState.NORMAL + extra_window.state = WindowState.NORMAL + + # Enter presentation mode again + app.enter_presentation_mode([app.main_window]) + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: app.main_window}, + ) + + assert app.is_presentation_mode + assert app.main_window.state == WindowState.PRESENTATION + assert extra_window.state != WindowState.PRESENTATION + + # Changing window state of extra window should make the app exit presentation mode. + extra_window.state = new_window_state + assert_action_performed_with( + extra_window, + f"set window state to {new_window_state}", + state=new_window_state, + ) + + assert not app.is_presentation_mode + assert_action_performed( + app, + "exit presentation mode", + ) + assert app.main_window.state != WindowState.PRESENTATION + assert extra_window.state == new_window_state + + # Reset window states + app.main_window.state = WindowState.NORMAL + extra_window.state = WindowState.NORMAL + + def test_show_hide_cursor(app): """The app cursor can be shown and hidden.""" app.hide_cursor() diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 8083a34043..79d4dde0c3 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -137,6 +137,9 @@ def get_window_state(self): return self._get_value("state", WindowState.NORMAL) def set_window_state(self, state): + if self.interface.app.is_presentation_mode and state != WindowState.NORMAL: + self.interface.app.exit_presentation_mode() + self.set_window_state(state) self._action(f"set window state to {state}", state=state) self._set_value("state", state) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 14baf916fc..60ecae280f 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -258,6 +258,7 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): try: window_information_list = list() excess_windows_list = list() + # Generate an additional window compared to the number of screens present. for i in range(len(app.screens) + 1): window = toga.MainWindow( title=f"Test Window {i}", @@ -336,6 +337,88 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): window.close() +@pytest.mark.parametrize( + "new_window_state", + [ + WindowState.MINIMIZED, + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + ], +) +async def test_presentation_mode_exit_on_window_state_change( + app, app_probe, main_window, main_window_probe, new_window_state +): + """Changing window state exits presentation mode and sets the new state.""" + try: + main_window.toolbar.add(app.cmd1) + extra_window = toga.MainWindow( + title="Extra Window", position=(150, 150), size=(200, 200) + ) + extra_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + extra_window_probe = window_probe(app, extra_window) + extra_window.show() + # Add delay for gtk to show the windows + await app_probe.redraw("Extra window is shown", delay=0.5) + + # Enter presentation mode + app.enter_presentation_mode([main_window]) + # Add delay for gtk to show the windows + await app_probe.redraw("App is in presentation mode", delay=0.5) + + assert app.is_presentation_mode + assert main_window_probe.get_window_state() == WindowState.PRESENTATION + assert extra_window_probe.get_window_state() != WindowState.PRESENTATION + + # Changing window state of main window should make the app exit presentation mode. + main_window.state = new_window_state + # Add delay for gtk to show the windows + await app_probe.redraw( + "App is not in presentation mode" f"\nMain Window is in {new_window_state}", + delay=0.5, + ) + + assert not app.is_presentation_mode + assert main_window_probe.get_window_state != WindowState.PRESENTATION + assert main_window_probe.get_window_state() == new_window_state + + # Reset window states + main_window.state = WindowState.NORMAL + extra_window.state = WindowState.NORMAL + # Add delay for gtk to show the windows + await app_probe.redraw("All windows are in WindowState.NORMAL", delay=0.5) + + # Enter presentation mode again + app.enter_presentation_mode([main_window]) + # Add delay for gtk to show the windows + await app_probe.redraw("App is in presentation mode", delay=0.5) + assert app.is_presentation_mode + assert main_window_probe.get_window_state() == WindowState.PRESENTATION + assert extra_window_probe.get_window_state() != WindowState.PRESENTATION + + # Changing window state of extra window should make the app exit presentation mode. + extra_window.state = new_window_state + # Add delay for gtk to show the windows + await app_probe.redraw( + "App is not in presentation mode" + f"\nExtra Window is in {new_window_state}", + delay=0.5, + ) + + assert not app.is_presentation_mode + assert main_window_probe.get_window_state() != WindowState.PRESENTATION + assert extra_window_probe.get_window_state() == new_window_state + + # Reset window states + main_window.state = WindowState.NORMAL + extra_window.state = WindowState.NORMAL + # Add delay for gtk to show the windows + await app_probe.redraw("All windows are in WindowState.NORMAL", delay=0.5) + + finally: + main_window.toolbar.clear() + extra_window.close() + + async def test_show_hide_cursor(app, app_probe): """The app cursor can be hidden and shown""" assert app_probe.is_cursor_visible From 74685d10dcc0c3015f4f50bc1e98162f7b930795 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 6 Jul 2024 08:24:57 -0700 Subject: [PATCH 095/248] Restored to app presentation mode test to non-parameterized form to continue CI --- core/tests/app/test_app.py | 190 +++++++++++++++++++++---------------- 1 file changed, 106 insertions(+), 84 deletions(-) diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 3a18fc1019..1a8ef8864a 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -466,84 +466,86 @@ def startup(self): BadMainWindowApp(formal_name="Test App", app_id="org.example.test") -@pytest.mark.parametrize( - "app, windows_list", - [ - (toga.App(formal_name="Test App", app_id="org.example.test"), []), - (toga.App(formal_name="Test App", app_id="org.example.test"), [toga.Window()]), - ( - toga.App(formal_name="Test App", app_id="org.example.test"), - [toga.Window(), toga.Window()], - ), - ], -) -def test_presentation_mode_with_windows_list(event_loop, app, windows_list): +def test_presentation_mode_with_windows_list(event_loop): """The app can enter presentation mode with a windows list.""" + app = toga.App(formal_name="Test App", app_id="org.example.test") + window1 = toga.Window() + window2 = toga.Window() + assert not app.is_presentation_mode - app.enter_presentation_mode(windows_list) - if not windows_list: - # Entering presentation mode with an empty windows list, is a no-op: - assert not app.is_presentation_mode - assert_action_not_performed(app, "enter presentation mode") - else: - assert app.is_presentation_mode - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={ - app.screens[i]: window for i, window in enumerate(windows_list) - }, - ) + # Entering presentation mode with an empty windows list, is a no-op: + app.enter_presentation_mode([]) + assert not app.is_presentation_mode + assert_action_not_performed(app, "enter presentation mode") - # Exit presentation mode: - app.exit_presentation_mode() - assert not app.is_presentation_mode - assert_action_performed( - app, - "exit presentation mode", - ) + # Enter presentation mode with 1 window: + app.enter_presentation_mode([window1]) + assert app.is_presentation_mode + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window1}, + ) + + # Enter presentation mode with 2 windows: + app.enter_presentation_mode([window1, window2]) + assert app.is_presentation_mode + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, + ) + # Exit presentation mode: + app.exit_presentation_mode() + assert_action_performed( + app, + "exit presentation mode", + ) -@pytest.mark.parametrize( - "app, screen_window_dict", - [ - (toga.App(formal_name="Test App", app_id="org.example.test"), {}), - ( - toga.App(formal_name="Test App", app_id="org.example.test"), - {toga.App.app.screens[0]: toga.Window()}, - ), - ( - toga.App(formal_name="Test App", app_id="org.example.test"), - { - toga.App.app.screens[0]: toga.Window(), - toga.App.app.screens[1]: toga.Window(), - }, - ), - ], -) -def test_presentation_mode_with_screen_window_dict(event_loop, app, screen_window_dict): +def test_presentation_mode_with_screen_window_dict(event_loop): """The app can enter presentation mode with a screen-window paired dict.""" + app = toga.App(formal_name="Test App", app_id="org.example.test") + window1 = toga.Window() + window2 = toga.Window() + assert not app.is_presentation_mode - app.enter_presentation_mode(screen_window_dict) - if not screen_window_dict: - # Entering presentation mode with an empty screen-window dict, is a no-op: - assert not app.is_presentation_mode - assert_action_not_performed(app, "enter presentation mode") - else: - assert app.is_presentation_mode - assert_action_performed_with( - app, "enter presentation mode", screen_window_dict=screen_window_dict - ) + # Entering presentation mode with an empty dict, is a no-op: + app.enter_presentation_mode({}) + assert not app.is_presentation_mode + assert_action_not_performed(app, "enter presentation mode") - # Exit presentation mode: - app.exit_presentation_mode() - assert not app.is_presentation_mode - assert_action_performed( - app, - "exit presentation mode", - ) + # Enter presentation mode with an 1 element screen-window dict: + app.enter_presentation_mode({app.screens[0]: window1}) + assert app.is_presentation_mode + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window1}, + ) + # Exit presentation mode: + app.exit_presentation_mode() + assert_action_performed( + app, + "exit presentation mode", + ) + + # Enter presentation mode with a 2 elements screen-window dict: + app.enter_presentation_mode({app.screens[0]: window1, app.screens[1]: window2}) + assert app.is_presentation_mode + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, + ) + # Exit presentation mode: + app.exit_presentation_mode() + assert_action_performed( + app, + "exit presentation mode", + ) def test_presentation_mode_with_excess_windows_list(event_loop): @@ -1004,21 +1006,33 @@ def test_deprecated_full_screen(event_loop): window1 = toga.Window() window2 = toga.Window() + is_full_screen_warning = ( + r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead." + ) + set_full_screen_warning = ( + r"`App.set_full_screen\(\)` is deprecated. " + r"Use `App.enter_presentation_mode\(\)` instead." + ) + exit_full_screen_warning = ( + r"`App.exit_full_screen\(\)` is deprecated. " + r"Use `App.exit_presentation_mode\(\)` instead." + ) + with pytest.warns( DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + match=is_full_screen_warning, ): assert not app.is_full_screen # If we're not full screen, exiting full screen is a no-op with pytest.warns( DeprecationWarning, - match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", + match=exit_full_screen_warning, ): app.exit_full_screen() with pytest.warns( DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + match=is_full_screen_warning, ): assert not app.is_full_screen assert_action_not_performed(app, "exit presentation mode") @@ -1026,13 +1040,13 @@ def test_deprecated_full_screen(event_loop): # Trying to enter full screen with no windows is a no-op with pytest.warns( DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + match=set_full_screen_warning, ): app.set_full_screen() with pytest.warns( DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + match=is_full_screen_warning, ): assert not app.is_full_screen assert_action_not_performed(app, "enter presentation mode") @@ -1040,12 +1054,12 @@ def test_deprecated_full_screen(event_loop): # Enter full screen with 2 windows with pytest.warns( DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + match=set_full_screen_warning, ): app.set_full_screen(window2, app.main_window) with pytest.warns( DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + match=is_full_screen_warning, ): assert app.is_full_screen assert_action_performed_with( @@ -1057,12 +1071,12 @@ def test_deprecated_full_screen(event_loop): # Change the screens that are full screen with pytest.warns( DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + match=set_full_screen_warning, ): app.set_full_screen(app.main_window, window1) with pytest.warns( DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + match=is_full_screen_warning, ): assert app.is_full_screen assert_action_performed_with( @@ -1074,12 +1088,12 @@ def test_deprecated_full_screen(event_loop): # Exit full screen mode with pytest.warns( DeprecationWarning, - match=r"`App.exit_full_screen\(\)` is deprecated. Use `App.exit_presentation_mode\(\)` instead.", + match=exit_full_screen_warning, ): app.exit_full_screen() with pytest.warns( DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + match=is_full_screen_warning, ): assert not app.is_full_screen assert_action_performed( @@ -1094,21 +1108,29 @@ def test_deprecated_set_empty_full_screen_window_list(event_loop): window1 = toga.Window() window2 = toga.Window() + is_full_screen_warning = ( + r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead." + ) + set_full_screen_warning = ( + r"`App.set_full_screen\(\)` is deprecated. " + r"Use `App.enter_presentation_mode\(\)` instead." + ) + with pytest.warns( DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + match=is_full_screen_warning, ): assert not app.is_full_screen # Change the screens that are full screen with pytest.warns( DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + match=set_full_screen_warning, ): app.set_full_screen(window1, window2) with pytest.warns( DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + match=is_full_screen_warning, ): assert app.is_full_screen assert_action_performed_with( @@ -1119,12 +1141,12 @@ def test_deprecated_set_empty_full_screen_window_list(event_loop): # Exit full screen mode by setting no windows full screen with pytest.warns( DeprecationWarning, - match=r"`App.set_full_screen\(\)` is deprecated. Use `App.enter_presentation_mode\(\)` instead.", + match=set_full_screen_warning, ): app.set_full_screen() with pytest.warns( DeprecationWarning, - match=r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead.", + match=is_full_screen_warning, ): assert not app.is_full_screen assert_action_performed( From 25c420f0ae365164852e36f2a3c843060ec8550a Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 6 Jul 2024 08:45:25 -0700 Subject: [PATCH 096/248] Fixed testbed failures --- testbed/tests/window/test_window.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index e58e56a865..c4dbcca639 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -860,6 +860,15 @@ async def test_window_state_direct_change( assert second_window_probe.get_window_state() != initial_state assert second_window_probe.get_window_state() != final_state + @pytest.mark.parametrize( + "second_window_class, second_window_kwargs", + [ + ( + toga.MainWindow, + 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.""" From 83dcab4fc359974fae18cdf2cec81097c3ae20f0 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 6 Jul 2024 04:10:07 -0700 Subject: [PATCH 097/248] Minor change on testbed --- testbed/tests/window/test_window.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index c4dbcca639..f62f9866ba 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -822,6 +822,12 @@ async def test_window_state_direct_change( ): second_window.toolbar.add(app.cmd1) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + second_window.show() + + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is visible", full_screen=True + ) assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.get_window_state() != initial_state From 8bba100624d7b9859c87d31173ff874505c221dc Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 6 Jul 2024 04:23:50 -0700 Subject: [PATCH 098/248] Minor change on cocoa --- cocoa/src/toga_cocoa/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 9a517eb8e2..c0737b1039 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -46,7 +46,7 @@ def windowShouldClose_(self, notification) -> bool: @objc_method def windowDidResize_(self, notification) -> None: - if self.interface.content: + if getattr(self, "interface", None) is not None and self.interface.content: # Set the window to the new size self.interface.content.refresh() From ab0ed4545ba4faf5d449b5bc986f5fbf9ac94267 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 6 Jul 2024 09:32:54 -0700 Subject: [PATCH 099/248] Fixed cocoa backend and tests --- cocoa/src/toga_cocoa/window.py | 12 +++++------- core/src/toga/window.py | 14 ++++++++++++++ gtk/src/toga_gtk/window.py | 12 +++++------- testbed/tests/window/test_window.py | 11 ----------- winforms/src/toga_winforms/window.py | 12 +++++------- 5 files changed, 29 insertions(+), 32 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index c0737b1039..70a0f0b16c 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -320,13 +320,11 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() - if self.interface.app.is_presentation_mode and state != WindowState.NORMAL: - # Exit app presentation mode before changing to other states as it - # can cause rendering glitches. - self.interface.app.exit_presentation_mode() - self.set_window_state(state) - - elif current_state != WindowState.NORMAL and state != WindowState.NORMAL: + if ( + current_state != WindowState.NORMAL + and state != WindowState.NORMAL + and (getattr(self, "_pending_window_state_transition", None) is None) + ): # Set Window state to NORMAL before changing to other states as some # states block changing window state without first exiting them or # can even cause rendering glitches. diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 2814283034..3df9345ca3 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -318,6 +318,13 @@ def _close(self): # The actual logic for closing a window. This is abstracted so that the testbed # can monkeypatch this method, recording the close request without actually # closing the app. + + # Restore to normal state if in presentation mode. On some backends (e.g., Cocoa), + # the content itself is in presentation mode, and not the window. Directly closing + # the window without the content exiting presentation mode can cause rendering issues. + if self.state == WindowState.PRESENTATION: + self.state = WindowState.NORMAL + if self.content: self.content.window = None self.app.windows.discard(self) @@ -465,6 +472,13 @@ def state(self) -> WindowState: @state.setter def state(self, state: WindowState) -> None: + # Changing the window state while the app is in presentation mode can cause + # rendering glitches. Hence, first exit presentation and then set the new + # window state. + # The check for determining if the app is currently in presentation + # mode, is already present in exit_presentation_mode(), so just call it. + self.app.exit_presentation_mode() + current_state = self._impl.get_window_state() if current_state != state: if not self.resizable and state in { diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index fcdc841066..3478293503 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -170,13 +170,11 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() - if self.interface.app.is_presentation_mode and state != WindowState.NORMAL: - # Exit app presentation mode before changing to other states as it - # can cause rendering glitches. - self.interface.app.exit_presentation_mode() - self.set_window_state(state) - - elif current_state != WindowState.NORMAL and state != WindowState.NORMAL: + if ( + current_state != WindowState.NORMAL + and state != WindowState.NORMAL + and (getattr(self, "_pending_window_state_transition", None) is None) + ): # Set Window state to NORMAL before changing to other states as some # states block changing window state without first exiting them or # can even cause rendering glitches. diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index f62f9866ba..c24a3b2fc6 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -855,17 +855,6 @@ async def test_window_state_direct_change( assert second_window_probe.get_window_state() == final_state assert second_window_probe.get_window_state() != initial_state - # Set to normal state - second_window.state = WindowState.NORMAL - # A longer delay to allow for genie animations - await second_window_probe.wait_for_window( - "Secondary window is in WindowState.NORMAL", full_screen=True - ) - - assert second_window_probe.get_window_state() == WindowState.NORMAL - assert second_window_probe.get_window_state() != initial_state - assert second_window_probe.get_window_state() != final_state - @pytest.mark.parametrize( "second_window_class, second_window_kwargs", [ diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 99715024a7..c184b8aa2b 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -211,13 +211,11 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() - if self.interface.app.is_presentation_mode and state != WindowState.NORMAL: - # Exit app presentation mode before changing to other states as it - # can cause rendering glitches. - self.interface.app.exit_presentation_mode() - self.set_window_state(state) - - elif current_state != WindowState.NORMAL and state != WindowState.NORMAL: + if ( + current_state != WindowState.NORMAL + and state != WindowState.NORMAL + and (getattr(self, "_pending_window_state_transition", None) is None) + ): # Set Window state to NORMAL before changing to other states as some # states block changing window state without first exiting them or # can even cause rendering glitches. From 4fa4af6f9363eb27a7259770a61ee6d0380bde52 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 6 Jul 2024 10:05:33 -0700 Subject: [PATCH 100/248] Fixed dummy backend --- dummy/src/toga_dummy/window.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 79d4dde0c3..8083a34043 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -137,9 +137,6 @@ def get_window_state(self): return self._get_value("state", WindowState.NORMAL) def set_window_state(self, state): - if self.interface.app.is_presentation_mode and state != WindowState.NORMAL: - self.interface.app.exit_presentation_mode() - self.set_window_state(state) self._action(f"set window state to {state}", state=state) self._set_value("state", state) From a308ea74d51c9bd3d07c36466e95ea58fe8e3388 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 7 Jul 2024 04:16:18 -0700 Subject: [PATCH 101/248] Fixed core and added new test --- core/src/toga/window.py | 10 +++++--- core/tests/window/test_window.py | 41 +++++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 3df9345ca3..9f29e3619b 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -475,9 +475,13 @@ def state(self, state: WindowState) -> None: # Changing the window state while the app is in presentation mode can cause # rendering glitches. Hence, first exit presentation and then set the new # window state. - # The check for determining if the app is currently in presentation - # mode, is already present in exit_presentation_mode(), so just call it. - self.app.exit_presentation_mode() + if state != WindowState.NORMAL or ( + any( + window.state == WindowState.PRESENTATION and window != self + for window in self.app.windows + ) + ): + self.app.exit_presentation_mode() current_state = self._impl.get_window_state() if current_state != state: diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index d7adc3a40e..909e825c63 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -343,8 +343,8 @@ def test_window_state(window, state): ], ) def test_window_state_same_as_previous(window, state): - # Setting the window state same as the current window state is a no-op. - # + """Setting the window state same as the current window state is a no-op.""" + # Here, setting the same state twice and then checking with assert_action_not_performed # will still report that the window state setting action was performed. This is because # the action was also performed for the first-time setting of the window state, @@ -383,6 +383,7 @@ def test_window_state_same_as_previous(window, state): ], ) def test_non_resizable_window_state(state): + """Non-resizable window's states other than minimized or normal are no-ops.""" non_resizable_window = toga.Window(title="Non-Resizable Window", resizable=False) with pytest.warns( UserWarning, @@ -395,6 +396,25 @@ def test_non_resizable_window_state(state): non_resizable_window.close() +def test_close_direct_in_presentation_mode(window, app): + """Directly closing a window in presentation mode restores to normal first.""" + window.state = WindowState.PRESENTATION + assert window.state == WindowState.PRESENTATION + assert_action_performed_with( + window, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + + window.close() + assert window.state == WindowState.NORMAL + assert_action_performed_with( + window, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + + def test_close_direct(window, app): """A window can be closed directly.""" on_close_handler = Mock(return_value=True) @@ -1243,19 +1263,22 @@ def test_deprecated_names_closeable(): def test_deprecated_full_screen(window, app): """A window can be set full screen using the deprecated API.""" + full_screen_warning = ( + "`Window.full_screen` is deprecated. Use `Window.state` instead." + ) with pytest.warns( DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + match=full_screen_warning, ): assert not window.full_screen with pytest.warns( DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + match=full_screen_warning, ): window.full_screen = True with pytest.warns( DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + match=full_screen_warning, ): assert window.full_screen assert_action_performed_with( @@ -1265,12 +1288,12 @@ def test_deprecated_full_screen(window, app): ) with pytest.warns( DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + match=full_screen_warning, ): window.full_screen = False with pytest.warns( DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + match=full_screen_warning, ): assert not window.full_screen assert_action_performed_with( @@ -1287,12 +1310,12 @@ def test_deprecated_full_screen(window, app): # Hence, this is just to reach coverage. with pytest.warns( DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + match=full_screen_warning, ): assert not window.full_screen with pytest.warns( DeprecationWarning, - match="`Window.full_screen` is deprecated. Use `Window.state` instead.", + match=full_screen_warning, ): window.full_screen = False # assert_action_not_performed(window, "set window state to WindowState.NORMAL") From 42642ee767c2e6a25332911530584417826a3137 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 7 Jul 2024 05:45:14 -0700 Subject: [PATCH 102/248] Fixed winforms --- cocoa/src/toga_cocoa/window.py | 4 +- testbed/tests/app/test_desktop.py | 68 +++++++++++++++++-------------- winforms/tests_backend/window.py | 2 +- 3 files changed, 42 insertions(+), 32 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 70a0f0b16c..f49e35c4bb 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -344,6 +344,7 @@ def set_window_state(self, state): self.interface.app.enter_presentation_mode( {self.interface.screen: self.interface} ) + # WindowState.NORMAL case: else: # If the window is maximized, restore it to its normal size @@ -363,7 +364,8 @@ def set_window_state(self, state): self.native.toggleFullScreen(self.native) # If the window is in presentation mode, exit presentation mode - elif current_state == WindowState.PRESENTATION: + # WindowState.PRESENTATION case: + else: self.interface.app.exit_presentation_mode() # Complete any pending window state transition. This operation is performed on the diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 60ecae280f..9b6138c1c4 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -346,77 +346,85 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): ], ) async def test_presentation_mode_exit_on_window_state_change( - app, app_probe, main_window, main_window_probe, new_window_state + app, app_probe, new_window_state ): """Changing window state exits presentation mode and sets the new state.""" try: - main_window.toolbar.add(app.cmd1) - extra_window = toga.MainWindow( - title="Extra Window", position=(150, 150), size=(200, 200) + window1 = toga.MainWindow( + title="Test Window 1", position=(150, 150), size=(200, 200) ) - extra_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - extra_window_probe = window_probe(app, extra_window) - extra_window.show() + window2 = toga.MainWindow( + title="Test Window 2", position=(160, 160), size=(200, 200) + ) + window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) + window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + window1.toolbar.add(app.cmd1) + window2.toolbar.add(app.cmd1) + window1_probe = window_probe(app, window1) + window2_probe = window_probe(app, window2) + window1.show() + window2.show() # Add delay for gtk to show the windows - await app_probe.redraw("Extra window is shown", delay=0.5) + await app_probe.redraw("Test windows are shown", delay=0.5) # Enter presentation mode - app.enter_presentation_mode([main_window]) + app.enter_presentation_mode([window1]) # Add delay for gtk to show the windows await app_probe.redraw("App is in presentation mode", delay=0.5) assert app.is_presentation_mode - assert main_window_probe.get_window_state() == WindowState.PRESENTATION - assert extra_window_probe.get_window_state() != WindowState.PRESENTATION + assert window1_probe.get_window_state() == WindowState.PRESENTATION + assert window2_probe.get_window_state() != WindowState.PRESENTATION # Changing window state of main window should make the app exit presentation mode. - main_window.state = new_window_state + window1.state = new_window_state # Add delay for gtk to show the windows await app_probe.redraw( - "App is not in presentation mode" f"\nMain Window is in {new_window_state}", + "App is not in presentation mode" + f"\nTest Window 1 is in {new_window_state}", delay=0.5, ) assert not app.is_presentation_mode - assert main_window_probe.get_window_state != WindowState.PRESENTATION - assert main_window_probe.get_window_state() == new_window_state + assert window1_probe.get_window_state != WindowState.PRESENTATION + assert window1_probe.get_window_state() == new_window_state # Reset window states - main_window.state = WindowState.NORMAL - extra_window.state = WindowState.NORMAL + window1.state = WindowState.NORMAL + window2.state = WindowState.NORMAL # Add delay for gtk to show the windows - await app_probe.redraw("All windows are in WindowState.NORMAL", delay=0.5) + await app_probe.redraw("All test windows are in WindowState.NORMAL", delay=0.5) # Enter presentation mode again - app.enter_presentation_mode([main_window]) + app.enter_presentation_mode([window1]) # Add delay for gtk to show the windows await app_probe.redraw("App is in presentation mode", delay=0.5) assert app.is_presentation_mode - assert main_window_probe.get_window_state() == WindowState.PRESENTATION - assert extra_window_probe.get_window_state() != WindowState.PRESENTATION + assert window1_probe.get_window_state() == WindowState.PRESENTATION + assert window2_probe.get_window_state() != WindowState.PRESENTATION # Changing window state of extra window should make the app exit presentation mode. - extra_window.state = new_window_state + window2.state = new_window_state # Add delay for gtk to show the windows await app_probe.redraw( "App is not in presentation mode" - f"\nExtra Window is in {new_window_state}", + f"\nTest Window 1 is in {new_window_state}", delay=0.5, ) assert not app.is_presentation_mode - assert main_window_probe.get_window_state() != WindowState.PRESENTATION - assert extra_window_probe.get_window_state() == new_window_state + assert window1_probe.get_window_state() != WindowState.PRESENTATION + assert window2_probe.get_window_state() == new_window_state # Reset window states - main_window.state = WindowState.NORMAL - extra_window.state = WindowState.NORMAL + window1.state = WindowState.NORMAL + window2.state = WindowState.NORMAL # Add delay for gtk to show the windows - await app_probe.redraw("All windows are in WindowState.NORMAL", delay=0.5) + await app_probe.redraw("All test windows are in WindowState.NORMAL", delay=0.5) finally: - main_window.toolbar.clear() - extra_window.close() + window1.close() + window2.close() async def test_show_hide_cursor(app, app_probe): diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index f68ecdf01d..a5f43dcc41 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -57,7 +57,7 @@ def get_window_state(self): window_state = self.native.WindowState if window_state == FormWindowState.Maximized: if self.native.FormBorderStyle == getattr(FormBorderStyle, "None"): - if getattr(self.impl, "_is_presentation_mode", False) is True: + if self.impl._is_presentation_mode: current_state = WindowState.PRESENTATION else: current_state = WindowState.FULLSCREEN From 6b513c78d8f3444545b00cda111a985e7f2e1a0e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 7 Jul 2024 02:18:10 -0400 Subject: [PATCH 103/248] Fixed gtk backend --- gtk/src/toga_gtk/window.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 3478293503..baab3a2224 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -35,6 +35,7 @@ def __init__(self, interface, title, position, size): # Use a shadow variable since a window without any app menu and toolbar # in presentation mode would be indistinguishable from full screen mode. self._is_presentation_mode = False + self._is_full_screen = False self.native.set_default_size(size[0], size[1]) @@ -163,8 +164,10 @@ def get_window_state(self): elif window_state_flags & Gdk.WindowState.FULLSCREEN: if self._is_presentation_mode: return WindowState.PRESENTATION - else: + elif self._is_full_screen: return WindowState.FULLSCREEN + else: + return WindowState.NORMAL else: return WindowState.NORMAL @@ -189,6 +192,7 @@ def set_window_state(self, state): elif state == WindowState.FULLSCREEN: self.native.fullscreen() + self._is_full_screen = True elif state == WindowState.PRESENTATION: self._before_presentation_mode_screen = self.interface.screen @@ -211,6 +215,7 @@ def set_window_state(self, state): # If the window is in full-screen mode, exit full-screen mode elif current_state == WindowState.FULLSCREEN: self.native.unfullscreen() + self._is_full_screen = False # If the window is in presentation mode, exit presentation mode elif current_state == WindowState.PRESENTATION: if isinstance(self.native, Gtk.ApplicationWindow): From 71f4314e67f52399d88666f4536acf2d5086f36d Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 7 Jul 2024 02:47:38 -0400 Subject: [PATCH 104/248] Fixed gtk test backend --- gtk/src/toga_gtk/window.py | 7 ++++--- gtk/tests_backend/window.py | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index baab3a2224..d3b9030174 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -32,8 +32,8 @@ def __init__(self, interface, title, position, size): self.native.connect("window-state-event", self.gtk_window_state_event) self._window_state_flags = None - # Use a shadow variable since a window without any app menu and toolbar - # in presentation mode would be indistinguishable from full screen mode. + + # Gdk.WindowState.FULLSCREEN is unreliable, use shadow variables. self._is_presentation_mode = False self._is_full_screen = False @@ -217,7 +217,8 @@ def set_window_state(self, state): self.native.unfullscreen() self._is_full_screen = False # If the window is in presentation mode, exit presentation mode - elif current_state == WindowState.PRESENTATION: + # WindowState.PRESENTATION case: + else: if isinstance(self.native, Gtk.ApplicationWindow): self.native.set_show_menubar(True) if getattr(self, "native_toolbar", None): diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index 00e5b32e67..5e393a0655 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -48,12 +48,12 @@ def get_window_state(self): elif window_state_flags & Gdk.WindowState.ICONIFIED: current_state = WindowState.MINIMIZED elif window_state_flags & Gdk.WindowState.FULLSCREEN: - # Use a shadow variable since a window without any app menu and toolbar - # in presentation mode would be indistinguishable from full screen mode. if getattr(self.impl, "_is_presentation_mode", False) is True: current_state = WindowState.PRESENTATION - else: + elif getattr(self.impl, "_is_full_screen", False) is True: current_state = WindowState.FULLSCREEN + else: + current_state = WindowState.NORMAL else: current_state = WindowState.NORMAL return current_state From ccc8f118f5d8675353c20919c9c230ded41a1ffa Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 7 Jul 2024 09:37:12 -0700 Subject: [PATCH 105/248] Fixed Android backend --- android/src/toga_android/window.py | 74 ++++++++++++++++++----------- android/tests_backend/window.py | 16 +++---- testbed/tests/app/test_desktop.py | 8 +++- testbed/tests/window/test_window.py | 52 ++++++++++++++++++++ 4 files changed, 112 insertions(+), 38 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index a116296ad6..2fbda0852c 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -145,7 +145,11 @@ def get_visible(self): ###################################################################### def get_window_state(self): - # Windows are always full screen + # window.state is called in _close(), which itself sometimes + # is called during early stages of app startup, during which + # the app attribute may not exist. In such cases, return NORMAL. + if getattr(self, "app", None) is None: + return WindowState.NORMAL decor_view = self.app.native.getWindow().getDecorView() system_ui_flags = decor_view.getSystemUiVisibility() if system_ui_flags & ( @@ -162,33 +166,49 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() decor_view = self.app.native.getWindow().getDecorView() - # On Android Maximized state is same as the Normal state - if state in {WindowState.NORMAL, WindowState.MAXIMIZED}: - if current_state in { - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - }: - decor_view.setSystemUiVisibility(0) - if current_state == WindowState.PRESENTATION: - # Marking this as no branch, since the testbed can't create a simple - # window, so we can't test the other branch. - if self._actionbar_shown_by_default: # pragma: no branch - self.app.native.getSupportActionBar().show() - self._is_presentation_mode = False - else: + if ( + current_state != WindowState.NORMAL + and state != WindowState.NORMAL + and (getattr(self, "_pending_window_state_transition", None) is None) + ): + # Set Window state to NORMAL before changing to other states as some + # states block changing window state without first exiting them or + # can even cause rendering glitches. + self._pending_window_state_transition = state self.set_window_state(WindowState.NORMAL) - if state in {WindowState.FULLSCREEN, WindowState.PRESENTATION}: - decor_view.setSystemUiVisibility( - decor_view.SYSTEM_UI_FLAG_FULLSCREEN - | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | decor_view.SYSTEM_UI_FLAG_IMMERSIVE - ) - if state == WindowState.PRESENTATION: - # Marking this as no branch, since the testbed can't create a simple - # window, so we can't test the other branch. - if self._actionbar_shown_by_default: # pragma: no branch - self.app.native.getSupportActionBar().hide() - self._is_presentation_mode = True + + elif state in {WindowState.FULLSCREEN, WindowState.PRESENTATION}: + decor_view.setSystemUiVisibility( + decor_view.SYSTEM_UI_FLAG_FULLSCREEN + | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | decor_view.SYSTEM_UI_FLAG_IMMERSIVE + ) + if state == WindowState.PRESENTATION: + # Marking this as no branch, since the testbed can't create a simple + # window, so we can't test the other branch. + if self._actionbar_shown_by_default: # pragma: no branch + self.app.native.getSupportActionBar().hide() + self._is_presentation_mode = True + + else: + # On Android Maximized state is same as the Normal state + if state in {WindowState.NORMAL, WindowState.MAXIMIZED}: + if current_state in { + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + }: + decor_view.setSystemUiVisibility(0) + if current_state == WindowState.PRESENTATION: + # Marking this as no branch, since the testbed can't create a simple + # window, so we can't test the other branch. + if self._actionbar_shown_by_default: # pragma: no branch + self.app.native.getSupportActionBar().show() + self._is_presentation_mode = False + + # Complete any pending window state transition. + if getattr(self, "_pending_window_state_transition", None) is not None: + self.set_window_state(self._pending_window_state_transition) + del self._pending_window_state_transition ###################################################################### # Window capabilities diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index 28a06b6752..294e3dc553 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -23,18 +23,14 @@ def content_size(self): ) def get_window_state(self): - # Windows are always full screen decor_view = self.native.getWindow().getDecorView() system_ui_flags = decor_view.getSystemUiVisibility() - if ( - system_ui_flags - & ( - decor_view.SYSTEM_UI_FLAG_FULLSCREEN - | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | decor_view.SYSTEM_UI_FLAG_IMMERSIVE - ) - ) != 0: - if not self.native.getSupportActionBar().isShowing(): + if system_ui_flags & ( + decor_view.SYSTEM_UI_FLAG_FULLSCREEN + | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | decor_view.SYSTEM_UI_FLAG_IMMERSIVE + ): + if self.window._impl._is_presentation_mode: current_state = WindowState.PRESENTATION else: current_state = WindowState.FULLSCREEN diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 9b6138c1c4..dca1356216 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -346,9 +346,15 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): ], ) async def test_presentation_mode_exit_on_window_state_change( - app, app_probe, new_window_state + app, app_probe, main_window_probe, new_window_state ): """Changing window state exits presentation mode and sets the new state.""" + if ( + new_window_state == WindowState.MINIMIZED + and not main_window_probe.supports_minimize + ): + pytest.xfail("This backend doesn't reliably support minimized window state.") + try: window1 = toga.MainWindow( title="Test Window 1", position=(150, 150), size=(200, 200) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index c24a3b2fc6..6b8d9c18b8 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -217,6 +217,50 @@ async def test_window_state_presentation(main_window, main_window_probe): assert main_window_probe.get_window_state() != WindowState.PRESENTATION assert main_window_probe.get_window_state() == WindowState.NORMAL + @pytest.mark.parametrize( + "initial_state, final_state", + [ + # Direct switch from FULLSCREEN: + (WindowState.FULLSCREEN, WindowState.PRESENTATION), + # Direct switch from PRESENTATION: + (WindowState.PRESENTATION, WindowState.FULLSCREEN), + ], + ) + async def test_window_state_direct_change( + app, initial_state, final_state, main_window, main_window_probe + ): + assert main_window_probe.get_window_state() == WindowState.NORMAL + assert main_window_probe.get_window_state() != initial_state + assert main_window_probe.get_window_state() != final_state + try: + # Set to initial state + main_window.state = initial_state + await main_window_probe.wait_for_window( + f"Main window is in {initial_state}" + ) + + assert main_window_probe.get_window_state() != WindowState.NORMAL + assert main_window_probe.get_window_state() == initial_state + assert main_window_probe.get_window_state() != final_state + + # Set to final state + main_window.state = final_state + await main_window_probe.wait_for_window(f"Main window is in {final_state}") + + assert main_window_probe.get_window_state() != WindowState.NORMAL + assert main_window_probe.get_window_state() == final_state + assert main_window_probe.get_window_state() != initial_state + finally: + # Set to NORMAL state + main_window.state = WindowState.NORMAL + await main_window_probe.wait_for_window( + "Main window is in WindowState.NORMAL" + ) + + assert main_window_probe.get_window_state() == WindowState.NORMAL + assert main_window_probe.get_window_state() != final_state + assert main_window_probe.get_window_state() != initial_state + 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) @@ -820,6 +864,14 @@ async def test_window_state_presentation(second_window, second_window_probe): async def test_window_state_direct_change( app, initial_state, final_state, second_window, second_window_probe ): + if ( + WindowState.MINIMIZED in {initial_state, final_state} + and not second_window_probe.supports_minimize + ): + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + second_window.toolbar.add(app.cmd1) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() From 56f32a7215fce2da70ddce53f71a2ad6c51a3c0d Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 7 Jul 2024 09:54:49 -0700 Subject: [PATCH 106/248] Test to diagnose wayland failure --- testbed/tests/app/test_desktop.py | 4 +++- testbed/tests/window/test_window.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index dca1356216..d0262f980b 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -330,7 +330,9 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): assert ( window_information["window_probe"].presentation_content_size == window_information["initial_content_size"] - ), f"{window_information['window'].title}:" + ), f"{window_information['window'].title}:\n" + f"Actual:{window_information['window'].state}\t" + f"Probe:{window_information['window_probe'].get_window_state()}" finally: for window in excess_windows_list: diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 6b8d9c18b8..08882178df 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -821,6 +821,11 @@ async def test_window_state_presentation(second_window, second_window_probe): "Secondary window is not in presentation mode", full_screen=True ) assert second_window_probe.get_window_state() != WindowState.PRESENTATION + assert ( + second_window_probe.get_window_state() != WindowState.NORMAL + ), f"Current window state: Actual:{second_window.state}\t" + f"Probe:{second_window_probe.get_window_state()}" + assert second_window_probe.is_resizable assert second_window_probe.presentation_content_size == initial_content_size From 5da8231c1ef12d14fbe9fbe4b4b4f19fda4427ed Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 7 Jul 2024 10:06:02 -0700 Subject: [PATCH 107/248] Removed diagnostic tests --- testbed/tests/app/test_desktop.py | 4 +--- testbed/tests/window/test_window.py | 5 ----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index d0262f980b..dca1356216 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -330,9 +330,7 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): assert ( window_information["window_probe"].presentation_content_size == window_information["initial_content_size"] - ), f"{window_information['window'].title}:\n" - f"Actual:{window_information['window'].state}\t" - f"Probe:{window_information['window_probe'].get_window_state()}" + ), f"{window_information['window'].title}:" finally: for window in excess_windows_list: diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 08882178df..6b8d9c18b8 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -821,11 +821,6 @@ async def test_window_state_presentation(second_window, second_window_probe): "Secondary window is not in presentation mode", full_screen=True ) assert second_window_probe.get_window_state() != WindowState.PRESENTATION - assert ( - second_window_probe.get_window_state() != WindowState.NORMAL - ), f"Current window state: Actual:{second_window.state}\t" - f"Probe:{second_window_probe.get_window_state()}" - assert second_window_probe.is_resizable assert second_window_probe.presentation_content_size == initial_content_size From cfa255aafe8a32a7f6e34e33f8ba992e2d436ba4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 9 Jul 2024 15:31:17 -0700 Subject: [PATCH 108/248] Various code cleanups --- android/src/toga_android/app.py | 9 +++++++-- core/src/toga/app.py | 9 ++------- core/src/toga/window.py | 14 +++++++------- core/tests/app/test_app.py | 5 ----- dummy/src/toga_dummy/app.py | 4 ++-- gtk/src/toga_gtk/app.py | 4 ++-- testbed/tests/app/test_desktop.py | 6 ++---- testbed/tests/window/test_window.py | 20 ++++++++++++++++++++ winforms/src/toga_winforms/app.py | 4 ++-- 9 files changed, 44 insertions(+), 31 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index a649f02719..9f303dd92c 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -326,10 +326,15 @@ def set_current_window(self, window): ###################################################################### def enter_presentation_mode(self, screen_window_dict): - self.interface.main_window.state = WindowState.PRESENTATION + for screen, window in screen_window_dict.items(): + window._impl._before_presentation_mode_screen = window.screen + window.screen = screen + window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self): - self.interface.main_window.state = WindowState.NORMAL + for window in self.interface.windows: + if window.state == WindowState.PRESENTATION: + window._impl.set_window_state(WindowState.NORMAL) ###################################################################### # Platform-specific APIs diff --git a/core/src/toga/app.py b/core/src/toga/app.py index d66aa2f206..9a3b894120 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -802,9 +802,7 @@ def enter_presentation_mode( :raises ValueError: If the presentation layout supplied is not a list of windows or or a dict mapping windows to screens. """ - if not windows: - return - else: + if windows: screen_window_dict = dict() if isinstance(windows, list): for window, screen in zip(windows, self.screens): @@ -819,10 +817,7 @@ def enter_presentation_mode( def exit_presentation_mode(self) -> None: """Exit presentation mode.""" - if self.is_presentation_mode: - self._impl.exit_presentation_mode() - else: - return + self._impl.exit_presentation_mode() ###################################################################### # App events diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 9f29e3619b..9597969c0f 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -475,13 +475,13 @@ def state(self, state: WindowState) -> None: # Changing the window state while the app is in presentation mode can cause # rendering glitches. Hence, first exit presentation and then set the new # window state. - if state != WindowState.NORMAL or ( - any( - window.state == WindowState.PRESENTATION and window != self - for window in self.app.windows - ) - ): - self.app.exit_presentation_mode() + # if state != WindowState.NORMAL or ( + # any( + # window.state == WindowState.PRESENTATION and window != self + # for window in self.app.windows + # ) + # ): + self.app.exit_presentation_mode() current_state = self._impl.get_window_state() if current_state != state: diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 1a8ef8864a..b1ba0694d2 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -581,11 +581,6 @@ def test_presentation_mode_no_op(event_loop): assert not app.is_presentation_mode - # If we're not in presentation mode, exiting presentation mode is a no-op. - app.exit_presentation_mode() - assert_action_not_performed(app, "exit presentation mode") - assert not app.is_presentation_mode - # Entering presentation mode without any window is a no-op. with pytest.raises(TypeError): app.enter_presentation_mode() diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index b9495b1d00..6cbbdb908e 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -174,13 +174,13 @@ def set_current_window(self, window): def enter_presentation_mode(self, screen_window_dict): self._action("enter presentation mode", screen_window_dict=screen_window_dict) for screen, window in screen_window_dict.items(): - window.state = WindowState.PRESENTATION + window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self): self._action("exit presentation mode") for window in self.interface.windows: if window.state == WindowState.PRESENTATION: - window.state = WindowState.NORMAL + window._impl.set_window_state(WindowState.NORMAL) ###################################################################### # Simulation interface diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 1826e3685d..26d6409c3c 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -282,12 +282,12 @@ def enter_presentation_mode(self, screen_window_dict): for screen, window in screen_window_dict.items(): window._impl._before_presentation_mode_screen = window.screen window.screen = screen - window.state = WindowState.PRESENTATION + window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self): for window in self.interface.windows: if window.state == WindowState.PRESENTATION: - window.state = WindowState.NORMAL + window._impl.set_window_state(WindowState.NORMAL) class DocumentApp(App): # pragma: no cover diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index dca1356216..0b8de68897 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -188,7 +188,7 @@ async def test_presentation_mode(app, app_probe): window_information_list = list() windows_list = list() for i in range(len(app.screens)): - window = toga.MainWindow( + window = toga.Window( title=f"Test Window {i}", position=(150 + (10 * i), 150 + (10 * i)), size=(200, 200), @@ -199,7 +199,6 @@ async def test_presentation_mode(app, app_probe): window.content = toga.Box( style=Pack(background_color=f"#{r:02X}{g:02X}{b:02X}") ) - window.toolbar.add(app.cmd1) window.show() # Add delay for gtk to show the windows await app_probe.redraw(f"Test Window {i} is visible", delay=0.5) @@ -260,7 +259,7 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): excess_windows_list = list() # Generate an additional window compared to the number of screens present. for i in range(len(app.screens) + 1): - window = toga.MainWindow( + window = toga.Window( title=f"Test Window {i}", position=(150 + (10 * i), 150 + (10 * i)), size=(200, 200), @@ -271,7 +270,6 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): window.content = toga.Box( style=Pack(background_color=f"#{r:02X}{g:02X}{b:02X}") ) - window.toolbar.add(app.cmd1) window.show() # Add delay for gtk to show the windows await app_probe.redraw(f"Test Window {i} is visible", delay=0.5) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 6b8d9c18b8..00d0b00ec3 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -640,6 +640,11 @@ async def test_window_state_minimized(second_window, second_window_probe): ) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + second_window.show() + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is shown", + ) assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.get_window_state() != WindowState.MINIMIZED @@ -684,6 +689,11 @@ async def test_window_state_minimized(second_window, second_window_probe): async def test_window_state_maximized(second_window, second_window_probe): """Window can have maximized window state""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + second_window.show() + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is shown", + ) assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.get_window_state() != WindowState.MAXIMIZED @@ -733,6 +743,11 @@ async def test_window_state_maximized(second_window, second_window_probe): async def test_window_state_full_screen(second_window, second_window_probe): """Window can have full screen window state""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + second_window.show() + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is shown", + ) assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.get_window_state() != WindowState.FULLSCREEN @@ -784,6 +799,11 @@ async def test_window_state_full_screen(second_window, second_window_probe): async def test_window_state_presentation(second_window, second_window_probe): """Window can have presentation window state""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + second_window.show() + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is shown", + ) assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.get_window_state() != WindowState.PRESENTATION diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index ccaf6c4a6f..8124521059 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -307,12 +307,12 @@ def enter_presentation_mode(self, screen_window_dict): for screen, window in screen_window_dict.items(): window._impl._before_presentation_mode_screen = window.screen window.screen = screen - window.state = WindowState.PRESENTATION + window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self): for window in self.interface.windows: if window.state == WindowState.PRESENTATION: - window.state = WindowState.NORMAL + window._impl.set_window_state(WindowState.NORMAL) class DocumentApp(App): # pragma: no cover From 87383f3ca5f37bb9b37fcc63a6c9e804d48736eb Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 9 Jul 2024 17:18:59 -0700 Subject: [PATCH 109/248] Fixed core --- android/src/toga_android/window.py | 2 ++ cocoa/src/toga_cocoa/window.py | 2 ++ core/src/toga/window.py | 16 +++++----------- gtk/src/toga_gtk/window.py | 2 ++ winforms/src/toga_winforms/window.py | 2 ++ 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 2fbda0852c..31c10c646c 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -165,6 +165,8 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() + if current_state == state: + return decor_view = self.app.native.getWindow().getDecorView() if ( current_state != WindowState.NORMAL diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index f49e35c4bb..7fe36004b8 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -320,6 +320,8 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() + if current_state == state: + return if ( current_state != WindowState.NORMAL and state != WindowState.NORMAL diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 9597969c0f..33f9aad3ca 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -472,17 +472,6 @@ def state(self) -> WindowState: @state.setter def state(self, state: WindowState) -> None: - # Changing the window state while the app is in presentation mode can cause - # rendering glitches. Hence, first exit presentation and then set the new - # window state. - # if state != WindowState.NORMAL or ( - # any( - # window.state == WindowState.PRESENTATION and window != self - # for window in self.app.windows - # ) - # ): - self.app.exit_presentation_mode() - current_state = self._impl.get_window_state() if current_state != state: if not self.resizable and state in { @@ -494,6 +483,11 @@ def state(self, state: WindowState) -> None: f"Cannot set window state to {state} of a non-resizable window." ) else: + # Changing the window state while the app is in presentation mode + # can cause rendering glitches. Hence, first exit presentation and + # then set the new window state. + self.app.exit_presentation_mode() + self._impl.set_window_state(state) ###################################################################### diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index d3b9030174..22c281d18d 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -173,6 +173,8 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() + if current_state == state: + return if ( current_state != WindowState.NORMAL and state != WindowState.NORMAL diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index c184b8aa2b..4a31d47eb8 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -211,6 +211,8 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() + if current_state == state: + return if ( current_state != WindowState.NORMAL and state != WindowState.NORMAL From c3f32c744fc5f827cdd4877ba50740b364b36da1 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 9 Jul 2024 10:46:46 -0700 Subject: [PATCH 110/248] Minor cleanup --- testbed/tests/app/test_desktop.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 0b8de68897..ae96a3b7ed 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -354,16 +354,14 @@ async def test_presentation_mode_exit_on_window_state_change( pytest.xfail("This backend doesn't reliably support minimized window state.") try: - window1 = toga.MainWindow( + window1 = toga.Window( title="Test Window 1", position=(150, 150), size=(200, 200) ) - window2 = toga.MainWindow( + window2 = toga.Window( title="Test Window 2", position=(160, 160), size=(200, 200) ) window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - window1.toolbar.add(app.cmd1) - window2.toolbar.add(app.cmd1) window1_probe = window_probe(app, window1) window2_probe = window_probe(app, window2) window1.show() From 4c6070d27631d5936a2ed9b49bb3166d25c18bf9 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 9 Jul 2024 11:07:46 -0700 Subject: [PATCH 111/248] Diagnostic test --- testbed/tests/app/test_desktop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index ae96a3b7ed..bcfd7bfa2a 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -343,6 +343,7 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): WindowState.FULLSCREEN, ], ) +@pytest.mark.skip async def test_presentation_mode_exit_on_window_state_change( app, app_probe, main_window_probe, new_window_state ): From 4b2fdbbc388b9e20bbd8d198500488b495c79401 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 7 Jul 2024 09:54:49 -0700 Subject: [PATCH 112/248] Removed diagnostic tests Various code cleanups Fixed core Minor cleanup Diagnostic test Removed diagnostic code diagnosing failure Revert change --- .github/workflows/ci.yml | 2 +- android/src/toga_android/app.py | 9 +++++++-- android/src/toga_android/window.py | 2 ++ cocoa/src/toga_cocoa/window.py | 2 ++ core/src/toga/app.py | 9 ++------- core/src/toga/window.py | 16 +++++----------- core/tests/app/test_app.py | 5 ----- dummy/src/toga_dummy/app.py | 4 ++-- gtk/src/toga_gtk/app.py | 4 ++-- gtk/src/toga_gtk/window.py | 2 ++ testbed/tests/app/test_desktop.py | 14 +++++--------- testbed/tests/window/test_window.py | 20 ++++++++++++++++++++ winforms/src/toga_winforms/app.py | 4 ++-- winforms/src/toga_winforms/window.py | 2 ++ 14 files changed, 54 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0354ce380f..8bc564e1b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -355,7 +355,7 @@ jobs: # This step is only needed if you're trying to diagnose test failures that # only occur in CI, and can't be reproduced locally. When it runs, it will # open an SSH server (URL reported in the logs) so you can ssh into the CI - # machine. + # # machine. # - name: Setup tmate session # uses: mxschmitt/action-tmate@v3 # if: failure() diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index a649f02719..9f303dd92c 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -326,10 +326,15 @@ def set_current_window(self, window): ###################################################################### def enter_presentation_mode(self, screen_window_dict): - self.interface.main_window.state = WindowState.PRESENTATION + for screen, window in screen_window_dict.items(): + window._impl._before_presentation_mode_screen = window.screen + window.screen = screen + window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self): - self.interface.main_window.state = WindowState.NORMAL + for window in self.interface.windows: + if window.state == WindowState.PRESENTATION: + window._impl.set_window_state(WindowState.NORMAL) ###################################################################### # Platform-specific APIs diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 2fbda0852c..31c10c646c 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -165,6 +165,8 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() + if current_state == state: + return decor_view = self.app.native.getWindow().getDecorView() if ( current_state != WindowState.NORMAL diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index f49e35c4bb..7fe36004b8 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -320,6 +320,8 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() + if current_state == state: + return if ( current_state != WindowState.NORMAL and state != WindowState.NORMAL diff --git a/core/src/toga/app.py b/core/src/toga/app.py index d66aa2f206..9a3b894120 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -802,9 +802,7 @@ def enter_presentation_mode( :raises ValueError: If the presentation layout supplied is not a list of windows or or a dict mapping windows to screens. """ - if not windows: - return - else: + if windows: screen_window_dict = dict() if isinstance(windows, list): for window, screen in zip(windows, self.screens): @@ -819,10 +817,7 @@ def enter_presentation_mode( def exit_presentation_mode(self) -> None: """Exit presentation mode.""" - if self.is_presentation_mode: - self._impl.exit_presentation_mode() - else: - return + self._impl.exit_presentation_mode() ###################################################################### # App events diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 9f29e3619b..33f9aad3ca 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -472,17 +472,6 @@ def state(self) -> WindowState: @state.setter def state(self, state: WindowState) -> None: - # Changing the window state while the app is in presentation mode can cause - # rendering glitches. Hence, first exit presentation and then set the new - # window state. - if state != WindowState.NORMAL or ( - any( - window.state == WindowState.PRESENTATION and window != self - for window in self.app.windows - ) - ): - self.app.exit_presentation_mode() - current_state = self._impl.get_window_state() if current_state != state: if not self.resizable and state in { @@ -494,6 +483,11 @@ def state(self, state: WindowState) -> None: f"Cannot set window state to {state} of a non-resizable window." ) else: + # Changing the window state while the app is in presentation mode + # can cause rendering glitches. Hence, first exit presentation and + # then set the new window state. + self.app.exit_presentation_mode() + self._impl.set_window_state(state) ###################################################################### diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 1a8ef8864a..b1ba0694d2 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -581,11 +581,6 @@ def test_presentation_mode_no_op(event_loop): assert not app.is_presentation_mode - # If we're not in presentation mode, exiting presentation mode is a no-op. - app.exit_presentation_mode() - assert_action_not_performed(app, "exit presentation mode") - assert not app.is_presentation_mode - # Entering presentation mode without any window is a no-op. with pytest.raises(TypeError): app.enter_presentation_mode() diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index b9495b1d00..6cbbdb908e 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -174,13 +174,13 @@ def set_current_window(self, window): def enter_presentation_mode(self, screen_window_dict): self._action("enter presentation mode", screen_window_dict=screen_window_dict) for screen, window in screen_window_dict.items(): - window.state = WindowState.PRESENTATION + window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self): self._action("exit presentation mode") for window in self.interface.windows: if window.state == WindowState.PRESENTATION: - window.state = WindowState.NORMAL + window._impl.set_window_state(WindowState.NORMAL) ###################################################################### # Simulation interface diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 1826e3685d..26d6409c3c 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -282,12 +282,12 @@ def enter_presentation_mode(self, screen_window_dict): for screen, window in screen_window_dict.items(): window._impl._before_presentation_mode_screen = window.screen window.screen = screen - window.state = WindowState.PRESENTATION + window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self): for window in self.interface.windows: if window.state == WindowState.PRESENTATION: - window.state = WindowState.NORMAL + window._impl.set_window_state(WindowState.NORMAL) class DocumentApp(App): # pragma: no cover diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index d3b9030174..22c281d18d 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -173,6 +173,8 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() + if current_state == state: + return if ( current_state != WindowState.NORMAL and state != WindowState.NORMAL diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index dca1356216..e294a62460 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -188,7 +188,7 @@ async def test_presentation_mode(app, app_probe): window_information_list = list() windows_list = list() for i in range(len(app.screens)): - window = toga.MainWindow( + window = toga.Window( title=f"Test Window {i}", position=(150 + (10 * i), 150 + (10 * i)), size=(200, 200), @@ -199,7 +199,6 @@ async def test_presentation_mode(app, app_probe): window.content = toga.Box( style=Pack(background_color=f"#{r:02X}{g:02X}{b:02X}") ) - window.toolbar.add(app.cmd1) window.show() # Add delay for gtk to show the windows await app_probe.redraw(f"Test Window {i} is visible", delay=0.5) @@ -260,7 +259,7 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): excess_windows_list = list() # Generate an additional window compared to the number of screens present. for i in range(len(app.screens) + 1): - window = toga.MainWindow( + window = toga.Window( title=f"Test Window {i}", position=(150 + (10 * i), 150 + (10 * i)), size=(200, 200), @@ -271,7 +270,6 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): window.content = toga.Box( style=Pack(background_color=f"#{r:02X}{g:02X}{b:02X}") ) - window.toolbar.add(app.cmd1) window.show() # Add delay for gtk to show the windows await app_probe.redraw(f"Test Window {i} is visible", delay=0.5) @@ -346,7 +344,7 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): ], ) async def test_presentation_mode_exit_on_window_state_change( - app, app_probe, main_window_probe, new_window_state + app, app_probe, main_window, main_window_probe, new_window_state ): """Changing window state exits presentation mode and sets the new state.""" if ( @@ -356,16 +354,14 @@ async def test_presentation_mode_exit_on_window_state_change( pytest.xfail("This backend doesn't reliably support minimized window state.") try: - window1 = toga.MainWindow( + window1 = toga.Window( title="Test Window 1", position=(150, 150), size=(200, 200) ) - window2 = toga.MainWindow( + window2 = toga.Window( title="Test Window 2", position=(160, 160), size=(200, 200) ) window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - window1.toolbar.add(app.cmd1) - window2.toolbar.add(app.cmd1) window1_probe = window_probe(app, window1) window2_probe = window_probe(app, window2) window1.show() diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 6b8d9c18b8..00d0b00ec3 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -640,6 +640,11 @@ async def test_window_state_minimized(second_window, second_window_probe): ) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + second_window.show() + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is shown", + ) assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.get_window_state() != WindowState.MINIMIZED @@ -684,6 +689,11 @@ async def test_window_state_minimized(second_window, second_window_probe): async def test_window_state_maximized(second_window, second_window_probe): """Window can have maximized window state""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + second_window.show() + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is shown", + ) assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.get_window_state() != WindowState.MAXIMIZED @@ -733,6 +743,11 @@ async def test_window_state_maximized(second_window, second_window_probe): async def test_window_state_full_screen(second_window, second_window_probe): """Window can have full screen window state""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + second_window.show() + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is shown", + ) assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.get_window_state() != WindowState.FULLSCREEN @@ -784,6 +799,11 @@ async def test_window_state_full_screen(second_window, second_window_probe): async def test_window_state_presentation(second_window, second_window_probe): """Window can have presentation window state""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + second_window.show() + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is shown", + ) assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.get_window_state() != WindowState.PRESENTATION diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index ccaf6c4a6f..8124521059 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -307,12 +307,12 @@ def enter_presentation_mode(self, screen_window_dict): for screen, window in screen_window_dict.items(): window._impl._before_presentation_mode_screen = window.screen window.screen = screen - window.state = WindowState.PRESENTATION + window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self): for window in self.interface.windows: if window.state == WindowState.PRESENTATION: - window.state = WindowState.NORMAL + window._impl.set_window_state(WindowState.NORMAL) class DocumentApp(App): # pragma: no cover diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index c184b8aa2b..4a31d47eb8 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -211,6 +211,8 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() + if current_state == state: + return if ( current_state != WindowState.NORMAL and state != WindowState.NORMAL From b592ee207f0ad3d9391c8f67b9cb35cf40066f2f Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 9 Jul 2024 21:05:39 -0700 Subject: [PATCH 113/248] Added review notes --- cocoa/src/toga_cocoa/window.py | 42 ++++++++++++++++++++++------- testbed/tests/window/test_window.py | 19 +++++++------ 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 7fe36004b8..0e969d2197 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -59,7 +59,6 @@ def windowDidDeminiaturize_(self, notification) -> None: @objc_method def windowDidExitFullScreen_(self, notification) -> None: - # This can be used to ensure that the window has completed exiting full screen. # Complete any pending window state transition. if getattr(self.impl, "_pending_window_state_transition", None) is not None: self.impl.set_window_state(self.impl._pending_window_state_transition) @@ -359,19 +358,42 @@ def set_window_state(self, state): # If the window is in full-screen mode, exit full-screen mode elif current_state == WindowState.FULLSCREEN: - # This doesn't wait for completely exiting full screen mode. Hence, - # this will cause problems when rapid window state switching is done. - # We should wait until `windowDidExitFullScreen_` is notified and then - # return to user. self.native.toggleFullScreen(self.native) # If the window is in presentation mode, exit presentation mode # WindowState.PRESENTATION case: - else: - self.interface.app.exit_presentation_mode() - - # Complete any pending window state transition. This operation is performed on the - # window delegate notifications for MINIMIZED and FULLSCREEN. Hence, exclude them here. + else: # branch: no cover + # --- Review Notes: Will be removed after review --- + # Marking this as no cover, since exit_presentation_mode() is triggered + # on any call to window.state setter. It checks if any window is in + # presentation mode and sets those windows' state to NORMAL. + # + # So, if the window was in PRESENTATION state and window.state is set to NORMAL, then + # exit_presentation_mode() would be called in the window.state setter and would exit + # app presentation mode. Since the window would now be in NORMAL state, this + # branch would never be triggered. + # + # On other backends presentation mode is window-based(i.e., window is manipulated + # to create presentation mode), as they do not have a native presentation mode. + # However, on cocoa, presentation mode is app-based(i.e., window is not manipulated + # to create presentation mode), since cocoa natively supports a separate presentation + # mode. + # + # Hence, this branch is required on other backends(gtk, winforms), but not on cocoa. + + # self.interface.app.exit_presentation_mode() + pass + + # Complete any pending window state transition. + # + # `setIsMiniaturized()` and `toggleFullScreen()` do not wait for completely exiting + # `MINIMIZED` and `FULLSCREEN` respectively. Hence,they will cause problems + # when direct window state switching is done. We should wait until + # `windowDidDeminiaturize_` and `windowDidExitFullScreen_` are notified and then + # set the pending window state. + + # This operation is performed on the window delegate notifications for MINIMIZED + # and FULLSCREEN. Hence, exclude them here. if current_state in {WindowState.MAXIMIZED, WindowState.PRESENTATION}: if getattr(self, "_pending_window_state_transition", None) is not None: self.set_window_state(self._pending_window_state_transition) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 00d0b00ec3..d971837622 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -851,15 +851,6 @@ async def test_window_state_presentation(second_window, second_window_probe): assert second_window_probe.get_window_state() != WindowState.PRESENTATION assert second_window_probe.presentation_content_size == initial_content_size - @pytest.mark.parametrize( - "second_window_class, second_window_kwargs", - [ - ( - toga.MainWindow, - dict(title="Secondary Window", position=(200, 150)), - ) - ], - ) @pytest.mark.parametrize( "initial_state, final_state", [ @@ -881,6 +872,15 @@ async def test_window_state_presentation(second_window, second_window_probe): (WindowState.PRESENTATION, WindowState.FULLSCREEN), ], ) + @pytest.mark.parametrize( + "second_window_class, second_window_kwargs", + [ + ( + toga.Window, + dict(title="Secondary Window", position=(200, 150)), + ) + ], + ) async def test_window_state_direct_change( app, initial_state, final_state, second_window, second_window_probe ): @@ -892,7 +892,6 @@ async def test_window_state_direct_change( "This backend doesn't reliably support minimized window state." ) - second_window.toolbar.add(app.cmd1) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() From a5891e8fc82923630eb3ddf10e792d214ac22a71 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 9 Jul 2024 21:21:10 -0700 Subject: [PATCH 114/248] Fixed testbed tests --- testbed/tests/app/test_desktop.py | 16 ++++++++++++++-- testbed/tests/window/test_window.py | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index e294a62460..5a6059803a 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -182,7 +182,7 @@ async def test_menu_minimize(app, app_probe): window1.close() -async def test_presentation_mode(app, app_probe): +async def test_presentation_mode(app, app_probe, main_window): """The app can enter presentation mode.""" try: window_information_list = list() @@ -250,9 +250,13 @@ async def test_presentation_mode(app, app_probe): finally: for window in windows_list: window.close() + app.current_window = main_window + # Add delay for gtk to show the windows + await app_probe.redraw("main_window is now the current window", delay=0.5) + assert app.current_window == main_window -async def test_presentation_mode_with_excess_windows_list(app, app_probe): +async def test_presentation_mode_with_excess_windows_list(app, app_probe, main_window): """Entering presentation mode limits windows to available displays.""" try: window_information_list = list() @@ -333,6 +337,10 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): finally: for window in excess_windows_list: window.close() + app.current_window = main_window + # Add delay for gtk to show the windows + await app_probe.redraw("main_window is now the current window", delay=0.5) + assert app.current_window == main_window @pytest.mark.parametrize( @@ -427,6 +435,10 @@ async def test_presentation_mode_exit_on_window_state_change( finally: window1.close() window2.close() + app.current_window = main_window + # Add delay for gtk to show the windows + await app_probe.redraw("main_window is now the current window", delay=0.5) + assert app.current_window == main_window async def test_show_hide_cursor(app, app_probe): diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index d971837622..d755fdf497 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -876,7 +876,7 @@ async def test_window_state_presentation(second_window, second_window_probe): "second_window_class, second_window_kwargs", [ ( - toga.Window, + toga.MainWindow, dict(title="Secondary Window", position=(200, 150)), ) ], @@ -891,7 +891,7 @@ async def test_window_state_direct_change( pytest.xfail( "This backend doesn't reliably support minimized window state." ) - + second_window.toolbar.add(app.cmd1) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() From 8684038acd7d3e2b4599dc3531a0a70bf98df82d Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 10 Jul 2024 01:54:30 -0700 Subject: [PATCH 115/248] Removed diagnostic code --- testbed/tests/app/test_desktop.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index bcfd7bfa2a..e294a62460 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -343,9 +343,8 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe): WindowState.FULLSCREEN, ], ) -@pytest.mark.skip async def test_presentation_mode_exit_on_window_state_change( - app, app_probe, main_window_probe, new_window_state + app, app_probe, main_window, main_window_probe, new_window_state ): """Changing window state exits presentation mode and sets the new state.""" if ( From 1b53c1645e1b66d323a8ec493788072a209f2f39 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 10 Jul 2024 02:06:42 -0700 Subject: [PATCH 116/248] diagnosing failure --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0354ce380f..ae40750e5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -356,6 +356,6 @@ jobs: # only occur in CI, and can't be reproduced locally. When it runs, it will # open an SSH server (URL reported in the logs) so you can ssh into the CI # machine. - # - name: Setup tmate session - # uses: mxschmitt/action-tmate@v3 - # if: failure() + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + if: failure() From 3cebc7683e7adf7c7a51636f3127221c54e48f06 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 10 Jul 2024 02:34:36 -0700 Subject: [PATCH 117/248] Revert change --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae40750e5b..8bc564e1b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -355,7 +355,7 @@ jobs: # This step is only needed if you're trying to diagnose test failures that # only occur in CI, and can't be reproduced locally. When it runs, it will # open an SSH server (URL reported in the logs) so you can ssh into the CI - # machine. - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - if: failure() + # # machine. + # - name: Setup tmate session + # uses: mxschmitt/action-tmate@v3 + # if: failure() From 4485cda9267cedf6ae12c7eb67cbe3ca2161162e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 10 Jul 2024 01:26:37 -0700 Subject: [PATCH 118/248] 100% coverage --- .github/workflows/ci.yml | 2 +- cocoa/src/toga_cocoa/window.py | 8 ++++---- core/src/toga/window.py | 2 +- testbed/tests/app/test_desktop.py | 6 ++++++ 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bc564e1b4..0354ce380f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -355,7 +355,7 @@ jobs: # This step is only needed if you're trying to diagnose test failures that # only occur in CI, and can't be reproduced locally. When it runs, it will # open an SSH server (URL reported in the logs) so you can ssh into the CI - # # machine. + # machine. # - name: Setup tmate session # uses: mxschmitt/action-tmate@v3 # if: failure() diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 0e969d2197..bbde82e7a3 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -362,10 +362,10 @@ def set_window_state(self, state): # If the window is in presentation mode, exit presentation mode # WindowState.PRESENTATION case: - else: # branch: no cover + else: # --- Review Notes: Will be removed after review --- # Marking this as no cover, since exit_presentation_mode() is triggered - # on any call to window.state setter. It checks if any window is in + # on any call to window.state setter, which checks if any window is in # presentation mode and sets those windows' state to NORMAL. # # So, if the window was in PRESENTATION state and window.state is set to NORMAL, then @@ -382,7 +382,7 @@ def set_window_state(self, state): # Hence, this branch is required on other backends(gtk, winforms), but not on cocoa. # self.interface.app.exit_presentation_mode() - pass + pass # branch: no cover # Complete any pending window state transition. # @@ -391,7 +391,7 @@ def set_window_state(self, state): # when direct window state switching is done. We should wait until # `windowDidDeminiaturize_` and `windowDidExitFullScreen_` are notified and then # set the pending window state. - + # # This operation is performed on the window delegate notifications for MINIMIZED # and FULLSCREEN. Hence, exclude them here. if current_state in {WindowState.MAXIMIZED, WindowState.PRESENTATION}: diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 33f9aad3ca..6857e2b1f6 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -321,7 +321,7 @@ def _close(self): # Restore to normal state if in presentation mode. On some backends (e.g., Cocoa), # the content itself is in presentation mode, and not the window. Directly closing - # the window without the content exiting presentation mode can cause rendering issues. + # the window without the content exiting presentation mode can cause rendering glitches. if self.state == WindowState.PRESENTATION: self.state = WindowState.NORMAL diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 5a6059803a..97c2c7f7d1 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -250,6 +250,8 @@ async def test_presentation_mode(app, app_probe, main_window): finally: for window in windows_list: window.close() + # After closing the window, the input focus might not be on main_window. + # Ensure that main_window will be in focus for other tests. app.current_window = main_window # Add delay for gtk to show the windows await app_probe.redraw("main_window is now the current window", delay=0.5) @@ -337,6 +339,8 @@ async def test_presentation_mode_with_excess_windows_list(app, app_probe, main_w finally: for window in excess_windows_list: window.close() + # After closing the window, the input focus might not be on main_window. + # Ensure that main_window will be in focus for other tests. app.current_window = main_window # Add delay for gtk to show the windows await app_probe.redraw("main_window is now the current window", delay=0.5) @@ -435,6 +439,8 @@ async def test_presentation_mode_exit_on_window_state_change( finally: window1.close() window2.close() + # After closing the window, the input focus might not be on main_window. + # Ensure that main_window will be in focus for other tests. app.current_window = main_window # Add delay for gtk to show the windows await app_probe.redraw("main_window is now the current window", delay=0.5) From e8ad898dbcbae80e86df4209d6db7981bf577d31 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 10 Jul 2024 01:36:05 -0700 Subject: [PATCH 119/248] 100% coverage --- cocoa/src/toga_cocoa/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index bbde82e7a3..2516864929 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -362,7 +362,7 @@ def set_window_state(self, state): # If the window is in presentation mode, exit presentation mode # WindowState.PRESENTATION case: - else: + else: # pragma: no cover # --- Review Notes: Will be removed after review --- # Marking this as no cover, since exit_presentation_mode() is triggered # on any call to window.state setter, which checks if any window is in @@ -382,7 +382,7 @@ def set_window_state(self, state): # Hence, this branch is required on other backends(gtk, winforms), but not on cocoa. # self.interface.app.exit_presentation_mode() - pass # branch: no cover + pass # Complete any pending window state transition. # From f429307e83169be44197cc331bc01f45a571cad7 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 24 Jul 2024 18:53:18 -0700 Subject: [PATCH 120/248] Removed overspecified assertions and test --- testbed/tests/app/test_desktop.py | 113 +++------------------------ testbed/tests/app/test_mobile.py | 4 - testbed/tests/window/test_window.py | 116 +++++++--------------------- 3 files changed, 37 insertions(+), 196 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 97c2c7f7d1..212fd3759b 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -200,7 +200,7 @@ async def test_presentation_mode(app, app_probe, main_window): style=Pack(background_color=f"#{r:02X}{g:02X}{b:02X}") ) window.show() - # Add delay for gtk to show the windows + # Add delay to ensure windows are visible after animation. await app_probe.redraw(f"Test Window {i} is visible", delay=0.5) window_information = dict() @@ -219,7 +219,7 @@ async def test_presentation_mode(app, app_probe, main_window): # Enter presentation mode with a screen-window dict via the app app.enter_presentation_mode(screen_window_dict) - # Add delay for gtk to show the windows + # Add delay to ensure windows are visible after animation. await app_probe.redraw("App is in presentation mode", delay=0.5) assert app.is_presentation_mode @@ -253,96 +253,7 @@ async def test_presentation_mode(app, app_probe, main_window): # After closing the window, the input focus might not be on main_window. # Ensure that main_window will be in focus for other tests. app.current_window = main_window - # Add delay for gtk to show the windows - await app_probe.redraw("main_window is now the current window", delay=0.5) - assert app.current_window == main_window - - -async def test_presentation_mode_with_excess_windows_list(app, app_probe, main_window): - """Entering presentation mode limits windows to available displays.""" - try: - window_information_list = list() - excess_windows_list = list() - # Generate an additional window compared to the number of screens present. - for i in range(len(app.screens) + 1): - window = toga.Window( - title=f"Test Window {i}", - position=(150 + (10 * i), 150 + (10 * i)), - size=(200, 200), - ) - r = random.randint(0, 255) - g = random.randint(0, 255) - b = random.randint(0, 255) - window.content = toga.Box( - style=Pack(background_color=f"#{r:02X}{g:02X}{b:02X}") - ) - window.show() - # Add delay for gtk to show the windows - await app_probe.redraw(f"Test Window {i} is visible", delay=0.5) - - window_information = dict() - window_information["window"] = window - window_information["window_probe"] = window_probe(app, window) - window_information["initial_content_size"] = window_information[ - "window_probe" - ].presentation_content_size - - window_information_list.append(window_information) - excess_windows_list.append(window) - - last_window = window_information_list[-1]["window"] - last_window_probe = window_information_list[-1]["window_probe"] - last_window_initial_content_size = window_information_list[-1][ - "initial_content_size" - ] - - # Enter presentation mode with excess windows via the app, but - # the last window should be dropped as there are less screens. - app.enter_presentation_mode(excess_windows_list) - # Add delay for gtk to show the windows - await app_probe.redraw("App is in presentation mode", delay=0.5) - assert app.is_presentation_mode - - # Last window should not be in presentation mode. - assert ( - last_window_probe.get_window_state() != WindowState.PRESENTATION - ), f"Last Window({last_window.title}):" - assert ( - last_window_probe.presentation_content_size - == last_window_initial_content_size - ), f"Last Window({last_window.title}):" - - # All other windows should be in presentation mode. - for window_information in window_information_list[:-1]: - assert ( - window_information["window_probe"].get_window_state() - == WindowState.PRESENTATION - ), f"{window_information['window'].title}:" - assert ( - window_information["window_probe"].presentation_content_size[0] > 1000 - ), f"{window_information['window'].title}:" - assert ( - window_information["window_probe"].presentation_content_size[1] > 700 - ), f"{window_information['window'].title}:" - - # Exit presentation mode - app.exit_presentation_mode() - await app_probe.redraw("App is no longer in presentation mode", delay=0.5) - assert not app.is_presentation_mode - - for window_information in window_information_list: - assert ( - window_information["window_probe"].presentation_content_size - == window_information["initial_content_size"] - ), f"{window_information['window'].title}:" - - finally: - for window in excess_windows_list: - window.close() - # After closing the window, the input focus might not be on main_window. - # Ensure that main_window will be in focus for other tests. - app.current_window = main_window - # Add delay for gtk to show the windows + # Add delay to ensure windows are visible after animation. await app_probe.redraw("main_window is now the current window", delay=0.5) assert app.current_window == main_window @@ -378,21 +289,20 @@ async def test_presentation_mode_exit_on_window_state_change( window2_probe = window_probe(app, window2) window1.show() window2.show() - # Add delay for gtk to show the windows + # Add delay to ensure windows are visible after animation. await app_probe.redraw("Test windows are shown", delay=0.5) # Enter presentation mode app.enter_presentation_mode([window1]) - # Add delay for gtk to show the windows + # Add delay to ensure windows are visible after animation. await app_probe.redraw("App is in presentation mode", delay=0.5) assert app.is_presentation_mode assert window1_probe.get_window_state() == WindowState.PRESENTATION - assert window2_probe.get_window_state() != WindowState.PRESENTATION # Changing window state of main window should make the app exit presentation mode. window1.state = new_window_state - # Add delay for gtk to show the windows + # Add delay to ensure windows are visible after animation. await app_probe.redraw( "App is not in presentation mode" f"\nTest Window 1 is in {new_window_state}", @@ -400,26 +310,24 @@ async def test_presentation_mode_exit_on_window_state_change( ) assert not app.is_presentation_mode - assert window1_probe.get_window_state != WindowState.PRESENTATION assert window1_probe.get_window_state() == new_window_state # Reset window states window1.state = WindowState.NORMAL window2.state = WindowState.NORMAL - # Add delay for gtk to show the windows + # Add delay to ensure windows are visible after animation. await app_probe.redraw("All test windows are in WindowState.NORMAL", delay=0.5) # Enter presentation mode again app.enter_presentation_mode([window1]) - # Add delay for gtk to show the windows + # Add delay to ensure windows are visible after animation. await app_probe.redraw("App is in presentation mode", delay=0.5) assert app.is_presentation_mode assert window1_probe.get_window_state() == WindowState.PRESENTATION - assert window2_probe.get_window_state() != WindowState.PRESENTATION # Changing window state of extra window should make the app exit presentation mode. window2.state = new_window_state - # Add delay for gtk to show the windows + # Add delay to ensure windows are visible after animation. await app_probe.redraw( "App is not in presentation mode" f"\nTest Window 1 is in {new_window_state}", @@ -427,7 +335,6 @@ async def test_presentation_mode_exit_on_window_state_change( ) assert not app.is_presentation_mode - assert window1_probe.get_window_state() != WindowState.PRESENTATION assert window2_probe.get_window_state() == new_window_state # Reset window states @@ -442,7 +349,7 @@ async def test_presentation_mode_exit_on_window_state_change( # After closing the window, the input focus might not be on main_window. # Ensure that main_window will be in focus for other tests. app.current_window = main_window - # Add delay for gtk to show the windows + # Add delay to ensure windows are visible after animation. await app_probe.redraw("main_window is now the current window", delay=0.5) assert app.current_window == main_window diff --git a/testbed/tests/app/test_mobile.py b/testbed/tests/app/test_mobile.py index 5fced58df1..6322d04859 100644 --- a/testbed/tests/app/test_mobile.py +++ b/testbed/tests/app/test_mobile.py @@ -30,7 +30,6 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ) assert app.is_presentation_mode - assert main_window_probe.get_window_state() != WindowState.NORMAL assert main_window_probe.get_window_state() == WindowState.PRESENTATION # Exit presentation mode @@ -41,7 +40,6 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ) assert not app.is_presentation_mode - assert main_window_probe.get_window_state() != WindowState.PRESENTATION assert main_window_probe.get_window_state() == WindowState.NORMAL # Enter presentation mode with a screen-window dict via the app @@ -51,7 +49,6 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ) assert app.is_presentation_mode - assert main_window_probe.get_window_state() != WindowState.NORMAL assert main_window_probe.get_window_state() == WindowState.PRESENTATION # Exit presentation mode @@ -62,7 +59,6 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ) assert not app.is_presentation_mode - assert main_window_probe.get_window_state() != WindowState.PRESENTATION assert main_window_probe.get_window_state() == WindowState.NORMAL diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index d755fdf497..3dfd217e5c 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -143,78 +143,57 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): finally: main_window.content = orig_content + @pytest.mark.skipif(toga.platform.current_platform == "iOS") async def test_window_state_minimized(main_window, main_window_probe): """Window can have minimized window state""" assert main_window_probe.get_window_state() == WindowState.NORMAL - assert main_window_probe.get_window_state() != WindowState.MINIMIZED main_window.state = WindowState.MINIMIZED await main_window_probe.wait_for_window("WindowState.MINIMIZED is a no-op") - assert main_window_probe.get_window_state() != WindowState.MINIMIZED assert main_window_probe.get_window_state() == WindowState.NORMAL + @pytest.mark.skipif(toga.platform.current_platform == "iOS") async def test_window_state_maximized(main_window, main_window_probe): """Window can have maximized window state""" assert main_window_probe.get_window_state() == WindowState.NORMAL - assert main_window_probe.get_window_state() != WindowState.MAXIMIZED main_window.state = WindowState.MAXIMIZED await main_window_probe.wait_for_window("WindowState.MAXIMIZED is a no-op") - assert main_window_probe.get_window_state() != WindowState.MAXIMIZED assert main_window_probe.get_window_state() == WindowState.NORMAL + @pytest.mark.skipif(toga.platform.current_platform == "iOS") async def test_window_state_full_screen(main_window, main_window_probe): """Window can have full screen window state""" assert main_window_probe.get_window_state() == WindowState.NORMAL - assert main_window_probe.get_window_state() != WindowState.FULLSCREEN # Make main window full screen main_window.state = WindowState.FULLSCREEN await main_window_probe.wait_for_window("Main window is full screen") assert main_window_probe.get_window_state() == WindowState.FULLSCREEN - assert main_window_probe.get_window_state() != WindowState.NORMAL main_window.state = WindowState.FULLSCREEN await main_window_probe.wait_for_window("Main window is still full screen") assert main_window_probe.get_window_state() == WindowState.FULLSCREEN - assert main_window_probe.get_window_state() != WindowState.NORMAL # Exit full screen main_window.state = WindowState.NORMAL await main_window_probe.wait_for_window("Main window is not full screen") - assert main_window_probe.get_window_state() != WindowState.FULLSCREEN - assert main_window_probe.get_window_state() == WindowState.NORMAL - - main_window.state = WindowState.NORMAL - await main_window_probe.wait_for_window("Main window is still not full screen") - assert main_window_probe.get_window_state() != WindowState.FULLSCREEN assert main_window_probe.get_window_state() == WindowState.NORMAL + @pytest.mark.skipif(toga.platform.current_platform == "iOS") async def test_window_state_presentation(main_window, main_window_probe): - # WindowState.PRESENTATION is implemented on Android but not on iOS. assert main_window_probe.get_window_state() == WindowState.NORMAL - assert main_window_probe.get_window_state() != WindowState.PRESENTATION # Enter presentation mode with main window main_window.state = WindowState.PRESENTATION await main_window_probe.wait_for_window("Main window is in presentation mode") assert main_window_probe.get_window_state() == WindowState.PRESENTATION - assert main_window_probe.get_window_state() != WindowState.NORMAL main_window.state = WindowState.PRESENTATION await main_window_probe.wait_for_window( "Main window is still in presentation mode" ) assert main_window_probe.get_window_state() == WindowState.PRESENTATION - assert main_window_probe.get_window_state() != WindowState.NORMAL # Exit presentation mode main_window.state = WindowState.NORMAL await main_window_probe.wait_for_window( "Main window is not in presentation mode" ) - assert main_window_probe.get_window_state() != WindowState.PRESENTATION - assert main_window_probe.get_window_state() == WindowState.NORMAL - - main_window.state = WindowState.NORMAL - await main_window_probe.wait_for_window( - "Main window is still not in presentation mode" - ) - assert main_window_probe.get_window_state() != WindowState.PRESENTATION assert main_window_probe.get_window_state() == WindowState.NORMAL @pytest.mark.parametrize( @@ -230,8 +209,6 @@ async def test_window_state_direct_change( app, initial_state, final_state, main_window, main_window_probe ): assert main_window_probe.get_window_state() == WindowState.NORMAL - assert main_window_probe.get_window_state() != initial_state - assert main_window_probe.get_window_state() != final_state try: # Set to initial state main_window.state = initial_state @@ -239,17 +216,13 @@ async def test_window_state_direct_change( f"Main window is in {initial_state}" ) - assert main_window_probe.get_window_state() != WindowState.NORMAL assert main_window_probe.get_window_state() == initial_state - assert main_window_probe.get_window_state() != final_state # Set to final state main_window.state = final_state await main_window_probe.wait_for_window(f"Main window is in {final_state}") - assert main_window_probe.get_window_state() != WindowState.NORMAL assert main_window_probe.get_window_state() == final_state - assert main_window_probe.get_window_state() != initial_state finally: # Set to NORMAL state main_window.state = WindowState.NORMAL @@ -258,8 +231,6 @@ async def test_window_state_direct_change( ) assert main_window_probe.get_window_state() == WindowState.NORMAL - assert main_window_probe.get_window_state() != final_state - assert main_window_probe.get_window_state() != initial_state async def test_screen(main_window, main_window_probe): """The window can be relocated to another screen, using both absolute and relative screen positions.""" @@ -641,18 +612,17 @@ async def test_window_state_minimized(second_window, second_window_probe): second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is shown", ) assert second_window_probe.get_window_state() == WindowState.NORMAL - assert second_window_probe.get_window_state() != WindowState.MINIMIZED if second_window_probe.supports_minimizable: assert second_window_probe.is_minimizable second_window.state = WindowState.MINIMIZED - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is minimized", minimize=True ) @@ -663,20 +633,14 @@ async def test_window_state_minimized(second_window, second_window_probe): assert second_window_probe.get_window_state() == WindowState.MINIMIZED second_window.state = WindowState.NORMAL - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is not minimized", minimize=True ) - assert second_window_probe.get_window_state() != WindowState.MINIMIZED + assert second_window_probe.get_window_state() == WindowState.NORMAL if second_window_probe.supports_minimizable: assert second_window_probe.is_minimizable - second_window.state = WindowState.NORMAL - await second_window_probe.wait_for_window( - "Secondary window is still not minimized", minimize=True - ) - assert second_window_probe.get_window_state() != WindowState.MINIMIZED - @pytest.mark.parametrize( "second_window_class, second_window_kwargs", [ @@ -690,18 +654,17 @@ async def test_window_state_maximized(second_window, second_window_probe): """Window can have maximized window state""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is shown", ) assert second_window_probe.get_window_state() == WindowState.NORMAL - assert second_window_probe.get_window_state() != WindowState.MAXIMIZED assert second_window_probe.is_resizable initial_content_size = second_window_probe.content_size second_window.state = WindowState.MAXIMIZED - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is maximized", ) @@ -716,21 +679,14 @@ async def test_window_state_maximized(second_window, second_window_probe): assert second_window_probe.content_size[1] > initial_content_size[1] second_window.state = WindowState.NORMAL - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is not maximized", ) - assert second_window_probe.get_window_state() != WindowState.MAXIMIZED + assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.is_resizable assert second_window_probe.content_size == initial_content_size - second_window.state = WindowState.NORMAL - await second_window_probe.wait_for_window( - "Secondary window is still not maximized" - ) - assert second_window_probe.get_window_state() != WindowState.MAXIMIZED - assert second_window_probe.content_size == initial_content_size - @pytest.mark.parametrize( "second_window_class, second_window_kwargs", [ @@ -744,18 +700,17 @@ async def test_window_state_full_screen(second_window, second_window_probe): """Window can have full screen window state""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is shown", ) assert second_window_probe.get_window_state() == WindowState.NORMAL - assert second_window_probe.get_window_state() != WindowState.FULLSCREEN assert second_window_probe.is_resizable initial_content_size = second_window_probe.content_size second_window.state = WindowState.FULLSCREEN - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is full screen", full_screen=True ) @@ -772,21 +727,14 @@ async def test_window_state_full_screen(second_window, second_window_probe): assert second_window_probe.content_size[1] > initial_content_size[1] second_window.state = WindowState.NORMAL - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is not full screen", full_screen=True ) - assert second_window_probe.get_window_state() != WindowState.FULLSCREEN + assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.is_resizable assert second_window_probe.content_size == initial_content_size - second_window.state = WindowState.NORMAL - await second_window_probe.wait_for_window( - "Secondary window is still not full screen", full_screen=True - ) - assert second_window_probe.get_window_state() != WindowState.FULLSCREEN - assert second_window_probe.content_size == initial_content_size - @pytest.mark.parametrize( "second_window_class, second_window_kwargs", [ @@ -800,18 +748,17 @@ async def test_window_state_presentation(second_window, second_window_probe): """Window can have presentation window state""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is shown", ) assert second_window_probe.get_window_state() == WindowState.NORMAL - assert second_window_probe.get_window_state() != WindowState.PRESENTATION assert second_window_probe.is_resizable initial_content_size = second_window_probe.presentation_content_size second_window.state = WindowState.PRESENTATION - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is in presentation mode", full_screen=True ) @@ -836,37 +783,34 @@ async def test_window_state_presentation(second_window, second_window_probe): ) second_window.state = WindowState.NORMAL - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is not in presentation mode", full_screen=True ) - assert second_window_probe.get_window_state() != WindowState.PRESENTATION + assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.is_resizable assert second_window_probe.presentation_content_size == initial_content_size - second_window.state = WindowState.NORMAL - await second_window_probe.wait_for_window( - "Secondary window is still not in presentation mode", full_screen=True - ) - assert second_window_probe.get_window_state() != WindowState.PRESENTATION - assert second_window_probe.presentation_content_size == initial_content_size - @pytest.mark.parametrize( "initial_state, final_state", [ # Direct switch from MINIMIZED: + (WindowState.MINIMIZED, WindowState.NORMAL), (WindowState.MINIMIZED, WindowState.MAXIMIZED), (WindowState.MINIMIZED, WindowState.FULLSCREEN), (WindowState.MINIMIZED, WindowState.PRESENTATION), # Direct switch from MAXIMIZED: + (WindowState.MAXIMIZED, WindowState.NORMAL), (WindowState.MAXIMIZED, WindowState.MINIMIZED), (WindowState.MAXIMIZED, WindowState.FULLSCREEN), (WindowState.MAXIMIZED, WindowState.PRESENTATION), # Direct switch from FULLSCREEN: + (WindowState.FULLSCREEN, WindowState.NORMAL), (WindowState.FULLSCREEN, WindowState.MINIMIZED), (WindowState.FULLSCREEN, WindowState.MAXIMIZED), (WindowState.FULLSCREEN, WindowState.PRESENTATION), # Direct switch from PRESENTATION: + (WindowState.PRESENTATION, WindowState.NORMAL), (WindowState.PRESENTATION, WindowState.MINIMIZED), (WindowState.PRESENTATION, WindowState.MAXIMIZED), (WindowState.PRESENTATION, WindowState.FULLSCREEN), @@ -895,36 +839,30 @@ async def test_window_state_direct_change( second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is visible", full_screen=True ) assert second_window_probe.get_window_state() == WindowState.NORMAL - assert second_window_probe.get_window_state() != initial_state - assert second_window_probe.get_window_state() != final_state # Set to initial state second_window.state = initial_state - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( f"Secondary window is in {initial_state}", full_screen=True ) - assert second_window_probe.get_window_state() != WindowState.NORMAL assert second_window_probe.get_window_state() == initial_state - assert second_window_probe.get_window_state() != final_state # Set to final state second_window.state = final_state - # A longer delay to allow for genie animations + # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( f"Secondary window is in {final_state}", full_screen=True ) - assert second_window_probe.get_window_state() != WindowState.NORMAL assert second_window_probe.get_window_state() == final_state - assert second_window_probe.get_window_state() != initial_state @pytest.mark.parametrize( "second_window_class, second_window_kwargs", From 6aedeea70272e5a88c4be92f24a78333e1ef98d2 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 24 Jul 2024 23:56:55 -0700 Subject: [PATCH 121/248] changed is_presentation_mode to in_presentation_mode --- android/src/toga_android/window.py | 8 +++--- android/tests_backend/window.py | 2 +- cocoa/src/toga_cocoa/window.py | 1 - core/src/toga/app.py | 10 +++---- core/tests/app/test_app.py | 40 ++++++++++++++-------------- examples/window/window/app.py | 2 +- gtk/src/toga_gtk/window.py | 8 +++--- gtk/tests_backend/window.py | 2 +- testbed/tests/app/test_desktop.py | 12 ++++----- testbed/tests/app/test_mobile.py | 10 +++---- winforms/src/toga_winforms/window.py | 8 +++--- winforms/tests_backend/window.py | 2 +- 12 files changed, 52 insertions(+), 53 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 31c10c646c..6db75e4ace 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -42,7 +42,7 @@ def __init__(self, interface, title, position, size): self._initial_title = title # Use a shadow variable since the presence of ActionBar is not # a reliable indicator for confirmation of presentation mode. - self._is_presentation_mode = False + self._in_presentation_mode = False ###################################################################### # Window properties @@ -157,7 +157,7 @@ def get_window_state(self): | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION | decor_view.SYSTEM_UI_FLAG_IMMERSIVE ): - if self._is_presentation_mode: + if self._in_presentation_mode: return WindowState.PRESENTATION else: return WindowState.FULLSCREEN @@ -190,7 +190,7 @@ def set_window_state(self, state): # window, so we can't test the other branch. if self._actionbar_shown_by_default: # pragma: no branch self.app.native.getSupportActionBar().hide() - self._is_presentation_mode = True + self._in_presentation_mode = True else: # On Android Maximized state is same as the Normal state @@ -205,7 +205,7 @@ def set_window_state(self, state): # window, so we can't test the other branch. if self._actionbar_shown_by_default: # pragma: no branch self.app.native.getSupportActionBar().show() - self._is_presentation_mode = False + self._in_presentation_mode = False # Complete any pending window state transition. if getattr(self, "_pending_window_state_transition", None) is not None: diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index 294e3dc553..d66280b3ad 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -30,7 +30,7 @@ def get_window_state(self): | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION | decor_view.SYSTEM_UI_FLAG_IMMERSIVE ): - if self.window._impl._is_presentation_mode: + if self.window._impl._in_presentation_mode: current_state = WindowState.PRESENTATION else: current_state = WindowState.FULLSCREEN diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 2516864929..b9da1f2ec1 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -363,7 +363,6 @@ def set_window_state(self, state): # If the window is in presentation mode, exit presentation mode # WindowState.PRESENTATION case: else: # pragma: no cover - # --- Review Notes: Will be removed after review --- # Marking this as no cover, since exit_presentation_mode() is triggered # on any call to window.state setter, which checks if any window is in # presentation mode and sets those windows' state to NORMAL. diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 9a3b894120..2be1dd4d0b 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -781,7 +781,7 @@ def current_window(self, window: Window) -> None: ###################################################################### @property - def is_presentation_mode(self) -> bool: + def in_presentation_mode(self) -> bool: """Is the app currently in presentation mode?""" return any(window.state == WindowState.PRESENTATION for window in self.windows) @@ -894,20 +894,20 @@ def exit_full_screen(self) -> None: DeprecationWarning, stacklevel=2, ) - if self.is_presentation_mode: + if self.in_presentation_mode: self._impl.exit_presentation_mode() @property def is_full_screen(self) -> bool: - """**DEPRECATED** – Use :any:`App.is_presentation_mode`.""" + """**DEPRECATED** – Use :any:`App.in_presentation_mode`.""" warnings.warn( ( - "`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead." + "`App.is_full_screen` is deprecated. Use `App.in_presentation_mode` instead." ), DeprecationWarning, stacklevel=2, ) - return self.is_presentation_mode + return self.in_presentation_mode def set_full_screen(self, *windows: Window) -> None: """**DEPRECATED** – Use :any:`App.enter_presentation_mode()` and :any:`App.exit_presentation_mode()`.""" diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index b1ba0694d2..05466bf3c9 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -472,16 +472,16 @@ def test_presentation_mode_with_windows_list(event_loop): window1 = toga.Window() window2 = toga.Window() - assert not app.is_presentation_mode + assert not app.in_presentation_mode # Entering presentation mode with an empty windows list, is a no-op: app.enter_presentation_mode([]) - assert not app.is_presentation_mode + assert not app.in_presentation_mode assert_action_not_performed(app, "enter presentation mode") # Enter presentation mode with 1 window: app.enter_presentation_mode([window1]) - assert app.is_presentation_mode + assert app.in_presentation_mode assert_action_performed_with( app, "enter presentation mode", @@ -490,7 +490,7 @@ def test_presentation_mode_with_windows_list(event_loop): # Enter presentation mode with 2 windows: app.enter_presentation_mode([window1, window2]) - assert app.is_presentation_mode + assert app.in_presentation_mode assert_action_performed_with( app, "enter presentation mode", @@ -510,16 +510,16 @@ def test_presentation_mode_with_screen_window_dict(event_loop): window1 = toga.Window() window2 = toga.Window() - assert not app.is_presentation_mode + assert not app.in_presentation_mode # Entering presentation mode with an empty dict, is a no-op: app.enter_presentation_mode({}) - assert not app.is_presentation_mode + assert not app.in_presentation_mode assert_action_not_performed(app, "enter presentation mode") # Enter presentation mode with an 1 element screen-window dict: app.enter_presentation_mode({app.screens[0]: window1}) - assert app.is_presentation_mode + assert app.in_presentation_mode assert_action_performed_with( app, "enter presentation mode", @@ -534,7 +534,7 @@ def test_presentation_mode_with_screen_window_dict(event_loop): # Enter presentation mode with a 2 elements screen-window dict: app.enter_presentation_mode({app.screens[0]: window1, app.screens[1]: window2}) - assert app.is_presentation_mode + assert app.in_presentation_mode assert_action_performed_with( app, "enter presentation mode", @@ -554,12 +554,12 @@ def test_presentation_mode_with_excess_windows_list(event_loop): window1 = toga.Window() window2 = toga.Window() - assert not app.is_presentation_mode + assert not app.in_presentation_mode # Entering presentation mode with 3 windows should drop the last window, # as the app has only 2 screens: app.enter_presentation_mode([app.main_window, window2, window1]) - assert app.is_presentation_mode + assert app.in_presentation_mode assert_action_performed_with( app, "enter presentation mode", @@ -568,7 +568,7 @@ def test_presentation_mode_with_excess_windows_list(event_loop): # Exit presentation mode: app.exit_presentation_mode() - assert not app.is_presentation_mode + assert not app.in_presentation_mode assert_action_performed( app, "exit presentation mode", @@ -579,13 +579,13 @@ def test_presentation_mode_no_op(event_loop): """Entering presentation mode with invalid conditions is a no-op.""" app = toga.App(formal_name="Test App", app_id="org.example.test") - assert not app.is_presentation_mode + assert not app.in_presentation_mode # Entering presentation mode without any window is a no-op. with pytest.raises(TypeError): app.enter_presentation_mode() assert_action_not_performed(app, "enter presentation mode") - assert not app.is_presentation_mode + assert not app.in_presentation_mode # Entering presentation mode without proper type of parameter is a no-op. with pytest.raises( @@ -594,7 +594,7 @@ def test_presentation_mode_no_op(event_loop): ): app.enter_presentation_mode(app.main_window) assert_action_not_performed(app, "enter presentation mode") - assert not app.is_presentation_mode + assert not app.in_presentation_mode @pytest.mark.parametrize( @@ -618,7 +618,7 @@ def test_presentation_mode_exit_on_window_state_change(event_loop, new_window_st screen_window_dict={app.screens[0]: app.main_window}, ) - assert app.is_presentation_mode + assert app.in_presentation_mode assert app.main_window.state == WindowState.PRESENTATION assert extra_window.state != WindowState.PRESENTATION @@ -630,7 +630,7 @@ def test_presentation_mode_exit_on_window_state_change(event_loop, new_window_st state=new_window_state, ) - assert not app.is_presentation_mode + assert not app.in_presentation_mode assert_action_performed( app, "exit presentation mode", @@ -650,7 +650,7 @@ def test_presentation_mode_exit_on_window_state_change(event_loop, new_window_st screen_window_dict={app.screens[0]: app.main_window}, ) - assert app.is_presentation_mode + assert app.in_presentation_mode assert app.main_window.state == WindowState.PRESENTATION assert extra_window.state != WindowState.PRESENTATION @@ -662,7 +662,7 @@ def test_presentation_mode_exit_on_window_state_change(event_loop, new_window_st state=new_window_state, ) - assert not app.is_presentation_mode + assert not app.in_presentation_mode assert_action_performed( app, "exit presentation mode", @@ -1002,7 +1002,7 @@ def test_deprecated_full_screen(event_loop): window2 = toga.Window() is_full_screen_warning = ( - r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead." + r"`App.is_full_screen` is deprecated. Use `App.in_presentation_mode` instead." ) set_full_screen_warning = ( r"`App.set_full_screen\(\)` is deprecated. " @@ -1104,7 +1104,7 @@ def test_deprecated_set_empty_full_screen_window_list(event_loop): window2 = toga.Window() is_full_screen_warning = ( - r"`App.is_full_screen` is deprecated. Use `App.is_presentation_mode` instead." + r"`App.is_full_screen` is deprecated. Use `App.in_presentation_mode` instead." ) set_full_screen_warning = ( r"`App.set_full_screen\(\)` is deprecated. " diff --git a/examples/window/window/app.py b/examples/window/window/app.py index f3f3a8d65b..9d443bb5c7 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -53,7 +53,7 @@ def do_window_state_presentation(self, widget, **kwargs): self.main_window.state = WindowState.PRESENTATION def do_app_presentation_mode(self, widget, **kwargs): - if self.is_presentation_mode: + if self.in_presentation_mode: self.exit_presentation_mode() else: self.enter_presentation_mode([self.main_window]) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 22c281d18d..8d7a31d01d 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -34,7 +34,7 @@ def __init__(self, interface, title, position, size): self._window_state_flags = None # Gdk.WindowState.FULLSCREEN is unreliable, use shadow variables. - self._is_presentation_mode = False + self._in_presentation_mode = False self._is_full_screen = False self.native.set_default_size(size[0], size[1]) @@ -162,7 +162,7 @@ def get_window_state(self): elif window_state_flags & Gdk.WindowState.ICONIFIED: return WindowState.MINIMIZED # pragma: no-cover-if-linux-wayland elif window_state_flags & Gdk.WindowState.FULLSCREEN: - if self._is_presentation_mode: + if self._in_presentation_mode: return WindowState.PRESENTATION elif self._is_full_screen: return WindowState.FULLSCREEN @@ -203,7 +203,7 @@ def set_window_state(self, state): if getattr(self, "native_toolbar", None): self.native_toolbar.set_visible(False) self.native.fullscreen() - self._is_presentation_mode = True + self._in_presentation_mode = True # WindowState.NORMAL case: else: @@ -229,7 +229,7 @@ def set_window_state(self, state): self.interface.screen = self._before_presentation_mode_screen del self._before_presentation_mode_screen - self._is_presentation_mode = False + self._in_presentation_mode = False # Complete any pending window state transition. if getattr(self, "_pending_window_state_transition", None) is not None: diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index 5e393a0655..5258e5bac6 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -48,7 +48,7 @@ def get_window_state(self): elif window_state_flags & Gdk.WindowState.ICONIFIED: current_state = WindowState.MINIMIZED elif window_state_flags & Gdk.WindowState.FULLSCREEN: - if getattr(self.impl, "_is_presentation_mode", False) is True: + if getattr(self.impl, "_in_presentation_mode", False) is True: current_state = WindowState.PRESENTATION elif getattr(self.impl, "_is_full_screen", False) is True: current_state = WindowState.FULLSCREEN diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 212fd3759b..70b9683362 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -222,7 +222,7 @@ async def test_presentation_mode(app, app_probe, main_window): # Add delay to ensure windows are visible after animation. await app_probe.redraw("App is in presentation mode", delay=0.5) - assert app.is_presentation_mode + assert app.in_presentation_mode # All the windows should be in presentation mode. for window_information in window_information_list: assert ( @@ -240,7 +240,7 @@ async def test_presentation_mode(app, app_probe, main_window): app.exit_presentation_mode() await app_probe.redraw("App is no longer in presentation mode", delay=0.5) - assert not app.is_presentation_mode + assert not app.in_presentation_mode for window_information in window_information_list: assert ( window_information["window_probe"].presentation_content_size @@ -297,7 +297,7 @@ async def test_presentation_mode_exit_on_window_state_change( # Add delay to ensure windows are visible after animation. await app_probe.redraw("App is in presentation mode", delay=0.5) - assert app.is_presentation_mode + assert app.in_presentation_mode assert window1_probe.get_window_state() == WindowState.PRESENTATION # Changing window state of main window should make the app exit presentation mode. @@ -309,7 +309,7 @@ async def test_presentation_mode_exit_on_window_state_change( delay=0.5, ) - assert not app.is_presentation_mode + assert not app.in_presentation_mode assert window1_probe.get_window_state() == new_window_state # Reset window states @@ -322,7 +322,7 @@ async def test_presentation_mode_exit_on_window_state_change( app.enter_presentation_mode([window1]) # Add delay to ensure windows are visible after animation. await app_probe.redraw("App is in presentation mode", delay=0.5) - assert app.is_presentation_mode + assert app.in_presentation_mode assert window1_probe.get_window_state() == WindowState.PRESENTATION # Changing window state of extra window should make the app exit presentation mode. @@ -334,7 +334,7 @@ async def test_presentation_mode_exit_on_window_state_change( delay=0.5, ) - assert not app.is_presentation_mode + assert not app.in_presentation_mode assert window2_probe.get_window_state() == new_window_state # Reset window states diff --git a/testbed/tests/app/test_mobile.py b/testbed/tests/app/test_mobile.py index 6322d04859..e6418be021 100644 --- a/testbed/tests/app/test_mobile.py +++ b/testbed/tests/app/test_mobile.py @@ -20,7 +20,7 @@ async def test_show_hide_cursor(app): async def test_presentation_mode(app, app_probe, main_window, main_window_probe): """The app can enter into presentation mode""" - assert not app.is_presentation_mode + assert not app.in_presentation_mode assert main_window_probe.get_window_state() != WindowState.PRESENTATION # Enter presentation mode with main window via the app @@ -29,7 +29,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) "Main window is in presentation mode", full_screen=True ) - assert app.is_presentation_mode + assert app.in_presentation_mode assert main_window_probe.get_window_state() == WindowState.PRESENTATION # Exit presentation mode @@ -39,7 +39,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) full_screen=True, ) - assert not app.is_presentation_mode + assert not app.in_presentation_mode assert main_window_probe.get_window_state() == WindowState.NORMAL # Enter presentation mode with a screen-window dict via the app @@ -48,7 +48,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) "Main window is in presentation mode", full_screen=True ) - assert app.is_presentation_mode + assert app.in_presentation_mode assert main_window_probe.get_window_state() == WindowState.PRESENTATION # Exit presentation mode @@ -58,7 +58,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) full_screen=True, ) - assert not app.is_presentation_mode + assert not app.in_presentation_mode assert main_window_probe.get_window_state() == WindowState.NORMAL diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 4a31d47eb8..32ae6305ee 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -38,7 +38,7 @@ def __init__(self, interface, title, position, size): # Use a shadow variable since a window without any app menu and toolbar # in presentation mode would be indistinguishable from full screen mode. - self._is_presentation_mode = False + self._in_presentation_mode = False self.set_title(title) self.set_size(size) @@ -194,7 +194,7 @@ def get_window_state(self): window_state = self.native.WindowState if window_state == WinForms.FormWindowState.Maximized: if self.native.FormBorderStyle == getattr(WinForms.FormBorderStyle, "None"): - if self._is_presentation_mode: + if self._in_presentation_mode: return WindowState.PRESENTATION else: return WindowState.FULLSCREEN @@ -242,7 +242,7 @@ def set_window_state(self, state): self.toolbar_native.Visible = False self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") self.native.WindowState = WinForms.FormWindowState.Maximized - self._is_presentation_mode = True + self._in_presentation_mode = True # WindowState.NORMAL case: else: @@ -254,7 +254,7 @@ def set_window_state(self, state): self.interface.screen = self._before_presentation_mode_screen del self._before_presentation_mode_screen - self._is_presentation_mode = False + self._in_presentation_mode = False self.native.FormBorderStyle = getattr( WinForms.FormBorderStyle, diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index a5f43dcc41..f1e487dc09 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -57,7 +57,7 @@ def get_window_state(self): window_state = self.native.WindowState if window_state == FormWindowState.Maximized: if self.native.FormBorderStyle == getattr(FormBorderStyle, "None"): - if self.impl._is_presentation_mode: + if self.impl._in_presentation_mode: current_state = WindowState.PRESENTATION else: current_state = WindowState.FULLSCREEN From d7228ba9255a0314bd2487a26dc97aac5006120f Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 25 Jul 2024 00:32:19 -0700 Subject: [PATCH 122/248] skipped some tests on the unimplemented iOS backend --- testbed/tests/window/test_window.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 3dfd217e5c..12b806f045 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -143,7 +143,9 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): finally: main_window.content = orig_content - @pytest.mark.skipif(toga.platform.current_platform == "iOS") + @pytest.mark.skipif( + toga.platform.current_platform == "iOS", reason="Not implemented on iOS" + ) async def test_window_state_minimized(main_window, main_window_probe): """Window can have minimized window state""" assert main_window_probe.get_window_state() == WindowState.NORMAL @@ -151,7 +153,9 @@ async def test_window_state_minimized(main_window, main_window_probe): await main_window_probe.wait_for_window("WindowState.MINIMIZED is a no-op") assert main_window_probe.get_window_state() == WindowState.NORMAL - @pytest.mark.skipif(toga.platform.current_platform == "iOS") + @pytest.mark.skipif( + toga.platform.current_platform == "iOS", reason="Not implemented on iOS" + ) async def test_window_state_maximized(main_window, main_window_probe): """Window can have maximized window state""" assert main_window_probe.get_window_state() == WindowState.NORMAL @@ -159,7 +163,9 @@ async def test_window_state_maximized(main_window, main_window_probe): await main_window_probe.wait_for_window("WindowState.MAXIMIZED is a no-op") assert main_window_probe.get_window_state() == WindowState.NORMAL - @pytest.mark.skipif(toga.platform.current_platform == "iOS") + @pytest.mark.skipif( + toga.platform.current_platform == "iOS", reason="Not implemented on iOS" + ) async def test_window_state_full_screen(main_window, main_window_probe): """Window can have full screen window state""" assert main_window_probe.get_window_state() == WindowState.NORMAL @@ -176,7 +182,9 @@ async def test_window_state_full_screen(main_window, main_window_probe): await main_window_probe.wait_for_window("Main window is not full screen") assert main_window_probe.get_window_state() == WindowState.NORMAL - @pytest.mark.skipif(toga.platform.current_platform == "iOS") + @pytest.mark.skipif( + toga.platform.current_platform == "iOS", reason="Not implemented on iOS" + ) async def test_window_state_presentation(main_window, main_window_probe): assert main_window_probe.get_window_state() == WindowState.NORMAL # Enter presentation mode with main window From 600e4c96cbcc0067c1596699d05d78445b1a4462 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 29 Jul 2024 06:42:25 -0700 Subject: [PATCH 123/248] Modified cocoa implementation --- cocoa/src/toga_cocoa/window.py | 106 +++++++++++++-------------------- 1 file changed, 42 insertions(+), 64 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index b9da1f2ec1..ec61ee654a 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -1,3 +1,5 @@ +import queue + from rubicon.objc import ( SEL, CGSize, @@ -52,17 +54,11 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidDeminiaturize_(self, notification) -> None: - # Complete any pending window state transition. - if getattr(self.impl, "_pending_window_state_transition", None) is not None: - self.impl.set_window_state(self.impl._pending_window_state_transition) - del self.impl._pending_window_state_transition + self._process_pending_state_transitions() @objc_method def windowDidExitFullScreen_(self, notification) -> None: - # Complete any pending window state transition. - if getattr(self.impl, "_pending_window_state_transition", None) is not None: - self.impl.set_window_state(self.impl._pending_window_state_transition) - del self.impl._pending_window_state_transition + self._process_pending_state_transitions() ###################################################################### # Toolbar delegate methods @@ -181,6 +177,10 @@ def __init__(self, interface, title, position, size): # in response to the close. self.native.retain() + # Window state transition queue and flag: + self._pending_state_transitions_queue = queue.Queue() + self._is_state_transitioning = False + self.set_title(title) self.set_size(size) self.set_position(position if position is not None else _initial_position()) @@ -317,40 +317,50 @@ def get_window_state(self): else: return WindowState.NORMAL + def _process_pending_state_transitions(self): + while True: + try: + requested_state = self._pending_state_transitions_queue.get(timeout=1) + self.set_window_state(requested_state) + except queue.Empty: + self._is_state_transitioning = False + break + def set_window_state(self, state): current_state = self.get_window_state() if current_state == state: return - if ( - current_state != WindowState.NORMAL - and state != WindowState.NORMAL - and (getattr(self, "_pending_window_state_transition", None) is None) - ): - # Set Window state to NORMAL before changing to other states as some - # states block changing window state without first exiting them or - # can even cause rendering glitches. - self._pending_window_state_transition = state - self.set_window_state(WindowState.NORMAL) - elif state == WindowState.MAXIMIZED: - self.native.setIsZoomed(True) + elif self._is_state_transitioning: + self._pending_state_transitions_queue.put(state) - elif state == WindowState.MINIMIZED: - self.native.setIsMiniaturized(True) + elif current_state == WindowState.NORMAL: + if state == WindowState.MAXIMIZED: + self.native.setIsZoomed(True) - elif state == WindowState.FULLSCREEN: - self.native.toggleFullScreen(self.native) + elif state == WindowState.MINIMIZED: + self.native.setIsMiniaturized(True) - elif state == WindowState.PRESENTATION: - self.interface.app.enter_presentation_mode( - {self.interface.screen: self.interface} - ) + elif state == WindowState.FULLSCREEN: + self.native.toggleFullScreen(self.native) - # WindowState.NORMAL case: + elif state == WindowState.PRESENTATION: + self.interface.app.enter_presentation_mode( + {self.interface.screen: self.interface} + ) + + # current_state != WindowState.NORMAL: else: + # If requested state was not NORMAL, then put the requested + # state to pending state transitions queue. + if state != WindowState.NORMAL: + self._pending_state_transitions_queue.put(state) + self._is_state_transitioning = True + # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: self.native.setIsZoomed(False) + self._process_pending_state_transitions() # Deminiaturize the window to restore it to its previous state elif current_state == WindowState.MINIMIZED: @@ -362,41 +372,9 @@ def set_window_state(self, state): # If the window is in presentation mode, exit presentation mode # WindowState.PRESENTATION case: - else: # pragma: no cover - # Marking this as no cover, since exit_presentation_mode() is triggered - # on any call to window.state setter, which checks if any window is in - # presentation mode and sets those windows' state to NORMAL. - # - # So, if the window was in PRESENTATION state and window.state is set to NORMAL, then - # exit_presentation_mode() would be called in the window.state setter and would exit - # app presentation mode. Since the window would now be in NORMAL state, this - # branch would never be triggered. - # - # On other backends presentation mode is window-based(i.e., window is manipulated - # to create presentation mode), as they do not have a native presentation mode. - # However, on cocoa, presentation mode is app-based(i.e., window is not manipulated - # to create presentation mode), since cocoa natively supports a separate presentation - # mode. - # - # Hence, this branch is required on other backends(gtk, winforms), but not on cocoa. - - # self.interface.app.exit_presentation_mode() - pass - - # Complete any pending window state transition. - # - # `setIsMiniaturized()` and `toggleFullScreen()` do not wait for completely exiting - # `MINIMIZED` and `FULLSCREEN` respectively. Hence,they will cause problems - # when direct window state switching is done. We should wait until - # `windowDidDeminiaturize_` and `windowDidExitFullScreen_` are notified and then - # set the pending window state. - # - # This operation is performed on the window delegate notifications for MINIMIZED - # and FULLSCREEN. Hence, exclude them here. - if current_state in {WindowState.MAXIMIZED, WindowState.PRESENTATION}: - if getattr(self, "_pending_window_state_transition", None) is not None: - self.set_window_state(self._pending_window_state_transition) - del self._pending_window_state_transition + else: + self.interface.app.exit_presentation_mode() + self._process_pending_state_transitions() ###################################################################### # Window capabilities From 3689314cb8dc9da84d05bf5f22af4af8d1554e44 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 29 Jul 2024 06:44:10 -0700 Subject: [PATCH 124/248] Typo fix on cocoa --- cocoa/src/toga_cocoa/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index ec61ee654a..2f227e7f64 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -54,11 +54,11 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidDeminiaturize_(self, notification) -> None: - self._process_pending_state_transitions() + self.impl._process_pending_state_transitions() @objc_method def windowDidExitFullScreen_(self, notification) -> None: - self._process_pending_state_transitions() + self.impl._process_pending_state_transitions() ###################################################################### # Toolbar delegate methods From 2885d1c4101e1dcdf9ba1d8180e465d6408badad Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 29 Jul 2024 07:02:57 -0700 Subject: [PATCH 125/248] Typo fix on cocoa --- cocoa/src/toga_cocoa/window.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 2f227e7f64..402a1af144 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -318,13 +318,13 @@ def get_window_state(self): return WindowState.NORMAL def _process_pending_state_transitions(self): - while True: + while not self._pending_state_transitions_queue.empty(): try: - requested_state = self._pending_state_transitions_queue.get(timeout=1) + requested_state = self._pending_state_transitions_queue.get_nowait() self.set_window_state(requested_state) except queue.Empty: - self._is_state_transitioning = False break + self._is_state_transitioning = False def set_window_state(self, state): current_state = self.get_window_state() @@ -333,6 +333,7 @@ def set_window_state(self, state): elif self._is_state_transitioning: self._pending_state_transitions_queue.put(state) + return elif current_state == WindowState.NORMAL: if state == WindowState.MAXIMIZED: @@ -351,11 +352,8 @@ def set_window_state(self, state): # current_state != WindowState.NORMAL: else: - # If requested state was not NORMAL, then put the requested - # state to pending state transitions queue. - if state != WindowState.NORMAL: - self._pending_state_transitions_queue.put(state) - self._is_state_transitioning = True + self._pending_state_transitions_queue.put(state) + self._is_state_transitioning = True # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: From cd9e00b896437b2ff4bb6abd52d4cf72b6cae619 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 29 Jul 2024 07:02:57 -0700 Subject: [PATCH 126/248] Typo fix on cocoa --- cocoa/src/toga_cocoa/window.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 2f227e7f64..4ebdabf3f3 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -318,13 +318,13 @@ def get_window_state(self): return WindowState.NORMAL def _process_pending_state_transitions(self): - while True: + while not self._pending_state_transitions_queue.empty(): try: - requested_state = self._pending_state_transitions_queue.get(timeout=1) + requested_state = self._pending_state_transitions_queue.get_nowait() self.set_window_state(requested_state) except queue.Empty: - self._is_state_transitioning = False break + self._is_state_transitioning = False def set_window_state(self, state): current_state = self.get_window_state() @@ -333,8 +333,11 @@ def set_window_state(self, state): elif self._is_state_transitioning: self._pending_state_transitions_queue.put(state) + return - elif current_state == WindowState.NORMAL: + self._is_state_transitioning = True + + if current_state == WindowState.NORMAL: if state == WindowState.MAXIMIZED: self.native.setIsZoomed(True) @@ -351,11 +354,8 @@ def set_window_state(self, state): # current_state != WindowState.NORMAL: else: - # If requested state was not NORMAL, then put the requested - # state to pending state transitions queue. - if state != WindowState.NORMAL: - self._pending_state_transitions_queue.put(state) - self._is_state_transitioning = True + self._pending_state_transitions_queue.put(state) + self._is_state_transitioning = True # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: From 7835fe885a9f9609b8bcf0f9f98de1128712f5e4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 29 Jul 2024 07:52:11 -0700 Subject: [PATCH 127/248] Fix cocoa --- cocoa/src/toga_cocoa/window.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 4ebdabf3f3..73ad318474 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -335,9 +335,7 @@ def set_window_state(self, state): self._pending_state_transitions_queue.put(state) return - self._is_state_transitioning = True - - if current_state == WindowState.NORMAL: + elif current_state == WindowState.NORMAL: if state == WindowState.MAXIMIZED: self.native.setIsZoomed(True) @@ -351,6 +349,7 @@ def set_window_state(self, state): self.interface.app.enter_presentation_mode( {self.interface.screen: self.interface} ) + return # current_state != WindowState.NORMAL: else: @@ -373,7 +372,7 @@ def set_window_state(self, state): # If the window is in presentation mode, exit presentation mode # WindowState.PRESENTATION case: else: - self.interface.app.exit_presentation_mode() + # self.interface.app.exit_presenstation_mode() self._process_pending_state_transitions() ###################################################################### From 1c0a96941beb0f7de0a63287e88394651034a0ca Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 6 Aug 2024 07:05:34 -0400 Subject: [PATCH 128/248] Implemented new window states handling on cocoa and gtk --- cocoa/src/toga_cocoa/app.py | 2 + cocoa/src/toga_cocoa/window.py | 108 ++++++++++++++++++++------------- gtk/src/toga_gtk/window.py | 93 +++++++++++++++++----------- 3 files changed, 125 insertions(+), 78 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 6ac8169c1d..6d362c879b 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -520,6 +520,8 @@ def exit_presentation_mode(self): if window.state == WindowState.PRESENTATION: window.content._impl.native.exitFullScreenModeWithOptions(opts) window.content.refresh() + # Process any pending window state. + window._impl._process_pending_state() class DocumentApp(App): # pragma: no cover diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 73ad318474..1a1ea9e5fc 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -1,5 +1,3 @@ -import queue - from rubicon.objc import ( SEL, CGSize, @@ -54,11 +52,11 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidDeminiaturize_(self, notification) -> None: - self.impl._process_pending_state_transitions() + self.impl._process_pending_state() @objc_method def windowDidExitFullScreen_(self, notification) -> None: - self.impl._process_pending_state_transitions() + self.impl._process_pending_state() ###################################################################### # Toolbar delegate methods @@ -177,9 +175,9 @@ def __init__(self, interface, title, position, size): # in response to the close. self.native.retain() - # Window state transition queue and flag: - self._pending_state_transitions_queue = queue.Queue() - self._is_state_transitioning = False + # Pending Window state transition variable and flag: + self._pending_state_transition = None + self._processing_pending_state = False self.set_title(title) self.set_size(size) @@ -317,63 +315,89 @@ def get_window_state(self): else: return WindowState.NORMAL - def _process_pending_state_transitions(self): - while not self._pending_state_transitions_queue.empty(): - try: - requested_state = self._pending_state_transitions_queue.get_nowait() - self.set_window_state(requested_state) - except queue.Empty: - break - self._is_state_transitioning = False - def set_window_state(self, state): current_state = self.get_window_state() + if current_state == state: return - elif self._is_state_transitioning: - self._pending_state_transitions_queue.put(state) + elif self._processing_pending_state: + # If we're processing a transition then store the requested state + # in the class variable. + self._pending_state_transition = state return - elif current_state == WindowState.NORMAL: - if state == WindowState.MAXIMIZED: - self.native.setIsZoomed(True) + # Set Window state to NORMAL before changing to other states as some + # states block changing window state without first exiting them or + # can even cause rendering glitches. + elif current_state != WindowState.NORMAL: + self._pending_state_transition = state + self._processing_pending_state = True + self._apply_state(WindowState.NORMAL) - elif state == WindowState.MINIMIZED: - self.native.setIsMiniaturized(True) + # elif current_state == WindowState.NORMAL: + else: + self._processing_pending_state = True + self._apply_state(state) - elif state == WindowState.FULLSCREEN: - self.native.toggleFullScreen(self.native) + def _process_pending_state(self): + pending_state = self._pending_state_transition + self._pending_state_transition = None + if (pending_state is not None) and (self.get_window_state() != pending_state): + self._apply_state(pending_state) - elif state == WindowState.PRESENTATION: - self.interface.app.enter_presentation_mode( - {self.interface.screen: self.interface} - ) - return + if self._pending_state_transition is not None: + # The new requested state must have been added while the pending + # state was being applied. Hence, process the new requested state. + self._process_pending_state() - # current_state != WindowState.NORMAL: - else: - self._pending_state_transitions_queue.put(state) - self._is_state_transitioning = True + self._processing_pending_state = False + def _apply_state(self, target_state): + if target_state == WindowState.NORMAL: + current_state = self.get_window_state() # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: self.native.setIsZoomed(False) - self._process_pending_state_transitions() - + self._process_pending_state() # Deminiaturize the window to restore it to its previous state elif current_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(False) - # If the window is in full-screen mode, exit full-screen mode elif current_state == WindowState.FULLSCREEN: self.native.toggleFullScreen(self.native) - # If the window is in presentation mode, exit presentation mode - # WindowState.PRESENTATION case: - else: - # self.interface.app.exit_presenstation_mode() - self._process_pending_state_transitions() + # elif current_state == WindowState.PRESENTATION: + else: # pragma: no cover + # Marking this as no cover, since exit_presentation_mode() is triggered on + # window.state setter, which sets windows in presentation mode to NORMAL. + # Thus, if a window was in PRESENTATION state and switched to NORMAL, + # exit_presentation_mode() would handle it, making this branch unreachable. + # + # On other backends (gtk, winforms), presentation mode is window-based and + # manipulated directly. On cocoa, it's app-based with native support, so + # this branch isn't needed. + # + # self.interface.app.exit_presentation_mode() + # Pending window state is processed at app._impl.exit_presentation_mode() + pass + elif target_state == WindowState.MAXIMIZED: + self.native.setIsZoomed(True) + elif target_state == WindowState.MINIMIZED: + self.native.setIsMiniaturized(True) + elif target_state == WindowState.FULLSCREEN: + self.native.toggleFullScreen(self.native) + # elif target_state == WindowState.PRESENTATION: + else: + self.interface.app.enter_presentation_mode( + {self.interface.screen: self.interface} + ) + + # Skip pending state processing when target state is NORMAL. This is handled by + # window delegation notifications (`windowDidDeminiaturize_` and `windowDidExitFullScreen_`). + # Methods `setIsMiniaturized()` and `toggleFullScreen()` don't wait for full state exit. + if target_state != WindowState.NORMAL: + self._process_pending_state() ###################################################################### # Window capabilities diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 8d7a31d01d..7f39cced5e 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -37,6 +37,10 @@ def __init__(self, interface, title, position, size): self._in_presentation_mode = False self._is_full_screen = False + # Pending Window state transition variable and flag: + self._pending_state_transition = None + self._processing_pending_state = False + self.native.set_default_size(size[0], size[1]) self.set_title(title) @@ -173,68 +177,85 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() + if current_state == state: return - if ( - current_state != WindowState.NORMAL - and state != WindowState.NORMAL - and (getattr(self, "_pending_window_state_transition", None) is None) - ): - # Set Window state to NORMAL before changing to other states as some - # states block changing window state without first exiting them or - # can even cause rendering glitches. - self._pending_window_state_transition = state - self.set_window_state(WindowState.NORMAL) - - elif state == WindowState.MAXIMIZED: - self.native.maximize() - - elif state == WindowState.MINIMIZED: - self.native.iconify() # pragma: no-cover-if-linux-wayland - elif state == WindowState.FULLSCREEN: - self.native.fullscreen() - self._is_full_screen = True + elif self._processing_pending_state: + # If we're processing a transition then store the requested state + # in the class variable. + self._pending_state_transition = state + return - elif state == WindowState.PRESENTATION: - self._before_presentation_mode_screen = self.interface.screen - if isinstance(self.native, Gtk.ApplicationWindow): - self.native.set_show_menubar(False) - if getattr(self, "native_toolbar", None): - self.native_toolbar.set_visible(False) - self.native.fullscreen() - self._in_presentation_mode = True + # Set Window state to NORMAL before changing to other states as some + # states block changing window state without first exiting them or + # can even cause rendering glitches. + elif current_state != WindowState.NORMAL: + self._pending_state_transition = state + self._processing_pending_state = True + self._apply_state(WindowState.NORMAL) - # WindowState.NORMAL case: + # elif current_state == WindowState.NORMAL: else: + self._processing_pending_state = True + self._apply_state(state) + + def _process_pending_state(self): + pending_state = self._pending_state_transition + self._pending_state_transition = None + if (pending_state is not None) and (self.get_window_state() != pending_state): + self._apply_state(pending_state) + + if self._pending_state_transition is not None: + # The new requested state must have been added while the pending + # state was being applied. Hence, process the new requested state. + self._process_pending_state() + + self._processing_pending_state = False + + def _apply_state(self, target_state): + if target_state == WindowState.NORMAL: + current_state = self.get_window_state() # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: self.native.unmaximize() # Deminiaturize the window to restore it to its previous state elif current_state == WindowState.MINIMIZED: - # deconify() doesn't work - self.native.present() # pragma: no-cover-if-linux-wayland + # deiconify() doesn't work + self.native.present() # If the window is in full-screen mode, exit full-screen mode elif current_state == WindowState.FULLSCREEN: self.native.unfullscreen() self._is_full_screen = False # If the window is in presentation mode, exit presentation mode - # WindowState.PRESENTATION case: + # elif current_state == WindowState.PRESENTATION: else: if isinstance(self.native, Gtk.ApplicationWindow): self.native.set_show_menubar(True) if getattr(self, "native_toolbar", None): self.native_toolbar.set_visible(True) self.native.unfullscreen() - self.interface.screen = self._before_presentation_mode_screen del self._before_presentation_mode_screen self._in_presentation_mode = False + elif target_state == WindowState.MAXIMIZED: + self.native.maximize() + elif target_state == WindowState.MINIMIZED: + self.native.iconify() # pragma: no-cover-if-linux-wayland + elif target_state == WindowState.FULLSCREEN: + self.native.fullscreen() + self._is_full_screen = True + # elif target_state == WindowState.PRESENTATION: + else: + self._before_presentation_mode_screen = self.interface.screen + if isinstance(self.native, Gtk.ApplicationWindow): + self.native.set_show_menubar(False) + if getattr(self, "native_toolbar", None): + self.native_toolbar.set_visible(False) + self.native.fullscreen() + self._in_presentation_mode = True - # Complete any pending window state transition. - if getattr(self, "_pending_window_state_transition", None) is not None: - self.set_window_state(self._pending_window_state_transition) - del self._pending_window_state_transition + self._process_pending_state() ###################################################################### # Window capabilities From cf23ce9c6b08e26a59358f5e4bbb8f4e49553bbf Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 6 Aug 2024 09:19:55 -0400 Subject: [PATCH 129/248] Fixed cocoa implementation --- cocoa/src/toga_cocoa/window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 1a1ea9e5fc..5954241f79 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -366,6 +366,7 @@ def _apply_state(self, target_state): # If the window is in full-screen mode, exit full-screen mode elif current_state == WindowState.FULLSCREEN: self.native.toggleFullScreen(self.native) + self._process_pending_state() # If the window is in presentation mode, exit presentation mode # elif current_state == WindowState.PRESENTATION: else: # pragma: no cover @@ -380,6 +381,7 @@ def _apply_state(self, target_state): # # self.interface.app.exit_presentation_mode() # Pending window state is processed at app._impl.exit_presentation_mode() + # self._process_pending_state() pass elif target_state == WindowState.MAXIMIZED: self.native.setIsZoomed(True) From e6f4dcc258fd77c02227e0002ad86cb10e64f066 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 10 Aug 2024 03:39:04 -0400 Subject: [PATCH 130/248] Reimplemented on cocoa backend --- cocoa/src/toga_cocoa/app.py | 11 ++- cocoa/src/toga_cocoa/window.py | 105 ++++++++++++++-------------- core/src/toga/app.py | 16 ++++- core/src/toga/constants/__init__.py | 2 + core/src/toga/window.py | 39 ++++++----- core/tests/app/test_app.py | 51 ++++++++++---- core/tests/window/test_window.py | 54 ++++---------- gtk/src/toga_gtk/window.py | 48 ++----------- testbed/tests/window/test_window.py | 31 ++++++-- 9 files changed, 181 insertions(+), 176 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 6d362c879b..c4c5e075cb 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -511,17 +511,24 @@ def enter_presentation_mode(self, screen_window_dict): window.content._impl.native.window.interface = window window.content.refresh() + # Process any pending window state. + window._impl._requested_state_applied = True + window._impl._process_pending_state() + def exit_presentation_mode(self): opts = NSMutableDictionary.alloc().init() opts.setObject( NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" ) + for window in self.interface.windows: if window.state == WindowState.PRESENTATION: window.content._impl.native.exitFullScreenModeWithOptions(opts) window.content.refresh() - # Process any pending window state. - window._impl._process_pending_state() + + # Process any pending window state. + window._impl._requested_state_applied = True + window._impl._process_pending_state() class DocumentApp(App): # pragma: no cover diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 5954241f79..9fd453d1be 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -50,12 +50,33 @@ def windowDidResize_(self, notification) -> None: # Set the window to the new size self.interface.content.refresh() + @objc_method + def windowDidMiniaturize_(self, notification) -> None: + self.impl._requested_state_applied = True + self.impl._process_pending_state() + @objc_method def windowDidDeminiaturize_(self, notification) -> None: + self.impl._requested_state_applied = True + self.impl._process_pending_state() + + @objc_method + def windowDidEnterFullScreen_(self, notification) -> None: + self.performSelector_withObject_afterDelay_(SEL("enteredFullScreen:"), None, 0) + + @objc_method + def enteredFullScreen_(self, sender) -> None: + # Doing the following directly in `windowDidEnterFullScreen_` will result in error: + # ````2024-08-09 15:46:39.050 python[2646:37395] not in fullscreen state```` + # and any subsequent window state calls to the OS will not work or will be glitchy. + self.impl._in_full_screen = True + self.impl._requested_state_applied = True self.impl._process_pending_state() @objc_method def windowDidExitFullScreen_(self, notification) -> None: + self.impl._in_full_screen = False + self.impl._requested_state_applied = True self.impl._process_pending_state() ###################################################################### @@ -175,9 +196,9 @@ def __init__(self, interface, title, position, size): # in response to the close. self.native.retain() - # Pending Window state transition variable and flag: + # Pending Window state transition variable and flags: self._pending_state_transition = None - self._processing_pending_state = False + self._requested_state_applied = True self.set_title(title) self.set_size(size) @@ -316,90 +337,70 @@ def get_window_state(self): return WindowState.NORMAL def set_window_state(self, state): - current_state = self.get_window_state() + self._pending_state_transition = state + self._process_pending_state() - if current_state == state: + def _process_pending_state(self): + if not self._requested_state_applied: return - elif self._processing_pending_state: - # If we're processing a transition then store the requested state - # in the class variable. - self._pending_state_transition = state + pending_state = self._pending_state_transition + if pending_state is None or self.get_window_state() == pending_state: return - # Set Window state to NORMAL before changing to other states as some - # states block changing window state without first exiting them or - # can even cause rendering glitches. - elif current_state != WindowState.NORMAL: - self._pending_state_transition = state - self._processing_pending_state = True - self._apply_state(WindowState.NORMAL) + self._pending_state_transition = None - # elif current_state == WindowState.NORMAL: - else: - self._processing_pending_state = True - self._apply_state(state) + if self.get_window_state() != WindowState.NORMAL: + self._apply_state(WindowState.NORMAL) + if not self._requested_state_applied: + self._pending_state_transition = pending_state + return - def _process_pending_state(self): - pending_state = self._pending_state_transition - self._pending_state_transition = None - if (pending_state is not None) and (self.get_window_state() != pending_state): - self._apply_state(pending_state) + self._apply_state(pending_state) + if not self._requested_state_applied: + return if self._pending_state_transition is not None: - # The new requested state must have been added while the pending - # state was being applied. Hence, process the new requested state. self._process_pending_state() - self._processing_pending_state = False - def _apply_state(self, target_state): - if target_state == WindowState.NORMAL: + self._requested_state_applied = False + if target_state == self.get_window_state(): + self._requested_state_applied = True + return + elif target_state == WindowState.NORMAL: current_state = self.get_window_state() - # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: self.native.setIsZoomed(False) + self._requested_state_applied = True self._process_pending_state() - # Deminiaturize the window to restore it to its previous state elif current_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(False) - # If the window is in full-screen mode, exit full-screen mode + # `self._requested_state_applied` is confirmed at `windowDidDeminiaturize_`. elif current_state == WindowState.FULLSCREEN: self.native.toggleFullScreen(self.native) - self._process_pending_state() - # If the window is in presentation mode, exit presentation mode + # `self._requested_state_applied` is confirmed at `windowDidExitFullScreen_`. # elif current_state == WindowState.PRESENTATION: else: # pragma: no cover - # Marking this as no cover, since exit_presentation_mode() is triggered on - # window.state setter, which sets windows in presentation mode to NORMAL. - # Thus, if a window was in PRESENTATION state and switched to NORMAL, - # exit_presentation_mode() would handle it, making this branch unreachable. - # - # On other backends (gtk, winforms), presentation mode is window-based and - # manipulated directly. On cocoa, it's app-based with native support, so - # this branch isn't needed. - # - # self.interface.app.exit_presentation_mode() - # Pending window state is processed at app._impl.exit_presentation_mode() - # self._process_pending_state() + # Presentation mode is natively supported by cocoa and is app-based. + # `exit_presentation_mode()` is called early in `window.state` setter. + # Hence, this branch will never be reached. pass elif target_state == WindowState.MAXIMIZED: self.native.setIsZoomed(True) + self._requested_state_applied = True elif target_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(True) + # `self._requested_state_applied` is confirmed at `windowDidMiniaturize_`. elif target_state == WindowState.FULLSCREEN: self.native.toggleFullScreen(self.native) + # `self._requested_state_applied` is confirmed at `windowDidEnterFullScreen_`. # elif target_state == WindowState.PRESENTATION: else: self.interface.app.enter_presentation_mode( {self.interface.screen: self.interface} ) - - # Skip pending state processing when target state is NORMAL. This is handled by - # window delegation notifications (`windowDidDeminiaturize_` and `windowDidExitFullScreen_`). - # Methods `setIsMiniaturized()` and `toggleFullScreen()` don't wait for full state exit. - if target_state != WindowState.NORMAL: - self._process_pending_state() + # `self._requested_state_applied` is confirmed at `app._impl.enter_presentation_mode()`. ###################################################################### # Window capabilities diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 2be1dd4d0b..4639501bbe 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -797,17 +797,27 @@ def enter_presentation_mode( :param windows: A list of windows, or a dictionary mapping screens to windows, to go into presentation, in order of allocation to screens. If the number of windows exceeds the number of available displays, - those windows will not be visible. + those windows will not be visible. The windows must have a content set on them. :raises ValueError: If the presentation layout supplied is not a list of windows or - or a dict mapping windows to screens. + or a dict mapping windows to screens, or if any window does not have content. """ if windows: screen_window_dict = dict() if isinstance(windows, list): for window, screen in zip(windows, self.screens): - screen_window_dict[screen] = window + if window.content is None: + raise ValueError( + f"Cannot enter presentation mode on {window.title} window without a content." + ) + else: + screen_window_dict[screen] = window elif isinstance(windows, dict): + for _, window in windows.items(): + if window.content is None: + raise ValueError( + f"Cannot enter presentation mode on {window.title} window without a content." + ) screen_window_dict = windows else: raise ValueError( diff --git a/core/src/toga/constants/__init__.py b/core/src/toga/constants/__init__.py index 594e9d8e34..c17e404227 100644 --- a/core/src/toga/constants/__init__.py +++ b/core/src/toga/constants/__init__.py @@ -105,4 +105,6 @@ class WindowState(Enum): A good example is a slideshow app in presentation mode - the only visible content is the slide. + + The window must have a content set on it, before entering presentation mode. """ diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 6857e2b1f6..aebe76a2c6 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -472,23 +472,28 @@ def state(self) -> WindowState: @state.setter def state(self, state: WindowState) -> None: - current_state = self._impl.get_window_state() - if current_state != state: - if not self.resizable and state in { - WindowState.MAXIMIZED, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - }: - warnings.warn( - f"Cannot set window state to {state} of a non-resizable window." - ) - else: - # Changing the window state while the app is in presentation mode - # can cause rendering glitches. Hence, first exit presentation and - # then set the new window state. - self.app.exit_presentation_mode() - - self._impl.set_window_state(state) + if not self.resizable and state in { + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + }: + warnings.warn( + f"Cannot set window state to {state} of a non-resizable window." + ) + elif self.content is None and state == WindowState.PRESENTATION: + warnings.warn( + "Cannot enter presentation mode on a window without a content." + ) + else: + # Changing the window state while the app is in presentation mode + # can cause rendering glitches. Hence, first exit presentation and + # then set the new window state. + self.app.exit_presentation_mode() + + # State checks are done at the backend because backends like Cocoa use + # non-blocking OS calls. Checking state at the core could occur during + # transitions, leading to incorrect assertions and potential glitches. + self._impl.set_window_state(state) ###################################################################### # Window capabilities diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 05466bf3c9..7c9202d971 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -469,8 +469,8 @@ def startup(self): def test_presentation_mode_with_windows_list(event_loop): """The app can enter presentation mode with a windows list.""" app = toga.App(formal_name="Test App", app_id="org.example.test") - window1 = toga.Window() - window2 = toga.Window() + window1 = toga.Window(content=toga.Box()) + window2 = toga.Window(content=toga.Box()) assert not app.in_presentation_mode @@ -507,8 +507,8 @@ def test_presentation_mode_with_windows_list(event_loop): def test_presentation_mode_with_screen_window_dict(event_loop): """The app can enter presentation mode with a screen-window paired dict.""" app = toga.App(formal_name="Test App", app_id="org.example.test") - window1 = toga.Window() - window2 = toga.Window() + window1 = toga.Window(content=toga.Box()) + window2 = toga.Window(content=toga.Box()) assert not app.in_presentation_mode @@ -551,19 +551,20 @@ def test_presentation_mode_with_screen_window_dict(event_loop): def test_presentation_mode_with_excess_windows_list(event_loop): """Entering presentation mode limits windows to available displays.""" app = toga.App(formal_name="Test App", app_id="org.example.test") - window1 = toga.Window() - window2 = toga.Window() + window1 = toga.Window(content=toga.Box()) + window2 = toga.Window(content=toga.Box()) + window3 = toga.Window(content=toga.Box()) assert not app.in_presentation_mode # Entering presentation mode with 3 windows should drop the last window, # as the app has only 2 screens: - app.enter_presentation_mode([app.main_window, window2, window1]) + app.enter_presentation_mode([window1, window2, window3]) assert app.in_presentation_mode assert_action_performed_with( app, "enter presentation mode", - screen_window_dict={app.screens[0]: app.main_window, app.screens[1]: window2}, + screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, ) # Exit presentation mode: @@ -592,7 +593,26 @@ def test_presentation_mode_no_op(event_loop): ValueError, match="Presentation layout should be a list of windows, or a dict mapping windows to screens.", ): - app.enter_presentation_mode(app.main_window) + app.enter_presentation_mode(toga.Window(content=toga.Box())) + assert_action_not_performed(app, "enter presentation mode") + assert not app.in_presentation_mode + + # Entering presentation mode with a window having no content is a no-op: + no_content_window = toga.Window() + # Window passed as a windows list: + with pytest.raises( + ValueError, + match=f"Cannot enter presentation mode on {no_content_window.title} window without a content.", + ): + app.enter_presentation_mode([no_content_window]) + assert_action_not_performed(app, "enter presentation mode") + assert not app.in_presentation_mode + # Window passed as a screen-window dict: + with pytest.raises( + ValueError, + match=f"Cannot enter presentation mode on {no_content_window.title} window without a content.", + ): + app.enter_presentation_mode({app.screens[0]: no_content_window}) assert_action_not_performed(app, "enter presentation mode") assert not app.in_presentation_mode @@ -608,7 +628,8 @@ def test_presentation_mode_no_op(event_loop): def test_presentation_mode_exit_on_window_state_change(event_loop, new_window_state): """Changing window state exits presentation mode and sets the new state.""" app = toga.App(formal_name="Test App", app_id="org.example.test") - extra_window = toga.Window() + app.main_window.content = toga.Box() + extra_window = toga.Window(content=toga.Box()) # Enter presentation mode app.enter_presentation_mode([app.main_window]) @@ -998,8 +1019,9 @@ async def waiter(): def test_deprecated_full_screen(event_loop): """The app can be put into full screen mode using the deprecated API.""" app = toga.App(formal_name="Test App", app_id="org.example.test") - window1 = toga.Window() - window2 = toga.Window() + app.main_window.content = toga.Box() + window1 = toga.Window(content=toga.Box()) + window2 = toga.Window(content=toga.Box()) is_full_screen_warning = ( r"`App.is_full_screen` is deprecated. Use `App.in_presentation_mode` instead." @@ -1100,8 +1122,9 @@ def test_deprecated_full_screen(event_loop): def test_deprecated_set_empty_full_screen_window_list(event_loop): """Setting the full screen window list to [] is an explicit exit.""" app = toga.App(formal_name="Test App", app_id="org.example.test") - window1 = toga.Window() - window2 = toga.Window() + app.main_window.content = toga.Box() + window1 = toga.Window(content=toga.Box()) + window2 = toga.Window(content=toga.Box()) is_full_screen_warning = ( r"`App.is_full_screen` is deprecated. Use `App.in_presentation_mode` instead." diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index 909e825c63..fdb57d598a 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -314,6 +314,7 @@ def test_visibility(window, app): ) def test_window_state(window, state): """A window can have different states.""" + window.content = toga.Box() assert window.state == WindowState.NORMAL window.state = state @@ -333,45 +334,6 @@ def test_window_state(window, state): ) -@pytest.mark.parametrize( - "state", - [ - WindowState.MAXIMIZED, - WindowState.MINIMIZED, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - ], -) -def test_window_state_same_as_previous(window, state): - """Setting the window state same as the current window state is a no-op.""" - - # Here, setting the same state twice and then checking with assert_action_not_performed - # will still report that the window state setting action was performed. This is because - # the action was also performed for the first-time setting of the window state, - # hence the action will still be in the EventLog and we cannot check if the - # action was done by the first call or the second call to the setter. - # - # For example: For the test, we need to set window state to WindowState.MAXIMIZED and - # then again the window state needs to be set to WindowState.MAXIMIZED. - # But doing so will cause the above mentioned problem. - # Hence, this is just to reach coverage. - window.state = state - assert window.state == state - window.state = state - assert window.state == state - # assert_action_not_performed( - # window, "set window state to WindowState.MAXIMIZED" - # ) - - window.state = WindowState.NORMAL - assert window.state == WindowState.NORMAL - assert_action_performed_with( - window, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, - ) - - @pytest.mark.parametrize( "state", [ @@ -385,6 +347,7 @@ def test_window_state_same_as_previous(window, state): def test_non_resizable_window_state(state): """Non-resizable window's states other than minimized or normal are no-ops.""" non_resizable_window = toga.Window(title="Non-Resizable Window", resizable=False) + non_resizable_window.content = toga.Box() with pytest.warns( UserWarning, match=f"Cannot set window state to {state} of a non-resizable window.", @@ -396,8 +359,21 @@ def test_non_resizable_window_state(state): non_resizable_window.close() +def test_no_content_window_presentation_state(window): + """Setting window states on a window without content is a no-op.""" + with pytest.warns( + UserWarning, + match="Cannot enter presentation mode on a window without a content.", + ): + window.state = WindowState.PRESENTATION + assert_action_not_performed( + window, "set window state to WindowState.PRESENTATION" + ) + + def test_close_direct_in_presentation_mode(window, app): """Directly closing a window in presentation mode restores to normal first.""" + window.content = toga.Box() window.state = WindowState.PRESENTATION assert window.state == WindowState.PRESENTATION assert_action_performed_with( diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 7f39cced5e..254ea215c9 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -37,10 +37,6 @@ def __init__(self, interface, title, position, size): self._in_presentation_mode = False self._is_full_screen = False - # Pending Window state transition variable and flag: - self._pending_state_transition = None - self._processing_pending_state = False - self.native.set_default_size(size[0], size[1]) self.set_title(title) @@ -176,58 +172,26 @@ def get_window_state(self): return WindowState.NORMAL def set_window_state(self, state): - current_state = self.get_window_state() - - if current_state == state: - return - - elif self._processing_pending_state: - # If we're processing a transition then store the requested state - # in the class variable. - self._pending_state_transition = state - return - # Set Window state to NORMAL before changing to other states as some # states block changing window state without first exiting them or # can even cause rendering glitches. - elif current_state != WindowState.NORMAL: - self._pending_state_transition = state - self._processing_pending_state = True + if self.get_window_state() != WindowState.NORMAL: self._apply_state(WindowState.NORMAL) - - # elif current_state == WindowState.NORMAL: - else: - self._processing_pending_state = True - self._apply_state(state) - - def _process_pending_state(self): - pending_state = self._pending_state_transition - self._pending_state_transition = None - if (pending_state is not None) and (self.get_window_state() != pending_state): - self._apply_state(pending_state) - - if self._pending_state_transition is not None: - # The new requested state must have been added while the pending - # state was being applied. Hence, process the new requested state. - self._process_pending_state() - - self._processing_pending_state = False + self._apply_state(state) def _apply_state(self, target_state): - if target_state == WindowState.NORMAL: + if target_state == self.get_window_state(): + return + elif target_state == WindowState.NORMAL: current_state = self.get_window_state() - # If the window is maximized, restore it to its normal size if current_state == WindowState.MAXIMIZED: self.native.unmaximize() - # Deminiaturize the window to restore it to its previous state elif current_state == WindowState.MINIMIZED: # deiconify() doesn't work self.native.present() - # If the window is in full-screen mode, exit full-screen mode elif current_state == WindowState.FULLSCREEN: self.native.unfullscreen() self._is_full_screen = False - # If the window is in presentation mode, exit presentation mode # elif current_state == WindowState.PRESENTATION: else: if isinstance(self.native, Gtk.ApplicationWindow): @@ -255,8 +219,6 @@ def _apply_state(self, target_state): self.native.fullscreen() self._in_presentation_mode = True - self._process_pending_state() - ###################################################################### # Window capabilities ###################################################################### diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 12b806f045..3c5a26efe8 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -1,4 +1,5 @@ import gc +import random import re import weakref from importlib import import_module @@ -834,10 +835,27 @@ async def test_window_state_presentation(second_window, second_window_probe): ], ) async def test_window_state_direct_change( - app, initial_state, final_state, second_window, second_window_probe + app, + app_probe, + initial_state, + final_state, + second_window, + second_window_probe, + intermediate_states=tuple( + random.sample( + [ + WindowState.NORMAL, + WindowState.MINIMIZED, + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ], + 5, + ) + ), ): if ( - WindowState.MINIMIZED in {initial_state, final_state} + WindowState.MINIMIZED in {initial_state, final_state, *intermediate_states} and not second_window_probe.supports_minimize ): pytest.xfail( @@ -863,13 +881,14 @@ async def test_window_state_direct_change( assert second_window_probe.get_window_state() == initial_state + # Set to the intermediate states but don't wait for the OS delay. + for state in intermediate_states: + second_window.state = state + # Set to final state second_window.state = final_state # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - f"Secondary window is in {final_state}", full_screen=True - ) - + await app_probe.redraw(f"Secondary window is in {final_state}", delay=1.5) assert second_window_probe.get_window_state() == final_state @pytest.mark.parametrize( From 30e5968ed1844019f6435d43cff5fa73aa1e2a3a Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 11 Aug 2024 19:10:08 -0700 Subject: [PATCH 131/248] Reimplemented on winforms --- cocoa/src/toga_cocoa/window.py | 5 ++- winforms/src/toga_winforms/window.py | 64 ++++++++++++---------------- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 9fd453d1be..34f8d887cc 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -360,7 +360,10 @@ def _process_pending_state(self): if not self._requested_state_applied: return - if self._pending_state_transition is not None: + if self._pending_state_transition is not None: # pragma: no cover + # Marking as no cover since it cannot be tested consistently, + # as the delay in processing next pending state is OS dependent. + # So, this branch is reached only sometimes. self._process_pending_state() def _apply_state(self, target_state): diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 32ae6305ee..e72c486cf3 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -211,41 +211,11 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() + if current_state == state: return - if ( - current_state != WindowState.NORMAL - and state != WindowState.NORMAL - and (getattr(self, "_pending_window_state_transition", None) is None) - ): - # Set Window state to NORMAL before changing to other states as some - # states block changing window state without first exiting them or - # can even cause rendering glitches. - self._pending_window_state_transition = state - self.set_window_state(WindowState.NORMAL) - - elif state == WindowState.MAXIMIZED: - self.native.WindowState = WinForms.FormWindowState.Maximized - - elif state == WindowState.MINIMIZED: - self.native.WindowState = WinForms.FormWindowState.Minimized - - elif state == WindowState.FULLSCREEN: - self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") - self.native.WindowState = WinForms.FormWindowState.Maximized - - elif state == WindowState.PRESENTATION: - self._before_presentation_mode_screen = self.interface.screen - if self.native.MainMenuStrip: - self.native.MainMenuStrip.Visible = False - if getattr(self, "toolbar_native", None): - self.toolbar_native.Visible = False - self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") - self.native.WindowState = WinForms.FormWindowState.Maximized - self._in_presentation_mode = True - - # WindowState.NORMAL case: - else: + + elif current_state != WindowState.NORMAL: if current_state == WindowState.PRESENTATION: if self.native.MainMenuStrip: self.native.MainMenuStrip.Visible = True @@ -261,10 +231,30 @@ def set_window_state(self, state): "Sizable" if self.interface.resizable else "FixedSingle", ) self.native.WindowState = WinForms.FormWindowState.Normal - # Complete any pending window state transition. - if getattr(self, "_pending_window_state_transition", None) is not None: - self.set_window_state(self._pending_window_state_transition) - del self._pending_window_state_transition + + self.set_window_state(state) + + elif current_state == WindowState.NORMAL: + if state == WindowState.MAXIMIZED: + self.native.WindowState = WinForms.FormWindowState.Maximized + + elif state == WindowState.MINIMIZED: + self.native.WindowState = WinForms.FormWindowState.Minimized + + elif state == WindowState.FULLSCREEN: + self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") + self.native.WindowState = WinForms.FormWindowState.Maximized + + # elif state == WindowState.PRESENTATION: + else: + self._before_presentation_mode_screen = self.interface.screen + if self.native.MainMenuStrip: + self.native.MainMenuStrip.Visible = False + if getattr(self, "toolbar_native", None): + self.toolbar_native.Visible = False + self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") + self.native.WindowState = WinForms.FormWindowState.Maximized + self._in_presentation_mode = True ###################################################################### # Window capabilities From c623254bab6ff47d90ebc76ab0b3fb1027396ce7 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 12 Aug 2024 09:10:26 -0700 Subject: [PATCH 132/248] =?UTF-8?q?=C2=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cocoa/src/toga_cocoa/window.py | 7 ++++--- winforms/src/toga_winforms/window.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 34f8d887cc..f8ac43322f 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -75,9 +75,10 @@ def enteredFullScreen_(self, sender) -> None: @objc_method def windowDidExitFullScreen_(self, notification) -> None: - self.impl._in_full_screen = False - self.impl._requested_state_applied = True - self.impl._process_pending_state() + if self.impl: + self.impl._in_full_screen = False + self.impl._requested_state_applied = True + self.impl._process_pending_state() ###################################################################### # Toolbar delegate methods diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index e72c486cf3..9c5d0178a5 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -234,7 +234,8 @@ def set_window_state(self, state): self.set_window_state(state) - elif current_state == WindowState.NORMAL: + # elif current_state == WindowState.NORMAL: + else: if state == WindowState.MAXIMIZED: self.native.WindowState = WinForms.FormWindowState.Maximized From f2bd7503780e077f80699fd3826715d15af8e659 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 12 Aug 2024 09:31:00 -0700 Subject: [PATCH 133/248] =?UTF-8?q?=C2=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cocoa/src/toga_cocoa/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index f8ac43322f..dea88a6cb7 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -75,7 +75,7 @@ def enteredFullScreen_(self, sender) -> None: @objc_method def windowDidExitFullScreen_(self, notification) -> None: - if self.impl: + if self.impl: # pragma: no branch self.impl._in_full_screen = False self.impl._requested_state_applied = True self.impl._process_pending_state() From 11444e3974a96f7d61eb62fe8705fe065f36207c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 14 Aug 2024 06:42:38 -0400 Subject: [PATCH 134/248] Reimplemented on gtk --- cocoa/src/toga_cocoa/app.py | 16 ++++++++--- cocoa/src/toga_cocoa/window.py | 40 +++++++++++++++++++++------ core/src/toga/window.py | 8 ++++-- core/tests/window/test_window.py | 22 ++++++++++++++- gtk/src/toga_gtk/window.py | 47 +++++++++++++++++++++++++++++--- 5 files changed, 112 insertions(+), 21 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index c4c5e075cb..540fb89e66 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -512,8 +512,10 @@ def enter_presentation_mode(self, screen_window_dict): window.content.refresh() # Process any pending window state. - window._impl._requested_state_applied = True - window._impl._process_pending_state() + if window._impl._requested_state == WindowState.PRESENTATION: + window._impl._requested_state_applied = True + window._impl._requested_state = None + window._impl._process_pending_state() def exit_presentation_mode(self): opts = NSMutableDictionary.alloc().init() @@ -527,8 +529,14 @@ def exit_presentation_mode(self): window.content.refresh() # Process any pending window state. - window._impl._requested_state_applied = True - window._impl._process_pending_state() + if ( + window._impl._requested_state == WindowState.NORMAL + and window._impl._previous_state == WindowState.PRESENTATION + ): + window._impl._requested_state_applied = True + window._impl._requested_state = None + window._impl._previous_state = None + window._impl._process_pending_state() class DocumentApp(App): # pragma: no cover diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index dea88a6cb7..6c74f69587 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -52,13 +52,21 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidMiniaturize_(self, notification) -> None: - self.impl._requested_state_applied = True - self.impl._process_pending_state() + if self.impl._requested_state == WindowState.MINIMIZED: + self.impl._requested_state_applied = True + self.impl._requested_state = None + self.impl._process_pending_state() @objc_method def windowDidDeminiaturize_(self, notification) -> None: - self.impl._requested_state_applied = True - self.impl._process_pending_state() + if ( + self.impl._requested_state == WindowState.NORMAL + and self.impl._previous_state == WindowState.MINIMIZED + ): + self.impl._requested_state_applied = True + self.impl._requested_state = None + self.impl._previous_state = None + self.impl._process_pending_state() @objc_method def windowDidEnterFullScreen_(self, notification) -> None: @@ -69,15 +77,22 @@ def enteredFullScreen_(self, sender) -> None: # Doing the following directly in `windowDidEnterFullScreen_` will result in error: # ````2024-08-09 15:46:39.050 python[2646:37395] not in fullscreen state```` # and any subsequent window state calls to the OS will not work or will be glitchy. - self.impl._in_full_screen = True - self.impl._requested_state_applied = True - self.impl._process_pending_state() + if self.impl._requested_state == WindowState.FULLSCREEN: + self.impl._in_full_screen = True + self.impl._requested_state_applied = True + self.impl._requested_state = None + self.impl._process_pending_state() @objc_method def windowDidExitFullScreen_(self, notification) -> None: - if self.impl: # pragma: no branch + if ( + self.impl._requested_state == WindowState.NORMAL + and self.impl._previous_state == WindowState.FULLSCREEN + ): self.impl._in_full_screen = False self.impl._requested_state_applied = True + self.impl._requested_state = None + self.impl._previous_state = None self.impl._process_pending_state() ###################################################################### @@ -199,6 +214,8 @@ def __init__(self, interface, title, position, size): # Pending Window state transition variable and flags: self._pending_state_transition = None + self._previous_state = None + self._requested_state = None self._requested_state_applied = True self.set_title(title) @@ -368,16 +385,20 @@ def _process_pending_state(self): self._process_pending_state() def _apply_state(self, target_state): + self._requested_state = target_state self._requested_state_applied = False if target_state == self.get_window_state(): + self._requested_state = None self._requested_state_applied = True return elif target_state == WindowState.NORMAL: current_state = self.get_window_state() + self._previous_state = current_state if current_state == WindowState.MAXIMIZED: self.native.setIsZoomed(False) self._requested_state_applied = True - self._process_pending_state() + self._requested_state = None + self._previous_state = None elif current_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(False) # `self._requested_state_applied` is confirmed at `windowDidDeminiaturize_`. @@ -393,6 +414,7 @@ def _apply_state(self, target_state): elif target_state == WindowState.MAXIMIZED: self.native.setIsZoomed(True) self._requested_state_applied = True + self._requested_state = None elif target_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(True) # `self._requested_state_applied` is confirmed at `windowDidMiniaturize_`. diff --git a/core/src/toga/window.py b/core/src/toga/window.py index aebe76a2c6..241fc10c66 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -320,9 +320,11 @@ def _close(self): # closing the app. # Restore to normal state if in presentation mode. On some backends (e.g., Cocoa), - # the content itself is in presentation mode, and not the window. Directly closing - # the window without the content exiting presentation mode can cause rendering glitches. - if self.state == WindowState.PRESENTATION: + # the content is in presentation mode, not the window. Closing the window directly + # can cause rendering glitches. Also, restore if in fullscreen, as delegate events + # on Cocoa might trigger even after the impl reference is lost, causing attribute + # access errors for `self.impl`. + if self.state in {WindowState.PRESENTATION, WindowState.FULLSCREEN}: self.state = WindowState.NORMAL if self.content: diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index fdb57d598a..8c997674d4 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -371,7 +371,7 @@ def test_no_content_window_presentation_state(window): ) -def test_close_direct_in_presentation_mode(window, app): +def test_close_direct_in_presentation(window, app): """Directly closing a window in presentation mode restores to normal first.""" window.content = toga.Box() window.state = WindowState.PRESENTATION @@ -391,6 +391,26 @@ def test_close_direct_in_presentation_mode(window, app): ) +def test_close_direct_in_fullscreen(window, app): + """Directly closing a window in fullscreen mode restores to normal first.""" + window.content = toga.Box() + window.state = WindowState.FULLSCREEN + assert window.state == WindowState.FULLSCREEN + assert_action_performed_with( + window, + "set window state to WindowState.FULLSCREEN", + state=WindowState.FULLSCREEN, + ) + + window.close() + assert window.state == WindowState.NORMAL + assert_action_performed_with( + window, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + + def test_close_direct(window, app): """A window can be closed directly.""" on_close_handler = Mock(return_value=True) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 254ea215c9..c25de4d52a 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -37,6 +37,11 @@ def __init__(self, interface, title, position, size): self._in_presentation_mode = False self._is_full_screen = False + # Pending Window state transition variable and flags: + self._pending_state_transition = None + self._requested_state = None + self._requested_state_applied = True + self.native.set_default_size(size[0], size[1]) self.set_title(title) @@ -72,6 +77,14 @@ def gtk_window_state_event(self, widget, event): # Get the window state flags self._window_state_flags = event.new_window_state + # Check if any requested state's application is completed. + if self._requested_state: + current_state = self.get_window_state() + if current_state == self._requested_state: + self._requested_state_applied = True + self._requested_state = None + self._process_pending_state() + def gtk_delete_event(self, widget, data): # Return value of the GTK on_close handler indicates whether the event has been # fully handled. Returning True indicates the event has been handled, so further @@ -172,15 +185,41 @@ def get_window_state(self): return WindowState.NORMAL def set_window_state(self, state): - # Set Window state to NORMAL before changing to other states as some - # states block changing window state without first exiting them or - # can even cause rendering glitches. + self._pending_state_transition = state + self._process_pending_state() + + def _process_pending_state(self): + if not self._requested_state_applied: + return + + pending_state = self._pending_state_transition + if pending_state is None or self.get_window_state() == pending_state: + return + + self._pending_state_transition = None + if self.get_window_state() != WindowState.NORMAL: self._apply_state(WindowState.NORMAL) - self._apply_state(state) + if not self._requested_state_applied: + self._pending_state_transition = pending_state + return + + self._apply_state(pending_state) + if not self._requested_state_applied: + return + + if self._pending_state_transition is not None: # pragma: no cover + # Marking as no cover since it cannot be tested consistently, + # as the delay in processing next pending state is OS dependent. + # So, this branch is reached only sometimes. + self._process_pending_state() def _apply_state(self, target_state): + self._requested_state = target_state + self._requested_state_applied = False if target_state == self.get_window_state(): + self._requested_state = None + self._requested_state_applied = True return elif target_state == WindowState.NORMAL: current_state = self.get_window_state() From 0771349df2fd5b6bbdb82d25728812bd4d369053 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Wed, 14 Aug 2024 06:58:07 -0400 Subject: [PATCH 135/248] Update 1857.feature.rst From c9b7b908cf0759b76da734459c34a4b266d78207 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 14 Aug 2024 05:20:36 -0700 Subject: [PATCH 136/248] Removed unncessary complexity from cocoa implementation --- cocoa/src/toga_cocoa/app.py | 16 +++-------- cocoa/src/toga_cocoa/window.py | 44 +++++++---------------------- gtk/src/toga_gtk/window.py | 9 ++++-- testbed/tests/window/test_window.py | 11 ++++++-- 4 files changed, 28 insertions(+), 52 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 540fb89e66..c4c5e075cb 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -512,10 +512,8 @@ def enter_presentation_mode(self, screen_window_dict): window.content.refresh() # Process any pending window state. - if window._impl._requested_state == WindowState.PRESENTATION: - window._impl._requested_state_applied = True - window._impl._requested_state = None - window._impl._process_pending_state() + window._impl._requested_state_applied = True + window._impl._process_pending_state() def exit_presentation_mode(self): opts = NSMutableDictionary.alloc().init() @@ -529,14 +527,8 @@ def exit_presentation_mode(self): window.content.refresh() # Process any pending window state. - if ( - window._impl._requested_state == WindowState.NORMAL - and window._impl._previous_state == WindowState.PRESENTATION - ): - window._impl._requested_state_applied = True - window._impl._requested_state = None - window._impl._previous_state = None - window._impl._process_pending_state() + window._impl._requested_state_applied = True + window._impl._process_pending_state() class DocumentApp(App): # pragma: no cover diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 6c74f69587..23659c7b3c 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -52,21 +52,13 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidMiniaturize_(self, notification) -> None: - if self.impl._requested_state == WindowState.MINIMIZED: - self.impl._requested_state_applied = True - self.impl._requested_state = None - self.impl._process_pending_state() + self.impl._requested_state_applied = True + self.impl._process_pending_state() @objc_method def windowDidDeminiaturize_(self, notification) -> None: - if ( - self.impl._requested_state == WindowState.NORMAL - and self.impl._previous_state == WindowState.MINIMIZED - ): - self.impl._requested_state_applied = True - self.impl._requested_state = None - self.impl._previous_state = None - self.impl._process_pending_state() + self.impl._requested_state_applied = True + self.impl._process_pending_state() @objc_method def windowDidEnterFullScreen_(self, notification) -> None: @@ -77,23 +69,15 @@ def enteredFullScreen_(self, sender) -> None: # Doing the following directly in `windowDidEnterFullScreen_` will result in error: # ````2024-08-09 15:46:39.050 python[2646:37395] not in fullscreen state```` # and any subsequent window state calls to the OS will not work or will be glitchy. - if self.impl._requested_state == WindowState.FULLSCREEN: - self.impl._in_full_screen = True - self.impl._requested_state_applied = True - self.impl._requested_state = None - self.impl._process_pending_state() + self.impl._in_full_screen = True + self.impl._requested_state_applied = True + self.impl._process_pending_state() @objc_method def windowDidExitFullScreen_(self, notification) -> None: - if ( - self.impl._requested_state == WindowState.NORMAL - and self.impl._previous_state == WindowState.FULLSCREEN - ): - self.impl._in_full_screen = False - self.impl._requested_state_applied = True - self.impl._requested_state = None - self.impl._previous_state = None - self.impl._process_pending_state() + self.impl._in_full_screen = False + self.impl._requested_state_applied = True + self.impl._process_pending_state() ###################################################################### # Toolbar delegate methods @@ -214,8 +198,6 @@ def __init__(self, interface, title, position, size): # Pending Window state transition variable and flags: self._pending_state_transition = None - self._previous_state = None - self._requested_state = None self._requested_state_applied = True self.set_title(title) @@ -385,20 +367,15 @@ def _process_pending_state(self): self._process_pending_state() def _apply_state(self, target_state): - self._requested_state = target_state self._requested_state_applied = False if target_state == self.get_window_state(): - self._requested_state = None self._requested_state_applied = True return elif target_state == WindowState.NORMAL: current_state = self.get_window_state() - self._previous_state = current_state if current_state == WindowState.MAXIMIZED: self.native.setIsZoomed(False) self._requested_state_applied = True - self._requested_state = None - self._previous_state = None elif current_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(False) # `self._requested_state_applied` is confirmed at `windowDidDeminiaturize_`. @@ -414,7 +391,6 @@ def _apply_state(self, target_state): elif target_state == WindowState.MAXIMIZED: self.native.setIsZoomed(True) self._requested_state_applied = True - self._requested_state = None elif target_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(True) # `self._requested_state_applied` is confirmed at `windowDidMiniaturize_`. diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index c25de4d52a..4c503c9873 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -80,7 +80,8 @@ def gtk_window_state_event(self, widget, event): # Check if any requested state's application is completed. if self._requested_state: current_state = self.get_window_state() - if current_state == self._requested_state: + # The omitted else branch is difficult to manufacture in testbed. + if current_state == self._requested_state: # pragma: no branch self._requested_state_applied = True self._requested_state = None self._process_pending_state() @@ -200,7 +201,8 @@ def _process_pending_state(self): if self.get_window_state() != WindowState.NORMAL: self._apply_state(WindowState.NORMAL) - if not self._requested_state_applied: + # The omitted else branch is difficult to manufacture in testbed. + if not self._requested_state_applied: # pragma: no branch self._pending_state_transition = pending_state return @@ -217,7 +219,8 @@ def _process_pending_state(self): def _apply_state(self, target_state): self._requested_state = target_state self._requested_state_applied = False - if target_state == self.get_window_state(): + # This branch is difficult to manufacture in testbed. + if target_state == self.get_window_state(): # pragma: no cover self._requested_state = None self._requested_state_applied = True return diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 3c5a26efe8..6d67f8fa45 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -753,7 +753,9 @@ async def test_window_state_full_screen(second_window, second_window_probe): ) ], ) - async def test_window_state_presentation(second_window, second_window_probe): + async def test_window_state_presentation( + second_window, second_window_probe, app_probe + ): """Window can have presentation window state""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() @@ -793,8 +795,11 @@ async def test_window_state_presentation(second_window, second_window_probe): second_window.state = WindowState.NORMAL # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is not in presentation mode", full_screen=True + # await second_window_probe.wait_for_window( + # "Secondary window is not in presentation mode", full_screen=True + # ) + await app_probe.redraw( + "Secondary window is not in presentation mode", delay=1.5 ) assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.is_resizable From 3441cb37b8da4b9bf504816160873bb671e17304 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 14 Aug 2024 05:50:36 -0700 Subject: [PATCH 137/248] Misc fixes --- cocoa/src/toga_cocoa/window.py | 7 ++++--- core/src/toga/window.py | 3 ++- core/tests/window/test_window.py | 20 -------------------- 3 files changed, 6 insertions(+), 24 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 23659c7b3c..f10d7a61c8 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -75,9 +75,10 @@ def enteredFullScreen_(self, sender) -> None: @objc_method def windowDidExitFullScreen_(self, notification) -> None: - self.impl._in_full_screen = False - self.impl._requested_state_applied = True - self.impl._process_pending_state() + if self.impl: # pragma: no branch + self.impl._in_full_screen = False + self.impl._requested_state_applied = True + self.impl._process_pending_state() ###################################################################### # Toolbar delegate methods diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 241fc10c66..444123a47a 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -324,7 +324,8 @@ def _close(self): # can cause rendering glitches. Also, restore if in fullscreen, as delegate events # on Cocoa might trigger even after the impl reference is lost, causing attribute # access errors for `self.impl`. - if self.state in {WindowState.PRESENTATION, WindowState.FULLSCREEN}: + # if self.state in {WindowState.PRESENTATION, WindowState.FULLSCREEN}: + if self.state == WindowState.PRESENTATION: self.state = WindowState.NORMAL if self.content: diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index 8c997674d4..12f43308f8 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -391,26 +391,6 @@ def test_close_direct_in_presentation(window, app): ) -def test_close_direct_in_fullscreen(window, app): - """Directly closing a window in fullscreen mode restores to normal first.""" - window.content = toga.Box() - window.state = WindowState.FULLSCREEN - assert window.state == WindowState.FULLSCREEN - assert_action_performed_with( - window, - "set window state to WindowState.FULLSCREEN", - state=WindowState.FULLSCREEN, - ) - - window.close() - assert window.state == WindowState.NORMAL - assert_action_performed_with( - window, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, - ) - - def test_close_direct(window, app): """A window can be closed directly.""" on_close_handler = Mock(return_value=True) From 62dcd7df9fc043a86d64cd4aa47c0be48f1e53cc Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Thu, 15 Aug 2024 06:38:57 -0400 Subject: [PATCH 138/248] Update cocoa/src/toga_cocoa/window.py Co-authored-by: Russell Keith-Magee --- cocoa/src/toga_cocoa/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index f10d7a61c8..805e09858f 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -62,7 +62,7 @@ def windowDidDeminiaturize_(self, notification) -> None: @objc_method def windowDidEnterFullScreen_(self, notification) -> None: - self.performSelector_withObject_afterDelay_(SEL("enteredFullScreen:"), None, 0) + self.performSelector(SEL("enteredFullScreen:"), withObject=None, afterDelay=0) @objc_method def enteredFullScreen_(self, sender) -> None: From 4f36f28ec463f025290ba449d11ec0bb92a8071c Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Thu, 15 Aug 2024 06:51:45 -0400 Subject: [PATCH 139/248] =?UTF-8?q?=E3=85=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/toga/window.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 444123a47a..f7e319be78 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -321,10 +321,7 @@ def _close(self): # Restore to normal state if in presentation mode. On some backends (e.g., Cocoa), # the content is in presentation mode, not the window. Closing the window directly - # can cause rendering glitches. Also, restore if in fullscreen, as delegate events - # on Cocoa might trigger even after the impl reference is lost, causing attribute - # access errors for `self.impl`. - # if self.state in {WindowState.PRESENTATION, WindowState.FULLSCREEN}: + # can cause rendering glitches. if self.state == WindowState.PRESENTATION: self.state = WindowState.NORMAL From ea8058a971d8abba5c3fee847fa9cc552a0c9324 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Thu, 15 Aug 2024 06:54:21 -0400 Subject: [PATCH 140/248] Update test_window.py --- testbed/tests/window/test_window.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 6d67f8fa45..78f7e7d642 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -795,11 +795,8 @@ async def test_window_state_presentation( second_window.state = WindowState.NORMAL # Add delay to ensure windows are visible after animation. - # await second_window_probe.wait_for_window( - # "Secondary window is not in presentation mode", full_screen=True - # ) - await app_probe.redraw( - "Secondary window is not in presentation mode", delay=1.5 + await second_window_probe.wait_for_window( + "Secondary window is not in presentation mode", full_screen=True ) assert second_window_probe.get_window_state() == WindowState.NORMAL assert second_window_probe.is_resizable From 11c3ec3c95f236c5a1824ef6260d61a52d9dc4bc Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 16 Aug 2024 07:47:37 -0400 Subject: [PATCH 141/248] Reimplemented on cocoa backend --- cocoa/src/toga_cocoa/app.py | 12 ++-- cocoa/src/toga_cocoa/window.py | 102 +++++++++++++++++---------------- 2 files changed, 62 insertions(+), 52 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index c4c5e075cb..8e99ecfaaa 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -512,8 +512,13 @@ def enter_presentation_mode(self, screen_window_dict): window.content.refresh() # Process any pending window state. - window._impl._requested_state_applied = True - window._impl._process_pending_state() + if ( + window._impl._pending_state_transition + and window._impl._pending_state_transition != WindowState.PRESENTATION + ): + window._impl._apply_state(WindowState.NORMAL) + else: + window._impl._pending_state_transition = None def exit_presentation_mode(self): opts = NSMutableDictionary.alloc().init() @@ -527,8 +532,7 @@ def exit_presentation_mode(self): window.content.refresh() # Process any pending window state. - window._impl._requested_state_applied = True - window._impl._process_pending_state() + window._impl._apply_state(window._impl._pending_state_transition) class DocumentApp(App): # pragma: no cover diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 805e09858f..038eab4f82 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -52,13 +52,17 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidMiniaturize_(self, notification) -> None: - self.impl._requested_state_applied = True - self.impl._process_pending_state() + if ( + self.impl._pending_state_transition + and self.impl._pending_state_transition != WindowState.MINIMIZED + ): + self.impl._apply_state(WindowState.NORMAL) + else: + self.impl._pending_state_transition = None @objc_method def windowDidDeminiaturize_(self, notification) -> None: - self.impl._requested_state_applied = True - self.impl._process_pending_state() + self.impl._apply_state(self.impl._pending_state_transition) @objc_method def windowDidEnterFullScreen_(self, notification) -> None: @@ -69,16 +73,19 @@ def enteredFullScreen_(self, sender) -> None: # Doing the following directly in `windowDidEnterFullScreen_` will result in error: # ````2024-08-09 15:46:39.050 python[2646:37395] not in fullscreen state```` # and any subsequent window state calls to the OS will not work or will be glitchy. - self.impl._in_full_screen = True - self.impl._requested_state_applied = True - self.impl._process_pending_state() + if self.impl: # pragma: no branch + if ( + self.impl._pending_state_transition + and self.impl._pending_state_transition != WindowState.FULLSCREEN + ): + self.impl._apply_state(WindowState.NORMAL) + else: + self.impl._pending_state_transition = None @objc_method def windowDidExitFullScreen_(self, notification) -> None: if self.impl: # pragma: no branch - self.impl._in_full_screen = False - self.impl._requested_state_applied = True - self.impl._process_pending_state() + self.impl._apply_state(self.impl._pending_state_transition) ###################################################################### # Toolbar delegate methods @@ -197,9 +204,8 @@ def __init__(self, interface, title, position, size): # in response to the close. self.native.retain() - # Pending Window state transition variable and flags: + # Pending Window state transition variable: self._pending_state_transition = None - self._requested_state_applied = True self.set_title(title) self.set_size(size) @@ -338,51 +344,47 @@ def get_window_state(self): return WindowState.NORMAL def set_window_state(self, state): - self._pending_state_transition = state - self._process_pending_state() - - def _process_pending_state(self): - if not self._requested_state_applied: - return + # Since the requests to the OS for changing window states are non-blocking, + # if we are in the middle of processing a state, we need to store the + # user-requested state and apply the state when we have completed processing + # a transition. There are 2 types of callbacks: + # * EnteredState: + # Here, we need to check if the current state is the same as the pending + # state. + # -- If yes: Clear the pending state variable and return. + # -- If no: Apply NORMAL state, which will later apply the pending state + # when the state is NORMAL. + # * ExitedState: + # Here, since we are in NORMAL state, we just apply the pending state. + # When we enter the user-requested pending state, then clear the pending + # state variable and return. + + if self._pending_state_transition: + self._pending_state_transition = state + else: + self._pending_state_transition = state + if self.get_window_state() != WindowState.NORMAL: + self._apply_state(WindowState.NORMAL) + else: + self._apply_state(state) - pending_state = self._pending_state_transition - if pending_state is None or self.get_window_state() == pending_state: + def _apply_state(self, target_state): + if target_state is None: return - self._pending_state_transition = None - - if self.get_window_state() != WindowState.NORMAL: - self._apply_state(WindowState.NORMAL) - if not self._requested_state_applied: - self._pending_state_transition = pending_state - return - - self._apply_state(pending_state) - if not self._requested_state_applied: + elif target_state == self.get_window_state(): + self._pending_state_transition = None return - if self._pending_state_transition is not None: # pragma: no cover - # Marking as no cover since it cannot be tested consistently, - # as the delay in processing next pending state is OS dependent. - # So, this branch is reached only sometimes. - self._process_pending_state() - - def _apply_state(self, target_state): - self._requested_state_applied = False - if target_state == self.get_window_state(): - self._requested_state_applied = True - return elif target_state == WindowState.NORMAL: current_state = self.get_window_state() if current_state == WindowState.MAXIMIZED: self.native.setIsZoomed(False) - self._requested_state_applied = True + self._apply_state(self._pending_state_transition) elif current_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(False) - # `self._requested_state_applied` is confirmed at `windowDidDeminiaturize_`. elif current_state == WindowState.FULLSCREEN: self.native.toggleFullScreen(self.native) - # `self._requested_state_applied` is confirmed at `windowDidExitFullScreen_`. # elif current_state == WindowState.PRESENTATION: else: # pragma: no cover # Presentation mode is natively supported by cocoa and is app-based. @@ -391,19 +393,23 @@ def _apply_state(self, target_state): pass elif target_state == WindowState.MAXIMIZED: self.native.setIsZoomed(True) - self._requested_state_applied = True + if ( + self._pending_state_transition + and self._pending_state_transition != WindowState.MAXIMIZED + ): + self._apply_state(WindowState.NORMAL) + else: + self._pending_state_transition = None + elif target_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(True) - # `self._requested_state_applied` is confirmed at `windowDidMiniaturize_`. elif target_state == WindowState.FULLSCREEN: self.native.toggleFullScreen(self.native) - # `self._requested_state_applied` is confirmed at `windowDidEnterFullScreen_`. # elif target_state == WindowState.PRESENTATION: else: self.interface.app.enter_presentation_mode( {self.interface.screen: self.interface} ) - # `self._requested_state_applied` is confirmed at `app._impl.enter_presentation_mode()`. ###################################################################### # Window capabilities From 0bd68c49bf3f1fcd00a487da8aa68c9234cef513 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 16 Aug 2024 08:08:02 -0400 Subject: [PATCH 142/248] Reimplemented on gtk --- gtk/src/toga_gtk/window.py | 60 +++++++++++++------------------------- 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 4c503c9873..1ea5a20b26 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -77,14 +77,15 @@ def gtk_window_state_event(self, widget, event): # Get the window state flags self._window_state_flags = event.new_window_state - # Check if any requested state's application is completed. - if self._requested_state: + if self._pending_state_transition: current_state = self.get_window_state() - # The omitted else branch is difficult to manufacture in testbed. - if current_state == self._requested_state: # pragma: no branch - self._requested_state_applied = True - self._requested_state = None - self._process_pending_state() + if current_state != WindowState.NORMAL: + if self._pending_state_transition != current_state: + self._apply_state(WindowState.NORMAL) + else: + self._pending_state_transition = None + else: + self._apply_state(self._pending_state_transition) def gtk_delete_event(self, widget, data): # Return value of the GTK on_close handler indicates whether the event has been @@ -186,44 +187,23 @@ def get_window_state(self): return WindowState.NORMAL def set_window_state(self, state): - self._pending_state_transition = state - self._process_pending_state() + if self._pending_state_transition: + self._pending_state_transition = state + else: + self._pending_state_transition = state + if self.get_window_state() != WindowState.NORMAL: + self._apply_state(WindowState.NORMAL) + else: + self._apply_state(state) - def _process_pending_state(self): - if not self._requested_state_applied: + def _apply_state(self, target_state): + if target_state is None: return - pending_state = self._pending_state_transition - if pending_state is None or self.get_window_state() == pending_state: + elif target_state == self.get_window_state(): + self._pending_state_transition = None return - self._pending_state_transition = None - - if self.get_window_state() != WindowState.NORMAL: - self._apply_state(WindowState.NORMAL) - # The omitted else branch is difficult to manufacture in testbed. - if not self._requested_state_applied: # pragma: no branch - self._pending_state_transition = pending_state - return - - self._apply_state(pending_state) - if not self._requested_state_applied: - return - - if self._pending_state_transition is not None: # pragma: no cover - # Marking as no cover since it cannot be tested consistently, - # as the delay in processing next pending state is OS dependent. - # So, this branch is reached only sometimes. - self._process_pending_state() - - def _apply_state(self, target_state): - self._requested_state = target_state - self._requested_state_applied = False - # This branch is difficult to manufacture in testbed. - if target_state == self.get_window_state(): # pragma: no cover - self._requested_state = None - self._requested_state_applied = True - return elif target_state == WindowState.NORMAL: current_state = self.get_window_state() if current_state == WindowState.MAXIMIZED: From 3c8b0f15f673f485ac8d735be1c6b306ca057de2 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 17 Aug 2024 09:08:37 -0400 Subject: [PATCH 143/248] Correcting test failure on cocoa --- cocoa/src/toga_cocoa/window.py | 6 ++++++ cocoa/tests_backend/window.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 038eab4f82..b06acaa45e 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -52,6 +52,12 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidMiniaturize_(self, notification) -> None: + self.performSelector( + SEL("enteredMiniaturize:"), withObject=None, afterDelay=0.2 + ) + + @objc_method + def enteredMiniaturize_(self, sender) -> None: if ( self.impl._pending_state_transition and self.impl._pending_state_transition != WindowState.MINIMIZED diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 517f426649..18e5e2f029 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -26,7 +26,7 @@ def __init__(self, app, window): async def wait_for_window(self, message, minimize=False, full_screen=False): await self.redraw( message, - delay=0.75 if full_screen else 0.5 if minimize else 0.1, + delay=0.75 if full_screen else 0.5 if minimize else 0.5, ) def close(self): From e915d728e5328805b081baf43b6a72436ab689e6 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 17 Aug 2024 09:21:35 -0400 Subject: [PATCH 144/248] Correcting test failure on cocoa --- cocoa/src/toga_cocoa/window.py | 4 +--- cocoa/tests_backend/window.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index b06acaa45e..1e72697580 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -52,9 +52,7 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidMiniaturize_(self, notification) -> None: - self.performSelector( - SEL("enteredMiniaturize:"), withObject=None, afterDelay=0.2 - ) + self.performSelector(SEL("enteredMiniaturize:"), withObject=None, afterDelay=0) @objc_method def enteredMiniaturize_(self, sender) -> None: diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 18e5e2f029..517f426649 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -26,7 +26,7 @@ def __init__(self, app, window): async def wait_for_window(self, message, minimize=False, full_screen=False): await self.redraw( message, - delay=0.75 if full_screen else 0.5 if minimize else 0.5, + delay=0.75 if full_screen else 0.5 if minimize else 0.1, ) def close(self): From fb6e6574863f1e7c037cacc8622e4a007417aa47 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 17 Aug 2024 09:29:49 -0400 Subject: [PATCH 145/248] Correcting test failure on cocoa --- cocoa/src/toga_cocoa/window.py | 4 ---- cocoa/tests_backend/window.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 1e72697580..038eab4f82 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -52,10 +52,6 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidMiniaturize_(self, notification) -> None: - self.performSelector(SEL("enteredMiniaturize:"), withObject=None, afterDelay=0) - - @objc_method - def enteredMiniaturize_(self, sender) -> None: if ( self.impl._pending_state_transition and self.impl._pending_state_transition != WindowState.MINIMIZED diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 517f426649..9819d007f1 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -26,7 +26,7 @@ def __init__(self, app, window): async def wait_for_window(self, message, minimize=False, full_screen=False): await self.redraw( message, - delay=0.75 if full_screen else 0.5 if minimize else 0.1, + delay=0.75 if full_screen else 0.5 if minimize else 0.3, ) def close(self): From b810d6531e0e952fdf36260aea66000d25c2642e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 17 Aug 2024 09:51:44 -0400 Subject: [PATCH 146/248] Correcting test failure on cocoa --- cocoa/tests_backend/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 9819d007f1..18e5e2f029 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -26,7 +26,7 @@ def __init__(self, app, window): async def wait_for_window(self, message, minimize=False, full_screen=False): await self.redraw( message, - delay=0.75 if full_screen else 0.5 if minimize else 0.3, + delay=0.75 if full_screen else 0.5 if minimize else 0.5, ) def close(self): From 1e59da1933e54a4672da39bcf857e361838158df Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 17 Aug 2024 10:04:43 -0400 Subject: [PATCH 147/248] Correcting test failure on cocoa --- cocoa/src/toga_cocoa/window.py | 6 ++++++ cocoa/tests_backend/window.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 038eab4f82..16f70de0fd 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -52,6 +52,12 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidMiniaturize_(self, notification) -> None: + self.performSelector( + SEL("enteredMiniaturize:"), withObject=None, afterDelay=0.1 + ) + + @objc_method + def enteredMiniaturize_(self, sender) -> None: if ( self.impl._pending_state_transition and self.impl._pending_state_transition != WindowState.MINIMIZED diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 18e5e2f029..517f426649 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -26,7 +26,7 @@ def __init__(self, app, window): async def wait_for_window(self, message, minimize=False, full_screen=False): await self.redraw( message, - delay=0.75 if full_screen else 0.5 if minimize else 0.5, + delay=0.75 if full_screen else 0.5 if minimize else 0.1, ) def close(self): From 9dba56b104587fee0032a96dbaaf548b324001d4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 17 Aug 2024 10:22:37 -0400 Subject: [PATCH 148/248] Fix delegate error on cocoa --- cocoa/src/toga_cocoa/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 16f70de0fd..c069c1ae46 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -244,6 +244,7 @@ def set_title(self, title): ###################################################################### def close(self): + self.native.delegate = None self.native.close() def set_app(self, app): From 56cc124b0111662fb5d092e6f0748912cfc551fe Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 17 Aug 2024 10:36:36 -0400 Subject: [PATCH 149/248] Fixing delegate error on cocoa --- cocoa/src/toga_cocoa/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index c069c1ae46..7d5e355b1d 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -244,7 +244,7 @@ def set_title(self, title): ###################################################################### def close(self): - self.native.delegate = None + # self.native.delegate = None self.native.close() def set_app(self, app): From 2d290725254cce6ea9c9736058a6a5b28100cb7b Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 17 Aug 2024 10:45:05 -0400 Subject: [PATCH 150/248] Add check guards for invalid impl on cocoa delegate --- cocoa/src/toga_cocoa/window.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 7d5e355b1d..6f7a227ac9 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -46,7 +46,7 @@ def windowShouldClose_(self, notification) -> bool: @objc_method def windowDidResize_(self, notification) -> None: - if getattr(self, "interface", None) is not None and self.interface.content: + if self.interface and self.interface.content: # Set the window to the new size self.interface.content.refresh() @@ -58,17 +58,19 @@ def windowDidMiniaturize_(self, notification) -> None: @objc_method def enteredMiniaturize_(self, sender) -> None: - if ( - self.impl._pending_state_transition - and self.impl._pending_state_transition != WindowState.MINIMIZED - ): - self.impl._apply_state(WindowState.NORMAL) - else: - self.impl._pending_state_transition = None + if self.impl: # pragma: no branch + if ( + self.impl._pending_state_transition + and self.impl._pending_state_transition != WindowState.MINIMIZED + ): + self.impl._apply_state(WindowState.NORMAL) + else: + self.impl._pending_state_transition = None @objc_method def windowDidDeminiaturize_(self, notification) -> None: - self.impl._apply_state(self.impl._pending_state_transition) + if self.impl: # pragma: no branch + self.impl._apply_state(self.impl._pending_state_transition) @objc_method def windowDidEnterFullScreen_(self, notification) -> None: @@ -244,7 +246,6 @@ def set_title(self, title): ###################################################################### def close(self): - # self.native.delegate = None self.native.close() def set_app(self, app): From c2e88e5029b150ce33b35e41fc26cec18bc54cf4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 17 Aug 2024 11:36:38 -0400 Subject: [PATCH 151/248] Removed unused state flags from gtk --- cocoa/src/toga_cocoa/window.py | 1 + gtk/src/toga_gtk/window.py | 4 +--- testbed/tests/window/test_window.py | 5 +++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 6f7a227ac9..772b1a32fd 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -52,6 +52,7 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidMiniaturize_(self, notification) -> None: + # Needs a delay on macOS-arm64 testbed, delaying longer on the testbed doesn't work. self.performSelector( SEL("enteredMiniaturize:"), withObject=None, afterDelay=0.1 ) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 1ea5a20b26..8de453dbd0 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -37,10 +37,8 @@ def __init__(self, interface, title, position, size): self._in_presentation_mode = False self._is_full_screen = False - # Pending Window state transition variable and flags: + # Pending Window state transition variable: self._pending_state_transition = None - self._requested_state = None - self._requested_state_applied = True self.native.set_default_size(size[0], size[1]) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 78f7e7d642..238a60b911 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -805,6 +805,11 @@ async def test_window_state_presentation( @pytest.mark.parametrize( "initial_state, final_state", [ + # Direct switch from NORMAL: + (WindowState.NORMAL, WindowState.MINIMIZED), + (WindowState.NORMAL, WindowState.MAXIMIZED), + (WindowState.NORMAL, WindowState.FULLSCREEN), + (WindowState.NORMAL, WindowState.PRESENTATION), # Direct switch from MINIMIZED: (WindowState.MINIMIZED, WindowState.NORMAL), (WindowState.MINIMIZED, WindowState.MAXIMIZED), From 279dfda7c2abf73789a9200cec11132a676d7199 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 17 Aug 2024 12:21:42 -0400 Subject: [PATCH 152/248] Removed unused branch in cocoa implementation --- cocoa/src/toga_cocoa/window.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 772b1a32fd..f5a44f7a62 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -402,13 +402,9 @@ def _apply_state(self, target_state): pass elif target_state == WindowState.MAXIMIZED: self.native.setIsZoomed(True) - if ( - self._pending_state_transition - and self._pending_state_transition != WindowState.MAXIMIZED - ): - self._apply_state(WindowState.NORMAL) - else: - self._pending_state_transition = None + # No need to check for other pending states, since this is completely + # applied here only. + self._pending_state_transition = None elif target_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(True) From 73bf7aa38a69b31336d5e253fbdf748a28cc7baa Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 18 Aug 2024 00:32:37 -0700 Subject: [PATCH 153/248] Added checks for wayland --- cocoa/tests_backend/window.py | 2 ++ gtk/src/toga_gtk/window.py | 48 +++++++++++++++++++---------- gtk/tests_backend/window.py | 6 ++-- testbed/tests/window/test_window.py | 25 ++++++++++++++- winforms/tests_backend/window.py | 2 ++ 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 517f426649..5d65a337f8 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -13,6 +13,8 @@ class WindowProbe(BaseProbe, DialogsMixin): supports_move_while_hidden = True supports_unminimize = True supports_minimize = True + supports_fullscreen = True + supports_presentation = True supports_placement = True def __init__(self, app, window): diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 8de453dbd0..652c1e35a8 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os from typing import TYPE_CHECKING from toga.command import Separator @@ -33,9 +34,9 @@ def __init__(self, interface, title, position, size): self._window_state_flags = None - # Gdk.WindowState.FULLSCREEN is unreliable, use shadow variables. - self._in_presentation_mode = False - self._is_full_screen = False + # Gdk.WindowState.FULLSCREEN is unreliable, use shadow variables also. + self._in_presentation = False + self._in_fullscreen = False # Pending Window state transition variable: self._pending_state_transition = None @@ -175,9 +176,9 @@ def get_window_state(self): elif window_state_flags & Gdk.WindowState.ICONIFIED: return WindowState.MINIMIZED # pragma: no-cover-if-linux-wayland elif window_state_flags & Gdk.WindowState.FULLSCREEN: - if self._in_presentation_mode: + if self._in_presentation: return WindowState.PRESENTATION - elif self._is_full_screen: + elif self._in_fullscreen: return WindowState.FULLSCREEN else: return WindowState.NORMAL @@ -185,17 +186,30 @@ def get_window_state(self): return WindowState.NORMAL def set_window_state(self, state): - if self._pending_state_transition: - self._pending_state_transition = state - else: - self._pending_state_transition = state - if self.get_window_state() != WindowState.NORMAL: - self._apply_state(WindowState.NORMAL) + if ("WAYLAND_DISPLAY" in os.environ) and ( + state + in {WindowState.MINIMIZED, WindowState.FULLSCREEN, WindowState.PRESENTATION} + ): # pragma: no-cover-if-linux-x + # Not implemented on wayland due to wayland security policies. + self.interface.factory.not_implemented( + f"Window.set_window_state({state}) on Wayland" + ) + else: # pragma: no-cover-if-linux-wayland + if self._pending_state_transition: + self._pending_state_transition = state else: - self._apply_state(state) + self._pending_state_transition = state + if self.get_window_state() != WindowState.NORMAL: + self._apply_state(WindowState.NORMAL) + else: + self._apply_state(state) def _apply_state(self, target_state): - if target_state is None: + if target_state is None: # pragma: no cover + # This is OS delay related and is only sometimes triggered + # when there is a delay in processing the states by the OS. + # Hence, this branch cannot be consistently reached by the + # testbed coverage. return elif target_state == self.get_window_state(): @@ -211,7 +225,7 @@ def _apply_state(self, target_state): self.native.present() elif current_state == WindowState.FULLSCREEN: self.native.unfullscreen() - self._is_full_screen = False + self._in_fullscreen = False # elif current_state == WindowState.PRESENTATION: else: if isinstance(self.native, Gtk.ApplicationWindow): @@ -221,14 +235,14 @@ def _apply_state(self, target_state): self.native.unfullscreen() self.interface.screen = self._before_presentation_mode_screen del self._before_presentation_mode_screen - self._in_presentation_mode = False + self._in_presentation = False elif target_state == WindowState.MAXIMIZED: self.native.maximize() elif target_state == WindowState.MINIMIZED: self.native.iconify() # pragma: no-cover-if-linux-wayland elif target_state == WindowState.FULLSCREEN: self.native.fullscreen() - self._is_full_screen = True + self._in_fullscreen = True # elif target_state == WindowState.PRESENTATION: else: self._before_presentation_mode_screen = self.interface.screen @@ -237,7 +251,7 @@ def _apply_state(self, target_state): if getattr(self, "native_toolbar", None): self.native_toolbar.set_visible(False) self.native.fullscreen() - self._in_presentation_mode = True + self._in_presentation = True ###################################################################### # Window capabilities diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index 5258e5bac6..83a30366be 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -14,6 +14,8 @@ class WindowProbe(BaseProbe, DialogsMixin): supports_unminimize = False # Wayland mostly prohibits interaction with the larger windowing environment supports_minimize = not BaseProbe.IS_WAYLAND + supports_fullscreen = not BaseProbe.IS_WAYLAND + supports_presentation = not BaseProbe.IS_WAYLAND supports_placement = not BaseProbe.IS_WAYLAND def __init__(self, app, window): @@ -48,9 +50,9 @@ def get_window_state(self): elif window_state_flags & Gdk.WindowState.ICONIFIED: current_state = WindowState.MINIMIZED elif window_state_flags & Gdk.WindowState.FULLSCREEN: - if getattr(self.impl, "_in_presentation_mode", False) is True: + if getattr(self.impl, "_in_presentation", False) is True: current_state = WindowState.PRESENTATION - elif getattr(self.impl, "_is_full_screen", False) is True: + elif getattr(self.impl, "_in_fullscreen", False) is True: current_state = WindowState.FULLSCREEN else: current_state = WindowState.NORMAL diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 238a60b911..2a15610474 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -707,6 +707,10 @@ async def test_window_state_maximized(second_window, second_window_probe): ) async def test_window_state_full_screen(second_window, second_window_probe): """Window can have full screen window state""" + if not second_window_probe.supports_fullscreen: + pytest.xfail( + "This backend doesn't reliably support fullscreen window state." + ) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() # Add delay to ensure windows are visible after animation. @@ -757,6 +761,10 @@ async def test_window_state_presentation( second_window, second_window_probe, app_probe ): """Window can have presentation window state""" + if not second_window_probe.supports_presentation: + pytest.xfail( + "This backend doesn't reliably support presentation window state." + ) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() # Add delay to ensure windows are visible after animation. @@ -862,12 +870,27 @@ async def test_window_state_direct_change( ), ): if ( - WindowState.MINIMIZED in {initial_state, final_state, *intermediate_states} + WindowState.MINIMIZED in {initial_state, final_state} and not second_window_probe.supports_minimize ): pytest.xfail( "This backend doesn't reliably support minimized window state." ) + elif ( + WindowState.FULLSCREEN in {initial_state, final_state} + and not second_window_probe.supports_fullscreen + ): + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + elif ( + WindowState.PRESENTATION in {initial_state, final_state} + and not second_window_probe.supports_presentation + ): + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + second_window.toolbar.add(app.cmd1) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index f1e487dc09..38f326e83c 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -23,6 +23,8 @@ class WindowProbe(BaseProbe, DialogsMixin): supports_move_while_hidden = True supports_unminimize = True supports_minimize = True + supports_fullscreen = True + supports_presentation = True supports_placement = True def __init__(self, app, window): From af6a2927e55ee06de641cfe74d9fd1d42aee0f0c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 18 Aug 2024 00:55:43 -0700 Subject: [PATCH 154/248] Added checks for wayland --- testbed/tests/app/test_desktop.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 70b9683362..64a2bb6018 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -182,8 +182,11 @@ async def test_menu_minimize(app, app_probe): window1.close() -async def test_presentation_mode(app, app_probe, main_window): +async def test_presentation_mode(app, app_probe, main_window, main_window_probe): """The app can enter presentation mode.""" + if not main_window_probe.supports_presentation: + pytest.xfail("This backend doesn't reliably support presentation window state.") + try: window_information_list = list() windows_list = list() @@ -270,11 +273,8 @@ async def test_presentation_mode_exit_on_window_state_change( app, app_probe, main_window, main_window_probe, new_window_state ): """Changing window state exits presentation mode and sets the new state.""" - if ( - new_window_state == WindowState.MINIMIZED - and not main_window_probe.supports_minimize - ): - pytest.xfail("This backend doesn't reliably support minimized window state.") + if not main_window_probe.supports_presentation: + pytest.xfail("This backend doesn't reliably support presentation window state.") try: window1 = toga.Window( From 5a0352fbba1ac32921e6bc98218c5501d429454c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 19 Aug 2024 08:49:44 -0400 Subject: [PATCH 155/248] Minor gtk fix --- cocoa/src/toga_cocoa/window.py | 2 ++ gtk/src/toga_gtk/window.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index f5a44f7a62..14d00919c8 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -371,6 +371,8 @@ def set_window_state(self, state): if self._pending_state_transition: self._pending_state_transition = state else: + if self.get_window_state() == state: + return self._pending_state_transition = state if self.get_window_state() != WindowState.NORMAL: self._apply_state(WindowState.NORMAL) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 652c1e35a8..5d911c7e7d 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -198,6 +198,8 @@ def set_window_state(self, state): if self._pending_state_transition: self._pending_state_transition = state else: + if self.get_window_state() == state: + return self._pending_state_transition = state if self.get_window_state() != WindowState.NORMAL: self._apply_state(WindowState.NORMAL) From ef685729b9bbdb2f588b014e337e7778a6665bac Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 19 Aug 2024 09:06:06 -0400 Subject: [PATCH 156/248] Added no cover for wayland --- cocoa/src/toga_cocoa/window.py | 2 -- gtk/src/toga_gtk/app.py | 6 ++++-- gtk/src/toga_gtk/window.py | 30 +++++++++++++++++++++--------- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 14d00919c8..f5a44f7a62 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -371,8 +371,6 @@ def set_window_state(self, state): if self._pending_state_transition: self._pending_state_transition = state else: - if self.get_window_state() == state: - return self._pending_state_transition = state if self.get_window_state() != WindowState.NORMAL: self._apply_state(WindowState.NORMAL) diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 26d6409c3c..13a107db74 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -278,13 +278,15 @@ def set_current_window(self, window): # Presentation mode controls ###################################################################### - def enter_presentation_mode(self, screen_window_dict): + def enter_presentation_mode( + self, screen_window_dict + ): # pragma: no-cover-if-linux-wayland for screen, window in screen_window_dict.items(): window._impl._before_presentation_mode_screen = window.screen window.screen = screen window._impl.set_window_state(WindowState.PRESENTATION) - def exit_presentation_mode(self): + def exit_presentation_mode(self): # pragma: no-cover-if-linux-wayland for window in self.interface.windows: if window.state == WindowState.PRESENTATION: window._impl.set_window_state(WindowState.NORMAL) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 5d911c7e7d..06061701e2 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -80,7 +80,11 @@ def gtk_window_state_event(self, widget, event): current_state = self.get_window_state() if current_state != WindowState.NORMAL: if self._pending_state_transition != current_state: - self._apply_state(WindowState.NORMAL) + # Marking as no cover, since wayland has only MAXIMIZED & NORMAL state + # support, so no other type of state will be in pending_state. + self._apply_state( + WindowState.NORMAL + ) # pragma: no-cover-if-linux-wayland else: self._pending_state_transition = None else: @@ -175,7 +179,9 @@ def get_window_state(self): return WindowState.MAXIMIZED elif window_state_flags & Gdk.WindowState.ICONIFIED: return WindowState.MINIMIZED # pragma: no-cover-if-linux-wayland - elif window_state_flags & Gdk.WindowState.FULLSCREEN: + elif ( + window_state_flags & Gdk.WindowState.FULLSCREEN + ): # pragma: no-cover-if-linux-wayland if self._in_presentation: return WindowState.PRESENTATION elif self._in_fullscreen: @@ -222,14 +228,18 @@ def _apply_state(self, target_state): current_state = self.get_window_state() if current_state == WindowState.MAXIMIZED: self.native.unmaximize() - elif current_state == WindowState.MINIMIZED: + elif ( + current_state == WindowState.MINIMIZED + ): # pragma: no-cover-if-linux-wayland # deiconify() doesn't work self.native.present() - elif current_state == WindowState.FULLSCREEN: + elif ( + current_state == WindowState.FULLSCREEN + ): # pragma: no-cover-if-linux-wayland self.native.unfullscreen() self._in_fullscreen = False # elif current_state == WindowState.PRESENTATION: - else: + else: # pragma: no-cover-if-linux-wayland if isinstance(self.native, Gtk.ApplicationWindow): self.native.set_show_menubar(True) if getattr(self, "native_toolbar", None): @@ -240,13 +250,15 @@ def _apply_state(self, target_state): self._in_presentation = False elif target_state == WindowState.MAXIMIZED: self.native.maximize() - elif target_state == WindowState.MINIMIZED: - self.native.iconify() # pragma: no-cover-if-linux-wayland - elif target_state == WindowState.FULLSCREEN: + elif target_state == WindowState.MINIMIZED: # pragma: no-cover-if-linux-wayland + self.native.iconify() + elif ( + target_state == WindowState.FULLSCREEN + ): # pragma: no-cover-if-linux-wayland self.native.fullscreen() self._in_fullscreen = True # elif target_state == WindowState.PRESENTATION: - else: + else: # pragma: no-cover-if-linux-wayland self._before_presentation_mode_screen = self.interface.screen if isinstance(self.native, Gtk.ApplicationWindow): self.native.set_show_menubar(False) From ca48ba2f537d6dc9ab02fa1ffcf6ceb95184bc82 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 19 Aug 2024 17:31:44 -0700 Subject: [PATCH 157/248] parameterized tests on core --- core/src/toga/constants/__init__.py | 8 ++ core/tests/app/test_app.py | 135 ++++++++++++++-------------- testbed/tests/window/test_window.py | 42 ++++++++- 3 files changed, 113 insertions(+), 72 deletions(-) diff --git a/core/src/toga/constants/__init__.py b/core/src/toga/constants/__init__.py index c17e404227..4242f8433c 100644 --- a/core/src/toga/constants/__init__.py +++ b/core/src/toga/constants/__init__.py @@ -84,6 +84,8 @@ class WindowState(Enum): MINIMIZED = 1 """``MINIMIZED`` state is when the window isn't currently visible, although it will appear in any operating system's list of active windows. + + Supported Platforms: ``Windows``, ``macOS``, ``Linux-Xorg`` """ MAXIMIZED = 2 @@ -92,11 +94,15 @@ class WindowState(Enum): On Mobile Platforms(Like on Android) - The ``MAXIMIZED`` state is the same as the ``NORMAL`` state. + + Supported Platforms: ``Windows``, ``macOS``, ``Linux-Xorg``, ``Linux-Wayland`` """ FULLSCREEN = 3 """``FULLSCREEN`` state is when the window title bar and window chrome remain **hidden**; But app menu and toolbars remain **visible**. + + Supported Platforms: ``Windows``, ``macOS``, ``Linux-Xorg`` """ PRESENTATION = 4 @@ -107,4 +113,6 @@ class WindowState(Enum): is the slide. The window must have a content set on it, before entering presentation mode. + + Supported Platforms: ``Windows``, ``macOS``, ``Linux-Xorg`` """ diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 7c9202d971..477b8ed3ee 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -466,86 +466,85 @@ def startup(self): BadMainWindowApp(formal_name="Test App", app_id="org.example.test") -def test_presentation_mode_with_windows_list(event_loop): +@pytest.mark.parametrize( + "windows", + [ + [], # No windows + [{}], # One window + [{}, {}], # Two windows + ], +) +def test_presentation_mode_with_windows_list(event_loop, windows): """The app can enter presentation mode with a windows list.""" app = toga.App(formal_name="Test App", app_id="org.example.test") - window1 = toga.Window(content=toga.Box()) - window2 = toga.Window(content=toga.Box()) - - assert not app.in_presentation_mode + windows_list = [toga.Window(content=toga.Box()) for window in windows] - # Entering presentation mode with an empty windows list, is a no-op: - app.enter_presentation_mode([]) assert not app.in_presentation_mode - assert_action_not_performed(app, "enter presentation mode") - # Enter presentation mode with 1 window: - app.enter_presentation_mode([window1]) - assert app.in_presentation_mode - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window1}, - ) - - # Enter presentation mode with 2 windows: - app.enter_presentation_mode([window1, window2]) - assert app.in_presentation_mode - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, - ) - # Exit presentation mode: - app.exit_presentation_mode() - assert_action_performed( - app, - "exit presentation mode", - ) + if not windows_list: + # Entering presentation mode with an empty windows list, is a no-op: + app.enter_presentation_mode(windows_list) + assert not app.in_presentation_mode + assert_action_not_performed(app, "enter presentation mode") + else: + # Enter presentation mode with 1 or more windows: + app.enter_presentation_mode(windows_list) + assert app.in_presentation_mode + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={ + app.screens[i]: window for i, window in enumerate(windows_list) + }, + ) + # Exit presentation mode: + app.exit_presentation_mode() + assert not app.in_presentation_mode + assert_action_performed( + app, + "exit presentation mode", + ) -def test_presentation_mode_with_screen_window_dict(event_loop): +@pytest.mark.parametrize( + "windows", + [ + [], # No windows + [{}], # One window + [{}, {}], # Two windows + ], +) +def test_presentation_mode_with_screen_window_dict(event_loop, windows): """The app can enter presentation mode with a screen-window paired dict.""" app = toga.App(formal_name="Test App", app_id="org.example.test") - window1 = toga.Window(content=toga.Box()) - window2 = toga.Window(content=toga.Box()) - - assert not app.in_presentation_mode + screen_window_dict = { + app.screens[i]: toga.Window(content=toga.Box()) + for i, window in enumerate(windows) + } - # Entering presentation mode with an empty dict, is a no-op: - app.enter_presentation_mode({}) assert not app.in_presentation_mode - assert_action_not_performed(app, "enter presentation mode") - - # Enter presentation mode with an 1 element screen-window dict: - app.enter_presentation_mode({app.screens[0]: window1}) - assert app.in_presentation_mode - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window1}, - ) - # Exit presentation mode: - app.exit_presentation_mode() - assert_action_performed( - app, - "exit presentation mode", - ) - # Enter presentation mode with a 2 elements screen-window dict: - app.enter_presentation_mode({app.screens[0]: window1, app.screens[1]: window2}) - assert app.in_presentation_mode - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, - ) - # Exit presentation mode: - app.exit_presentation_mode() - assert_action_performed( - app, - "exit presentation mode", - ) + if not screen_window_dict: + # Entering presentation mode with an empty dict, is a no-op: + app.enter_presentation_mode({}) + assert not app.in_presentation_mode + assert_action_not_performed(app, "enter presentation mode") + else: + # Enter presentation mode with a 1 or more elements screen-window dict: + app.enter_presentation_mode(screen_window_dict) + assert app.in_presentation_mode + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict=screen_window_dict, + ) + # Exit presentation mode: + app.exit_presentation_mode() + assert not app.in_presentation_mode + assert_action_performed( + app, + "exit presentation mode", + ) def test_presentation_mode_with_excess_windows_list(event_loop): diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 2a15610474..97d9ee7384 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -613,7 +613,7 @@ async def test_move_and_resize(second_window, second_window_probe): ], ) async def test_window_state_minimized(second_window, second_window_probe): - """Window can have minimized window state""" + """Window can have minimized window state.""" if not second_window_probe.supports_minimize: pytest.xfail( "This backend doesn't reliably support minimized window state." @@ -660,7 +660,7 @@ async def test_window_state_minimized(second_window, second_window_probe): ], ) async def test_window_state_maximized(second_window, second_window_probe): - """Window can have maximized window state""" + """Window can have maximized window state.""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() # Add delay to ensure windows are visible after animation. @@ -706,7 +706,7 @@ async def test_window_state_maximized(second_window, second_window_probe): ], ) async def test_window_state_full_screen(second_window, second_window_probe): - """Window can have full screen window state""" + """Window can have full screen window state.""" if not second_window_probe.supports_fullscreen: pytest.xfail( "This backend doesn't reliably support fullscreen window state." @@ -760,7 +760,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): async def test_window_state_presentation( second_window, second_window_probe, app_probe ): - """Window can have presentation window state""" + """Window can have presentation window state.""" if not second_window_probe.supports_presentation: pytest.xfail( "This backend doesn't reliably support presentation window state." @@ -810,6 +810,39 @@ async def test_window_state_presentation( assert second_window_probe.is_resizable assert second_window_probe.presentation_content_size == initial_content_size + @pytest.mark.parametrize( + "state", + [ + WindowState.NORMAL, + WindowState.MINIMIZED, + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ], + ) + @pytest.mark.parametrize( + "second_window_class, second_window_kwargs", + [ + ( + toga.Window, + dict(title="Secondary Window", position=(200, 150)), + ) + ], + ) + async def test_window_state_same_as_previous( + second_window, second_window_probe, app_probe, state + ): + """Setting window state same as previous is a no-op.""" + # Set the window state: + second_window.state = state + # Add delay to ensure windows are visible after animation. + await app_probe.redraw(f"Secondary window is in {state}", delay=0.5) + assert second_window_probe.get_window_state() == state + + # Set the window state same as previous: + second_window.state = state + assert second_window_probe.get_window_state() == state + @pytest.mark.parametrize( "initial_state, final_state", [ @@ -869,6 +902,7 @@ async def test_window_state_direct_change( ) ), ): + "Window state can be directly changed to another state." if ( WindowState.MINIMIZED in {initial_state, final_state} and not second_window_probe.supports_minimize From cd8eba239e029957fa0e6076ecd82ba01dfaf7ee Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 19 Aug 2024 18:33:27 -0700 Subject: [PATCH 158/248] Reimplemented on Android --- android/src/toga_android/window.py | 95 +++++++++++++++-------------- gtk/src/toga_gtk/app.py | 10 +++ iOS/src/toga_iOS/window.py | 4 +- testbed/tests/window/test_window.py | 10 --- 4 files changed, 62 insertions(+), 57 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 6db75e4ace..5bf7816466 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -54,6 +54,9 @@ def get_title(self): def set_title(self, title): self.app.native.setTitle(title) + def show_actionbar(self, show): + pass + ###################################################################### # Window lifecycle ###################################################################### @@ -145,9 +148,9 @@ def get_visible(self): ###################################################################### def get_window_state(self): - # window.state is called in _close(), which itself sometimes - # is called during early stages of app startup, during which - # the app attribute may not exist. In such cases, return NORMAL. + # `window.state` is called in `_close()`, which itself is + # sometimes called during certain stages when the app + # attribute may not exist. In such cases, return NORMAL. if getattr(self, "app", None) is None: return WindowState.NORMAL decor_view = self.app.native.getWindow().getDecorView() @@ -165,52 +168,50 @@ def get_window_state(self): def set_window_state(self, state): current_state = self.get_window_state() + decor_view = self.app.native.getWindow().getDecorView() + if current_state == state: return - decor_view = self.app.native.getWindow().getDecorView() - if ( - current_state != WindowState.NORMAL - and state != WindowState.NORMAL - and (getattr(self, "_pending_window_state_transition", None) is None) - ): - # Set Window state to NORMAL before changing to other states as some - # states block changing window state without first exiting them or - # can even cause rendering glitches. - self._pending_window_state_transition = state - self.set_window_state(WindowState.NORMAL) - - elif state in {WindowState.FULLSCREEN, WindowState.PRESENTATION}: - decor_view.setSystemUiVisibility( - decor_view.SYSTEM_UI_FLAG_FULLSCREEN - | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | decor_view.SYSTEM_UI_FLAG_IMMERSIVE - ) - if state == WindowState.PRESENTATION: - # Marking this as no branch, since the testbed can't create a simple - # window, so we can't test the other branch. - if self._actionbar_shown_by_default: # pragma: no branch - self.app.native.getSupportActionBar().hide() - self._in_presentation_mode = True + elif current_state != WindowState.NORMAL: + if current_state == WindowState.FULLSCREEN: + decor_view.setSystemUiVisibility(0) + + # elif current_state == WindowState.PRESENTATION: + else: + decor_view.setSystemUiVisibility(0) + self.show_actionbar(True) + self._in_presentation_mode = False + + self.set_window_state(state) + + # elif current_state == WindowState.NORMAL: else: - # On Android Maximized state is same as the Normal state - if state in {WindowState.NORMAL, WindowState.MAXIMIZED}: - if current_state in { - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - }: - decor_view.setSystemUiVisibility(0) - if current_state == WindowState.PRESENTATION: - # Marking this as no branch, since the testbed can't create a simple - # window, so we can't test the other branch. - if self._actionbar_shown_by_default: # pragma: no branch - self.app.native.getSupportActionBar().show() - self._in_presentation_mode = False - - # Complete any pending window state transition. - if getattr(self, "_pending_window_state_transition", None) is not None: - self.set_window_state(self._pending_window_state_transition) - del self._pending_window_state_transition + if state == WindowState.MAXIMIZED: + # On Android Maximized state is same as the Normal state. + pass + + elif state == WindowState.MINIMIZED: + self.interface.factory.not_implemented( + "Window.set_window_state(WindowState.MINIMIZED)" + ) + + elif state == WindowState.FULLSCREEN: + decor_view.setSystemUiVisibility( + decor_view.SYSTEM_UI_FLAG_FULLSCREEN + | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | decor_view.SYSTEM_UI_FLAG_IMMERSIVE + ) + + # elif state == WindowState.PRESENTATION: + else: + decor_view.setSystemUiVisibility( + decor_view.SYSTEM_UI_FLAG_FULLSCREEN + | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | decor_view.SYSTEM_UI_FLAG_IMMERSIVE + ) + self.show_actionbar(False) + self._in_presentation_mode = True ###################################################################### # Window capabilities @@ -243,3 +244,7 @@ def create_toolbar(self): # Toolbar items are configured as part of onPrepareOptionsMenu; trigger that # handler. self.app.native.invalidateOptionsMenu() + + def show_actionbar(self, show): + actionbar = self.app.native.getSupportActionBar() + actionbar.show() if show else actionbar.hide() diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 13a107db74..cb69802973 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -281,12 +281,22 @@ def set_current_window(self, window): def enter_presentation_mode( self, screen_window_dict ): # pragma: no-cover-if-linux-wayland + if "WAYLAND_DISPLAY" in os.environ: # pragma: no-cover-if-linux-x + # Not implemented on wayland due to wayland security policies. + self.interface.factory.not_implemented( + "App.enter_presentation_mode() on Wayland" + ) for screen, window in screen_window_dict.items(): window._impl._before_presentation_mode_screen = window.screen window.screen = screen window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self): # pragma: no-cover-if-linux-wayland + if "WAYLAND_DISPLAY" in os.environ: # pragma: no-cover-if-linux-x + # Not implemented on wayland due to wayland security policies. + self.interface.factory.not_implemented( + "App.exit_presentation_mode() on Wayland" + ) for window in self.interface.windows: if window.state == WindowState.PRESENTATION: window._impl.set_window_state(WindowState.NORMAL) diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index e9aa01000c..b1303fa2c3 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -143,11 +143,11 @@ def hide(self): ###################################################################### def get_window_state(self): - # Windows are always normal + # Windows are always normal. return WindowState.NORMAL def set_window_state(self, state): - self.interface.factory.not_implemented("Window.set_window_state()") + self.interface.factory.not_implemented(f"Window.set_window_state({state})") ###################################################################### # Window capabilities diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 97d9ee7384..2a18aeae0b 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -144,16 +144,6 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): finally: main_window.content = orig_content - @pytest.mark.skipif( - toga.platform.current_platform == "iOS", reason="Not implemented on iOS" - ) - async def test_window_state_minimized(main_window, main_window_probe): - """Window can have minimized window state""" - assert main_window_probe.get_window_state() == WindowState.NORMAL - main_window.state = WindowState.MINIMIZED - await main_window_probe.wait_for_window("WindowState.MINIMIZED is a no-op") - assert main_window_probe.get_window_state() == WindowState.NORMAL - @pytest.mark.skipif( toga.platform.current_platform == "iOS", reason="Not implemented on iOS" ) From 26eb01c25b8dea788f14429e8bfb394408e21bdc Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 19 Aug 2024 18:41:35 -0700 Subject: [PATCH 159/248] Use the new wayland detection --- gtk/src/toga_gtk/app.py | 4 ++-- gtk/src/toga_gtk/window.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 062f2f0218..ed19550b58 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -280,7 +280,7 @@ def set_current_window(self, window): def enter_presentation_mode( self, screen_window_dict ): # pragma: no-cover-if-linux-wayland - if "WAYLAND_DISPLAY" in os.environ: # pragma: no-cover-if-linux-x + if IS_WAYLAND: # pragma: no-cover-if-linux-x # Not implemented on wayland due to wayland security policies. self.interface.factory.not_implemented( "App.enter_presentation_mode() on Wayland" @@ -291,7 +291,7 @@ def enter_presentation_mode( window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self): # pragma: no-cover-if-linux-wayland - if "WAYLAND_DISPLAY" in os.environ: # pragma: no-cover-if-linux-x + if IS_WAYLAND: # pragma: no-cover-if-linux-x # Not implemented on wayland due to wayland security policies. self.interface.factory.not_implemented( "App.exit_presentation_mode() on Wayland" diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 06061701e2..38c18690aa 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os from typing import TYPE_CHECKING from toga.command import Separator @@ -9,7 +8,7 @@ from toga.window import _initial_position from .container import TogaContainer -from .libs import Gdk, Gtk +from .libs import IS_WAYLAND, Gdk, Gtk from .screens import Screen as ScreenImpl if TYPE_CHECKING: # pragma: no cover @@ -192,7 +191,7 @@ def get_window_state(self): return WindowState.NORMAL def set_window_state(self, state): - if ("WAYLAND_DISPLAY" in os.environ) and ( + if IS_WAYLAND and ( state in {WindowState.MINIMIZED, WindowState.FULLSCREEN, WindowState.PRESENTATION} ): # pragma: no-cover-if-linux-x From 3a6aaf7f1dd5c911fc26ab37f2a52ed4d532ce89 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 19 Aug 2024 19:04:49 -0700 Subject: [PATCH 160/248] Removed legacy code from window example. --- core/src/toga/app.py | 4 +--- core/src/toga/window.py | 9 +++------ examples/window/window/app.py | 21 --------------------- testbed/tests/window/test_window.py | 7 +++++++ 4 files changed, 11 insertions(+), 30 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 4639501bbe..f8f6b0c1cb 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -929,9 +929,7 @@ def set_full_screen(self, *windows: Window) -> None: stacklevel=2, ) self.exit_presentation_mode() - if not windows: - return - else: + if windows: self.enter_presentation_mode([*windows]) ###################################################################### diff --git a/core/src/toga/window.py b/core/src/toga/window.py index f7e319be78..91e058fd01 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -850,12 +850,9 @@ def full_screen(self, is_full_screen: bool) -> None: ("`Window.full_screen` is deprecated. Use `Window.state` instead."), DeprecationWarning, ) - if is_full_screen and (self.state != WindowState.FULLSCREEN): - self._impl.set_window_state(WindowState.FULLSCREEN) - elif not is_full_screen and (self.state == WindowState.FULLSCREEN): - self._impl.set_window_state(WindowState.NORMAL) - else: - return + self._impl.set_window_state( + WindowState.FULLSCREEN if is_full_screen else WindowState.NORMAL + ) ###################################################################### # End Backwards compatibility diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 9d443bb5c7..d543f295b8 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -58,15 +58,6 @@ def do_app_presentation_mode(self, widget, **kwargs): else: self.enter_presentation_mode([self.main_window]) - def do_app_full_screen(self, widget, **kwargs): - if self.is_full_screen: - self.exit_full_screen() - else: - self.set_full_screen(self.main_window) - - def do_window_full_screen(self, widget, **kwargs): - self.main_window.full_screen = not self.main_window.full_screen - def do_title(self, widget, **kwargs): self.main_window.title = f"Time is {datetime.now()}" @@ -250,16 +241,6 @@ def startup(self): on_press=self.do_app_presentation_mode, style=btn_style, ) - btn_do_app_full_screen = toga.Button( - "Make app full screen(legacy)", - on_press=self.do_app_full_screen, - style=btn_style, - ) - btn_do_window_full_screen = toga.Button( - "Make window full screen(legacy)", - on_press=self.do_window_full_screen, - style=btn_style, - ) btn_do_title = toga.Button( "Change title", on_press=self.do_title, style=btn_style ) @@ -335,8 +316,6 @@ def startup(self): btn_do_window_state_full_screen, btn_do_window_state_presentation, btn_do_app_presentation_mode, - btn_do_app_full_screen, - btn_do_window_full_screen, btn_do_title, btn_do_new_windows, btn_do_current_window_cycling, diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 2a18aeae0b..1936fc604e 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -823,6 +823,13 @@ async def test_window_state_same_as_previous( second_window, second_window_probe, app_probe, state ): """Setting window state same as previous is a no-op.""" + second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + second_window.show() + # Add delay to ensure windows are visible after animation. + await second_window_probe.wait_for_window( + "Secondary window is shown", + ) + # Set the window state: second_window.state = state # Add delay to ensure windows are visible after animation. From 78b5ddf60e97b80933197059a9324129e82421e7 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 19 Aug 2024 19:16:34 -0700 Subject: [PATCH 161/248] Added checks for wayland --- testbed/tests/window/test_window.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 1936fc604e..9df12bae28 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -823,6 +823,25 @@ async def test_window_state_same_as_previous( second_window, second_window_probe, app_probe, state ): """Setting window state same as previous is a no-op.""" + if state == WindowState.MINIMIZED and not second_window_probe.supports_minimize: + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + elif ( + state == WindowState.FULLSCREEN + and not second_window_probe.supports_fullscreen + ): + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + elif ( + state == WindowState.PRESENTATION + and not second_window_probe.supports_presentation + ): + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() # Add delay to ensure windows are visible after animation. From e0f5bf0ccc07af12e4cb84932a6e64449327eaee Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 19 Aug 2024 19:17:38 -0700 Subject: [PATCH 162/248] Minor fix on testbed --- testbed/tests/window/test_window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 9df12bae28..799d57d933 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -857,6 +857,8 @@ async def test_window_state_same_as_previous( # Set the window state same as previous: second_window.state = state + # Add delay to ensure windows are visible after animation. + await app_probe.redraw(f"Secondary window is in {state}", delay=0.5) assert second_window_probe.get_window_state() == state @pytest.mark.parametrize( From 54cfaabd9faa140b9c078709531e1cbf04425fa1 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:31:03 -0700 Subject: [PATCH 163/248] Restart CI for intermittent failures From 8b8685e8b9a0a9f6a84d090ead754aa9a51a57a8 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 19 Aug 2024 19:46:14 -0700 Subject: [PATCH 164/248] Removed unused flags from Android implementation --- android/src/toga_android/window.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 5bf7816466..4649d56058 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -32,9 +32,6 @@ def onGlobalLayout(self): class Window(Container): - # ActionBar is always hidden on Window. - _actionbar_shown_by_default = False - def __init__(self, interface, title, position, size): super().__init__() self.interface = interface @@ -54,7 +51,9 @@ def get_title(self): def set_title(self, title): self.app.native.setTitle(title) - def show_actionbar(self, show): + def show_actionbar(self, show): # pragma: no cover + # The testbed can't create a simple window, so we can't test this. + # ActionBar is always hidden on Window. pass ###################################################################### @@ -233,9 +232,6 @@ def get_image_data(self): class MainWindow(Window): - # ActionBar is always hidden on MainWindow. - _actionbar_shown_by_default = True - def configure_titlebar(self): # Display the titlebar on a MainWindow. pass From 91b70daf1314186e35d404e31b527be3534fb771 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 20 Aug 2024 01:26:30 -0700 Subject: [PATCH 165/248] Reorganized test --- testbed/tests/window/test_window.py | 122 ++++++++++++++-------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 799d57d933..eeb6ba38cc 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -800,67 +800,6 @@ async def test_window_state_presentation( assert second_window_probe.is_resizable assert second_window_probe.presentation_content_size == initial_content_size - @pytest.mark.parametrize( - "state", - [ - WindowState.NORMAL, - WindowState.MINIMIZED, - WindowState.MAXIMIZED, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - ], - ) - @pytest.mark.parametrize( - "second_window_class, second_window_kwargs", - [ - ( - toga.Window, - dict(title="Secondary Window", position=(200, 150)), - ) - ], - ) - async def test_window_state_same_as_previous( - second_window, second_window_probe, app_probe, state - ): - """Setting window state same as previous is a no-op.""" - if state == WindowState.MINIMIZED and not second_window_probe.supports_minimize: - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) - elif ( - state == WindowState.FULLSCREEN - and not second_window_probe.supports_fullscreen - ): - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) - elif ( - state == WindowState.PRESENTATION - and not second_window_probe.supports_presentation - ): - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) - - second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - second_window.show() - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is shown", - ) - - # Set the window state: - second_window.state = state - # Add delay to ensure windows are visible after animation. - await app_probe.redraw(f"Secondary window is in {state}", delay=0.5) - assert second_window_probe.get_window_state() == state - - # Set the window state same as previous: - second_window.state = state - # Add delay to ensure windows are visible after animation. - await app_probe.redraw(f"Secondary window is in {state}", delay=0.5) - assert second_window_probe.get_window_state() == state - @pytest.mark.parametrize( "initial_state, final_state", [ @@ -973,6 +912,67 @@ async def test_window_state_direct_change( await app_probe.redraw(f"Secondary window is in {final_state}", delay=1.5) assert second_window_probe.get_window_state() == final_state + @pytest.mark.parametrize( + "state", + [ + WindowState.NORMAL, + WindowState.MINIMIZED, + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ], + ) + @pytest.mark.parametrize( + "second_window_class, second_window_kwargs", + [ + ( + toga.Window, + dict(title="Secondary Window", position=(200, 150)), + ) + ], + ) + async def test_window_state_same_as_previous( + second_window, second_window_probe, app_probe, state + ): + """Setting window state same as previous is a no-op.""" + if state == WindowState.MINIMIZED and not second_window_probe.supports_minimize: + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + elif ( + state == WindowState.FULLSCREEN + and not second_window_probe.supports_fullscreen + ): + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + elif ( + state == WindowState.PRESENTATION + and not second_window_probe.supports_presentation + ): + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + + second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + second_window.show() + # Add delay to ensure windows are visible after animation. + await second_window_probe.wait_for_window( + "Secondary window is shown", + ) + + # Set the window state: + second_window.state = state + # Add delay to ensure windows are visible after animation. + await app_probe.redraw(f"Secondary window is in {state}", delay=0.5) + assert second_window_probe.get_window_state() == state + + # Set the window state same as previous: + second_window.state = state + # Add delay to ensure windows are visible after animation. + await app_probe.redraw(f"Secondary window is in {state}", delay=0.5) + assert second_window_probe.get_window_state() == state + @pytest.mark.parametrize( "second_window_class, second_window_kwargs", [ From aa120e9dee21f4ae5d261204bd74fea7dcdf9d78 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Thu, 29 Aug 2024 11:43:40 +0000 Subject: [PATCH 166/248] Resolving file conflicts with main branch --- cocoa/src/toga_cocoa/app.py | 2 +- dummy/src/toga_dummy/app.py | 1 + gtk/src/toga_gtk/app.py | 2 +- testbed/tests/app/test_desktop.py | 1 + textual/src/toga_textual/app.py | 2 +- winforms/src/toga_winforms/app.py | 3 ++- 6 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 4c68af25c4..5f25bdd4a6 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -410,4 +410,4 @@ def exit_presentation_mode(self): window.content.refresh() # Process any pending window state. - window._impl._apply_state(window._impl._pending_state_transition) \ No newline at end of file + window._impl._apply_state(window._impl._pending_state_transition) diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index 9bb6d34a8c..0494c0374f 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -3,6 +3,7 @@ from pathlib import Path from toga.constants import WindowState + 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 bc2a34f585..6ec17d1813 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -259,4 +259,4 @@ def exit_presentation_mode(self): # pragma: no-cover-if-linux-wayland ) for window in self.interface.windows: if window.state == WindowState.PRESENTATION: - window._impl.set_window_state(WindowState.NORMAL) \ No newline at end of file + window._impl.set_window_state(WindowState.NORMAL) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 704fb9c600..07a533a3e7 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -340,6 +340,7 @@ async def test_presentation_mode_exit_on_window_state_change( await app_probe.redraw("main_window is now the current window", delay=0.5) assert app.current_window == main_window + async def test_show_hide_cursor(app, app_probe): """The app cursor can be hidden and shown""" assert app_probe.is_cursor_visible diff --git a/textual/src/toga_textual/app.py b/textual/src/toga_textual/app.py index 42f42186f3..81338b0563 100644 --- a/textual/src/toga_textual/app.py +++ b/textual/src/toga_textual/app.py @@ -111,4 +111,4 @@ def enter_presentation_mode(self, screen_window_dict): self.interface.factory.not_implemented("App.enter_presentation_mode()") def exit_presentation_mode(self): - self.interface.factory.not_implemented("App.exit_presentation_mode()") \ No newline at end of file + self.interface.factory.not_implemented("App.exit_presentation_mode()") diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 36d242dc5b..0d74583737 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -11,6 +11,7 @@ from System.Windows.Threading import Dispatcher from toga.constants import WindowState + from .libs.proactor import WinformsProactorEventLoop from .libs.wrapper import WeakrefCallable from .screens import Screen as ScreenImpl @@ -270,4 +271,4 @@ def enter_presentation_mode(self, screen_window_dict): def exit_presentation_mode(self): for window in self.interface.windows: if window.state == WindowState.PRESENTATION: - window._impl.set_window_state(WindowState.NORMAL) \ No newline at end of file + window._impl.set_window_state(WindowState.NORMAL) From c928ac12cda1ec255c0cb404b89aabf628e4b8db Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 2 Sep 2024 22:43:05 -0400 Subject: [PATCH 167/248] Fixed cocoa delegate --- android/src/toga_android/app.py | 5 +- android/src/toga_android/window.py | 22 +- cocoa/src/toga_cocoa/app.py | 16 +- cocoa/src/toga_cocoa/window.py | 82 ++-- cocoa/tests_backend/window.py | 18 +- core/src/toga/app.py | 14 +- core/src/toga/constants/__init__.py | 6 +- core/src/toga/window.py | 18 +- core/tests/app/test_app.py | 142 ++----- core/tests/window/test_window.py | 68 ++-- gtk/src/toga_gtk/window.py | 76 ++-- gtk/tests_backend/window.py | 20 +- testbed/tests/app/test_desktop.py | 34 +- testbed/tests/app/test_mobile.py | 15 +- testbed/tests/window/test_window.py | 564 +++++++++++++++------------ winforms/src/toga_winforms/window.py | 21 +- winforms/tests_backend/window.py | 20 +- 17 files changed, 563 insertions(+), 578 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index d2c0874b55..d2099f75d3 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -325,9 +325,8 @@ def enter_presentation_mode(self, screen_window_dict): window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self): - for window in self.interface.windows: - if window.state == WindowState.PRESENTATION: - window._impl.set_window_state(WindowState.NORMAL) + # There is only a single window on android. + self.interface.main_window._impl.set_window_state(WindowState.NORMAL) ###################################################################### # Platform-specific APIs diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 4649d56058..a221542f45 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -176,34 +176,31 @@ def set_window_state(self, state): if current_state == WindowState.FULLSCREEN: decor_view.setSystemUiVisibility(0) - # elif current_state == WindowState.PRESENTATION: - else: + else: # current_state == WindowState.PRESENTATION: decor_view.setSystemUiVisibility(0) self.show_actionbar(True) self._in_presentation_mode = False self.set_window_state(state) - # elif current_state == WindowState.NORMAL: - else: + else: # current_state == WindowState.NORMAL: if state == WindowState.MAXIMIZED: - # On Android Maximized state is same as the Normal state. + # no-op on Android. pass elif state == WindowState.MINIMIZED: - self.interface.factory.not_implemented( - "Window.set_window_state(WindowState.MINIMIZED)" - ) + # no-op on Android. + pass elif state == WindowState.FULLSCREEN: decor_view.setSystemUiVisibility( + # These constants are all marked as deprecated as of API 30. decor_view.SYSTEM_UI_FLAG_FULLSCREEN | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION | decor_view.SYSTEM_UI_FLAG_IMMERSIVE ) - # elif state == WindowState.PRESENTATION: - else: + else: # state == WindowState.PRESENTATION: decor_view.setSystemUiVisibility( decor_view.SYSTEM_UI_FLAG_FULLSCREEN | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION @@ -243,4 +240,7 @@ def create_toolbar(self): def show_actionbar(self, show): actionbar = self.app.native.getSupportActionBar() - actionbar.show() if show else actionbar.hide() + if show: + actionbar.show() + else: + actionbar.hide() diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 2c96ec2394..d77b8fa48c 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -378,12 +378,12 @@ def enter_presentation_mode(self, screen_window_dict): opts.setObject( NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" ) - + for screen, window in screen_window_dict.items(): # The widgets are actually added to window._impl.container.native, instead of # window.content._impl.native. And window._impl.native.contentView is # window._impl.container.native. Hence, we need to go fullscreen on - # window._impl.container.native instead. + # window._impl.container.native instead. window._impl.container.native.enterFullScreenMode( screen._impl.native, withOptions=opts ) @@ -394,15 +394,6 @@ def enter_presentation_mode(self, screen_window_dict): window._impl.container.native.window.interface = window window.content.refresh() - # Process any pending window state. - if ( - window._impl._pending_state_transition - and window._impl._pending_state_transition != WindowState.PRESENTATION - ): - window._impl._apply_state(WindowState.NORMAL) - else: - window._impl._pending_state_transition = None - def exit_presentation_mode(self): opts = NSMutableDictionary.alloc().init() opts.setObject( @@ -413,6 +404,3 @@ def exit_presentation_mode(self): if window.state == WindowState.PRESENTATION: window._impl.container.native.exitFullScreenModeWithOptions(opts) window.content.refresh() - - # Process any pending window state. - window._impl._apply_state(window._impl._pending_state_transition) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 2a4c708306..31d1b0fa35 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -44,15 +44,24 @@ def windowShouldClose_(self, notification) -> bool: self.interface.on_close() return False + @objc_method + def windowWillClose_(self, notification) -> None: + # Setting the toolbar and window delegate to None doesn't disconnect + # the delegates and still triggers the delegate events. + + # Hence, simply remove the toolbar instead. + self.toolbar = None + + # However, window delegate methods, need check guards. + @objc_method def windowDidResize_(self, notification) -> None: - if self.interface and self.interface.content: + if self.interface and self.interface.content: # pragma: no branch # Set the window to the new size self.interface.content.refresh() @objc_method def windowDidMiniaturize_(self, notification) -> None: - # Needs a delay on macOS-arm64 testbed, delaying longer on the testbed doesn't work. self.performSelector( SEL("enteredMiniaturize:"), withObject=None, afterDelay=0.1 ) @@ -196,6 +205,11 @@ def __init__(self, interface, title, position, size): if self.interface.minimizable: mask |= NSWindowStyleMask.Miniaturizable + # The objc callback methods can be called during the initialization + # of the NSWindow. So, any objc method referencing impl or interface + # will get an Attribute error. Hence, first allocate and assign the + # impl & interface to it. Then finally initialize the NSWindow. + # Create the window with a default frame; # we'll update size and position later. self.native = TogaWindow.alloc().initWithContentRect( @@ -220,7 +234,7 @@ def __init__(self, interface, title, position, size): self.set_size(size) self.set_position(position if position is not None else _initial_position()) - self.native.delegate = self.native + self.native.setDelegate(self.native) self.container = Container(on_refresh=self.content_refreshed) self.native.contentView = self.container.native @@ -339,9 +353,7 @@ def get_visible(self): ###################################################################### def get_window_state(self): - if self.interface.content and bool( - self.interface.content._impl.native.isInFullScreenMode() - ): + if bool(self.container.native.isInFullScreenMode()): return WindowState.PRESENTATION elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): return WindowState.FULLSCREEN @@ -371,50 +383,66 @@ def set_window_state(self, state): if self._pending_state_transition: self._pending_state_transition = state else: + # If the app is in presentation mode, but this window isn't, then + # exit app presentation mode before setting the requested state. + if any( + window.state == WindowState.PRESENTATION and window != self.interface + for window in self.interface.app.windows + ): + self.interface.app.exit_presentation_mode() + + current_state = self.get_window_state() + if current_state == state: + return self._pending_state_transition = state - if self.get_window_state() != WindowState.NORMAL: + if current_state != WindowState.NORMAL: self._apply_state(WindowState.NORMAL) else: self._apply_state(state) def _apply_state(self, target_state): + current_state = self.get_window_state() if target_state is None: return - elif target_state == self.get_window_state(): + elif target_state == current_state: self._pending_state_transition = None return - elif target_state == WindowState.NORMAL: - current_state = self.get_window_state() - if current_state == WindowState.MAXIMIZED: - self.native.setIsZoomed(False) - self._apply_state(self._pending_state_transition) - elif current_state == WindowState.MINIMIZED: - self.native.setIsMiniaturized(False) - elif current_state == WindowState.FULLSCREEN: - self.native.toggleFullScreen(self.native) - # elif current_state == WindowState.PRESENTATION: - else: # pragma: no cover - # Presentation mode is natively supported by cocoa and is app-based. - # `exit_presentation_mode()` is called early in `window.state` setter. - # Hence, this branch will never be reached. - pass elif target_state == WindowState.MAXIMIZED: self.native.setIsZoomed(True) - # No need to check for other pending states, since this is completely - # applied here only. + # No need to check for other pending states, + # since this is fully applied at this point. self._pending_state_transition = None elif target_state == WindowState.MINIMIZED: self.native.setIsMiniaturized(True) + elif target_state == WindowState.FULLSCREEN: self.native.toggleFullScreen(self.native) - # elif target_state == WindowState.PRESENTATION: - else: + + elif target_state == WindowState.PRESENTATION: self.interface.app.enter_presentation_mode( {self.interface.screen: self.interface} ) + # No need to check for other pending states, + # since this is fully applied at this point. + self._pending_state_transition = None + + else: # target_state == WindowState.NORMAL: + if current_state == WindowState.MAXIMIZED: + self.native.setIsZoomed(False) + self._apply_state(self._pending_state_transition) + + elif current_state == WindowState.MINIMIZED: + self.native.setIsMiniaturized(False) + + elif current_state == WindowState.FULLSCREEN: + self.native.toggleFullScreen(self.native) + + else: # current_state == WindowState.PRESENTATION: + self.interface.app.exit_presentation_mode() + self._apply_state(self._pending_state_transition) ###################################################################### # Window capabilities diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 5d65a337f8..625a30b782 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -1,6 +1,5 @@ from rubicon.objc import objc_id, send_message -from toga.constants import WindowState from toga_cocoa.libs import NSWindow, NSWindowStyleMask from .dialogs import DialogsMixin @@ -48,21 +47,6 @@ def presentation_content_size(self): self.window.content._impl.native.frame.size.height, ) - def get_window_state(self): - if self.window.content and bool( - self.window.content._impl.native.isInFullScreenMode() - ): - current_state = WindowState.PRESENTATION - elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): - current_state = WindowState.FULLSCREEN - elif bool(self.native.isZoomed): - current_state = WindowState.MAXIMIZED - elif bool(self.native.isMiniaturized): - current_state = WindowState.MINIMIZED - else: - current_state = WindowState.NORMAL - return current_state - @property def is_resizable(self): return bool(self.native.styleMask & NSWindowStyleMask.Resizable) @@ -77,7 +61,7 @@ def is_minimizable(self): @property def is_minimized(self): - return bool(self.get_window_state() == WindowState.MINIMIZED) + return bool(self.native.isMiniaturized) def minimize(self): self.native.performMiniaturize(None) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 58eb0d429d..86c2603c24 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -346,8 +346,6 @@ def __init__( self._main_window = App._UNDEFINED self._windows = WindowSet(self) - self._full_screen_windows: tuple[Window, ...] | None = None - # Create the implementation. This will trigger any startup logic. self.factory.App(interface=self) @@ -833,18 +831,8 @@ def enter_presentation_mode( screen_window_dict = dict() if isinstance(windows, list): for window, screen in zip(windows, self.screens): - if window.content is None: - raise ValueError( - f"Cannot enter presentation mode on {window.title} window without a content." - ) - else: - screen_window_dict[screen] = window + screen_window_dict[screen] = window elif isinstance(windows, dict): - for _, window in windows.items(): - if window.content is None: - raise ValueError( - f"Cannot enter presentation mode on {window.title} window without a content." - ) screen_window_dict = windows else: raise ValueError( diff --git a/core/src/toga/constants/__init__.py b/core/src/toga/constants/__init__.py index 4242f8433c..35ab3b63df 100644 --- a/core/src/toga/constants/__init__.py +++ b/core/src/toga/constants/__init__.py @@ -71,11 +71,7 @@ def __str__(self) -> str: class WindowState(Enum): - """The possible window states of an app. - - Note: Changing window state while the app is in presentation mode will cause - the app to exit presentation mode, and the new window state will be set. - """ + """The possible window states of an app.""" NORMAL = 0 """The ``NORMAL`` state represents the default state of the window or app when it is diff --git a/core/src/toga/window.py b/core/src/toga/window.py index bdf43dfeb6..12d3bb31c7 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -482,22 +482,14 @@ def state(self, state: WindowState) -> None: WindowState.FULLSCREEN, WindowState.PRESENTATION, }: - warnings.warn( + raise RuntimeError( f"Cannot set window state to {state} of a non-resizable window." ) - elif self.content is None and state == WindowState.PRESENTATION: - warnings.warn( - "Cannot enter presentation mode on a window without a content." - ) else: - # Changing the window state while the app is in presentation mode - # can cause rendering glitches. Hence, first exit presentation and - # then set the new window state. - self.app.exit_presentation_mode() - - # State checks are done at the backend because backends like Cocoa use - # non-blocking OS calls. Checking state at the core could occur during - # transitions, leading to incorrect assertions and potential glitches. + # State checks are handled by the backend (e.g., Cocoa) to + # accommodate non-blocking OS calls. Performing these + # checks at the core level could interfere with transitions, + # resulting in incorrect assertions and potential glitches. self._impl.set_window_state(state) ###################################################################### diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index c39c84d536..2ad407fa59 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -477,7 +477,7 @@ def startup(self): def test_presentation_mode_with_windows_list(event_loop, windows): """The app can enter presentation mode with a windows list.""" app = toga.App(formal_name="Test App", app_id="org.example.test") - windows_list = [toga.Window(content=toga.Box()) for window in windows] + windows_list = [toga.Window() for window in windows] assert not app.in_presentation_mode @@ -518,8 +518,7 @@ def test_presentation_mode_with_screen_window_dict(event_loop, windows): """The app can enter presentation mode with a screen-window paired dict.""" app = toga.App(formal_name="Test App", app_id="org.example.test") screen_window_dict = { - app.screens[i]: toga.Window(content=toga.Box()) - for i, window in enumerate(windows) + app.screens[i]: toga.Window() for i, window in enumerate(windows) } assert not app.in_presentation_mode @@ -550,9 +549,9 @@ def test_presentation_mode_with_screen_window_dict(event_loop, windows): def test_presentation_mode_with_excess_windows_list(event_loop): """Entering presentation mode limits windows to available displays.""" app = toga.App(formal_name="Test App", app_id="org.example.test") - window1 = toga.Window(content=toga.Box()) - window2 = toga.Window(content=toga.Box()) - window3 = toga.Window(content=toga.Box()) + window1 = toga.Window() + window2 = toga.Window() + window3 = toga.Window() assert not app.in_presentation_mode @@ -575,124 +574,57 @@ def test_presentation_mode_with_excess_windows_list(event_loop): ) -def test_presentation_mode_no_op(event_loop): - """Entering presentation mode with invalid conditions is a no-op.""" +def test_presentation_mode_with_some_windows(event_loop): + """The app can enter presentation mode for some windows while others stay normal.""" app = toga.App(formal_name="Test App", app_id="org.example.test") + window1 = toga.Window() + window2 = toga.Window() assert not app.in_presentation_mode - # Entering presentation mode without any window is a no-op. - with pytest.raises(TypeError): - app.enter_presentation_mode() - assert_action_not_performed(app, "enter presentation mode") - assert not app.in_presentation_mode - - # Entering presentation mode without proper type of parameter is a no-op. - with pytest.raises( - ValueError, - match="Presentation layout should be a list of windows, or a dict mapping windows to screens.", - ): - app.enter_presentation_mode(toga.Window(content=toga.Box())) - assert_action_not_performed(app, "enter presentation mode") - assert not app.in_presentation_mode - - # Entering presentation mode with a window having no content is a no-op: - no_content_window = toga.Window() - # Window passed as a windows list: - with pytest.raises( - ValueError, - match=f"Cannot enter presentation mode on {no_content_window.title} window without a content.", - ): - app.enter_presentation_mode([no_content_window]) - assert_action_not_performed(app, "enter presentation mode") - assert not app.in_presentation_mode - # Window passed as a screen-window dict: - with pytest.raises( - ValueError, - match=f"Cannot enter presentation mode on {no_content_window.title} window without a content.", - ): - app.enter_presentation_mode({app.screens[0]: no_content_window}) - assert_action_not_performed(app, "enter presentation mode") - assert not app.in_presentation_mode - - -@pytest.mark.parametrize( - "new_window_state", - [ - WindowState.MINIMIZED, - WindowState.MAXIMIZED, - WindowState.FULLSCREEN, - ], -) -def test_presentation_mode_exit_on_window_state_change(event_loop, new_window_state): - """Changing window state exits presentation mode and sets the new state.""" - app = toga.App(formal_name="Test App", app_id="org.example.test") - app.main_window.content = toga.Box() - extra_window = toga.Window(content=toga.Box()) - - # Enter presentation mode - app.enter_presentation_mode([app.main_window]) + # Entering presentation mode with one window should not put the other + # window into presentation mode. + app.enter_presentation_mode([window1]) + assert app.in_presentation_mode assert_action_performed_with( app, "enter presentation mode", - screen_window_dict={app.screens[0]: app.main_window}, - ) - - assert app.in_presentation_mode - assert app.main_window.state == WindowState.PRESENTATION - assert extra_window.state != WindowState.PRESENTATION - - # Changing window state of main_window should make the app exit presentation mode. - app.main_window.state = new_window_state - assert_action_performed_with( - app.main_window, - f"set window state to {new_window_state}", - state=new_window_state, + screen_window_dict={app.screens[0]: window1}, ) + assert window1.state == WindowState.PRESENTATION + assert window2.state != WindowState.PRESENTATION + # Exit presentation mode: + app.exit_presentation_mode() assert not app.in_presentation_mode assert_action_performed( app, "exit presentation mode", ) - assert app.main_window.state != WindowState.PRESENTATION - assert app.main_window.state == new_window_state + assert window1.state != WindowState.PRESENTATION + assert window2.state != WindowState.PRESENTATION - # Reset window states - app.main_window.state = WindowState.NORMAL - extra_window.state = WindowState.NORMAL - # Enter presentation mode again - app.enter_presentation_mode([app.main_window]) - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: app.main_window}, - ) - - assert app.in_presentation_mode - assert app.main_window.state == WindowState.PRESENTATION - assert extra_window.state != WindowState.PRESENTATION - - # Changing window state of extra window should make the app exit presentation mode. - extra_window.state = new_window_state - assert_action_performed_with( - extra_window, - f"set window state to {new_window_state}", - state=new_window_state, - ) +def test_presentation_mode_no_op(event_loop): + """Entering presentation mode with invalid conditions is a no-op.""" + app = toga.App(formal_name="Test App", app_id="org.example.test") assert not app.in_presentation_mode - assert_action_performed( - app, - "exit presentation mode", - ) - assert app.main_window.state != WindowState.PRESENTATION - assert extra_window.state == new_window_state - # Reset window states - app.main_window.state = WindowState.NORMAL - extra_window.state = WindowState.NORMAL + # Entering presentation mode without any window is a no-op. + with pytest.raises(TypeError): + app.enter_presentation_mode() + assert_action_not_performed(app, "enter presentation mode") + assert not app.in_presentation_mode + + # Entering presentation mode without proper type of parameter is a no-op. + with pytest.raises( + ValueError, + match="Presentation layout should be a list of windows, or a dict mapping windows to screens.", + ): + app.enter_presentation_mode(toga.Window()) + assert_action_not_performed(app, "enter presentation mode") + assert not app.in_presentation_mode def test_show_hide_cursor(app): diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index 92372ae52d..a542b287cc 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -304,33 +304,53 @@ def test_visibility(window, app): @pytest.mark.parametrize( - "state", + "initial_state, final_state", [ - WindowState.MAXIMIZED, - WindowState.MINIMIZED, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, + # Direct switch from NORMAL: + (WindowState.NORMAL, WindowState.MINIMIZED), + (WindowState.NORMAL, WindowState.MAXIMIZED), + (WindowState.NORMAL, WindowState.FULLSCREEN), + (WindowState.NORMAL, WindowState.PRESENTATION), + # Direct switch from MINIMIZED: + (WindowState.MINIMIZED, WindowState.NORMAL), + (WindowState.MINIMIZED, WindowState.MAXIMIZED), + (WindowState.MINIMIZED, WindowState.FULLSCREEN), + (WindowState.MINIMIZED, WindowState.PRESENTATION), + # Direct switch from MAXIMIZED: + (WindowState.MAXIMIZED, WindowState.NORMAL), + (WindowState.MAXIMIZED, WindowState.MINIMIZED), + (WindowState.MAXIMIZED, WindowState.FULLSCREEN), + (WindowState.MAXIMIZED, WindowState.PRESENTATION), + # Direct switch from FULLSCREEN: + (WindowState.FULLSCREEN, WindowState.NORMAL), + (WindowState.FULLSCREEN, WindowState.MINIMIZED), + (WindowState.FULLSCREEN, WindowState.MAXIMIZED), + (WindowState.FULLSCREEN, WindowState.PRESENTATION), + # Direct switch from PRESENTATION: + (WindowState.PRESENTATION, WindowState.NORMAL), + (WindowState.PRESENTATION, WindowState.MINIMIZED), + (WindowState.PRESENTATION, WindowState.MAXIMIZED), + (WindowState.PRESENTATION, WindowState.FULLSCREEN), ], ) -def test_window_state(window, state): +def test_window_state(window, initial_state, final_state): """A window can have different states.""" - window.content = toga.Box() assert window.state == WindowState.NORMAL - window.state = state - assert window.state == state + window.state = initial_state + assert window.state == initial_state assert_action_performed_with( window, - f"set window state to {state}", - state=state, + f"set window state to {initial_state}", + state=initial_state, ) - window.state = WindowState.NORMAL - assert window.state == WindowState.NORMAL + window.state = final_state + assert window.state == final_state assert_action_performed_with( window, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, + f"set window state to {final_state}", + state=final_state, ) @@ -347,9 +367,8 @@ def test_window_state(window, state): def test_non_resizable_window_state(state): """Non-resizable window's states other than minimized or normal are no-ops.""" non_resizable_window = toga.Window(title="Non-Resizable Window", resizable=False) - non_resizable_window.content = toga.Box() - with pytest.warns( - UserWarning, + with pytest.raises( + RuntimeError, match=f"Cannot set window state to {state} of a non-resizable window.", ): non_resizable_window.state = state @@ -359,21 +378,8 @@ def test_non_resizable_window_state(state): non_resizable_window.close() -def test_no_content_window_presentation_state(window): - """Setting window states on a window without content is a no-op.""" - with pytest.warns( - UserWarning, - match="Cannot enter presentation mode on a window without a content.", - ): - window.state = WindowState.PRESENTATION - assert_action_not_performed( - window, "set window state to WindowState.PRESENTATION" - ) - - def test_close_direct_in_presentation(window, app): """Directly closing a window in presentation mode restores to normal first.""" - window.content = toga.Box() window.state = WindowState.PRESENTATION assert window.state == WindowState.PRESENTATION assert_action_performed_with( diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index e1b085aaf6..773c36b616 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -33,7 +33,9 @@ def __init__(self, interface, title, position, size): self._window_state_flags = None - # Gdk.WindowState.FULLSCREEN is unreliable, use shadow variables also. + # Gdk.WindowState.FULLSCREEN is notified before it fully transitions to fullscreen, + # Use shadow variables to differentiate between presentation, fullscreen & in-between + # transition state. self._in_presentation = False self._in_fullscreen = False @@ -174,6 +176,8 @@ def hide(self): def get_window_state(self): window_state_flags = self._window_state_flags + if not window_state_flags: + return if window_state_flags & Gdk.WindowState.MAXIMIZED: return WindowState.MAXIMIZED elif window_state_flags & Gdk.WindowState.ICONIFIED: @@ -186,7 +190,7 @@ def get_window_state(self): elif self._in_fullscreen: return WindowState.FULLSCREEN else: - return WindowState.NORMAL + return None else: return WindowState.NORMAL @@ -203,15 +207,27 @@ def set_window_state(self, state): if self._pending_state_transition: self._pending_state_transition = state else: - if self.get_window_state() == state: + # If the app is in presentation mode, but this window isn't, then + # exit app presentation mode before setting the requested state. + if any( + window.state == WindowState.PRESENTATION + and window != self.interface + for window in self.interface.app.windows + ): + self.interface.app.exit_presentation_mode() + + current_state = self.get_window_state() + if current_state == state: return self._pending_state_transition = state - if self.get_window_state() != WindowState.NORMAL: + if current_state != WindowState.NORMAL: self._apply_state(WindowState.NORMAL) else: self._apply_state(state) def _apply_state(self, target_state): + current_state = self.get_window_state() + if target_state is None: # pragma: no cover # This is OS delay related and is only sometimes triggered # when there is a delay in processing the states by the OS. @@ -219,26 +235,50 @@ def _apply_state(self, target_state): # testbed coverage. return - elif target_state == self.get_window_state(): + elif target_state == current_state: self._pending_state_transition = None return - elif target_state == WindowState.NORMAL: - current_state = self.get_window_state() + elif target_state == WindowState.MAXIMIZED: + self.native.maximize() + + elif target_state == WindowState.MINIMIZED: # pragma: no-cover-if-linux-wayland + self.native.iconify() + + elif ( + target_state == WindowState.FULLSCREEN + ): # pragma: no-cover-if-linux-wayland + self.native.fullscreen() + self._in_fullscreen = True + + elif ( + target_state == WindowState.PRESENTATION + ): # pragma: no-cover-if-linux-wayland + self._before_presentation_mode_screen = self.interface.screen + if isinstance(self.native, Gtk.ApplicationWindow): + self.native.set_show_menubar(False) + if getattr(self, "native_toolbar", None): + self.native_toolbar.set_visible(False) + self.native.fullscreen() + self._in_presentation = True + + else: # target_state == WindowState.NORMAL: if current_state == WindowState.MAXIMIZED: self.native.unmaximize() + elif ( current_state == WindowState.MINIMIZED ): # pragma: no-cover-if-linux-wayland # deiconify() doesn't work self.native.present() + elif ( current_state == WindowState.FULLSCREEN ): # pragma: no-cover-if-linux-wayland self.native.unfullscreen() self._in_fullscreen = False - # elif current_state == WindowState.PRESENTATION: - else: # pragma: no-cover-if-linux-wayland + + else: # current_state == WindowState.PRESENTATION: # pragma: no-cover-if-linux-wayland if isinstance(self.native, Gtk.ApplicationWindow): self.native.set_show_menubar(True) if getattr(self, "native_toolbar", None): @@ -247,24 +287,6 @@ def _apply_state(self, target_state): self.interface.screen = self._before_presentation_mode_screen del self._before_presentation_mode_screen self._in_presentation = False - elif target_state == WindowState.MAXIMIZED: - self.native.maximize() - elif target_state == WindowState.MINIMIZED: # pragma: no-cover-if-linux-wayland - self.native.iconify() - elif ( - target_state == WindowState.FULLSCREEN - ): # pragma: no-cover-if-linux-wayland - self.native.fullscreen() - self._in_fullscreen = True - # elif target_state == WindowState.PRESENTATION: - else: # pragma: no-cover-if-linux-wayland - self._before_presentation_mode_screen = self.interface.screen - if isinstance(self.native, Gtk.ApplicationWindow): - self.native.set_show_menubar(False) - if getattr(self, "native_toolbar", None): - self.native_toolbar.set_visible(False) - self.native.fullscreen() - self._in_presentation = True ###################################################################### # Window capabilities diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index 45e316a83e..ecc71bf985 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -1,4 +1,3 @@ -from toga.constants import WindowState from toga_gtk.libs import IS_WAYLAND, Gdk, Gtk from .dialogs import DialogsMixin @@ -43,23 +42,6 @@ def content_size(self): def presentation_content_size(self): return self.content_size - def get_window_state(self): - window_state_flags = self.impl._window_state_flags - if window_state_flags & Gdk.WindowState.MAXIMIZED: - current_state = WindowState.MAXIMIZED - elif window_state_flags & Gdk.WindowState.ICONIFIED: - current_state = WindowState.MINIMIZED - elif window_state_flags & Gdk.WindowState.FULLSCREEN: - if getattr(self.impl, "_in_presentation", False) is True: - current_state = WindowState.PRESENTATION - elif getattr(self.impl, "_in_fullscreen", False) is True: - current_state = WindowState.FULLSCREEN - else: - current_state = WindowState.NORMAL - else: - current_state = WindowState.NORMAL - return current_state - @property def is_resizable(self): return self.native.get_resizable() @@ -70,7 +52,7 @@ def is_closable(self): @property def is_minimized(self): - return bool(self.get_window_state() == WindowState.MINIMIZED) + return self.impl._window_state_flags & Gdk.WindowState.ICONIFIED def minimize(self): self.native.iconify() diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index ff935a0db1..a653a4d0d1 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -204,8 +204,8 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ].presentation_content_size window_information["widget_probe"] = get_probe(window_widget) window_information["initial_widget_size"] = ( - window_information["widget_probe"].width, - window_information["widget_probe"].height, + window_information["widget_probe"].width, + window_information["widget_probe"].height, ) window_information_list.append(window_information) windows_list.append(window) @@ -223,8 +223,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) # All the windows should be in presentation mode. for window_information in window_information_list: assert ( - window_information["window_probe"].get_window_state() - == WindowState.PRESENTATION + window_information["window"].state == WindowState.PRESENTATION ), f"{window_information['window'].title}:" assert ( window_information["window_probe"].presentation_content_size[0] > 1000 @@ -233,8 +232,10 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) window_information["window_probe"].presentation_content_size[1] > 700 ), f"{window_information['window'].title}:" assert ( - window_information["widget_probe"].width > window_information["initial_widget_size"][0] - and window_information["widget_probe"].height > window_information["initial_widget_size"][1] + window_information["widget_probe"].width + > window_information["initial_widget_size"][0] + and window_information["widget_probe"].height + > window_information["initial_widget_size"][1] ), f"{window_information['window'].title}:" # Exit presentation mode @@ -244,16 +245,17 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) assert not app.in_presentation_mode for window_information in window_information_list: assert ( - window_information["window_probe"].get_window_state() - == WindowState.NORMAL + window_information["window"].state == WindowState.NORMAL ), f"{window_information['window'].title}:" assert ( window_information["window_probe"].presentation_content_size == window_information["initial_content_size"] ), f"{window_information['window'].title}:" assert ( - window_information["widget_probe"].width == window_information["initial_widget_size"][0] - and window_information["widget_probe"].height == window_information["initial_widget_size"][1] + window_information["widget_probe"].width + == window_information["initial_widget_size"][0] + and window_information["widget_probe"].height + == window_information["initial_widget_size"][1] ), f"{window_information['window'].title}:" finally: @@ -291,8 +293,6 @@ async def test_presentation_mode_exit_on_window_state_change( ) window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - window1_probe = window_probe(app, window1) - window2_probe = window_probe(app, window2) window1.show() window2.show() # Add delay to ensure windows are visible after animation. @@ -304,7 +304,7 @@ async def test_presentation_mode_exit_on_window_state_change( await app_probe.redraw("App is in presentation mode", delay=0.5) assert app.in_presentation_mode - assert window1_probe.get_window_state() == WindowState.PRESENTATION + assert window1.state == WindowState.PRESENTATION # Changing window state of main window should make the app exit presentation mode. window1.state = new_window_state @@ -316,20 +316,20 @@ async def test_presentation_mode_exit_on_window_state_change( ) assert not app.in_presentation_mode - assert window1_probe.get_window_state() == new_window_state + assert window1.state == new_window_state # Reset window states window1.state = WindowState.NORMAL window2.state = WindowState.NORMAL # Add delay to ensure windows are visible after animation. - await app_probe.redraw("All test windows are in WindowState.NORMAL", delay=0.5) + await app_probe.redraw("All test windows are in WindowState.NORMAL", delay=1) # Enter presentation mode again app.enter_presentation_mode([window1]) # Add delay to ensure windows are visible after animation. await app_probe.redraw("App is in presentation mode", delay=0.5) assert app.in_presentation_mode - assert window1_probe.get_window_state() == WindowState.PRESENTATION + assert window1.state == WindowState.PRESENTATION # Changing window state of extra window should make the app exit presentation mode. window2.state = new_window_state @@ -341,7 +341,7 @@ async def test_presentation_mode_exit_on_window_state_change( ) assert not app.in_presentation_mode - assert window2_probe.get_window_state() == new_window_state + assert window2.state == new_window_state # Reset window states window1.state = WindowState.NORMAL diff --git a/testbed/tests/app/test_mobile.py b/testbed/tests/app/test_mobile.py index e6418be021..61f518528a 100644 --- a/testbed/tests/app/test_mobile.py +++ b/testbed/tests/app/test_mobile.py @@ -18,10 +18,13 @@ async def test_show_hide_cursor(app): app.hide_cursor() -async def test_presentation_mode(app, app_probe, main_window, main_window_probe): +@pytest.mark.skipif( + toga.platform.current_platform == "iOS", reason="Not implemented on iOS" +) +async def test_presentation_mode(app, main_window, main_window_probe): """The app can enter into presentation mode""" assert not app.in_presentation_mode - assert main_window_probe.get_window_state() != WindowState.PRESENTATION + assert main_window.state != WindowState.PRESENTATION # Enter presentation mode with main window via the app app.enter_presentation_mode([main_window]) @@ -30,7 +33,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ) assert app.in_presentation_mode - assert main_window_probe.get_window_state() == WindowState.PRESENTATION + assert main_window.state == WindowState.PRESENTATION # Exit presentation mode app.exit_presentation_mode() @@ -40,7 +43,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ) assert not app.in_presentation_mode - assert main_window_probe.get_window_state() == WindowState.NORMAL + assert main_window.state == WindowState.NORMAL # Enter presentation mode with a screen-window dict via the app app.enter_presentation_mode({app.screens[0]: main_window}) @@ -49,7 +52,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ) assert app.in_presentation_mode - assert main_window_probe.get_window_state() == WindowState.PRESENTATION + assert main_window.state == WindowState.PRESENTATION # Exit presentation mode app.exit_presentation_mode() @@ -59,7 +62,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ) assert not app.in_presentation_mode - assert main_window_probe.get_window_state() == WindowState.NORMAL + assert main_window.state == WindowState.NORMAL async def test_current_window(app, main_window, main_window_probe): diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index d139e2de68..763e490655 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -12,6 +12,8 @@ from toga.constants import WindowState from toga.style.pack import COLUMN, Pack +from ..widgets.probe import get_probe + def window_probe(app, window): module = import_module("tests_backend.window") @@ -143,67 +145,24 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): @pytest.mark.skipif( toga.platform.current_platform == "iOS", reason="Not implemented on iOS" ) - async def test_window_state_maximized(main_window, main_window_probe): - """Window can have maximized window state""" - assert main_window_probe.get_window_state() == WindowState.NORMAL - main_window.state = WindowState.MAXIMIZED - await main_window_probe.wait_for_window("WindowState.MAXIMIZED is a no-op") - assert main_window_probe.get_window_state() == WindowState.NORMAL - - @pytest.mark.skipif( - toga.platform.current_platform == "iOS", reason="Not implemented on iOS" - ) - async def test_window_state_full_screen(main_window, main_window_probe): - """Window can have full screen window state""" - assert main_window_probe.get_window_state() == WindowState.NORMAL - # Make main window full screen - main_window.state = WindowState.FULLSCREEN - await main_window_probe.wait_for_window("Main window is full screen") - assert main_window_probe.get_window_state() == WindowState.FULLSCREEN - - main_window.state = WindowState.FULLSCREEN - await main_window_probe.wait_for_window("Main window is still full screen") - assert main_window_probe.get_window_state() == WindowState.FULLSCREEN - # Exit full screen - main_window.state = WindowState.NORMAL - await main_window_probe.wait_for_window("Main window is not full screen") - assert main_window_probe.get_window_state() == WindowState.NORMAL - - @pytest.mark.skipif( - toga.platform.current_platform == "iOS", reason="Not implemented on iOS" - ) - async def test_window_state_presentation(main_window, main_window_probe): - assert main_window_probe.get_window_state() == WindowState.NORMAL - # Enter presentation mode with main window - main_window.state = WindowState.PRESENTATION - await main_window_probe.wait_for_window("Main window is in presentation mode") - assert main_window_probe.get_window_state() == WindowState.PRESENTATION - - main_window.state = WindowState.PRESENTATION - await main_window_probe.wait_for_window( - "Main window is still in presentation mode" - ) - assert main_window_probe.get_window_state() == WindowState.PRESENTATION - # Exit presentation mode - main_window.state = WindowState.NORMAL - await main_window_probe.wait_for_window( - "Main window is not in presentation mode" - ) - assert main_window_probe.get_window_state() == WindowState.NORMAL - @pytest.mark.parametrize( "initial_state, final_state", [ + # Direct switch from NORMAL: + (WindowState.NORMAL, WindowState.FULLSCREEN), + (WindowState.NORMAL, WindowState.PRESENTATION), # Direct switch from FULLSCREEN: + (WindowState.FULLSCREEN, WindowState.NORMAL), (WindowState.FULLSCREEN, WindowState.PRESENTATION), # Direct switch from PRESENTATION: + (WindowState.PRESENTATION, WindowState.NORMAL), (WindowState.PRESENTATION, WindowState.FULLSCREEN), ], ) async def test_window_state_direct_change( app, initial_state, final_state, main_window, main_window_probe ): - assert main_window_probe.get_window_state() == WindowState.NORMAL + """Window state can be directly changed to another state.""" try: # Set to initial state main_window.state = initial_state @@ -211,13 +170,13 @@ async def test_window_state_direct_change( f"Main window is in {initial_state}" ) - assert main_window_probe.get_window_state() == initial_state + assert main_window.state == initial_state # Set to final state main_window.state = final_state await main_window_probe.wait_for_window(f"Main window is in {final_state}") - assert main_window_probe.get_window_state() == final_state + assert main_window.state == final_state finally: # Set to NORMAL state main_window.state = WindowState.NORMAL @@ -225,7 +184,130 @@ async def test_window_state_direct_change( "Main window is in WindowState.NORMAL" ) - assert main_window_probe.get_window_state() == WindowState.NORMAL + assert main_window.state == WindowState.NORMAL + + @pytest.mark.skipif( + toga.platform.current_platform == "iOS", reason="Not implemented on iOS" + ) + @pytest.mark.parametrize( + "state", + [ + WindowState.NORMAL, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ], + ) + async def test_window_state_same_as_current( + app, main_window, main_window_probe, state + ): + """Setting the window state the same as current is a no-op.""" + try: + # Set the window state: + main_window.state = state + await main_window_probe.wait_for_window(f"Secondary window is in {state}") + assert main_window.state == state + + # Set the window state the same as current: + main_window.state = state + assert main_window.state == state + + finally: + # Restore to NORMAL state. + main_window.state = WindowState.NORMAL + await main_window_probe.wait_for_window("Main window is not full screen") + assert main_window.state == WindowState.NORMAL + + @pytest.mark.skipif( + toga.platform.current_platform == "iOS", reason="Not implemented on iOS" + ) + async def test_window_state_fullscreen(main_window, main_window_probe): + """The window can enter into fullscreen state.""" + try: + widget = toga.Box(style=Pack(flex=1)) + widget_probe = get_probe(widget) + main_window.content = toga.Box(children=[widget]) + await main_window_probe.wait_for_window( + "Test widget has been added to the window" + ) + widget_initial_size = (widget_probe.width, widget_probe.height) + + # Make main window full screen + main_window.state = WindowState.FULLSCREEN + await main_window_probe.wait_for_window("Main window is full screen") + assert main_window.state == WindowState.FULLSCREEN + # At least one of the dimensions should have increased. + assert ( + widget_probe.width > widget_initial_size[0] + or widget_probe.height > widget_initial_size[1] + ) + finally: + # Exit full screen + main_window.state = WindowState.NORMAL + await main_window_probe.wait_for_window("Main window is not full screen") + assert main_window.state == WindowState.NORMAL + # Both dimensions should be the same. + assert ( + widget_probe.width == widget_initial_size[0] + and widget_probe.height == widget_initial_size[1] + ) + main_window.content.clear() + + @pytest.mark.skipif( + toga.platform.current_platform == "iOS", reason="Not implemented on iOS" + ) + async def test_window_state_presentation(main_window, main_window_probe): + """The window can enter into presentation state.""" + try: + widget = toga.Box(style=Pack(flex=1)) + widget_probe = get_probe(widget) + main_window.content = toga.Box(children=[widget]) + await main_window_probe.wait_for_window( + "Test widget has been added to the window" + ) + widget_initial_size = (widget_probe.width, widget_probe.height) + + # Enter presentation mode with main window + main_window.state = WindowState.PRESENTATION + await main_window_probe.wait_for_window( + "Main window is in presentation mode" + ) + assert main_window.state == WindowState.PRESENTATION + # At least one of the dimensions should have increased. + assert ( + widget_probe.width > widget_initial_size[0] + or widget_probe.height > widget_initial_size[1] + ) + finally: + # Exit presentation mode + main_window.state = WindowState.NORMAL + await main_window_probe.wait_for_window( + "Main window is not in presentation mode" + ) + assert main_window.state == WindowState.NORMAL + # Both dimensions should be the same. + assert ( + widget_probe.width == widget_initial_size[0] + and widget_probe.height == widget_initial_size[1] + ) + main_window.content.clear() + + @pytest.mark.skipif( + toga.platform.current_platform == "iOS", reason="Not implemented on iOS" + ) + @pytest.mark.parametrize( + "state", + [ + WindowState.MINIMIZED, + WindowState.MAXIMIZED, + ], + ) + async def test_window_state_no_op_states(main_window, main_window_probe, state): + """MINIMIZED and MAXIMIZED states are no-op on mobile platforms.""" + assert main_window.state == WindowState.NORMAL + # Assign the no-op state. + main_window.state = state + # The state should still be NORMAL: + assert main_window.state == WindowState.NORMAL async def test_screen(main_window, main_window_probe): """The window can be relocated to another screen, using both absolute and relative screen positions.""" @@ -586,6 +668,179 @@ async def test_move_and_resize(second_window, second_window_probe): assert second_window.size == (250 + extra_width, 210 + extra_height) assert second_window_probe.content_size == (250, 210) + @pytest.mark.parametrize( + "initial_state, final_state", + [ + # Direct switch from NORMAL: + (WindowState.NORMAL, WindowState.MINIMIZED), + (WindowState.NORMAL, WindowState.MAXIMIZED), + (WindowState.NORMAL, WindowState.FULLSCREEN), + (WindowState.NORMAL, WindowState.PRESENTATION), + # Direct switch from MINIMIZED: + (WindowState.MINIMIZED, WindowState.NORMAL), + (WindowState.MINIMIZED, WindowState.MAXIMIZED), + (WindowState.MINIMIZED, WindowState.FULLSCREEN), + (WindowState.MINIMIZED, WindowState.PRESENTATION), + # Direct switch from MAXIMIZED: + (WindowState.MAXIMIZED, WindowState.NORMAL), + (WindowState.MAXIMIZED, WindowState.MINIMIZED), + (WindowState.MAXIMIZED, WindowState.FULLSCREEN), + (WindowState.MAXIMIZED, WindowState.PRESENTATION), + # Direct switch from FULLSCREEN: + (WindowState.FULLSCREEN, WindowState.NORMAL), + (WindowState.FULLSCREEN, WindowState.MINIMIZED), + (WindowState.FULLSCREEN, WindowState.MAXIMIZED), + (WindowState.FULLSCREEN, WindowState.PRESENTATION), + # Direct switch from PRESENTATION: + (WindowState.PRESENTATION, WindowState.NORMAL), + (WindowState.PRESENTATION, WindowState.MINIMIZED), + (WindowState.PRESENTATION, WindowState.MAXIMIZED), + (WindowState.PRESENTATION, WindowState.FULLSCREEN), + ], + ) + @pytest.mark.parametrize( + "second_window_class, second_window_kwargs", + [ + ( + toga.MainWindow, + dict(title="Secondary Window", position=(200, 150)), + ) + ], + ) + async def test_window_state_direct_change( + app, + app_probe, + initial_state, + final_state, + second_window, + second_window_probe, + intermediate_states=tuple( + random.sample( + [ + WindowState.NORMAL, + WindowState.MINIMIZED, + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ], + 5, + ) + ), + ): + """Window state can be directly changed to another state.""" + if ( + WindowState.MINIMIZED in {initial_state, final_state} + and not second_window_probe.supports_minimize + ): + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + elif ( + WindowState.FULLSCREEN in {initial_state, final_state} + and not second_window_probe.supports_fullscreen + ): + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + elif ( + WindowState.PRESENTATION in {initial_state, final_state} + and not second_window_probe.supports_presentation + ): + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + + second_window.toolbar.add(app.cmd1) + second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + second_window.show() + + # Add delay to ensure windows are visible after animation. + await second_window_probe.wait_for_window( + "Secondary window is visible", full_screen=True + ) + + assert second_window.state == WindowState.NORMAL + + # Set to initial state + second_window.state = initial_state + # Add delay to ensure windows are visible after animation. + await second_window_probe.wait_for_window( + f"Secondary window is in {initial_state}", full_screen=True + ) + + assert second_window.state == initial_state + + # Set to the intermediate states but don't wait for the OS delay. + for state in intermediate_states: + second_window.state = state + + # Set to final state + second_window.state = final_state + # Add delay to ensure windows are visible after animation. + await app_probe.redraw(f"Secondary window is in {final_state}", delay=1.5) + assert second_window.state == final_state + + @pytest.mark.parametrize( + "state", + [ + WindowState.NORMAL, + WindowState.MINIMIZED, + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ], + ) + @pytest.mark.parametrize( + "second_window_class, second_window_kwargs", + [ + ( + toga.Window, + dict(title="Secondary Window", position=(200, 150)), + ) + ], + ) + async def test_window_state_same_as_current( + app_probe, second_window, second_window_probe, state + ): + """Setting window state the same as current is a no-op.""" + if state == WindowState.MINIMIZED and not second_window_probe.supports_minimize: + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + elif ( + state == WindowState.FULLSCREEN + and not second_window_probe.supports_fullscreen + ): + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + elif ( + state == WindowState.PRESENTATION + and not second_window_probe.supports_presentation + ): + pytest.xfail( + "This backend doesn't reliably support minimized window state." + ) + + second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + second_window.show() + # Add delay to ensure windows are visible after animation. + await second_window_probe.wait_for_window( + "Secondary window is shown", full_screen=True + ) + + # Set the window state: + second_window.state = state + # Add delay to ensure windows are visible after animation. + await second_window_probe.wait_for_window( + f"Secondary window is in {state}", full_screen=True + ) + assert second_window.state == state + + # Set the window state the same as current: + second_window.state = state + assert second_window.state == state + @pytest.mark.parametrize( "second_window_class, second_window_kwargs", [ @@ -609,7 +864,7 @@ async def test_window_state_minimized(second_window, second_window_probe): "Secondary window is shown", ) - assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window.state == WindowState.NORMAL if second_window_probe.supports_minimizable: assert second_window_probe.is_minimizable @@ -618,18 +873,18 @@ async def test_window_state_minimized(second_window, second_window_probe): await second_window_probe.wait_for_window( "Secondary window is minimized", minimize=True ) - assert second_window_probe.get_window_state() == WindowState.MINIMIZED + assert second_window.state == WindowState.MINIMIZED second_window.state = WindowState.MINIMIZED await second_window_probe.wait_for_window("Secondary window is still minimized") - assert second_window_probe.get_window_state() == WindowState.MINIMIZED + assert second_window.state == WindowState.MINIMIZED second_window.state = WindowState.NORMAL # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( "Secondary window is not minimized", minimize=True ) - assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window.state == WindowState.NORMAL if second_window_probe.supports_minimizable: assert second_window_probe.is_minimizable @@ -651,7 +906,7 @@ async def test_window_state_maximized(second_window, second_window_probe): "Secondary window is shown", ) - assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window.state == WindowState.NORMAL assert second_window_probe.is_resizable initial_content_size = second_window_probe.content_size @@ -660,13 +915,13 @@ async def test_window_state_maximized(second_window, second_window_probe): await second_window_probe.wait_for_window( "Secondary window is maximized", ) - assert second_window_probe.get_window_state() == WindowState.MAXIMIZED + assert second_window.state == WindowState.MAXIMIZED assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] second_window.state = WindowState.MAXIMIZED await second_window_probe.wait_for_window("Secondary window is still maximized") - assert second_window_probe.get_window_state() == WindowState.MAXIMIZED + assert second_window.state == WindowState.MAXIMIZED assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] @@ -675,7 +930,7 @@ async def test_window_state_maximized(second_window, second_window_probe): await second_window_probe.wait_for_window( "Secondary window is not maximized", ) - assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window.state == WindowState.NORMAL assert second_window_probe.is_resizable assert second_window_probe.content_size == initial_content_size @@ -701,7 +956,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): "Secondary window is shown", ) - assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window.state == WindowState.NORMAL assert second_window_probe.is_resizable initial_content_size = second_window_probe.content_size @@ -710,7 +965,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): await second_window_probe.wait_for_window( "Secondary window is full screen", full_screen=True ) - assert second_window_probe.get_window_state() == WindowState.FULLSCREEN + assert second_window.state == WindowState.FULLSCREEN assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] @@ -718,7 +973,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): await second_window_probe.wait_for_window( "Secondary window is still full screen", full_screen=True ) - assert second_window_probe.get_window_state() == WindowState.FULLSCREEN + assert second_window.state == WindowState.FULLSCREEN assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] @@ -727,7 +982,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): await second_window_probe.wait_for_window( "Secondary window is not full screen", full_screen=True ) - assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window.state == WindowState.NORMAL assert second_window_probe.is_resizable assert second_window_probe.content_size == initial_content_size @@ -755,7 +1010,7 @@ async def test_window_state_presentation( "Secondary window is shown", ) - assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window.state == WindowState.NORMAL assert second_window_probe.is_resizable initial_content_size = second_window_probe.presentation_content_size @@ -764,7 +1019,7 @@ async def test_window_state_presentation( await second_window_probe.wait_for_window( "Secondary window is in presentation mode", full_screen=True ) - assert second_window_probe.get_window_state() == WindowState.PRESENTATION + assert second_window.state == WindowState.PRESENTATION assert ( second_window_probe.presentation_content_size[0] > initial_content_size[0] ) @@ -776,7 +1031,7 @@ async def test_window_state_presentation( await second_window_probe.wait_for_window( "Secondary window is still in presentation mode", full_screen=True ) - assert second_window_probe.get_window_state() == WindowState.PRESENTATION + assert second_window.state == WindowState.PRESENTATION assert ( second_window_probe.presentation_content_size[0] > initial_content_size[0] ) @@ -789,132 +1044,10 @@ async def test_window_state_presentation( await second_window_probe.wait_for_window( "Secondary window is not in presentation mode", full_screen=True ) - assert second_window_probe.get_window_state() == WindowState.NORMAL + assert second_window.state == WindowState.NORMAL assert second_window_probe.is_resizable assert second_window_probe.presentation_content_size == initial_content_size - @pytest.mark.parametrize( - "initial_state, final_state", - [ - # Direct switch from NORMAL: - (WindowState.NORMAL, WindowState.MINIMIZED), - (WindowState.NORMAL, WindowState.MAXIMIZED), - (WindowState.NORMAL, WindowState.FULLSCREEN), - (WindowState.NORMAL, WindowState.PRESENTATION), - # Direct switch from MINIMIZED: - (WindowState.MINIMIZED, WindowState.NORMAL), - (WindowState.MINIMIZED, WindowState.MAXIMIZED), - (WindowState.MINIMIZED, WindowState.FULLSCREEN), - (WindowState.MINIMIZED, WindowState.PRESENTATION), - # Direct switch from MAXIMIZED: - (WindowState.MAXIMIZED, WindowState.NORMAL), - (WindowState.MAXIMIZED, WindowState.MINIMIZED), - (WindowState.MAXIMIZED, WindowState.FULLSCREEN), - (WindowState.MAXIMIZED, WindowState.PRESENTATION), - # Direct switch from FULLSCREEN: - (WindowState.FULLSCREEN, WindowState.NORMAL), - (WindowState.FULLSCREEN, WindowState.MINIMIZED), - (WindowState.FULLSCREEN, WindowState.MAXIMIZED), - (WindowState.FULLSCREEN, WindowState.PRESENTATION), - # Direct switch from PRESENTATION: - (WindowState.PRESENTATION, WindowState.NORMAL), - (WindowState.PRESENTATION, WindowState.MINIMIZED), - (WindowState.PRESENTATION, WindowState.MAXIMIZED), - (WindowState.PRESENTATION, WindowState.FULLSCREEN), - ], - ) - @pytest.mark.parametrize( - "second_window_class, second_window_kwargs", - [ - ( - toga.MainWindow, - dict(title="Secondary Window", position=(200, 150)), - ) - ], - ) - async def test_window_state_direct_change( - app, - app_probe, - initial_state, - final_state, - second_window, - second_window_probe, - intermediate_states=tuple( - random.sample( - [ - WindowState.NORMAL, - WindowState.MINIMIZED, - WindowState.MAXIMIZED, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - ], - 5, - ) - ), - ): - "Window state can be directly changed to another state." - if ( - WindowState.MINIMIZED in {initial_state, final_state} - and not second_window_probe.supports_minimize - ): - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) - elif ( - WindowState.FULLSCREEN in {initial_state, final_state} - and not second_window_probe.supports_fullscreen - ): - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) - elif ( - WindowState.PRESENTATION in {initial_state, final_state} - and not second_window_probe.supports_presentation - ): - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) - - second_window.toolbar.add(app.cmd1) - second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - second_window.show() - - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is visible", full_screen=True - ) - - assert second_window_probe.get_window_state() == WindowState.NORMAL - - # Set to initial state - second_window.state = initial_state - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - f"Secondary window is in {initial_state}", full_screen=True - ) - - assert second_window_probe.get_window_state() == initial_state - - # Set to the intermediate states but don't wait for the OS delay. - for state in intermediate_states: - second_window.state = state - - # Set to final state - second_window.state = final_state - # Add delay to ensure windows are visible after animation. - await app_probe.redraw(f"Secondary window is in {final_state}", delay=1.5) - assert second_window_probe.get_window_state() == final_state - - @pytest.mark.parametrize( - "state", - [ - WindowState.NORMAL, - WindowState.MINIMIZED, - WindowState.MAXIMIZED, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - ], - ) @pytest.mark.parametrize( "second_window_class, second_window_kwargs", [ @@ -924,57 +1057,6 @@ async def test_window_state_direct_change( ) ], ) - async def test_window_state_same_as_previous( - second_window, second_window_probe, app_probe, state - ): - """Setting window state same as previous is a no-op.""" - if state == WindowState.MINIMIZED and not second_window_probe.supports_minimize: - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) - elif ( - state == WindowState.FULLSCREEN - and not second_window_probe.supports_fullscreen - ): - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) - elif ( - state == WindowState.PRESENTATION - and not second_window_probe.supports_presentation - ): - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) - - second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - second_window.show() - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is shown", - ) - - # Set the window state: - second_window.state = state - # Add delay to ensure windows are visible after animation. - await app_probe.redraw(f"Secondary window is in {state}", delay=0.5) - assert second_window_probe.get_window_state() == state - - # Set the window state same as previous: - second_window.state = state - # Add delay to ensure windows are visible after animation. - await app_probe.redraw(f"Secondary window is in {state}", delay=0.5) - assert second_window_probe.get_window_state() == state - - @pytest.mark.parametrize( - "second_window_class, second_window_kwargs", - [ - ( - toga.MainWindow, - 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.""" diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index dab7d437b9..7abf7e4ac8 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -200,16 +200,19 @@ def get_window_state(self): return WindowState.MAXIMIZED elif window_state == WinForms.FormWindowState.Minimized: return WindowState.MINIMIZED - elif window_state == WinForms.FormWindowState.Normal: + else: # window_state == WinForms.FormWindowState.Normal: return WindowState.NORMAL - else: # pragma: no cover - # Marking this as no cover, since the above cases cover all the possible values - # of the FormWindowState enum type that can be returned by WinForms.WindowState. - return def set_window_state(self, state): - current_state = self.get_window_state() + # If the app is in presentation mode, but this window isn't, then + # exit app presentation mode before setting the requested state. + if any( + window.state == WindowState.PRESENTATION and window != self.interface + for window in self.interface.app.windows + ): + self.interface.app.exit_presentation_mode() + current_state = self.get_window_state() if current_state == state: return @@ -232,8 +235,7 @@ def set_window_state(self, state): self.set_window_state(state) - # elif current_state == WindowState.NORMAL: - else: + else: # current_state == WindowState.NORMAL: if state == WindowState.MAXIMIZED: self.native.WindowState = WinForms.FormWindowState.Maximized @@ -244,8 +246,7 @@ def set_window_state(self, state): self.native.FormBorderStyle = getattr(WinForms.FormBorderStyle, "None") self.native.WindowState = WinForms.FormWindowState.Maximized - # elif state == WindowState.PRESENTATION: - else: + else: # state == WindowState.PRESENTATION: self._before_presentation_mode_screen = self.interface.screen if self.native.MainMenuStrip: self.native.MainMenuStrip.Visible = False diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 38f326e83c..734e9ce4d8 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -8,8 +8,6 @@ ToolStripSeparator, ) -from toga.constants import WindowState - from .dialogs import DialogsMixin from .probe import BaseProbe @@ -55,22 +53,6 @@ def content_size(self): def presentation_content_size(self): return self.content_size - def get_window_state(self): - window_state = self.native.WindowState - if window_state == FormWindowState.Maximized: - if self.native.FormBorderStyle == getattr(FormBorderStyle, "None"): - if self.impl._in_presentation_mode: - current_state = WindowState.PRESENTATION - else: - current_state = WindowState.FULLSCREEN - else: - current_state = WindowState.MAXIMIZED - elif window_state == FormWindowState.Minimized: - current_state = WindowState.MINIMIZED - elif window_state == FormWindowState.Normal: - current_state = WindowState.NORMAL - return current_state - @property def is_resizable(self): return self.native.FormBorderStyle == FormBorderStyle.Sizable @@ -81,7 +63,7 @@ def is_minimizable(self): @property def is_minimized(self): - return bool(self.get_window_state() == WindowState.MINIMIZED) + return self.native.WindowState == FormWindowState.Minimized def minimize(self): if self.native.MinimizeBox: From f9c18f5aa41697a5e4a20b81d4cd74cdc83ce9ee Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 9 Sep 2024 12:14:22 -0400 Subject: [PATCH 168/248] Modified testbed test --- testbed/tests/window/test_window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 763e490655..c820b9b519 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -722,8 +722,9 @@ async def test_window_state_direct_change( WindowState.MAXIMIZED, WindowState.FULLSCREEN, WindowState.PRESENTATION, + WindowState.MINIMIZED, ], - 5, + 6, ) ), ): From ace13ceb894d35b319cebe1e978800983a690d92 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 9 Sep 2024 23:59:01 -0400 Subject: [PATCH 169/248] Testing native OS delay for each platform --- cocoa/src/toga_cocoa/window.py | 30 +++++++++++++++-------------- testbed/tests/window/test_window.py | 21 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 31d1b0fa35..8755ae0775 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -17,6 +17,7 @@ NSBackingStoreBuffered, NSImage, NSMutableArray, + NSObject, NSScreen, NSToolbar, NSToolbarItem, @@ -51,12 +52,16 @@ def windowWillClose_(self, notification) -> None: # Hence, simply remove the toolbar instead. self.toolbar = None + self.delegate = NSObject.alloc.init() - # However, window delegate methods, need check guards. + # Disconnecting the window delegate doesn't prevent custom + # methods like enteredMiniaturize_(which are performed with a + # delay i.e., having a delay != 0), from being triggered. + # Hence, check guards needs to be used in such custom methods. @objc_method def windowDidResize_(self, notification) -> None: - if self.interface and self.interface.content: # pragma: no branch + if self.interface.content: # Set the window to the new size self.interface.content.refresh() @@ -79,8 +84,7 @@ def enteredMiniaturize_(self, sender) -> None: @objc_method def windowDidDeminiaturize_(self, notification) -> None: - if self.impl: # pragma: no branch - self.impl._apply_state(self.impl._pending_state_transition) + self.impl._apply_state(self.impl._pending_state_transition) @objc_method def windowDidEnterFullScreen_(self, notification) -> None: @@ -91,19 +95,17 @@ def enteredFullScreen_(self, sender) -> None: # Doing the following directly in `windowDidEnterFullScreen_` will result in error: # ````2024-08-09 15:46:39.050 python[2646:37395] not in fullscreen state```` # and any subsequent window state calls to the OS will not work or will be glitchy. - if self.impl: # pragma: no branch - if ( - self.impl._pending_state_transition - and self.impl._pending_state_transition != WindowState.FULLSCREEN - ): - self.impl._apply_state(WindowState.NORMAL) - else: - self.impl._pending_state_transition = None + if ( + self.impl._pending_state_transition + and self.impl._pending_state_transition != WindowState.FULLSCREEN + ): + self.impl._apply_state(WindowState.NORMAL) + else: + self.impl._pending_state_transition = None @objc_method def windowDidExitFullScreen_(self, notification) -> None: - if self.impl: # pragma: no branch - self.impl._apply_state(self.impl._pending_state_transition) + self.impl._apply_state(self.impl._pending_state_transition) ###################################################################### # Toolbar delegate methods diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index c820b9b519..72ac62637e 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -668,6 +668,27 @@ async def test_move_and_resize(second_window, second_window_probe): assert second_window.size == (250 + extra_width, 210 + extra_height) assert second_window_probe.content_size == (250, 210) + @pytest.mark.parametrize( + "state", + [ + WindowState.MINIMIZED, + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ], + ) + @pytest.mark.parametrize( + "second_window_class, second_window_kwargs", + [ + ( + toga.MainWindow, + dict(title="Secondary Window", position=(200, 150)), + ) + ], + ) + async def test_timming(app_probe, second_window, second_window_probe, state): + pass + @pytest.mark.parametrize( "initial_state, final_state", [ From ef7b3dc6325bdf17366603bf083f13a4dd61c284 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:51:59 +0530 Subject: [PATCH 170/248] Restart CI as previous run dropped by intermittent failure From bc19c7fc9bcfe98fb1e1fd250d958d9f464af294 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:52:25 +0530 Subject: [PATCH 171/248] Re-running CI to confirm cocoa errors are consistently produced From c2235f702b83084df7a4abf78c35437859d9c977 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 10 Sep 2024 05:13:46 -0400 Subject: [PATCH 172/248] Testing native OS delay for each platform --- cocoa/src/toga_cocoa/window.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 8755ae0775..c346a0e1ee 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -17,7 +17,6 @@ NSBackingStoreBuffered, NSImage, NSMutableArray, - NSObject, NSScreen, NSToolbar, NSToolbarItem, @@ -47,12 +46,11 @@ def windowShouldClose_(self, notification) -> bool: @objc_method def windowWillClose_(self, notification) -> None: - # Setting the toolbar and window delegate to None doesn't disconnect - # the delegates and still triggers the delegate events. - + # Setting the toolbar to None doesn't disconnect the + # delegates and still triggers the delegate events. # Hence, simply remove the toolbar instead. self.toolbar = None - self.delegate = NSObject.alloc.init() + self.delegate = None # Disconnecting the window delegate doesn't prevent custom # methods like enteredMiniaturize_(which are performed with a From b0ead86d00ea1072c03f764d80698b2c981c71d6 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 10 Sep 2024 07:24:16 -0400 Subject: [PATCH 173/248] Removed unused flag from gtk --- gtk/src/toga_gtk/window.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 773c36b616..dd581f9c1f 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -37,7 +37,6 @@ def __init__(self, interface, title, position, size): # Use shadow variables to differentiate between presentation, fullscreen & in-between # transition state. self._in_presentation = False - self._in_fullscreen = False # Pending Window state transition variable: self._pending_state_transition = None @@ -187,10 +186,8 @@ def get_window_state(self): ): # pragma: no-cover-if-linux-wayland if self._in_presentation: return WindowState.PRESENTATION - elif self._in_fullscreen: - return WindowState.FULLSCREEN else: - return None + return WindowState.FULLSCREEN else: return WindowState.NORMAL @@ -249,7 +246,6 @@ def _apply_state(self, target_state): target_state == WindowState.FULLSCREEN ): # pragma: no-cover-if-linux-wayland self.native.fullscreen() - self._in_fullscreen = True elif ( target_state == WindowState.PRESENTATION @@ -276,7 +272,6 @@ def _apply_state(self, target_state): current_state == WindowState.FULLSCREEN ): # pragma: no-cover-if-linux-wayland self.native.unfullscreen() - self._in_fullscreen = False else: # current_state == WindowState.PRESENTATION: # pragma: no-cover-if-linux-wayland if isinstance(self.native, Gtk.ApplicationWindow): From 7bbf330fb78e983949eaa25987670ef37bd3097b Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:43:22 -0700 Subject: [PATCH 174/248] Restart CI for intermittent failures From 0f7fe4fbbed74e9cf1ac05357db5e6ba7c2eb70d Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 10 Sep 2024 13:36:11 -0400 Subject: [PATCH 175/248] Removed unused flag on gtk --- gtk/src/toga_gtk/window.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 773c36b616..dd581f9c1f 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -37,7 +37,6 @@ def __init__(self, interface, title, position, size): # Use shadow variables to differentiate between presentation, fullscreen & in-between # transition state. self._in_presentation = False - self._in_fullscreen = False # Pending Window state transition variable: self._pending_state_transition = None @@ -187,10 +186,8 @@ def get_window_state(self): ): # pragma: no-cover-if-linux-wayland if self._in_presentation: return WindowState.PRESENTATION - elif self._in_fullscreen: - return WindowState.FULLSCREEN else: - return None + return WindowState.FULLSCREEN else: return WindowState.NORMAL @@ -249,7 +246,6 @@ def _apply_state(self, target_state): target_state == WindowState.FULLSCREEN ): # pragma: no-cover-if-linux-wayland self.native.fullscreen() - self._in_fullscreen = True elif ( target_state == WindowState.PRESENTATION @@ -276,7 +272,6 @@ def _apply_state(self, target_state): current_state == WindowState.FULLSCREEN ): # pragma: no-cover-if-linux-wayland self.native.unfullscreen() - self._in_fullscreen = False else: # current_state == WindowState.PRESENTATION: # pragma: no-cover-if-linux-wayland if isinstance(self.native, Gtk.ApplicationWindow): From 7308e119132336d06b8bbbd46ced1fa8cec0accd Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 10 Sep 2024 23:41:51 -0400 Subject: [PATCH 176/248] Enable presentation and fullscreen mode on wayland --- cocoa/tests_backend/window.py | 2 - gtk/src/toga_gtk/app.py | 16 +------- gtk/src/toga_gtk/window.py | 31 +++++---------- gtk/tests_backend/window.py | 2 - testbed/tests/app/test_desktop.py | 7 +--- testbed/tests/window/test_window.py | 58 +---------------------------- winforms/tests_backend/window.py | 2 - 7 files changed, 14 insertions(+), 104 deletions(-) diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 625a30b782..ec5d7c829a 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -12,8 +12,6 @@ class WindowProbe(BaseProbe, DialogsMixin): supports_move_while_hidden = True supports_unminimize = True supports_minimize = True - supports_fullscreen = True - supports_presentation = True supports_placement = True def __init__(self, app, window): diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 6ec17d1813..8550c2f46a 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -238,25 +238,13 @@ def set_current_window(self, window): # Presentation mode controls ###################################################################### - def enter_presentation_mode( - self, screen_window_dict - ): # pragma: no-cover-if-linux-wayland - if IS_WAYLAND: # pragma: no-cover-if-linux-x - # Not implemented on wayland due to wayland security policies. - self.interface.factory.not_implemented( - "App.enter_presentation_mode() on Wayland" - ) + def enter_presentation_mode(self, screen_window_dict): for screen, window in screen_window_dict.items(): window._impl._before_presentation_mode_screen = window.screen window.screen = screen window._impl.set_window_state(WindowState.PRESENTATION) - def exit_presentation_mode(self): # pragma: no-cover-if-linux-wayland - if IS_WAYLAND: # pragma: no-cover-if-linux-x - # Not implemented on wayland due to wayland security policies. - self.interface.factory.not_implemented( - "App.exit_presentation_mode() on Wayland" - ) + def exit_presentation_mode(self): for window in self.interface.windows: if window.state == WindowState.PRESENTATION: window._impl.set_window_state(WindowState.NORMAL) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index dd581f9c1f..cf4d64890b 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -80,11 +80,7 @@ def gtk_window_state_event(self, widget, event): current_state = self.get_window_state() if current_state != WindowState.NORMAL: if self._pending_state_transition != current_state: - # Marking as no cover, since wayland has only MAXIMIZED & NORMAL state - # support, so no other type of state will be in pending_state. - self._apply_state( - WindowState.NORMAL - ) # pragma: no-cover-if-linux-wayland + self._apply_state(WindowState.NORMAL) else: self._pending_state_transition = None else: @@ -181,9 +177,7 @@ def get_window_state(self): return WindowState.MAXIMIZED elif window_state_flags & Gdk.WindowState.ICONIFIED: return WindowState.MINIMIZED # pragma: no-cover-if-linux-wayland - elif ( - window_state_flags & Gdk.WindowState.FULLSCREEN - ): # pragma: no-cover-if-linux-wayland + elif window_state_flags & Gdk.WindowState.FULLSCREEN: if self._in_presentation: return WindowState.PRESENTATION else: @@ -193,14 +187,13 @@ def get_window_state(self): def set_window_state(self, state): if IS_WAYLAND and ( - state - in {WindowState.MINIMIZED, WindowState.FULLSCREEN, WindowState.PRESENTATION} + state == WindowState.MINIMIZED ): # pragma: no-cover-if-linux-x # Not implemented on wayland due to wayland security policies. self.interface.factory.not_implemented( - f"Window.set_window_state({state}) on Wayland" + "Window.set_window_state(WindowState.MINIMIZED) on Wayland" ) - else: # pragma: no-cover-if-linux-wayland + else: if self._pending_state_transition: self._pending_state_transition = state else: @@ -242,14 +235,10 @@ def _apply_state(self, target_state): elif target_state == WindowState.MINIMIZED: # pragma: no-cover-if-linux-wayland self.native.iconify() - elif ( - target_state == WindowState.FULLSCREEN - ): # pragma: no-cover-if-linux-wayland + elif target_state == WindowState.FULLSCREEN: self.native.fullscreen() - elif ( - target_state == WindowState.PRESENTATION - ): # pragma: no-cover-if-linux-wayland + elif target_state == WindowState.PRESENTATION: self._before_presentation_mode_screen = self.interface.screen if isinstance(self.native, Gtk.ApplicationWindow): self.native.set_show_menubar(False) @@ -268,12 +257,10 @@ def _apply_state(self, target_state): # deiconify() doesn't work self.native.present() - elif ( - current_state == WindowState.FULLSCREEN - ): # pragma: no-cover-if-linux-wayland + elif current_state == WindowState.FULLSCREEN: self.native.unfullscreen() - else: # current_state == WindowState.PRESENTATION: # pragma: no-cover-if-linux-wayland + else: # current_state == WindowState.PRESENTATION: if isinstance(self.native, Gtk.ApplicationWindow): self.native.set_show_menubar(True) if getattr(self, "native_toolbar", None): diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index ecc71bf985..8c824c24d2 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -13,8 +13,6 @@ class WindowProbe(BaseProbe, DialogsMixin): supports_unminimize = False # Wayland mostly prohibits interaction with the larger windowing environment supports_minimize = not IS_WAYLAND - supports_fullscreen = not IS_WAYLAND - supports_presentation = not IS_WAYLAND supports_placement = not IS_WAYLAND def __init__(self, app, window): diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index a653a4d0d1..5eb542d548 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -172,9 +172,6 @@ async def test_menu_minimize(app, app_probe): async def test_presentation_mode(app, app_probe, main_window, main_window_probe): """The app can enter presentation mode.""" - if not main_window_probe.supports_presentation: - pytest.xfail("This backend doesn't reliably support presentation window state.") - try: window_information_list = list() windows_list = list() @@ -281,8 +278,8 @@ async def test_presentation_mode_exit_on_window_state_change( app, app_probe, main_window, main_window_probe, new_window_state ): """Changing window state exits presentation mode and sets the new state.""" - if not main_window_probe.supports_presentation: - pytest.xfail("This backend doesn't reliably support presentation window state.") + if not main_window_probe.supports_minimize: + pytest.xfail("This backend doesn't reliably support WindowState.MINIMIZED.") try: window1 = toga.Window( diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 72ac62637e..bf10e61ba6 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -668,27 +668,6 @@ async def test_move_and_resize(second_window, second_window_probe): assert second_window.size == (250 + extra_width, 210 + extra_height) assert second_window_probe.content_size == (250, 210) - @pytest.mark.parametrize( - "state", - [ - WindowState.MINIMIZED, - WindowState.MAXIMIZED, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - ], - ) - @pytest.mark.parametrize( - "second_window_class, second_window_kwargs", - [ - ( - toga.MainWindow, - dict(title="Secondary Window", position=(200, 150)), - ) - ], - ) - async def test_timming(app_probe, second_window, second_window_probe, state): - pass - @pytest.mark.parametrize( "initial_state, final_state", [ @@ -757,20 +736,6 @@ async def test_window_state_direct_change( pytest.xfail( "This backend doesn't reliably support minimized window state." ) - elif ( - WindowState.FULLSCREEN in {initial_state, final_state} - and not second_window_probe.supports_fullscreen - ): - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) - elif ( - WindowState.PRESENTATION in {initial_state, final_state} - and not second_window_probe.supports_presentation - ): - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) second_window.toolbar.add(app.cmd1) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) @@ -829,20 +794,6 @@ async def test_window_state_same_as_current( pytest.xfail( "This backend doesn't reliably support minimized window state." ) - elif ( - state == WindowState.FULLSCREEN - and not second_window_probe.supports_fullscreen - ): - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) - elif ( - state == WindowState.PRESENTATION - and not second_window_probe.supports_presentation - ): - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() @@ -861,6 +812,7 @@ async def test_window_state_same_as_current( # Set the window state the same as current: second_window.state = state + # No need to wait for OS delay as the above operation should be a no-op. assert second_window.state == state @pytest.mark.parametrize( @@ -967,10 +919,6 @@ async def test_window_state_maximized(second_window, second_window_probe): ) async def test_window_state_full_screen(second_window, second_window_probe): """Window can have full screen window state.""" - if not second_window_probe.supports_fullscreen: - pytest.xfail( - "This backend doesn't reliably support fullscreen window state." - ) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() # Add delay to ensure windows are visible after animation. @@ -1021,10 +969,6 @@ async def test_window_state_presentation( second_window, second_window_probe, app_probe ): """Window can have presentation window state.""" - if not second_window_probe.supports_presentation: - pytest.xfail( - "This backend doesn't reliably support presentation window state." - ) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() # Add delay to ensure windows are visible after animation. diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 734e9ce4d8..76b9b3f7e3 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -21,8 +21,6 @@ class WindowProbe(BaseProbe, DialogsMixin): supports_move_while_hidden = True supports_unminimize = True supports_minimize = True - supports_fullscreen = True - supports_presentation = True supports_placement = True def __init__(self, app, window): From 8f858beceab25f166fc3e0065098d8326e7e794c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 11 Sep 2024 00:24:07 -0400 Subject: [PATCH 177/248] Enable skipped test on wayland --- testbed/tests/app/test_desktop.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 5eb542d548..6fd3f75039 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -278,7 +278,9 @@ async def test_presentation_mode_exit_on_window_state_change( app, app_probe, main_window, main_window_probe, new_window_state ): """Changing window state exits presentation mode and sets the new state.""" - if not main_window_probe.supports_minimize: + if (new_window_state == WindowState.MINIMIZED) and ( + not main_window_probe.supports_minimize + ): pytest.xfail("This backend doesn't reliably support WindowState.MINIMIZED.") try: @@ -298,7 +300,7 @@ async def test_presentation_mode_exit_on_window_state_change( # Enter presentation mode app.enter_presentation_mode([window1]) # Add delay to ensure windows are visible after animation. - await app_probe.redraw("App is in presentation mode", delay=0.5) + await app_probe.redraw("App is in presentation mode", delay=0.75) assert app.in_presentation_mode assert window1.state == WindowState.PRESENTATION @@ -309,7 +311,7 @@ async def test_presentation_mode_exit_on_window_state_change( await app_probe.redraw( "App is not in presentation mode" f"\nTest Window 1 is in {new_window_state}", - delay=0.5, + delay=0.75, ) assert not app.in_presentation_mode @@ -324,7 +326,7 @@ async def test_presentation_mode_exit_on_window_state_change( # Enter presentation mode again app.enter_presentation_mode([window1]) # Add delay to ensure windows are visible after animation. - await app_probe.redraw("App is in presentation mode", delay=0.5) + await app_probe.redraw("App is in presentation mode", delay=0.75) assert app.in_presentation_mode assert window1.state == WindowState.PRESENTATION @@ -334,7 +336,7 @@ async def test_presentation_mode_exit_on_window_state_change( await app_probe.redraw( "App is not in presentation mode" f"\nTest Window 1 is in {new_window_state}", - delay=0.5, + delay=0.75, ) assert not app.in_presentation_mode From 295cd59488fb04fa3679941d38939667bd7e7a4a Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 11 Sep 2024 04:41:20 -0700 Subject: [PATCH 178/248] Fix test timing --- testbed/tests/app/test_desktop.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 6fd3f75039..86efe6ec86 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -300,7 +300,7 @@ async def test_presentation_mode_exit_on_window_state_change( # Enter presentation mode app.enter_presentation_mode([window1]) # Add delay to ensure windows are visible after animation. - await app_probe.redraw("App is in presentation mode", delay=0.75) + await app_probe.redraw("App is in presentation mode", delay=0.5) assert app.in_presentation_mode assert window1.state == WindowState.PRESENTATION @@ -311,7 +311,7 @@ async def test_presentation_mode_exit_on_window_state_change( await app_probe.redraw( "App is not in presentation mode" f"\nTest Window 1 is in {new_window_state}", - delay=0.75, + delay=0.5, ) assert not app.in_presentation_mode @@ -326,7 +326,7 @@ async def test_presentation_mode_exit_on_window_state_change( # Enter presentation mode again app.enter_presentation_mode([window1]) # Add delay to ensure windows are visible after animation. - await app_probe.redraw("App is in presentation mode", delay=0.75) + await app_probe.redraw("App is in presentation mode", delay=0.5) assert app.in_presentation_mode assert window1.state == WindowState.PRESENTATION @@ -336,7 +336,7 @@ async def test_presentation_mode_exit_on_window_state_change( await app_probe.redraw( "App is not in presentation mode" f"\nTest Window 1 is in {new_window_state}", - delay=0.75, + delay=0.5, ) assert not app.in_presentation_mode From 5bd410e1c20efab916444fae38167f0b32698cd0 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:17:01 -0700 Subject: [PATCH 179/248] Restart CI for intermittent failures From 00a742ee918917eecc3612bcdc21d6833d8493a4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 11 Sep 2024 07:46:10 -0400 Subject: [PATCH 180/248] Delay tests according to native platforms --- testbed/tests/app/test_desktop.py | 58 ++++++++++++++++++++--------- testbed/tests/window/test_window.py | 42 ++++++++------------- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 86efe6ec86..2447a6170e 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -191,7 +191,9 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ) window.show() # Add delay to ensure windows are visible after animation. - await app_probe.redraw(f"Test Window {i} is visible", delay=0.5) + await main_window_probe.wait_for_window( + f"Test Window {i} is visible", full_screen=True + ) window_information = dict() window_information["window"] = window @@ -214,8 +216,9 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) # Enter presentation mode with a screen-window dict via the app app.enter_presentation_mode(screen_window_dict) # Add delay to ensure windows are visible after animation. - await app_probe.redraw("App is in presentation mode", delay=0.5) - + await main_window_probe.wait_for_window( + f"Test Window {i} is visible", full_screen=True + ) assert app.in_presentation_mode # All the windows should be in presentation mode. for window_information in window_information_list: @@ -237,7 +240,9 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) # Exit presentation mode app.exit_presentation_mode() - await app_probe.redraw("App is no longer in presentation mode", delay=0.5) + await main_window_probe.wait_for_window( + f"Test Window {i} is visible", full_screen=True + ) assert not app.in_presentation_mode for window_information in window_information_list: @@ -262,7 +267,9 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) # Ensure that main_window will be in focus for other tests. app.current_window = main_window # Add delay to ensure windows are visible after animation. - await app_probe.redraw("main_window is now the current window", delay=0.5) + await main_window_probe.wait_for_window( + f"Test Window {i} is visible", full_screen=True + ) assert app.current_window == main_window @@ -295,12 +302,13 @@ async def test_presentation_mode_exit_on_window_state_change( window1.show() window2.show() # Add delay to ensure windows are visible after animation. - await app_probe.redraw("Test windows are shown", delay=0.5) - + await main_window_probe.wait_for_window("Test windows are shown") # Enter presentation mode app.enter_presentation_mode([window1]) # Add delay to ensure windows are visible after animation. - await app_probe.redraw("App is in presentation mode", delay=0.5) + await main_window_probe.wait_for_window( + "App is in presentation mode", full_screen=True + ) assert app.in_presentation_mode assert window1.state == WindowState.PRESENTATION @@ -308,10 +316,11 @@ async def test_presentation_mode_exit_on_window_state_change( # Changing window state of main window should make the app exit presentation mode. window1.state = new_window_state # Add delay to ensure windows are visible after animation. - await app_probe.redraw( + await main_window_probe.wait_for_window( "App is not in presentation mode" f"\nTest Window 1 is in {new_window_state}", - delay=0.5, + minimize=True if new_window_state == WindowState.MINIMIZED else False, + full_screen=True if new_window_state == WindowState.FULLSCREEN else False, ) assert not app.in_presentation_mode @@ -321,22 +330,31 @@ async def test_presentation_mode_exit_on_window_state_change( window1.state = WindowState.NORMAL window2.state = WindowState.NORMAL # Add delay to ensure windows are visible after animation. - await app_probe.redraw("All test windows are in WindowState.NORMAL", delay=1) + await main_window_probe.wait_for_window( + "All test windows are in WindowState.NORMAL", + minimize=True if new_window_state == WindowState.MINIMIZED else False, + full_screen=True if new_window_state == WindowState.FULLSCREEN else False, + ) # Enter presentation mode again app.enter_presentation_mode([window1]) # Add delay to ensure windows are visible after animation. - await app_probe.redraw("App is in presentation mode", delay=0.5) + await main_window_probe.wait_for_window( + "App is in presentation mode", + minimize=True if new_window_state == WindowState.MINIMIZED else False, + full_screen=True if new_window_state == WindowState.FULLSCREEN else False, + ) assert app.in_presentation_mode assert window1.state == WindowState.PRESENTATION # Changing window state of extra window should make the app exit presentation mode. window2.state = new_window_state # Add delay to ensure windows are visible after animation. - await app_probe.redraw( + await main_window_probe.wait_for_window( "App is not in presentation mode" - f"\nTest Window 1 is in {new_window_state}", - delay=0.5, + f"\nTest Window 2 is in {new_window_state}", + minimize=True if new_window_state == WindowState.MINIMIZED else False, + full_screen=True if new_window_state == WindowState.FULLSCREEN else False, ) assert not app.in_presentation_mode @@ -346,7 +364,11 @@ async def test_presentation_mode_exit_on_window_state_change( window1.state = WindowState.NORMAL window2.state = WindowState.NORMAL # Add delay for gtk to show the windows - await app_probe.redraw("All test windows are in WindowState.NORMAL", delay=0.5) + await main_window_probe.wait_for_window( + "All test windows are in WindowState.NORMAL", + minimize=True if new_window_state == WindowState.MINIMIZED else False, + full_screen=True if new_window_state == WindowState.FULLSCREEN else False, + ) finally: window1.close() @@ -355,7 +377,9 @@ async def test_presentation_mode_exit_on_window_state_change( # Ensure that main_window will be in focus for other tests. app.current_window = main_window # Add delay to ensure windows are visible after animation. - await app_probe.redraw("main_window is now the current window", delay=0.5) + await main_window_probe.wait_for_window( + "main_window is now the current window", full_screen=True + ) assert app.current_window == main_window diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index bf10e61ba6..024857c090 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -742,9 +742,7 @@ async def test_window_state_direct_change( second_window.show() # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is visible", full_screen=True - ) + await second_window_probe.wait_for_window("Secondary window is visible") assert second_window.state == WindowState.NORMAL @@ -752,7 +750,9 @@ async def test_window_state_direct_change( second_window.state = initial_state # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( - f"Secondary window is in {initial_state}", full_screen=True + f"Secondary window is in {initial_state}", + minimize=True if initial_state == WindowState.MINIMIZED else False, + full_screen=True if initial_state == WindowState.FULLSCREEN else False, ) assert second_window.state == initial_state @@ -764,7 +764,7 @@ async def test_window_state_direct_change( # Set to final state second_window.state = final_state # Add delay to ensure windows are visible after animation. - await app_probe.redraw(f"Secondary window is in {final_state}", delay=1.5) + await app_probe.redraw(f"Secondary window is in {final_state}", delay=2) assert second_window.state == final_state @pytest.mark.parametrize( @@ -798,15 +798,15 @@ async def test_window_state_same_as_current( second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is shown", full_screen=True - ) + await second_window_probe.wait_for_window("Secondary window is shown") # Set the window state: second_window.state = state # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( - f"Secondary window is in {state}", full_screen=True + f"Secondary window is in {state}", + minimize=True if state == WindowState.MINIMIZED else False, + full_screen=True if state == WindowState.FULLSCREEN else False, ) assert second_window.state == state @@ -834,9 +834,7 @@ async def test_window_state_minimized(second_window, second_window_probe): second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is shown", - ) + await second_window_probe.wait_for_window("Secondary window is shown") assert second_window.state == WindowState.NORMAL if second_window_probe.supports_minimizable: @@ -876,9 +874,7 @@ async def test_window_state_maximized(second_window, second_window_probe): second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is shown", - ) + await second_window_probe.wait_for_window("Secondary window is shown") assert second_window.state == WindowState.NORMAL assert second_window_probe.is_resizable @@ -886,9 +882,7 @@ async def test_window_state_maximized(second_window, second_window_probe): second_window.state = WindowState.MAXIMIZED # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is maximized", - ) + await second_window_probe.wait_for_window("Secondary window is maximized") assert second_window.state == WindowState.MAXIMIZED assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] @@ -901,9 +895,7 @@ async def test_window_state_maximized(second_window, second_window_probe): second_window.state = WindowState.NORMAL # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is not maximized", - ) + await second_window_probe.wait_for_window("Secondary window is not maximized") assert second_window.state == WindowState.NORMAL assert second_window_probe.is_resizable assert second_window_probe.content_size == initial_content_size @@ -922,9 +914,7 @@ async def test_window_state_full_screen(second_window, second_window_probe): second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is shown", - ) + await second_window_probe.wait_for_window("Secondary window is shown") assert second_window.state == WindowState.NORMAL assert second_window_probe.is_resizable @@ -972,9 +962,7 @@ async def test_window_state_presentation( second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is shown", - ) + await second_window_probe.wait_for_window("Secondary window is shown") assert second_window.state == WindowState.NORMAL assert second_window_probe.is_resizable From 4ef9c50798c91ea0deb6f725e0a2c1fc4ea6c989 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 12 Sep 2024 01:01:47 -0400 Subject: [PATCH 181/248] Covered missing coverage --- testbed/tests/window/test_window.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 024857c090..c1cef0f0ee 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -1,5 +1,4 @@ import gc -import random import re import weakref from importlib import import_module @@ -714,18 +713,12 @@ async def test_window_state_direct_change( final_state, second_window, second_window_probe, - intermediate_states=tuple( - random.sample( - [ - WindowState.NORMAL, - WindowState.MINIMIZED, - WindowState.MAXIMIZED, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - WindowState.MINIMIZED, - ], - 6, - ) + intermediate_states=( + WindowState.FULLSCREEN, + WindowState.MINIMIZED, + WindowState.NORMAL, + WindowState.MAXIMIZED, + WindowState.PRESENTATION, ), ): """Window state can be directly changed to another state.""" @@ -752,7 +745,7 @@ async def test_window_state_direct_change( await second_window_probe.wait_for_window( f"Secondary window is in {initial_state}", minimize=True if initial_state == WindowState.MINIMIZED else False, - full_screen=True if initial_state == WindowState.FULLSCREEN else False, + full_screen=(True if initial_state == WindowState.FULLSCREEN else False), ) assert second_window.state == initial_state @@ -764,8 +757,7 @@ async def test_window_state_direct_change( # Set to final state second_window.state = final_state # Add delay to ensure windows are visible after animation. - await app_probe.redraw(f"Secondary window is in {final_state}", delay=2) - assert second_window.state == final_state + await app_probe.redraw(f"Secondary window is in {final_state}", delay=1) @pytest.mark.parametrize( "state", From a1613be759c57c548dcb36efad26674e1d8b3f57 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 12 Sep 2024 01:34:51 -0400 Subject: [PATCH 182/248] Covered missing coverage --- testbed/tests/window/test_window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index c1cef0f0ee..155fd5e737 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -744,7 +744,6 @@ async def test_window_state_direct_change( # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( f"Secondary window is in {initial_state}", - minimize=True if initial_state == WindowState.MINIMIZED else False, full_screen=(True if initial_state == WindowState.FULLSCREEN else False), ) From 138d0e39a4a6169cad17c4d4fae0dc34a7167f7f Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 13 Sep 2024 01:59:12 -0400 Subject: [PATCH 183/248] Covered missing coverage --- cocoa/src/toga_cocoa/window.py | 8 +++++--- testbed/tests/window/test_window.py | 24 +++++++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index c346a0e1ee..c43f8e2156 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -46,10 +46,12 @@ def windowShouldClose_(self, notification) -> bool: @objc_method def windowWillClose_(self, notification) -> None: - # Setting the toolbar to None doesn't disconnect the - # delegates and still triggers the delegate events. + # Setting the toolbar delegate to None doesn't disconnect + # the delegates and still triggers the delegate events. # Hence, simply remove the toolbar instead. - self.toolbar = None + if self.toolbar: + self.toolbar = None + # Disconnect the window delegate. self.delegate = None # Disconnecting the window delegate doesn't prevent custom diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 155fd5e737..aa2948c954 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -1,4 +1,6 @@ import gc +import itertools +import random import re import weakref from importlib import import_module @@ -667,6 +669,13 @@ async def test_move_and_resize(second_window, second_window_probe): assert second_window.size == (250 + extra_width, 210 + extra_height) assert second_window_probe.content_size == (250, 210) + @pytest.fixture(scope="session") + def initial_intermediate_state(): + # Use a iterator cycling fixture to ensure each state + # is the initial intermediate state at least once. This + # is to ensure full code coverage for all backends. + return itertools.cycle(list(WindowState)) + @pytest.mark.parametrize( "initial_state, final_state", [ @@ -713,13 +722,7 @@ async def test_window_state_direct_change( final_state, second_window, second_window_probe, - intermediate_states=( - WindowState.FULLSCREEN, - WindowState.MINIMIZED, - WindowState.NORMAL, - WindowState.MAXIMIZED, - WindowState.PRESENTATION, - ), + initial_intermediate_state, ): """Window state can be directly changed to another state.""" if ( @@ -729,7 +732,9 @@ async def test_window_state_direct_change( pytest.xfail( "This backend doesn't reliably support minimized window state." ) - + intermediate_states = [next(initial_intermediate_state)] + random.sample( + [state for state in WindowState], len(WindowState) + ) second_window.toolbar.add(app.cmd1) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() @@ -744,7 +749,8 @@ async def test_window_state_direct_change( # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( f"Secondary window is in {initial_state}", - full_screen=(True if initial_state == WindowState.FULLSCREEN else False), + minimize=True if initial_state == WindowState.MINIMIZED else False, + full_screen=True if initial_state == WindowState.FULLSCREEN else False, ) assert second_window.state == initial_state From 450efad1f4cc0097abf1c0a2c2045a67674bed3e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 13 Sep 2024 05:39:46 -0400 Subject: [PATCH 184/248] Added XFAIL for iOS --- android/tests_backend/window.py | 5 ++- iOS/tests_backend/window.py | 3 ++ testbed/tests/app/test_mobile.py | 6 +-- testbed/tests/window/test_window.py | 57 +++++++++++++++++++---------- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index d66280b3ad..be61dcaec2 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -7,13 +7,16 @@ class WindowProbe(BaseProbe, DialogsMixin): + supports_fullscreen = True + supports_presentation = True + def __init__(self, app, window): super().__init__(app) self.native = self.app._impl.native self.window = window async def wait_for_window(self, message, minimize=False, full_screen=False): - await self.redraw(message) + await self.redraw(message, delay=0.1 if full_screen else 0) @property def content_size(self): diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py index 0877ab0fe4..c0eeec0174 100644 --- a/iOS/tests_backend/window.py +++ b/iOS/tests_backend/window.py @@ -7,6 +7,9 @@ class WindowProbe(BaseProbe, DialogsMixin): + supports_fullscreen = False + supports_presentation = False + def __init__(self, app, window): super().__init__() self.app = app diff --git a/testbed/tests/app/test_mobile.py b/testbed/tests/app/test_mobile.py index 61f518528a..b854f7c1ee 100644 --- a/testbed/tests/app/test_mobile.py +++ b/testbed/tests/app/test_mobile.py @@ -18,11 +18,11 @@ async def test_show_hide_cursor(app): app.hide_cursor() -@pytest.mark.skipif( - toga.platform.current_platform == "iOS", reason="Not implemented on iOS" -) async def test_presentation_mode(app, main_window, main_window_probe): """The app can enter into presentation mode""" + if not main_window_probe.supports_presentation: + pytest.xfail("This backend doesn't support presentation window state.") + assert not app.in_presentation_mode assert main_window.state != WindowState.PRESENTATION diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index aa2948c954..e11652db70 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -143,9 +143,6 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): finally: main_window.content = orig_content - @pytest.mark.skipif( - toga.platform.current_platform == "iOS", reason="Not implemented on iOS" - ) @pytest.mark.parametrize( "initial_state, final_state", [ @@ -164,6 +161,21 @@ async def test_window_state_direct_change( app, initial_state, final_state, main_window, main_window_probe ): """Window state can be directly changed to another state.""" + if not main_window_probe.supports_fullscreen and WindowState.FULLSCREEN in { + initial_state, + final_state, + }: + pytest.xfail("This backend doesn't support fullscreen window state.") + if ( + not main_window_probe.supports_presentation + and WindowState.PRESENTATION + in { + initial_state, + final_state, + } + ): + pytest.xfail("This backend doesn't support presentation window state.") + try: # Set to initial state main_window.state = initial_state @@ -187,9 +199,6 @@ async def test_window_state_direct_change( assert main_window.state == WindowState.NORMAL - @pytest.mark.skipif( - toga.platform.current_platform == "iOS", reason="Not implemented on iOS" - ) @pytest.mark.parametrize( "state", [ @@ -202,6 +211,17 @@ async def test_window_state_same_as_current( app, main_window, main_window_probe, state ): """Setting the window state the same as current is a no-op.""" + if ( + not main_window_probe.supports_fullscreen + and state == WindowState.FULLSCREEN + ): + pytest.xfail("This backend doesn't support fullscreen window state.") + if ( + not main_window_probe.supports_presentation + and state == WindowState.PRESENTATION + ): + pytest.xfail("This backend doesn't support presentation window state.") + try: # Set the window state: main_window.state = state @@ -218,11 +238,10 @@ async def test_window_state_same_as_current( await main_window_probe.wait_for_window("Main window is not full screen") assert main_window.state == WindowState.NORMAL - @pytest.mark.skipif( - toga.platform.current_platform == "iOS", reason="Not implemented on iOS" - ) async def test_window_state_fullscreen(main_window, main_window_probe): """The window can enter into fullscreen state.""" + if not main_window_probe.supports_fullscreen: + pytest.xfail("This backend doesn't support fullscreen window state.") try: widget = toga.Box(style=Pack(flex=1)) widget_probe = get_probe(widget) @@ -234,7 +253,9 @@ async def test_window_state_fullscreen(main_window, main_window_probe): # Make main window full screen main_window.state = WindowState.FULLSCREEN - await main_window_probe.wait_for_window("Main window is full screen") + await main_window_probe.wait_for_window( + "Main window is full screen", full_screen=True + ) assert main_window.state == WindowState.FULLSCREEN # At least one of the dimensions should have increased. assert ( @@ -244,7 +265,9 @@ async def test_window_state_fullscreen(main_window, main_window_probe): finally: # Exit full screen main_window.state = WindowState.NORMAL - await main_window_probe.wait_for_window("Main window is not full screen") + await main_window_probe.wait_for_window( + "Main window is not full screen", full_screen=True + ) assert main_window.state == WindowState.NORMAL # Both dimensions should be the same. assert ( @@ -253,11 +276,10 @@ async def test_window_state_fullscreen(main_window, main_window_probe): ) main_window.content.clear() - @pytest.mark.skipif( - toga.platform.current_platform == "iOS", reason="Not implemented on iOS" - ) async def test_window_state_presentation(main_window, main_window_probe): """The window can enter into presentation state.""" + if not main_window_probe.supports_presentation: + pytest.xfail("This backend doesn't support presentation window state.") try: widget = toga.Box(style=Pack(flex=1)) widget_probe = get_probe(widget) @@ -270,7 +292,7 @@ async def test_window_state_presentation(main_window, main_window_probe): # Enter presentation mode with main window main_window.state = WindowState.PRESENTATION await main_window_probe.wait_for_window( - "Main window is in presentation mode" + "Main window is in presentation mode", full_screen=True ) assert main_window.state == WindowState.PRESENTATION # At least one of the dimensions should have increased. @@ -282,7 +304,7 @@ async def test_window_state_presentation(main_window, main_window_probe): # Exit presentation mode main_window.state = WindowState.NORMAL await main_window_probe.wait_for_window( - "Main window is not in presentation mode" + "Main window is not in presentation mode", full_screen=True ) assert main_window.state == WindowState.NORMAL # Both dimensions should be the same. @@ -292,9 +314,6 @@ async def test_window_state_presentation(main_window, main_window_probe): ) main_window.content.clear() - @pytest.mark.skipif( - toga.platform.current_platform == "iOS", reason="Not implemented on iOS" - ) @pytest.mark.parametrize( "state", [ From 51d359066caaa4241c0fe867807c5972c8ff13ba Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 13 Sep 2024 09:17:47 -0700 Subject: [PATCH 185/248] Replace use of app_probe.redraw with window_probe.wait_for_window --- android/tests_backend/window.py | 8 ++++++-- cocoa/tests_backend/window.py | 10 ++++++++-- core/tests/window/test_window.py | 2 -- gtk/src/toga_gtk/window.py | 3 +-- gtk/tests_backend/window.py | 6 ++++-- iOS/tests_backend/window.py | 4 +++- testbed/tests/window/test_window.py | 4 +++- winforms/tests_backend/window.py | 4 +++- 8 files changed, 28 insertions(+), 13 deletions(-) diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index be61dcaec2..b54e3be067 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -15,8 +15,12 @@ def __init__(self, app, window): self.native = self.app._impl.native self.window = window - async def wait_for_window(self, message, minimize=False, full_screen=False): - await self.redraw(message, delay=0.1 if full_screen else 0) + async def wait_for_window( + self, message, minimize=False, full_screen=False, rapid_state_switching=False + ): + await self.redraw( + message, delay=0.1 if (full_screen or rapid_state_switching) else 0 + ) @property def content_size(self): diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index ec5d7c829a..a4557b7f1c 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -22,10 +22,16 @@ def __init__(self, app, window): self.native = window._impl.native assert isinstance(self.native, NSWindow) - async def wait_for_window(self, message, minimize=False, full_screen=False): + async def wait_for_window( + self, message, minimize=False, full_screen=False, rapid_state_switching=False + ): await self.redraw( message, - delay=0.75 if full_screen else 0.5 if minimize else 0.1, + delay=( + 1 + if rapid_state_switching + else 0.75 if full_screen else 0.5 if minimize else 0.1 + ), ) def close(self): diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index a542b287cc..4b02fc8ad8 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -357,8 +357,6 @@ def test_window_state(window, initial_state, final_state): @pytest.mark.parametrize( "state", [ - # Setting window state to any of the following when window is - # not resizable, is a no-op and should give a UserWarning. WindowState.MAXIMIZED, WindowState.FULLSCREEN, WindowState.PRESENTATION, diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index cf4d64890b..0bf19bb7e9 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -34,8 +34,7 @@ def __init__(self, interface, title, position, size): self._window_state_flags = None # Gdk.WindowState.FULLSCREEN is notified before it fully transitions to fullscreen, - # Use shadow variables to differentiate between presentation, fullscreen & in-between - # transition state. + # Use shadow variables to differentiate between the presentation & fullscreen state. self._in_presentation = False # Pending Window state transition variable: diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index 8c824c24d2..a4b8d550a4 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -23,8 +23,10 @@ def __init__(self, app, window): self.native = window._impl.native assert isinstance(self.native, Gtk.Window) - async def wait_for_window(self, message, minimize=False, full_screen=False): - await self.redraw(message, delay=0.5 if (full_screen or minimize) else 0.1) + async def wait_for_window( + self, message, minimize=False, full_screen=False, rapid_state_switching=False + ): + await self.redraw(message, delay=(0.5 if (full_screen or minimize) else 0.1)) def close(self): if self.is_closable: diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py index c0eeec0174..28db5fde9f 100644 --- a/iOS/tests_backend/window.py +++ b/iOS/tests_backend/window.py @@ -18,7 +18,9 @@ def __init__(self, app, window): self.native = window._impl.native assert isinstance(self.native, UIWindow) - async def wait_for_window(self, message, minimize=False, full_screen=False): + async def wait_for_window( + self, message, minimize=False, full_screen=False, rapid_state_switching=False + ): await self.redraw(message) @property diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index e11652db70..c69a808a70 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -781,7 +781,9 @@ async def test_window_state_direct_change( # Set to final state second_window.state = final_state # Add delay to ensure windows are visible after animation. - await app_probe.redraw(f"Secondary window is in {final_state}", delay=1) + await second_window_probe.wait_for_window( + f"Secondary window is in {final_state}", rapid_state_switching=True + ) @pytest.mark.parametrize( "state", diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 76b9b3f7e3..1aaa450bcd 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -31,7 +31,9 @@ def __init__(self, app, window): self.native = window._impl.native assert isinstance(self.native, Form) - async def wait_for_window(self, message, minimize=False, full_screen=False): + async def wait_for_window( + self, message, minimize=False, full_screen=False, rapid_state_switching=False + ): await self.redraw(message) def close(self): From 3de8eef83e7dd107604a02a78761982aea3843d9 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 16 Sep 2024 03:31:37 -0700 Subject: [PATCH 186/248] Moved presentation implementation on cocoa to window from app --- android/src/toga_android/app.py | 4 +-- android/tests_backend/window.py | 27 +++++-------------- cocoa/src/toga_cocoa/app.py | 30 +++------------------ cocoa/src/toga_cocoa/window.py | 46 +++++++++++++++++++++++++++----- cocoa/tests_backend/window.py | 6 ++++- gtk/tests_backend/window.py | 11 ++++++-- iOS/tests_backend/window.py | 9 ++++--- winforms/tests_backend/window.py | 6 ++++- 8 files changed, 76 insertions(+), 63 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index d2099f75d3..81b8f8bc20 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -320,8 +320,8 @@ def set_current_window(self, window): def enter_presentation_mode(self, screen_window_dict): for screen, window in screen_window_dict.items(): - window._impl._before_presentation_mode_screen = window.screen - window.screen = screen + # There is only a single window on android and moving between + # screens is not supported. window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self): diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index b54e3be067..8743733d20 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -1,7 +1,5 @@ from androidx.appcompat import R as appcompat_R -from toga.constants import WindowState - from .dialogs import DialogsMixin from .probe import BaseProbe @@ -16,10 +14,15 @@ def __init__(self, app, window): self.window = window async def wait_for_window( - self, message, minimize=False, full_screen=False, rapid_state_switching=False + self, + message, + minimize=False, + full_screen=False, + rapid_state_switching=False, ): await self.redraw( - message, delay=0.1 if (full_screen or rapid_state_switching) else 0 + message, + delay=(0.1 if (full_screen or rapid_state_switching) else 0), ) @property @@ -29,22 +32,6 @@ def content_size(self): self.root_view.getHeight() / self.scale_factor, ) - def get_window_state(self): - decor_view = self.native.getWindow().getDecorView() - system_ui_flags = decor_view.getSystemUiVisibility() - if system_ui_flags & ( - decor_view.SYSTEM_UI_FLAG_FULLSCREEN - | decor_view.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | decor_view.SYSTEM_UI_FLAG_IMMERSIVE - ): - if self.window._impl._in_presentation_mode: - current_state = WindowState.PRESENTATION - else: - current_state = WindowState.FULLSCREEN - else: - current_state = WindowState.NORMAL - return current_state - def _native_menu(self): return self.native.findViewById(appcompat_R.id.action_bar).getMenu() diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index d77b8fa48c..023e9bf726 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -30,7 +30,6 @@ NSCursor, NSMenu, NSMenuItem, - NSNumber, NSScreen, ) from .screens import Screen as ScreenImpl @@ -374,33 +373,12 @@ def set_current_window(self, window): ###################################################################### def enter_presentation_mode(self, screen_window_dict): - opts = NSMutableDictionary.alloc().init() - opts.setObject( - NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" - ) - for screen, window in screen_window_dict.items(): - # The widgets are actually added to window._impl.container.native, instead of - # window.content._impl.native. And window._impl.native.contentView is - # window._impl.container.native. Hence, we need to go fullscreen on - # window._impl.container.native instead. - window._impl.container.native.enterFullScreenMode( - screen._impl.native, withOptions=opts - ) - # Going presentation mode causes the window content to be re-homed - # in a NSFullScreenWindow; teach the new parent window about its - # Toga representations. - window._impl.container.native.window._impl = window._impl - window._impl.container.native.window.interface = window - window.content.refresh() + window._impl._before_presentation_mode_screen = window.screen + window.screen = screen + window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self): - opts = NSMutableDictionary.alloc().init() - opts.setObject( - NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" - ) - for window in self.interface.windows: if window.state == WindowState.PRESENTATION: - window._impl.container.native.exitFullScreenModeWithOptions(opts) - window.content.refresh() + window._impl.set_window_state(WindowState.NORMAL) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index c43f8e2156..77932fa3c1 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -17,6 +17,8 @@ NSBackingStoreBuffered, NSImage, NSMutableArray, + NSMutableDictionary, + NSNumber, NSScreen, NSToolbar, NSToolbarItem, @@ -88,13 +90,13 @@ def windowDidDeminiaturize_(self, notification) -> None: @objc_method def windowDidEnterFullScreen_(self, notification) -> None: + # Directly doing post-fullscreen operations here will result in error: + # ````2024-08-09 15:46:39.050 python[2646:37395] not in fullscreen state```` + # and any subsequent window state calls to the OS will not work or will be glitchy. self.performSelector(SEL("enteredFullScreen:"), withObject=None, afterDelay=0) @objc_method def enteredFullScreen_(self, sender) -> None: - # Doing the following directly in `windowDidEnterFullScreen_` will result in error: - # ````2024-08-09 15:46:39.050 python[2646:37395] not in fullscreen state```` - # and any subsequent window state calls to the OS will not work or will be glitchy. if ( self.impl._pending_state_transition and self.impl._pending_state_transition != WindowState.FULLSCREEN @@ -236,7 +238,7 @@ def __init__(self, interface, title, position, size): self.set_size(size) self.set_position(position if position is not None else _initial_position()) - self.native.setDelegate(self.native) + self.native.delegate = self.native self.container = Container(on_refresh=self.content_refreshed) self.native.contentView = self.container.native @@ -424,9 +426,30 @@ def _apply_state(self, target_state): self.native.toggleFullScreen(self.native) elif target_state == WindowState.PRESENTATION: - self.interface.app.enter_presentation_mode( - {self.interface.screen: self.interface} + self._before_presentation_mode_screen = self.interface.screen + opts = NSMutableDictionary.alloc().init() + opts.setObject( + NSNumber.numberWithBool(True), + forKey="NSFullScreenModeAllScreens", + ) + # The widgets are actually added to + # window._impl.container.native, instead of + # window.content._impl.native. And + # window._impl.native.contentView is + # window._impl.container.native. Hence, + # we need to go fullscreen on + # window._impl.container.native instead. + self.container.native.enterFullScreenMode( + self.interface.screen._impl.native, withOptions=opts ) + + # Going presentation mode causes the window content + # to be re-homed in a NSFullScreenWindow; teach the + # new parent window about its Toga representations. + self.container.native.window._impl = self + self.container.native.window.interface = self.interface + self.interface.content.refresh() + # No need to check for other pending states, # since this is fully applied at this point. self._pending_state_transition = None @@ -443,7 +466,16 @@ def _apply_state(self, target_state): self.native.toggleFullScreen(self.native) else: # current_state == WindowState.PRESENTATION: - self.interface.app.exit_presentation_mode() + opts = NSMutableDictionary.alloc().init() + opts.setObject( + NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens" + ) + self.container.native.exitFullScreenModeWithOptions(opts) + self.interface.content.refresh() + + self.interface.screen = self._before_presentation_mode_screen + del self._before_presentation_mode_screen + self._apply_state(self._pending_state_transition) ###################################################################### diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index a4557b7f1c..4078f1bc45 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -23,7 +23,11 @@ def __init__(self, app, window): assert isinstance(self.native, NSWindow) async def wait_for_window( - self, message, minimize=False, full_screen=False, rapid_state_switching=False + self, + message, + minimize=False, + full_screen=False, + rapid_state_switching=False, ): await self.redraw( message, diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index a4b8d550a4..6b1f188735 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -24,9 +24,16 @@ def __init__(self, app, window): assert isinstance(self.native, Gtk.Window) async def wait_for_window( - self, message, minimize=False, full_screen=False, rapid_state_switching=False + self, + message, + minimize=False, + full_screen=False, + rapid_state_switching=False, ): - await self.redraw(message, delay=(0.5 if (full_screen or minimize) else 0.1)) + await self.redraw( + message, + delay=(0.5 if (full_screen or minimize) else 0.1), + ) def close(self): if self.is_closable: diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py index 28db5fde9f..3106e80554 100644 --- a/iOS/tests_backend/window.py +++ b/iOS/tests_backend/window.py @@ -19,7 +19,11 @@ def __init__(self, app, window): assert isinstance(self.native, UIWindow) async def wait_for_window( - self, message, minimize=False, full_screen=False, rapid_state_switching=False + self, + message, + minimize=False, + full_screen=False, + rapid_state_switching=False, ): await self.redraw(message) @@ -35,8 +39,5 @@ def content_size(self): ), ) - def get_window_state(self): - pytest.skip("Window states are not implemented on iOS") - def has_toolbar(self): pytest.skip("Toolbars not implemented on iOS") diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 1aaa450bcd..b00d12bd67 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -32,7 +32,11 @@ def __init__(self, app, window): assert isinstance(self.native, Form) async def wait_for_window( - self, message, minimize=False, full_screen=False, rapid_state_switching=False + self, + message, + minimize=False, + full_screen=False, + rapid_state_switching=False, ): await self.redraw(message) From 77e2b7e0da2518706cd4695e6c00c42fd1096a9e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 16 Sep 2024 08:06:09 -0400 Subject: [PATCH 187/248] Removed miniaturization delay from cocoa --- cocoa/src/toga_cocoa/window.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 77932fa3c1..3ef688143d 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -69,20 +69,20 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidMiniaturize_(self, notification) -> None: - self.performSelector( - SEL("enteredMiniaturize:"), withObject=None, afterDelay=0.1 - ) + # self.performSelector( + # SEL("enteredMiniaturize:"), withObject=None, afterDelay=0.1 + # ) - @objc_method - def enteredMiniaturize_(self, sender) -> None: - if self.impl: # pragma: no branch - if ( - self.impl._pending_state_transition - and self.impl._pending_state_transition != WindowState.MINIMIZED - ): - self.impl._apply_state(WindowState.NORMAL) - else: - self.impl._pending_state_transition = None + # @objc_method + # def enteredMiniaturize_(self, sender) -> None: + # if self.impl: # pragma: no branch + if ( + self.impl._pending_state_transition + and self.impl._pending_state_transition != WindowState.MINIMIZED + ): + self.impl._apply_state(WindowState.NORMAL) + else: + self.impl._pending_state_transition = None @objc_method def windowDidDeminiaturize_(self, notification) -> None: From 7db80d20f4c74096d81d223fa42c52afb69b540c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 16 Sep 2024 08:37:53 -0400 Subject: [PATCH 188/248] Removed miniaturization delay from cocoa --- cocoa/src/toga_cocoa/window.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 3ef688143d..e5618d2a27 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -49,7 +49,7 @@ def windowShouldClose_(self, notification) -> bool: @objc_method def windowWillClose_(self, notification) -> None: # Setting the toolbar delegate to None doesn't disconnect - # the delegates and still triggers the delegate events. + # the delegate and still triggers the delegate events. # Hence, simply remove the toolbar instead. if self.toolbar: self.toolbar = None @@ -57,9 +57,9 @@ def windowWillClose_(self, notification) -> None: self.delegate = None # Disconnecting the window delegate doesn't prevent custom - # methods like enteredMiniaturize_(which are performed with a - # delay i.e., having a delay != 0), from being triggered. - # Hence, check guards needs to be used in such custom methods. + # methods which are performed with a delay i.e., having a + # delay != 0), from being triggered. Hence, check guards + # needs to be used in such custom methods. @objc_method def windowDidResize_(self, notification) -> None: @@ -69,13 +69,6 @@ def windowDidResize_(self, notification) -> None: @objc_method def windowDidMiniaturize_(self, notification) -> None: - # self.performSelector( - # SEL("enteredMiniaturize:"), withObject=None, afterDelay=0.1 - # ) - - # @objc_method - # def enteredMiniaturize_(self, sender) -> None: - # if self.impl: # pragma: no branch if ( self.impl._pending_state_transition and self.impl._pending_state_transition != WindowState.MINIMIZED From aaee1f9e5ae2e9b547ddd99425094e585e7b2068 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 16 Sep 2024 08:59:58 -0400 Subject: [PATCH 189/248] Ensuring coverage on macOS --- cocoa/tests_backend/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 4078f1bc45..9736ddc5b4 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -34,7 +34,7 @@ async def wait_for_window( delay=( 1 if rapid_state_switching - else 0.75 if full_screen else 0.5 if minimize else 0.1 + else 0.75 if full_screen else 0.2 if minimize else 0.1 ), ) From 903860b664c4155f719b08a69cffbc8b02af0feb Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 16 Sep 2024 09:12:00 -0400 Subject: [PATCH 190/248] Ensuring coverage on macOS --- cocoa/tests_backend/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 9736ddc5b4..4078f1bc45 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -34,7 +34,7 @@ async def wait_for_window( delay=( 1 if rapid_state_switching - else 0.75 if full_screen else 0.2 if minimize else 0.1 + else 0.75 if full_screen else 0.5 if minimize else 0.1 ), ) From a6df392fac35c22a6fb04c8c94262d7e8596e74b Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 18 Sep 2024 22:39:54 -0400 Subject: [PATCH 191/248] Ensuring coverage on macOS --- testbed/tests/window/test_window.py | 48 ++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index c69a808a70..26259aea3d 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -1,6 +1,5 @@ import gc import itertools -import random import re import weakref from importlib import import_module @@ -689,11 +688,48 @@ async def test_move_and_resize(second_window, second_window_probe): assert second_window_probe.content_size == (250, 210) @pytest.fixture(scope="session") - def initial_intermediate_state(): + def intermediate_states_cycle(): # Use a iterator cycling fixture to ensure each state # is the initial intermediate state at least once. This # is to ensure full code coverage for all backends. - return itertools.cycle(list(WindowState)) + intermediate_states = [ + ( + WindowState.NORMAL, + WindowState.PRESENTATION, + WindowState.FULLSCREEN, + WindowState.MAXIMIZED, + WindowState.MINIMIZED, + ), + ( + WindowState.MINIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + WindowState.NORMAL, + WindowState.MAXIMIZED, + ), + ( + WindowState.MAXIMIZED, + WindowState.MINIMIZED, + WindowState.NORMAL, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ), + ( + WindowState.FULLSCREEN, + WindowState.MAXIMIZED, + WindowState.MINIMIZED, + WindowState.PRESENTATION, + WindowState.NORMAL, + ), + ( + WindowState.PRESENTATION, + WindowState.FULLSCREEN, + WindowState.NORMAL, + WindowState.MAXIMIZED, + WindowState.MINIMIZED, + ), + ] + return itertools.cycle(intermediate_states) @pytest.mark.parametrize( "initial_state, final_state", @@ -741,7 +777,7 @@ async def test_window_state_direct_change( final_state, second_window, second_window_probe, - initial_intermediate_state, + intermediate_states_cycle, ): """Window state can be directly changed to another state.""" if ( @@ -751,9 +787,7 @@ async def test_window_state_direct_change( pytest.xfail( "This backend doesn't reliably support minimized window state." ) - intermediate_states = [next(initial_intermediate_state)] + random.sample( - [state for state in WindowState], len(WindowState) - ) + intermediate_states = next(intermediate_states_cycle) second_window.toolbar.add(app.cmd1) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() From ad427abf0cc9814a6e141834eac892d6b7f26fb1 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 18 Sep 2024 23:11:22 -0400 Subject: [PATCH 192/248] Ensuring coverage on macOS --- cocoa/tests_backend/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 4078f1bc45..9736ddc5b4 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -34,7 +34,7 @@ async def wait_for_window( delay=( 1 if rapid_state_switching - else 0.75 if full_screen else 0.5 if minimize else 0.1 + else 0.75 if full_screen else 0.2 if minimize else 0.1 ), ) From 56de33dfdfb782908239ea388c4ac45f42917d24 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 19 Sep 2024 00:10:37 -0400 Subject: [PATCH 193/248] Ensuring coverage on macOS --- cocoa/tests_backend/window.py | 4 ++-- testbed/tests/conftest.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 9736ddc5b4..c9f6685441 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -32,9 +32,9 @@ async def wait_for_window( await self.redraw( message, delay=( - 1 + 0.75 if rapid_state_switching - else 0.75 if full_screen else 0.2 if minimize else 0.1 + else 0.75 if full_screen else 0.5 if minimize else 0.1 ), ) diff --git a/testbed/tests/conftest.py b/testbed/tests/conftest.py index ad0a9d085f..9f2925e542 100644 --- a/testbed/tests/conftest.py +++ b/testbed/tests/conftest.py @@ -89,6 +89,10 @@ async def window_cleanup(app, main_window): # minimize garbage collection on the test thread. gc.collect() + # After closing the window, the input focus might not be on main_window. + # Ensure that main_window will be in focus for other tests. + app.current_window = main_window + @fixture(scope="session") async def main_window_probe(app, main_window): From 59012cd001edf447c65d80b69c4b38f9f6a841e4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 19 Sep 2024 01:52:59 -0400 Subject: [PATCH 194/248] Ensuring coverage on macOS --- testbed/tests/app/test_desktop.py | 14 +++----------- testbed/tests/conftest.py | 4 ---- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 2447a6170e..0cf8cb89c6 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -176,11 +176,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) window_information_list = list() windows_list = list() for i in range(len(app.screens)): - window = toga.Window( - title=f"Test Window {i}", - position=(150 + (10 * i), 150 + (10 * i)), - size=(200, 200), - ) + window = toga.Window(title=f"Test Window {i}", size=(200, 200)) r = random.randint(0, 255) g = random.randint(0, 255) b = random.randint(0, 255) @@ -291,12 +287,8 @@ async def test_presentation_mode_exit_on_window_state_change( pytest.xfail("This backend doesn't reliably support WindowState.MINIMIZED.") try: - window1 = toga.Window( - title="Test Window 1", position=(150, 150), size=(200, 200) - ) - window2 = toga.Window( - title="Test Window 2", position=(160, 160), size=(200, 200) - ) + window1 = toga.Window(title="Test Window 1", size=(200, 200)) + window2 = toga.Window(title="Test Window 2", size=(200, 200)) window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) window1.show() diff --git a/testbed/tests/conftest.py b/testbed/tests/conftest.py index 9f2925e542..ad0a9d085f 100644 --- a/testbed/tests/conftest.py +++ b/testbed/tests/conftest.py @@ -89,10 +89,6 @@ async def window_cleanup(app, main_window): # minimize garbage collection on the test thread. gc.collect() - # After closing the window, the input focus might not be on main_window. - # Ensure that main_window will be in focus for other tests. - app.current_window = main_window - @fixture(scope="session") async def main_window_probe(app, main_window): From bb22a9f051e43f7e36576bc6d82fbcb770f708b8 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 25 Sep 2024 09:03:31 -0400 Subject: [PATCH 195/248] Ensuring coverage on macOS --- cocoa/tests_backend/window.py | 2 +- testbed/tests/window/test_window.py | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index c9f6685441..4078f1bc45 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -32,7 +32,7 @@ async def wait_for_window( await self.redraw( message, delay=( - 0.75 + 1 if rapid_state_switching else 0.75 if full_screen else 0.5 if minimize else 0.1 ), diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 3ad513edab..f9522c9751 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -691,12 +691,13 @@ async def test_move_and_resize(second_window, second_window_probe): @pytest.fixture(scope="session") def intermediate_states_cycle(): - # Use a iterator cycling fixture to ensure each state - # is the initial intermediate state at least once. This - # is to ensure full code coverage for all backends. + # Use a iterator cycling fixture to ensure each state is the initial + # intermediate state at least once. This is to ensure full code + # coverage for all backends. intermediate_states = [ ( WindowState.NORMAL, + WindowState.MINIMIZED, WindowState.PRESENTATION, WindowState.FULLSCREEN, WindowState.MAXIMIZED, @@ -707,6 +708,7 @@ def intermediate_states_cycle(): WindowState.FULLSCREEN, WindowState.PRESENTATION, WindowState.NORMAL, + WindowState.FULLSCREEN, WindowState.MAXIMIZED, ), ( @@ -714,6 +716,7 @@ def intermediate_states_cycle(): WindowState.MINIMIZED, WindowState.NORMAL, WindowState.FULLSCREEN, + WindowState.MINIMIZED, WindowState.PRESENTATION, ), ( @@ -721,6 +724,7 @@ def intermediate_states_cycle(): WindowState.MAXIMIZED, WindowState.MINIMIZED, WindowState.PRESENTATION, + WindowState.FULLSCREEN, WindowState.NORMAL, ), ( @@ -729,6 +733,7 @@ def intermediate_states_cycle(): WindowState.NORMAL, WindowState.MAXIMIZED, WindowState.MINIMIZED, + WindowState.FULLSCREEN, ), ] return itertools.cycle(intermediate_states) @@ -737,26 +742,31 @@ def intermediate_states_cycle(): "initial_state, final_state", [ # Direct switch from NORMAL: + (WindowState.NORMAL, WindowState.NORMAL), (WindowState.NORMAL, WindowState.MINIMIZED), (WindowState.NORMAL, WindowState.MAXIMIZED), (WindowState.NORMAL, WindowState.FULLSCREEN), (WindowState.NORMAL, WindowState.PRESENTATION), # Direct switch from MINIMIZED: + (WindowState.MINIMIZED, WindowState.MINIMIZED), (WindowState.MINIMIZED, WindowState.NORMAL), (WindowState.MINIMIZED, WindowState.MAXIMIZED), (WindowState.MINIMIZED, WindowState.FULLSCREEN), (WindowState.MINIMIZED, WindowState.PRESENTATION), # Direct switch from MAXIMIZED: + (WindowState.MAXIMIZED, WindowState.MAXIMIZED), (WindowState.MAXIMIZED, WindowState.NORMAL), (WindowState.MAXIMIZED, WindowState.MINIMIZED), (WindowState.MAXIMIZED, WindowState.FULLSCREEN), (WindowState.MAXIMIZED, WindowState.PRESENTATION), # Direct switch from FULLSCREEN: + (WindowState.FULLSCREEN, WindowState.FULLSCREEN), (WindowState.FULLSCREEN, WindowState.NORMAL), (WindowState.FULLSCREEN, WindowState.MINIMIZED), (WindowState.FULLSCREEN, WindowState.MAXIMIZED), (WindowState.FULLSCREEN, WindowState.PRESENTATION), # Direct switch from PRESENTATION: + (WindowState.PRESENTATION, WindowState.PRESENTATION), (WindowState.PRESENTATION, WindowState.NORMAL), (WindowState.PRESENTATION, WindowState.MINIMIZED), (WindowState.PRESENTATION, WindowState.MAXIMIZED), @@ -772,7 +782,7 @@ def intermediate_states_cycle(): ) ], ) - async def test_window_state_direct_change( + async def test_window_state_direct_change_with_intermediate_states( app, app_probe, initial_state, @@ -781,7 +791,8 @@ async def test_window_state_direct_change( second_window_probe, intermediate_states_cycle, ): - """Window state can be directly changed to another state.""" + """Window state can be directly changed to another state while passing + through intermediate states with an expected OS delay.""" if ( WindowState.MINIMIZED in {initial_state, final_state} and not second_window_probe.supports_minimize @@ -840,10 +851,11 @@ async def test_window_state_direct_change( ) ], ) - async def test_window_state_same_as_current( + async def test_window_state_same_as_current_without_intermediate_states( app_probe, second_window, second_window_probe, state ): - """Setting window state the same as current is a no-op.""" + """Setting window state the same as current without any intermediate states is + a no-op and there should be no expected delay from the OS.""" if state == WindowState.MINIMIZED and not second_window_probe.supports_minimize: pytest.xfail( "This backend doesn't reliably support minimized window state." From f6a851d2fcbcdfa25d076f9028e6e392f360d1fd Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 25 Sep 2024 09:16:19 -0400 Subject: [PATCH 196/248] Modified rapid state switching delay timing --- cocoa/tests_backend/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 4078f1bc45..46c7c8226f 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -32,7 +32,7 @@ async def wait_for_window( await self.redraw( message, delay=( - 1 + 1.5 if rapid_state_switching else 0.75 if full_screen else 0.5 if minimize else 0.1 ), From 35bbb8e11d12ff15f7e52a43fcf475a201f93e3d Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 25 Sep 2024 09:55:34 -0400 Subject: [PATCH 197/248] Resolve error --- testbed/tests/window/test_window.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index f9522c9751..5752144d1b 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -742,31 +742,26 @@ def intermediate_states_cycle(): "initial_state, final_state", [ # Direct switch from NORMAL: - (WindowState.NORMAL, WindowState.NORMAL), (WindowState.NORMAL, WindowState.MINIMIZED), (WindowState.NORMAL, WindowState.MAXIMIZED), (WindowState.NORMAL, WindowState.FULLSCREEN), (WindowState.NORMAL, WindowState.PRESENTATION), # Direct switch from MINIMIZED: - (WindowState.MINIMIZED, WindowState.MINIMIZED), (WindowState.MINIMIZED, WindowState.NORMAL), (WindowState.MINIMIZED, WindowState.MAXIMIZED), (WindowState.MINIMIZED, WindowState.FULLSCREEN), (WindowState.MINIMIZED, WindowState.PRESENTATION), # Direct switch from MAXIMIZED: - (WindowState.MAXIMIZED, WindowState.MAXIMIZED), (WindowState.MAXIMIZED, WindowState.NORMAL), (WindowState.MAXIMIZED, WindowState.MINIMIZED), (WindowState.MAXIMIZED, WindowState.FULLSCREEN), (WindowState.MAXIMIZED, WindowState.PRESENTATION), # Direct switch from FULLSCREEN: - (WindowState.FULLSCREEN, WindowState.FULLSCREEN), (WindowState.FULLSCREEN, WindowState.NORMAL), (WindowState.FULLSCREEN, WindowState.MINIMIZED), (WindowState.FULLSCREEN, WindowState.MAXIMIZED), (WindowState.FULLSCREEN, WindowState.PRESENTATION), # Direct switch from PRESENTATION: - (WindowState.PRESENTATION, WindowState.PRESENTATION), (WindowState.PRESENTATION, WindowState.NORMAL), (WindowState.PRESENTATION, WindowState.MINIMIZED), (WindowState.PRESENTATION, WindowState.MAXIMIZED), From 8bddf0e5b11a4c976dcdf6bd921469577ffce5c4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 25 Sep 2024 10:12:23 -0400 Subject: [PATCH 198/248] Ensuring coverage on macOS --- testbed/tests/window/test_window.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 5752144d1b..3c1e8e8c1b 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -746,26 +746,31 @@ def intermediate_states_cycle(): (WindowState.NORMAL, WindowState.MAXIMIZED), (WindowState.NORMAL, WindowState.FULLSCREEN), (WindowState.NORMAL, WindowState.PRESENTATION), + (WindowState.NORMAL, WindowState.NORMAL), # Direct switch from MINIMIZED: (WindowState.MINIMIZED, WindowState.NORMAL), (WindowState.MINIMIZED, WindowState.MAXIMIZED), (WindowState.MINIMIZED, WindowState.FULLSCREEN), (WindowState.MINIMIZED, WindowState.PRESENTATION), + (WindowState.MINIMIZED, WindowState.MINIMIZED), # Direct switch from MAXIMIZED: (WindowState.MAXIMIZED, WindowState.NORMAL), (WindowState.MAXIMIZED, WindowState.MINIMIZED), (WindowState.MAXIMIZED, WindowState.FULLSCREEN), (WindowState.MAXIMIZED, WindowState.PRESENTATION), + (WindowState.MAXIMIZED, WindowState.MAXIMIZED), # Direct switch from FULLSCREEN: (WindowState.FULLSCREEN, WindowState.NORMAL), (WindowState.FULLSCREEN, WindowState.MINIMIZED), (WindowState.FULLSCREEN, WindowState.MAXIMIZED), (WindowState.FULLSCREEN, WindowState.PRESENTATION), + (WindowState.FULLSCREEN, WindowState.FULLSCREEN), # Direct switch from PRESENTATION: (WindowState.PRESENTATION, WindowState.NORMAL), (WindowState.PRESENTATION, WindowState.MINIMIZED), (WindowState.PRESENTATION, WindowState.MAXIMIZED), (WindowState.PRESENTATION, WindowState.FULLSCREEN), + (WindowState.PRESENTATION, WindowState.PRESENTATION), ], ) @pytest.mark.parametrize( From 03dd538a74892bff9b7ea25c32e9a585f3bc6b4a Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:32:40 -0400 Subject: [PATCH 199/248] Update core/src/toga/window.py Co-authored-by: Russell Keith-Magee --- core/src/toga/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 12d3bb31c7..0e14263562 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -482,8 +482,8 @@ def state(self, state: WindowState) -> None: WindowState.FULLSCREEN, WindowState.PRESENTATION, }: - raise RuntimeError( - f"Cannot set window state to {state} of a non-resizable window." + raise ValueError( + f"A non-resizable window cannot be set to a state of {state}." ) else: # State checks are handled by the backend (e.g., Cocoa) to From 2af1509bec66263cc392d74585a0b4df539a9f2c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 25 Sep 2024 11:51:09 -0400 Subject: [PATCH 200/248] Modified to correct expected error in test --- core/tests/window/test_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index 4b02fc8ad8..d3c2302373 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -366,8 +366,8 @@ def test_non_resizable_window_state(state): """Non-resizable window's states other than minimized or normal are no-ops.""" non_resizable_window = toga.Window(title="Non-Resizable Window", resizable=False) with pytest.raises( - RuntimeError, - match=f"Cannot set window state to {state} of a non-resizable window.", + ValueError, + match=f"A non-resizable window cannot be set to a state of {state}.", ): non_resizable_window.state = state assert_action_not_performed( From 8b56d66b763bc6cfe4b94013cc26abc4869b6f20 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 27 Sep 2024 08:36:54 -0400 Subject: [PATCH 201/248] Clarified reasoning comment regarding state checks being performed at the backends --- core/src/toga/window.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 0e14263562..7b3f87ca26 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -487,9 +487,11 @@ def state(self, state: WindowState) -> None: ) else: # State checks are handled by the backend (e.g., Cocoa) to - # accommodate non-blocking OS calls. Performing these - # checks at the core level could interfere with transitions, - # resulting in incorrect assertions and potential glitches. + # accommodate non-blocking OS calls. Performing these checks + # at the core level could result in incorrect inferences, as + # non-blocking OS calls may not have completed at the time + # the core checks the current state. This could lead to incorrect + # assertions and potential glitches. self._impl.set_window_state(state) ###################################################################### From 7cd1489f525244ee8cdb68067994037bf56ad1d1 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 27 Sep 2024 09:05:29 -0400 Subject: [PATCH 202/248] Clarified reasoning comment regarding state checks being performed at the backends --- core/src/toga/window.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 7b3f87ca26..dbf52a3637 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -486,10 +486,10 @@ def state(self, state: WindowState) -> None: f"A non-resizable window cannot be set to a state of {state}." ) else: - # State checks are handled by the backend (e.g., Cocoa) to - # accommodate non-blocking OS calls. Performing these checks - # at the core level could result in incorrect inferences, as - # non-blocking OS calls may not have completed at the time + # State checks are handled by the backend to accommodate for + # non-blocking native OS calls(e.g., Cocoa). Performing these + # checks at the core level could result in incorrect inferences, + # as non-blocking OS calls may not have completed at the time # the core checks the current state. This could lead to incorrect # assertions and potential glitches. self._impl.set_window_state(state) From 598021fb5c8d3be2e6e893b45d1d714aa5979afc Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 27 Sep 2024 11:25:55 -0400 Subject: [PATCH 203/248] Modified testbed tests --- testbed/tests/app/test_desktop.py | 322 ++++++++++++++---------------- testbed/tests/conftest.py | 14 +- 2 files changed, 161 insertions(+), 175 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 0cf8cb89c6..f4f0f652c1 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -172,101 +172,86 @@ async def test_menu_minimize(app, app_probe): async def test_presentation_mode(app, app_probe, main_window, main_window_probe): """The app can enter presentation mode.""" - try: - window_information_list = list() - windows_list = list() - for i in range(len(app.screens)): - window = toga.Window(title=f"Test Window {i}", size=(200, 200)) - r = random.randint(0, 255) - g = random.randint(0, 255) - b = random.randint(0, 255) - window_widget = toga.Box(style=Pack(flex=1)) - window.content = toga.Box( - children=[window_widget], - style=Pack(background_color=f"#{r:02X}{g:02X}{b:02X}"), - ) - window.show() - # Add delay to ensure windows are visible after animation. - await main_window_probe.wait_for_window( - f"Test Window {i} is visible", full_screen=True - ) - - window_information = dict() - window_information["window"] = window - window_information["window_probe"] = window_probe(app, window) - window_information["initial_content_size"] = window_information[ - "window_probe" - ].presentation_content_size - window_information["widget_probe"] = get_probe(window_widget) - window_information["initial_widget_size"] = ( - window_information["widget_probe"].width, - window_information["widget_probe"].height, - ) - window_information_list.append(window_information) - windows_list.append(window) - - screen_window_dict = dict() - for window, screen in zip(windows_list, app.screens): - screen_window_dict[screen] = window - - # Enter presentation mode with a screen-window dict via the app - app.enter_presentation_mode(screen_window_dict) - # Add delay to ensure windows are visible after animation. - await main_window_probe.wait_for_window( - f"Test Window {i} is visible", full_screen=True + window_information_list = list() + windows_list = list() + for i in range(len(app.screens)): + window = toga.Window(title=f"Test Window {i}", size=(200, 200)) + r = random.randint(0, 255) + g = random.randint(0, 255) + b = random.randint(0, 255) + window_widget = toga.Box( + style=Pack(flex=1, background_color=f"#{r:02X}{g:02X}{b:02X}") ) - assert app.in_presentation_mode - # All the windows should be in presentation mode. - for window_information in window_information_list: - assert ( - window_information["window"].state == WindowState.PRESENTATION - ), f"{window_information['window'].title}:" - assert ( - window_information["window_probe"].presentation_content_size[0] > 1000 - ), f"{window_information['window'].title}:" - assert ( - window_information["window_probe"].presentation_content_size[1] > 700 - ), f"{window_information['window'].title}:" - assert ( - window_information["widget_probe"].width - > window_information["initial_widget_size"][0] - and window_information["widget_probe"].height - > window_information["initial_widget_size"][1] - ), f"{window_information['window'].title}:" - - # Exit presentation mode - app.exit_presentation_mode() - await main_window_probe.wait_for_window( - f"Test Window {i} is visible", full_screen=True + window.content = window_widget + window.show() + + window_information = dict() + window_information["window"] = window + window_information["window_probe"] = window_probe(app, window) + window_information["initial_content_size"] = window_information[ + "window_probe" + ].presentation_content_size + window_information["widget_probe"] = get_probe(window_widget) + window_information["initial_widget_size"] = ( + window_information["widget_probe"].width, + window_information["widget_probe"].height, ) + window_information_list.append(window_information) + windows_list.append(window) - assert not app.in_presentation_mode - for window_information in window_information_list: - assert ( - window_information["window"].state == WindowState.NORMAL - ), f"{window_information['window'].title}:" - assert ( - window_information["window_probe"].presentation_content_size - == window_information["initial_content_size"] - ), f"{window_information['window'].title}:" - assert ( - window_information["widget_probe"].width - == window_information["initial_widget_size"][0] - and window_information["widget_probe"].height - == window_information["initial_widget_size"][1] - ), f"{window_information['window'].title}:" + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window("All Test Windows are visible") - finally: - for window in windows_list: - window.close() - # After closing the window, the input focus might not be on main_window. - # Ensure that main_window will be in focus for other tests. - app.current_window = main_window - # Add delay to ensure windows are visible after animation. - await main_window_probe.wait_for_window( - f"Test Window {i} is visible", full_screen=True - ) - assert app.current_window == main_window + screen_window_dict = dict() + for window, screen in zip(windows_list, app.screens): + screen_window_dict[screen] = window + + # Enter presentation mode with a screen-window dict via the app + app.enter_presentation_mode(screen_window_dict) + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window( + "App is in presentation mode", full_screen=True + ) + assert app.in_presentation_mode + # All the windows should be in presentation mode. + for window_information in window_information_list: + assert ( + window_information["window"].state == WindowState.PRESENTATION + ), f"{window_information['window'].title}:" + assert ( + window_information["window_probe"].presentation_content_size[0] > 1000 + ), f"{window_information['window'].title}:" + assert ( + window_information["window_probe"].presentation_content_size[1] > 700 + ), f"{window_information['window'].title}:" + assert ( + window_information["widget_probe"].width + > window_information["initial_widget_size"][0] + and window_information["widget_probe"].height + > window_information["initial_widget_size"][1] + ), f"{window_information['window'].title}:" + + # Exit presentation mode + app.exit_presentation_mode() + await main_window_probe.wait_for_window( + "App is not in presentation mode", full_screen=True + ) + + assert not app.in_presentation_mode + for window_information in window_information_list: + assert ( + window_information["window"].state == WindowState.NORMAL + ), f"{window_information['window'].title}:" + assert ( + window_information["window_probe"].presentation_content_size + == window_information["initial_content_size"] + ), f"{window_information['window'].title}:" + assert ( + window_information["widget_probe"].width + == window_information["initial_widget_size"][0] + and window_information["widget_probe"].height + == window_information["initial_widget_size"][1] + ), f"{window_information['window'].title}:" @pytest.mark.parametrize( @@ -286,93 +271,82 @@ async def test_presentation_mode_exit_on_window_state_change( ): pytest.xfail("This backend doesn't reliably support WindowState.MINIMIZED.") - try: - window1 = toga.Window(title="Test Window 1", size=(200, 200)) - window2 = toga.Window(title="Test Window 2", size=(200, 200)) - window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) - window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - window1.show() - window2.show() - # Add delay to ensure windows are visible after animation. - await main_window_probe.wait_for_window("Test windows are shown") - # Enter presentation mode - app.enter_presentation_mode([window1]) - # Add delay to ensure windows are visible after animation. - await main_window_probe.wait_for_window( - "App is in presentation mode", full_screen=True - ) - - assert app.in_presentation_mode - assert window1.state == WindowState.PRESENTATION - - # Changing window state of main window should make the app exit presentation mode. - window1.state = new_window_state - # Add delay to ensure windows are visible after animation. - await main_window_probe.wait_for_window( - "App is not in presentation mode" - f"\nTest Window 1 is in {new_window_state}", - minimize=True if new_window_state == WindowState.MINIMIZED else False, - full_screen=True if new_window_state == WindowState.FULLSCREEN else False, - ) + window1 = toga.Window(title="Test Window 1", size=(200, 200)) + window2 = toga.Window(title="Test Window 2", size=(200, 200)) + window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) + window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + window1.show() + window2.show() + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window("Test windows are shown") + # Enter presentation mode + app.enter_presentation_mode([window1]) + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window( + "App is in presentation mode", full_screen=True + ) - assert not app.in_presentation_mode - assert window1.state == new_window_state - - # Reset window states - window1.state = WindowState.NORMAL - window2.state = WindowState.NORMAL - # Add delay to ensure windows are visible after animation. - await main_window_probe.wait_for_window( - "All test windows are in WindowState.NORMAL", - minimize=True if new_window_state == WindowState.MINIMIZED else False, - full_screen=True if new_window_state == WindowState.FULLSCREEN else False, - ) + assert app.in_presentation_mode + assert window1.state == WindowState.PRESENTATION - # Enter presentation mode again - app.enter_presentation_mode([window1]) - # Add delay to ensure windows are visible after animation. - await main_window_probe.wait_for_window( - "App is in presentation mode", - minimize=True if new_window_state == WindowState.MINIMIZED else False, - full_screen=True if new_window_state == WindowState.FULLSCREEN else False, - ) - assert app.in_presentation_mode - assert window1.state == WindowState.PRESENTATION - - # Changing window state of extra window should make the app exit presentation mode. - window2.state = new_window_state - # Add delay to ensure windows are visible after animation. - await main_window_probe.wait_for_window( - "App is not in presentation mode" - f"\nTest Window 2 is in {new_window_state}", - minimize=True if new_window_state == WindowState.MINIMIZED else False, - full_screen=True if new_window_state == WindowState.FULLSCREEN else False, - ) + # Changing window state of main window should make the app exit presentation mode. + window1.state = new_window_state + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window( + "App is not in presentation mode" f"\nTest Window 1 is in {new_window_state}", + minimize=True if new_window_state == WindowState.MINIMIZED else False, + full_screen=True if new_window_state == WindowState.FULLSCREEN else False, + ) - assert not app.in_presentation_mode - assert window2.state == new_window_state - - # Reset window states - window1.state = WindowState.NORMAL - window2.state = WindowState.NORMAL - # Add delay for gtk to show the windows - await main_window_probe.wait_for_window( - "All test windows are in WindowState.NORMAL", - minimize=True if new_window_state == WindowState.MINIMIZED else False, - full_screen=True if new_window_state == WindowState.FULLSCREEN else False, - ) + assert not app.in_presentation_mode + assert window1.state == new_window_state + + # Reset window states + window1.state = WindowState.NORMAL + window2.state = WindowState.NORMAL + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window( + "All test windows are in WindowState.NORMAL", + minimize=True if new_window_state == WindowState.MINIMIZED else False, + full_screen=True if new_window_state == WindowState.FULLSCREEN else False, + ) + assert window1.state == WindowState.NORMAL + assert window2.state == WindowState.NORMAL + + # Enter presentation mode again + app.enter_presentation_mode([window1]) + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window( + "App is in presentation mode", + minimize=True if new_window_state == WindowState.MINIMIZED else False, + full_screen=True if new_window_state == WindowState.FULLSCREEN else False, + ) + assert app.in_presentation_mode + assert window1.state == WindowState.PRESENTATION + + # Changing window state of extra window should make the app exit presentation mode. + window2.state = new_window_state + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window( + "App is not in presentation mode" f"\nTest Window 2 is in {new_window_state}", + minimize=True if new_window_state == WindowState.MINIMIZED else False, + full_screen=True if new_window_state == WindowState.FULLSCREEN else False, + ) - finally: - window1.close() - window2.close() - # After closing the window, the input focus might not be on main_window. - # Ensure that main_window will be in focus for other tests. - app.current_window = main_window - # Add delay to ensure windows are visible after animation. - await main_window_probe.wait_for_window( - "main_window is now the current window", full_screen=True - ) - assert app.current_window == main_window + assert not app.in_presentation_mode + assert window2.state == new_window_state + + # Reset window states + window1.state = WindowState.NORMAL + window2.state = WindowState.NORMAL + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window( + "All test windows are in WindowState.NORMAL", + minimize=True if new_window_state == WindowState.MINIMIZED else False, + full_screen=True if new_window_state == WindowState.FULLSCREEN else False, + ) + assert window1.state == WindowState.NORMAL + assert window2.state == WindowState.NORMAL async def test_show_hide_cursor(app, app_probe): diff --git a/testbed/tests/conftest.py b/testbed/tests/conftest.py index ad0a9d085f..435fef1e05 100644 --- a/testbed/tests/conftest.py +++ b/testbed/tests/conftest.py @@ -8,6 +8,7 @@ import toga from toga.colors import GOLDENROD +from toga.constants import WindowState from toga.style import Pack # Ideally, we'd register rewrites for "tests" and get all the submodules @@ -70,7 +71,18 @@ def main_window(app): @fixture(autouse=True) -async def window_cleanup(app, main_window): +async def window_cleanup(app, main_window, main_window_probe): + # After closing the window, the input focus might not be on main_window. + # Ensure that main_window is in NORMAL state and will be in focus for + # other tests. + app.current_window = main_window + main_window.state = WindowState.NORMAL + await main_window_probe.wait_for_window( + "main_window is now the current window and is in NORMAL state" + ) + assert app.current_window == main_window + assert main_window.state == WindowState.NORMAL + # Ensure that at the end of every test, all windows that aren't the # main window have been closed and deleted. This needs to be done in # 2 passes because we can't modify the list while iterating over it. From 596acc405242adb474e9685cabe1e66ba9d9043e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 29 Sep 2024 06:41:20 -0400 Subject: [PATCH 204/248] Removed unused probe method from backends --- cocoa/tests_backend/app.py | 4 ---- cocoa/tests_backend/window.py | 11 ++--------- gtk/tests_backend/app.py | 4 ---- gtk/tests_backend/window.py | 4 ---- testbed/tests/app/test_desktop.py | 8 ++++---- testbed/tests/window/test_window.py | 20 ++++++-------------- winforms/tests_backend/app.py | 4 ---- winforms/tests_backend/window.py | 4 ---- 8 files changed, 12 insertions(+), 47 deletions(-) diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index 30caa3f654..e93d568463 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -15,7 +15,6 @@ from .dialogs import DialogsMixin from .probe import BaseProbe, NSRunLoop -from .window import WindowProbe NSPanel = ObjCClass("NSPanel") NSDate = ObjCClass("NSDate") @@ -56,9 +55,6 @@ def is_cursor_visible(self): # fall back to the implementation's proxy variable. return self.app._impl._cursor_visible - def content_size(self, window): - return WindowProbe(self.app, window).presentation_content_size - def assert_app_icon(self, icon): # We have no real way to check we've got the right icon; use pixel peeping as a # guess. Construct a PIL image from the current icon. diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 46c7c8226f..838c8255c1 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -44,15 +44,8 @@ def close(self): @property def content_size(self): return ( - self.native.contentView.frame.size.width, - self.native.contentView.frame.size.height, - ) - - @property - def presentation_content_size(self): - return ( - self.window.content._impl.native.frame.size.width, - self.window.content._impl.native.frame.size.height, + self.impl.container.native.frame.size.width, + self.impl.container.native.frame.size.height, ) @property diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index bbb180e725..4164fed26b 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -10,7 +10,6 @@ from .dialogs import DialogsMixin from .probe import BaseProbe -from .window import WindowProbe class AppProbe(BaseProbe, DialogsMixin): @@ -47,9 +46,6 @@ def logs_path(self): def is_cursor_visible(self): pytest.skip("Cursor visibility not implemented on GTK") - def content_size(self, window): - return WindowProbe(self.app, window).presentation_content_size - def assert_app_icon(self, icon): for window in self.app.windows: # We have no real way to check we've got the right icon; use pixel peeping as a diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index 6b1f188735..a41416bf7a 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -45,10 +45,6 @@ def content_size(self): content_allocation = self.impl.container.get_allocation() return (content_allocation.width, content_allocation.height) - @property - def presentation_content_size(self): - return self.content_size - @property def is_resizable(self): return self.native.get_resizable() diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index f4f0f652c1..74a355f4f3 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -190,7 +190,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) window_information["window_probe"] = window_probe(app, window) window_information["initial_content_size"] = window_information[ "window_probe" - ].presentation_content_size + ].content_size window_information["widget_probe"] = get_probe(window_widget) window_information["initial_widget_size"] = ( window_information["widget_probe"].width, @@ -219,10 +219,10 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) window_information["window"].state == WindowState.PRESENTATION ), f"{window_information['window'].title}:" assert ( - window_information["window_probe"].presentation_content_size[0] > 1000 + window_information["window_probe"].content_size[0] > 1000 ), f"{window_information['window'].title}:" assert ( - window_information["window_probe"].presentation_content_size[1] > 700 + window_information["window_probe"].content_size[1] > 700 ), f"{window_information['window'].title}:" assert ( window_information["widget_probe"].width @@ -243,7 +243,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) window_information["window"].state == WindowState.NORMAL ), f"{window_information['window'].title}:" assert ( - window_information["window_probe"].presentation_content_size + window_information["window_probe"].content_size == window_information["initial_content_size"] ), f"{window_information['window'].title}:" assert ( diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 3c1e8e8c1b..4dff6fe615 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -1032,7 +1032,7 @@ async def test_window_state_presentation( assert second_window.state == WindowState.NORMAL assert second_window_probe.is_resizable - initial_content_size = second_window_probe.presentation_content_size + initial_content_size = second_window_probe.content_size second_window.state = WindowState.PRESENTATION # Add delay to ensure windows are visible after animation. @@ -1040,24 +1040,16 @@ async def test_window_state_presentation( "Secondary window is in presentation mode", full_screen=True ) assert second_window.state == WindowState.PRESENTATION - assert ( - second_window_probe.presentation_content_size[0] > initial_content_size[0] - ) - assert ( - second_window_probe.presentation_content_size[1] > initial_content_size[1] - ) + assert second_window_probe.content_size[0] > initial_content_size[0] + assert second_window_probe.content_size[1] > initial_content_size[1] second_window.state = WindowState.PRESENTATION await second_window_probe.wait_for_window( "Secondary window is still in presentation mode", full_screen=True ) assert second_window.state == WindowState.PRESENTATION - assert ( - second_window_probe.presentation_content_size[0] > initial_content_size[0] - ) - assert ( - second_window_probe.presentation_content_size[1] > initial_content_size[1] - ) + assert second_window_probe.content_size[0] > initial_content_size[0] + assert second_window_probe.content_size[1] > initial_content_size[1] second_window.state = WindowState.NORMAL # Add delay to ensure windows are visible after animation. @@ -1066,7 +1058,7 @@ async def test_window_state_presentation( ) assert second_window.state == WindowState.NORMAL assert second_window_probe.is_resizable - assert second_window_probe.presentation_content_size == initial_content_size + assert second_window_probe.content_size == initial_content_size @pytest.mark.parametrize( "second_window_class, second_window_kwargs", diff --git a/winforms/tests_backend/app.py b/winforms/tests_backend/app.py index 76da704bec..7b36016ba1 100644 --- a/winforms/tests_backend/app.py +++ b/winforms/tests_backend/app.py @@ -13,7 +13,6 @@ from .dialogs import DialogsMixin from .probe import BaseProbe -from .window import WindowProbe class AppProbe(BaseProbe, DialogsMixin): @@ -101,9 +100,6 @@ class CURSORINFO(ctypes.Structure): # input through touch or pen instead of the mouse"). hCursor is more reliable. return info.hCursor is not None - def content_size(self, window): - return WindowProbe(self.app, window).presentation_content_size - def assert_app_icon(self, icon): for window in self.app.windows: # We have no real way to check we've got the right icon; use pixel peeping as a diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index b00d12bd67..4190a54cac 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -53,10 +53,6 @@ def content_size(self): ), ) - @property - def presentation_content_size(self): - return self.content_size - @property def is_resizable(self): return self.native.FormBorderStyle == FormBorderStyle.Sizable From d3f90bad8601a6469dcba29a9b95ebbc7d8e9308 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 29 Sep 2024 07:02:10 -0400 Subject: [PATCH 205/248] Removed repeated tests --- testbed/tests/conftest.py | 7 +- testbed/tests/window/test_window.py | 155 ++++------------------------ 2 files changed, 23 insertions(+), 139 deletions(-) diff --git a/testbed/tests/conftest.py b/testbed/tests/conftest.py index 435fef1e05..2df8bb95c0 100644 --- a/testbed/tests/conftest.py +++ b/testbed/tests/conftest.py @@ -71,14 +71,17 @@ def main_window(app): @fixture(autouse=True) -async def window_cleanup(app, main_window, main_window_probe): +async def window_cleanup(app, app_probe, main_window, main_window_probe): # After closing the window, the input focus might not be on main_window. # Ensure that main_window is in NORMAL state and will be in focus for # other tests. app.current_window = main_window + main_window_state = main_window.state main_window.state = WindowState.NORMAL await main_window_probe.wait_for_window( - "main_window is now the current window and is in NORMAL state" + "main_window is now the current window and is in NORMAL state", + minimize=True if main_window_state == WindowState.MINIMIZED else False, + full_screen=True if main_window_state == WindowState.FULLSCREEN else False, ) assert app.current_window == main_window assert main_window.state == WindowState.NORMAL diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 4dff6fe615..e85bc7b7e6 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -882,136 +882,13 @@ async def test_window_state_same_as_current_without_intermediate_states( assert second_window.state == state @pytest.mark.parametrize( - "second_window_class, second_window_kwargs", - [ - ( - toga.Window, - dict(title="Secondary Window", position=(200, 150)), - ) - ], - ) - async def test_window_state_minimized(second_window, second_window_probe): - """Window can have minimized window state.""" - if not second_window_probe.supports_minimize: - pytest.xfail( - "This backend doesn't reliably support minimized window state." - ) - - second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - second_window.show() - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window("Secondary window is shown") - - assert second_window.state == WindowState.NORMAL - if second_window_probe.supports_minimizable: - assert second_window_probe.is_minimizable - - second_window.state = WindowState.MINIMIZED - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is minimized", minimize=True - ) - assert second_window.state == WindowState.MINIMIZED - - second_window.state = WindowState.MINIMIZED - await second_window_probe.wait_for_window("Secondary window is still minimized") - assert second_window.state == WindowState.MINIMIZED - - second_window.state = WindowState.NORMAL - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is not minimized", minimize=True - ) - assert second_window.state == WindowState.NORMAL - if second_window_probe.supports_minimizable: - assert second_window_probe.is_minimizable - - @pytest.mark.parametrize( - "second_window_class, second_window_kwargs", - [ - ( - toga.Window, - dict(title="Secondary Window", position=(200, 150)), - ) - ], - ) - async def test_window_state_maximized(second_window, second_window_probe): - """Window can have maximized window state.""" - second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - second_window.show() - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window("Secondary window is shown") - - assert second_window.state == WindowState.NORMAL - assert second_window_probe.is_resizable - initial_content_size = second_window_probe.content_size - - second_window.state = WindowState.MAXIMIZED - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window("Secondary window is maximized") - assert second_window.state == WindowState.MAXIMIZED - assert second_window_probe.content_size[0] > initial_content_size[0] - assert second_window_probe.content_size[1] > initial_content_size[1] - - second_window.state = WindowState.MAXIMIZED - await second_window_probe.wait_for_window("Secondary window is still maximized") - assert second_window.state == WindowState.MAXIMIZED - assert second_window_probe.content_size[0] > initial_content_size[0] - assert second_window_probe.content_size[1] > initial_content_size[1] - - second_window.state = WindowState.NORMAL - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window("Secondary window is not maximized") - assert second_window.state == WindowState.NORMAL - assert second_window_probe.is_resizable - assert second_window_probe.content_size == initial_content_size - - @pytest.mark.parametrize( - "second_window_class, second_window_kwargs", + "state", [ - ( - toga.Window, - dict(title="Secondary Window", position=(200, 150)), - ) + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, ], ) - async def test_window_state_full_screen(second_window, second_window_probe): - """Window can have full screen window state.""" - second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - second_window.show() - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window("Secondary window is shown") - - assert second_window.state == WindowState.NORMAL - assert second_window_probe.is_resizable - initial_content_size = second_window_probe.content_size - - second_window.state = WindowState.FULLSCREEN - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is full screen", full_screen=True - ) - assert second_window.state == WindowState.FULLSCREEN - assert second_window_probe.content_size[0] > initial_content_size[0] - assert second_window_probe.content_size[1] > initial_content_size[1] - - second_window.state = WindowState.FULLSCREEN - await second_window_probe.wait_for_window( - "Secondary window is still full screen", full_screen=True - ) - assert second_window.state == WindowState.FULLSCREEN - assert second_window_probe.content_size[0] > initial_content_size[0] - assert second_window_probe.content_size[1] > initial_content_size[1] - - second_window.state = WindowState.NORMAL - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Secondary window is not full screen", full_screen=True - ) - assert second_window.state == WindowState.NORMAL - assert second_window_probe.is_resizable - assert second_window_probe.content_size == initial_content_size - @pytest.mark.parametrize( "second_window_class, second_window_kwargs", [ @@ -1021,10 +898,11 @@ async def test_window_state_full_screen(second_window, second_window_probe): ) ], ) - async def test_window_state_presentation( - second_window, second_window_probe, app_probe + async def test_window_state_content_size_increase( + second_window, second_window_probe, state ): - """Window can have presentation window state.""" + """The size of window content should increase when the window state is set to + maximized, fullscreen or presentation.""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() # Add delay to ensure windows are visible after animation. @@ -1034,27 +912,30 @@ async def test_window_state_presentation( assert second_window_probe.is_resizable initial_content_size = second_window_probe.content_size - second_window.state = WindowState.PRESENTATION + second_window.state = state # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( - "Secondary window is in presentation mode", full_screen=True + f"Secondary window is in {state}", + full_screen=True if state == WindowState.FULLSCREEN else False, ) - assert second_window.state == WindowState.PRESENTATION + assert second_window.state == state assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] - second_window.state = WindowState.PRESENTATION + second_window.state = state await second_window_probe.wait_for_window( - "Secondary window is still in presentation mode", full_screen=True + f"Secondary window is still in {state}", + full_screen=True if state == WindowState.FULLSCREEN else False, ) - assert second_window.state == WindowState.PRESENTATION + assert second_window.state == state assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] second_window.state = WindowState.NORMAL # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window( - "Secondary window is not in presentation mode", full_screen=True + f"Secondary window is not in {state}", + full_screen=True if state == WindowState.FULLSCREEN else False, ) assert second_window.state == WindowState.NORMAL assert second_window_probe.is_resizable From 945caf53a7e8cc84b84a74c8692467282e8808aa Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 29 Sep 2024 07:13:24 -0400 Subject: [PATCH 206/248] Removed repeated tests --- testbed/tests/conftest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/testbed/tests/conftest.py b/testbed/tests/conftest.py index 2df8bb95c0..f4d0b70857 100644 --- a/testbed/tests/conftest.py +++ b/testbed/tests/conftest.py @@ -83,8 +83,6 @@ async def window_cleanup(app, app_probe, main_window, main_window_probe): minimize=True if main_window_state == WindowState.MINIMIZED else False, full_screen=True if main_window_state == WindowState.FULLSCREEN else False, ) - assert app.current_window == main_window - assert main_window.state == WindowState.NORMAL # Ensure that at the end of every test, all windows that aren't the # main window have been closed and deleted. This needs to be done in From 116eec3840740d091baa325928b2b9f49cf8073e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 29 Sep 2024 07:46:28 -0400 Subject: [PATCH 207/248] Checking for coverage issues on macOS --- testbed/tests/window/test_window.py | 39 ++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index e85bc7b7e6..827435b70c 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -741,31 +741,31 @@ def intermediate_states_cycle(): @pytest.mark.parametrize( "initial_state, final_state", [ - # Direct switch from NORMAL: + # Switch from NORMAL: (WindowState.NORMAL, WindowState.MINIMIZED), (WindowState.NORMAL, WindowState.MAXIMIZED), (WindowState.NORMAL, WindowState.FULLSCREEN), (WindowState.NORMAL, WindowState.PRESENTATION), (WindowState.NORMAL, WindowState.NORMAL), - # Direct switch from MINIMIZED: + # Switch from MINIMIZED: (WindowState.MINIMIZED, WindowState.NORMAL), (WindowState.MINIMIZED, WindowState.MAXIMIZED), (WindowState.MINIMIZED, WindowState.FULLSCREEN), (WindowState.MINIMIZED, WindowState.PRESENTATION), (WindowState.MINIMIZED, WindowState.MINIMIZED), - # Direct switch from MAXIMIZED: + # Switch from MAXIMIZED: (WindowState.MAXIMIZED, WindowState.NORMAL), (WindowState.MAXIMIZED, WindowState.MINIMIZED), (WindowState.MAXIMIZED, WindowState.FULLSCREEN), (WindowState.MAXIMIZED, WindowState.PRESENTATION), (WindowState.MAXIMIZED, WindowState.MAXIMIZED), - # Direct switch from FULLSCREEN: + # Switch from FULLSCREEN: (WindowState.FULLSCREEN, WindowState.NORMAL), (WindowState.FULLSCREEN, WindowState.MINIMIZED), (WindowState.FULLSCREEN, WindowState.MAXIMIZED), (WindowState.FULLSCREEN, WindowState.PRESENTATION), (WindowState.FULLSCREEN, WindowState.FULLSCREEN), - # Direct switch from PRESENTATION: + # Switch from PRESENTATION: (WindowState.PRESENTATION, WindowState.NORMAL), (WindowState.PRESENTATION, WindowState.MINIMIZED), (WindowState.PRESENTATION, WindowState.MAXIMIZED), @@ -782,7 +782,7 @@ def intermediate_states_cycle(): ) ], ) - async def test_window_state_direct_change_with_intermediate_states( + async def test_window_state_change_with_intermediate_states( app, app_probe, initial_state, @@ -791,7 +791,7 @@ async def test_window_state_direct_change_with_intermediate_states( second_window_probe, intermediate_states_cycle, ): - """Window state can be directly changed to another state while passing + """Window state can be changed to another state while passing through intermediate states with an expected OS delay.""" if ( WindowState.MINIMIZED in {initial_state, final_state} @@ -901,8 +901,8 @@ async def test_window_state_same_as_current_without_intermediate_states( async def test_window_state_content_size_increase( second_window, second_window_probe, state ): - """The size of window content should increase when the window state is set to - maximized, fullscreen or presentation.""" + """The size of the window content should increase when the window state is set + to maximized, fullscreen or presentation.""" second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() # Add delay to ensure windows are visible after animation. @@ -941,6 +941,27 @@ async def test_window_state_content_size_increase( assert second_window_probe.is_resizable assert second_window_probe.content_size == initial_content_size + @pytest.mark.parametrize( + "second_window_class, second_window_kwargs", + [ + ( + toga.Window, + dict(title="Secondary Window", position=(200, 150)), + ) + ], + ) + async def test_coverage(second_window, second_window_probe): + second_window.show() + await second_window_probe.wait_for_window("Second window is shown") + second_window.state = WindowState.MINIMIZED + second_window.state = WindowState.FULLSCREEN + second_window.state = WindowState.MINIMIZED + second_window.state = WindowState.FULLSCREEN + # Add delay to ensure windows are visible after animation. + await second_window_probe.wait_for_window( + "Testing coverage", rapid_state_switching=True + ) + @pytest.mark.parametrize( "second_window_class, second_window_kwargs", [ From 71aec38816523a387379792284cf5888705895aa Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 29 Sep 2024 08:36:09 -0400 Subject: [PATCH 208/248] Added no-cover on cocoa --- cocoa/src/toga_cocoa/window.py | 4 +++- testbed/tests/window/test_window.py | 33 +++++++++-------------------- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index e5618d2a27..af8986f6e4 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -73,7 +73,9 @@ def windowDidMiniaturize_(self, notification) -> None: self.impl._pending_state_transition and self.impl._pending_state_transition != WindowState.MINIMIZED ): - self.impl._apply_state(WindowState.NORMAL) + # Marking as no cover, since the operation is native OS delay + # dependent and this doesn't get covered under macOS CI. + self.impl._apply_state(WindowState.NORMAL) # pragma: no cover else: self.impl._pending_state_transition = None diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 827435b70c..40c8f95987 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -692,8 +692,8 @@ async def test_move_and_resize(second_window, second_window_probe): @pytest.fixture(scope="session") def intermediate_states_cycle(): # Use a iterator cycling fixture to ensure each state is the initial - # intermediate state at least once. This is to ensure full code - # coverage for all backends. + # intermediate state at least once and to test specific problematic + # combinations. This is to ensure full code coverage for all backends. intermediate_states = [ ( WindowState.NORMAL, @@ -735,6 +735,14 @@ def intermediate_states_cycle(): WindowState.MINIMIZED, WindowState.FULLSCREEN, ), + ( + WindowState.MINIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + WindowState.MAXIMIZED, + WindowState.MINIMIZED, + WindowState.FULLSCREEN, + ), ] return itertools.cycle(intermediate_states) @@ -941,27 +949,6 @@ async def test_window_state_content_size_increase( assert second_window_probe.is_resizable assert second_window_probe.content_size == initial_content_size - @pytest.mark.parametrize( - "second_window_class, second_window_kwargs", - [ - ( - toga.Window, - dict(title="Secondary Window", position=(200, 150)), - ) - ], - ) - async def test_coverage(second_window, second_window_probe): - second_window.show() - await second_window_probe.wait_for_window("Second window is shown") - second_window.state = WindowState.MINIMIZED - second_window.state = WindowState.FULLSCREEN - second_window.state = WindowState.MINIMIZED - second_window.state = WindowState.FULLSCREEN - # Add delay to ensure windows are visible after animation. - await second_window_probe.wait_for_window( - "Testing coverage", rapid_state_switching=True - ) - @pytest.mark.parametrize( "second_window_class, second_window_kwargs", [ From 2559fa015efd7e22fe1b83ece2328df3b7b0b0df Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 30 Sep 2024 08:09:50 -0400 Subject: [PATCH 209/248] Improved tests --- core/tests/app/test_app.py | 88 +++++++++++++++----------------- core/tests/window/test_window.py | 29 ++++++----- dummy/src/toga_dummy/window.py | 5 +- 3 files changed, 62 insertions(+), 60 deletions(-) diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index f7244baa0f..112cc80981 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -469,7 +469,6 @@ def startup(self): @pytest.mark.parametrize( "windows", [ - [], # No windows [{}], # One window [{}, {}], # Two windows ], @@ -481,35 +480,28 @@ def test_presentation_mode_with_windows_list(event_loop, windows): assert not app.in_presentation_mode - if not windows_list: - # Entering presentation mode with an empty windows list, is a no-op: - app.enter_presentation_mode(windows_list) - assert not app.in_presentation_mode - assert_action_not_performed(app, "enter presentation mode") - else: - # Enter presentation mode with 1 or more windows: - app.enter_presentation_mode(windows_list) - assert app.in_presentation_mode - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={ - app.screens[i]: window for i, window in enumerate(windows_list) - }, - ) - # Exit presentation mode: - app.exit_presentation_mode() - assert not app.in_presentation_mode - assert_action_performed( - app, - "exit presentation mode", - ) + # Enter presentation mode with 1 or more windows: + app.enter_presentation_mode(windows_list) + assert app.in_presentation_mode + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict={ + app.screens[i]: window for i, window in enumerate(windows_list) + }, + ) + # Exit presentation mode: + app.exit_presentation_mode() + assert not app.in_presentation_mode + assert_action_performed( + app, + "exit presentation mode", + ) @pytest.mark.parametrize( "windows", [ - [], # No windows [{}], # One window [{}, {}], # Two windows ], @@ -523,27 +515,21 @@ def test_presentation_mode_with_screen_window_dict(event_loop, windows): assert not app.in_presentation_mode - if not screen_window_dict: - # Entering presentation mode with an empty dict, is a no-op: - app.enter_presentation_mode({}) - assert not app.in_presentation_mode - assert_action_not_performed(app, "enter presentation mode") - else: - # Enter presentation mode with a 1 or more elements screen-window dict: - app.enter_presentation_mode(screen_window_dict) - assert app.in_presentation_mode - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict=screen_window_dict, - ) - # Exit presentation mode: - app.exit_presentation_mode() - assert not app.in_presentation_mode - assert_action_performed( - app, - "exit presentation mode", - ) + # Enter presentation mode with a 1 or more elements screen-window dict: + app.enter_presentation_mode(screen_window_dict) + assert app.in_presentation_mode + assert_action_performed_with( + app, + "enter presentation mode", + screen_window_dict=screen_window_dict, + ) + # Exit presentation mode: + app.exit_presentation_mode() + assert not app.in_presentation_mode + assert_action_performed( + app, + "exit presentation mode", + ) def test_presentation_mode_with_excess_windows_list(event_loop): @@ -617,6 +603,16 @@ def test_presentation_mode_no_op(event_loop): assert_action_not_performed(app, "enter presentation mode") assert not app.in_presentation_mode + # Entering presentation mode with an empty dict, is a no-op: + app.enter_presentation_mode({}) + assert not app.in_presentation_mode + assert_action_not_performed(app, "enter presentation mode") + + # Entering presentation mode with an empty windows list, is a no-op: + app.enter_presentation_mode([]) + assert not app.in_presentation_mode + assert_action_not_performed(app, "enter presentation mode") + # Entering presentation mode without proper type of parameter is a no-op. with pytest.raises( ValueError, diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index d3c2302373..44251aff4a 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -6,6 +6,7 @@ import toga from toga.constants import WindowState from toga_dummy.utils import ( + EventLog, assert_action_not_performed, assert_action_performed, assert_action_performed_with, @@ -339,11 +340,16 @@ def test_window_state(window, initial_state, final_state): window.state = initial_state assert window.state == initial_state - assert_action_performed_with( - window, - f"set window state to {initial_state}", - state=initial_state, - ) + # A newly created window will always be in NORMAL state. + # Since, both the current state and initial_state, would + # be the same, hence "set window state to WindowState.NORMAL" + # action would not be performed again. + if initial_state != WindowState.NORMAL: + assert_action_performed_with( + window, + f"set window state to {initial_state}", + state=initial_state, + ) window.state = final_state assert window.state == final_state @@ -1282,12 +1288,10 @@ def test_deprecated_full_screen(window, app): state=WindowState.NORMAL, ) - # Setting full screen to False when window is not in full screen, is a no-op - # - # We cannot assert that the action was not performed, since the same action - # was performed previously and would still be in EventLog. Therefore, we - # cannot check if the action was done by the first call or the second call. - # Hence, this is just to reach coverage. + # Clear the test event log to check that the previous task was not re-performed. + EventLog.reset() + + assert window.state == WindowState.NORMAL with pytest.warns( DeprecationWarning, match=full_screen_warning, @@ -1298,4 +1302,5 @@ def test_deprecated_full_screen(window, app): match=full_screen_warning, ): window.full_screen = False - # assert_action_not_performed(window, "set window state to WindowState.NORMAL") + + assert_action_not_performed(window, "set window state to WindowState.NORMAL") diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 1d1dc48fc9..aad3828506 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -137,8 +137,9 @@ def get_window_state(self): return self._get_value("state", WindowState.NORMAL) def set_window_state(self, state): - self._action(f"set window state to {state}", state=state) - self._set_value("state", state) + if state != self.get_window_state(): + self._action(f"set window state to {state}", state=state) + self._set_value("state", state) ###################################################################### # Window capabilities From d822ff03b8c3c819e5b156fffaddab06371f316b Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 2 Oct 2024 14:31:23 -0400 Subject: [PATCH 210/248] Added platform notes --- core/src/toga/constants/__init__.py | 19 +++++-------------- docs/reference/api/window.rst | 7 +++++++ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/core/src/toga/constants/__init__.py b/core/src/toga/constants/__init__.py index 35ab3b63df..1f57598482 100644 --- a/core/src/toga/constants/__init__.py +++ b/core/src/toga/constants/__init__.py @@ -71,7 +71,11 @@ def __str__(self) -> str: class WindowState(Enum): - """The possible window states of an app.""" + """The possible window states of an app. + + NOTE: Some platforms do not fully support all states; see the :any:`toga.Window`'s + platform notes for details. + """ NORMAL = 0 """The ``NORMAL`` state represents the default state of the window or app when it is @@ -80,25 +84,16 @@ class WindowState(Enum): MINIMIZED = 1 """``MINIMIZED`` state is when the window isn't currently visible, although it will appear in any operating system's list of active windows. - - Supported Platforms: ``Windows``, ``macOS``, ``Linux-Xorg`` """ MAXIMIZED = 2 """The window is the largest size it can be on the screen with title bar and window chrome still visible. - - On Mobile Platforms(Like on Android) - The ``MAXIMIZED`` state is the same as the - ``NORMAL`` state. - - Supported Platforms: ``Windows``, ``macOS``, ``Linux-Xorg``, ``Linux-Wayland`` """ FULLSCREEN = 3 """``FULLSCREEN`` state is when the window title bar and window chrome remain **hidden**; But app menu and toolbars remain **visible**. - - Supported Platforms: ``Windows``, ``macOS``, ``Linux-Xorg`` """ PRESENTATION = 4 @@ -107,8 +102,4 @@ class WindowState(Enum): A good example is a slideshow app in presentation mode - the only visible content is the slide. - - The window must have a content set on it, before entering presentation mode. - - Supported Platforms: ``Windows``, ``macOS``, ``Linux-Xorg`` """ diff --git a/docs/reference/api/window.rst b/docs/reference/api/window.rst index 0455fb4ced..810e7433fd 100644 --- a/docs/reference/api/window.rst +++ b/docs/reference/api/window.rst @@ -100,6 +100,13 @@ Notes window on a mobile platform. If you try to modify the size, position, or visibility of the main window, the request will be ignored. +* On mobile platforms, a window's state cannot be in :any:`WindowState.MINIMIZED` and + :any:`WindowState.MAXIMIZED` states, and requests for these states will be ignored. + +* On linux wayland, window state request for :any:`WindowState.MINIMIZED` will be + ignored, and the window cannot be restored from MINIMIZED state into NORMAL state + with :any:`WindowState.NORMAL` due to wayland security policies. + Reference --------- From 14b64b7bd325e1fb487c85ca349b62466b36dcc2 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 3 Oct 2024 03:41:40 -0400 Subject: [PATCH 211/248] Added platform notes --- docs/reference/api/window.rst | 6 +++--- testbed/tests/app/test_mobile.py | 19 ------------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/docs/reference/api/window.rst b/docs/reference/api/window.rst index 810e7433fd..605527edc5 100644 --- a/docs/reference/api/window.rst +++ b/docs/reference/api/window.rst @@ -100,12 +100,12 @@ Notes window on a mobile platform. If you try to modify the size, position, or visibility of the main window, the request will be ignored. -* On mobile platforms, a window's state cannot be in :any:`WindowState.MINIMIZED` and +* On mobile platforms, a window's state cannot be :any:`WindowState.MINIMIZED` or :any:`WindowState.MAXIMIZED` states, and requests for these states will be ignored. -* On linux wayland, window state request for :any:`WindowState.MINIMIZED` will be +* On Linux Wayland, window state request for :any:`WindowState.MINIMIZED` will be ignored, and the window cannot be restored from MINIMIZED state into NORMAL state - with :any:`WindowState.NORMAL` due to wayland security policies. + with :any:`WindowState.NORMAL` due to Wayland security policies. Reference --------- diff --git a/testbed/tests/app/test_mobile.py b/testbed/tests/app/test_mobile.py index a471daad8c..ca07cc8035 100644 --- a/testbed/tests/app/test_mobile.py +++ b/testbed/tests/app/test_mobile.py @@ -45,25 +45,6 @@ async def test_presentation_mode(app, main_window, main_window_probe): assert main_window.state != WindowState.PRESENTATION # Enter presentation mode with main window via the app - app.enter_presentation_mode([main_window]) - await main_window_probe.wait_for_window( - "Main window is in presentation mode", full_screen=True - ) - - assert app.in_presentation_mode - assert main_window.state == WindowState.PRESENTATION - - # Exit presentation mode - app.exit_presentation_mode() - await main_window_probe.wait_for_window( - "Main window is no longer in presentation mode", - full_screen=True, - ) - - assert not app.in_presentation_mode - assert main_window.state == WindowState.NORMAL - - # Enter presentation mode with a screen-window dict via the app app.enter_presentation_mode({app.screens[0]: main_window}) await main_window_probe.wait_for_window( "Main window is in presentation mode", full_screen=True From d8c842237774acab52fef0195f7448b77e7b16de Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 3 Oct 2024 09:18:49 -0700 Subject: [PATCH 212/248] Improved tests on mobile platform --- testbed/tests/window/test_window.py | 254 ++++++++++++++++------------ 1 file changed, 145 insertions(+), 109 deletions(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 40c8f95987..46b045ff91 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -12,8 +12,6 @@ from toga.constants import WindowState from toga.style.pack import COLUMN, Pack -from ..widgets.probe import get_probe - def window_probe(app, window): module = import_module("tests_backend.window") @@ -142,6 +140,63 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): finally: main_window.content = orig_content + @pytest.fixture(scope="session") + def intermediate_states_cycle(): + # Use a iterator cycling fixture to ensure each state is the initial + # intermediate state at least once and to test specific problematic + # combinations. This is to ensure full code coverage for all backends. + intermediate_states = [ + ( + WindowState.NORMAL, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + WindowState.NORMAL, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ), + ( + WindowState.FULLSCREEN, + WindowState.NORMAL, + WindowState.PRESENTATION, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + WindowState.NORMAL, + ), + ( + WindowState.PRESENTATION, + WindowState.FULLSCREEN, + WindowState.NORMAL, + WindowState.PRESENTATION, + WindowState.NORMAL, + WindowState.FULLSCREEN, + ), + ( + WindowState.NORMAL, + WindowState.PRESENTATION, + WindowState.FULLSCREEN, + WindowState.NORMAL, + WindowState.PRESENTATION, + WindowState.FULLSCREEN, + ), + ( + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + WindowState.NORMAL, + WindowState.FULLSCREEN, + WindowState.NORMAL, + WindowState.PRESENTATION, + ), + ( + WindowState.PRESENTATION, + WindowState.NORMAL, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + WindowState.FULLSCREEN, + WindowState.NORMAL, + ), + ] + return itertools.cycle(intermediate_states) + @pytest.mark.parametrize( "initial_state, final_state", [ @@ -156,8 +211,13 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): (WindowState.PRESENTATION, WindowState.FULLSCREEN), ], ) - async def test_window_state_direct_change( - app, initial_state, final_state, main_window, main_window_probe + async def test_window_state_change_with_intermediate_states( + app, + initial_state, + final_state, + main_window, + main_window_probe, + intermediate_states_cycle, ): """Window state can be directly changed to another state.""" if not main_window_probe.supports_fullscreen and WindowState.FULLSCREEN in { @@ -174,29 +234,23 @@ async def test_window_state_direct_change( } ): pytest.xfail("This backend doesn't support presentation window state.") + intermediate_states = next(intermediate_states_cycle) - try: - # Set to initial state - main_window.state = initial_state - await main_window_probe.wait_for_window( - f"Main window is in {initial_state}" - ) + # Set to initial state + main_window.state = initial_state + await main_window_probe.wait_for_window(f"Main window is in {initial_state}") - assert main_window.state == initial_state + assert main_window.state == initial_state - # Set to final state - main_window.state = final_state - await main_window_probe.wait_for_window(f"Main window is in {final_state}") + # Set to the intermediate states but don't wait for the OS delay. + for state in intermediate_states: + second_window.state = state - assert main_window.state == final_state - finally: - # Set to NORMAL state - main_window.state = WindowState.NORMAL - await main_window_probe.wait_for_window( - "Main window is in WindowState.NORMAL" - ) + # Set to final state + main_window.state = final_state + await main_window_probe.wait_for_window(f"Main window is in {final_state}") - assert main_window.state == WindowState.NORMAL + assert main_window.state == final_state @pytest.mark.parametrize( "state", @@ -206,10 +260,11 @@ async def test_window_state_direct_change( WindowState.PRESENTATION, ], ) - async def test_window_state_same_as_current( + async def test_window_state_same_as_current_without_intermediate_states( app, main_window, main_window_probe, state ): - """Setting the window state the same as current is a no-op.""" + """Setting window state the same as current without any intermediate states is + a no-op and there should be no expected delay from the OS.""" if ( not main_window_probe.supports_fullscreen and state == WindowState.FULLSCREEN @@ -221,97 +276,78 @@ async def test_window_state_same_as_current( ): pytest.xfail("This backend doesn't support presentation window state.") - try: - # Set the window state: - main_window.state = state - await main_window_probe.wait_for_window(f"Secondary window is in {state}") - assert main_window.state == state + # Set the window state: + main_window.state = state + await main_window_probe.wait_for_window(f"Secondary window is in {state}") + assert main_window.state == state - # Set the window state the same as current: - main_window.state = state - assert main_window.state == state + # Set the window state the same as current: + main_window.state = state + assert main_window.state == state - finally: - # Restore to NORMAL state. - main_window.state = WindowState.NORMAL - await main_window_probe.wait_for_window("Main window is not full screen") - assert main_window.state == WindowState.NORMAL - - async def test_window_state_fullscreen(main_window, main_window_probe): - """The window can enter into fullscreen state.""" - if not main_window_probe.supports_fullscreen: + @pytest.mark.parametrize( + "state", + [ + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ], + ) + async def test_window_state_content_size_increase( + main_window, main_window_probe, state + ): + """The size of the window content should increase when the window state is set + to maximized, fullscreen or presentation.""" + if ( + not main_window_probe.supports_fullscreen + and state == WindowState.FULLSCREEN + ): pytest.xfail("This backend doesn't support fullscreen window state.") - try: - widget = toga.Box(style=Pack(flex=1)) - widget_probe = get_probe(widget) - main_window.content = toga.Box(children=[widget]) - await main_window_probe.wait_for_window( - "Test widget has been added to the window" - ) - widget_initial_size = (widget_probe.width, widget_probe.height) + if ( + not main_window_probe.supports_presentation + and state == WindowState.PRESENTATION + ): + pytest.xfail("This backend doesn't support presentation window state.") - # Make main window full screen - main_window.state = WindowState.FULLSCREEN - await main_window_probe.wait_for_window( - "Main window is full screen", full_screen=True - ) - assert main_window.state == WindowState.FULLSCREEN - # At least one of the dimensions should have increased. - assert ( - widget_probe.width > widget_initial_size[0] - or widget_probe.height > widget_initial_size[1] - ) - finally: - # Exit full screen - main_window.state = WindowState.NORMAL - await main_window_probe.wait_for_window( - "Main window is not full screen", full_screen=True - ) - assert main_window.state == WindowState.NORMAL - # Both dimensions should be the same. - assert ( - widget_probe.width == widget_initial_size[0] - and widget_probe.height == widget_initial_size[1] - ) - main_window.content.clear() + main_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window("Main window is shown") - async def test_window_state_presentation(main_window, main_window_probe): - """The window can enter into presentation state.""" - if not main_window_probe.supports_presentation: - pytest.xfail("This backend doesn't support presentation window state.") - try: - widget = toga.Box(style=Pack(flex=1)) - widget_probe = get_probe(widget) - main_window.content = toga.Box(children=[widget]) - await main_window_probe.wait_for_window( - "Test widget has been added to the window" - ) - widget_initial_size = (widget_probe.width, widget_probe.height) + assert main_window.state == WindowState.NORMAL + initial_content_size = main_window_probe.content_size - # Enter presentation mode with main window - main_window.state = WindowState.PRESENTATION - await main_window_probe.wait_for_window( - "Main window is in presentation mode", full_screen=True - ) - assert main_window.state == WindowState.PRESENTATION - # At least one of the dimensions should have increased. - assert ( - widget_probe.width > widget_initial_size[0] - or widget_probe.height > widget_initial_size[1] - ) - finally: - # Exit presentation mode - main_window.state = WindowState.NORMAL - await main_window_probe.wait_for_window( - "Main window is not in presentation mode", full_screen=True - ) - assert main_window.state == WindowState.NORMAL - # Both dimensions should be the same. - assert ( - widget_probe.width == widget_initial_size[0] - and widget_probe.height == widget_initial_size[1] - ) - main_window.content.clear() + main_window.state = state + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window( + f"Main window is in {state}", + full_screen=True if state == WindowState.FULLSCREEN else False, + ) + assert main_window.state == state + # At least one of the dimension should have increased. + assert ( + main_window_probe.content_size[0] > initial_content_size[0] + or main_window_probe.content_size[1] > initial_content_size[1] + ) + + main_window.state = state + await main_window_probe.wait_for_window( + f"Main window is still in {state}", + full_screen=True if state == WindowState.FULLSCREEN else False, + ) + assert main_window.state == state + # At least one of the dimension should have increased. + assert ( + main_window_probe.content_size[0] > initial_content_size[0] + or main_window_probe.content_size[1] > initial_content_size[1] + ) + + main_window.state = WindowState.NORMAL + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window( + f"Main window is not in {state}", + full_screen=True if state == WindowState.FULLSCREEN else False, + ) + assert main_window.state == WindowState.NORMAL + assert main_window_probe.content_size == initial_content_size @pytest.mark.parametrize( "state", From eb2d9a5383ccdd383c272a95f5dec606f4af924a Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Mon, 7 Oct 2024 01:07:35 -0700 Subject: [PATCH 213/248] Apply suggestions from code review Co-authored-by: Russell Keith-Magee --- core/src/toga/constants/__init__.py | 4 ++-- core/tests/app/test_app.py | 8 ++++---- docs/reference/api/window.rst | 9 +++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/core/src/toga/constants/__init__.py b/core/src/toga/constants/__init__.py index 1f57598482..6ace32d11b 100644 --- a/core/src/toga/constants/__init__.py +++ b/core/src/toga/constants/__init__.py @@ -93,12 +93,12 @@ class WindowState(Enum): FULLSCREEN = 3 """``FULLSCREEN`` state is when the window title bar and window chrome remain - **hidden**; But app menu and toolbars remain **visible**. + hidden; But app menu and toolbars remain visible. """ PRESENTATION = 4 """``PRESENTATION`` state is when the window title bar, window chrome, app menu - and toolbars all remain **hidden**. + and toolbars all remain hidden. A good example is a slideshow app in presentation mode - the only visible content is the slide. diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 112cc80981..a97694b8d5 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -600,8 +600,8 @@ def test_presentation_mode_no_op(event_loop): # Entering presentation mode without any window is a no-op. with pytest.raises(TypeError): app.enter_presentation_mode() - assert_action_not_performed(app, "enter presentation mode") - assert not app.in_presentation_mode + assert_action_not_performed(app, "enter presentation mode") + assert not app.in_presentation_mode # Entering presentation mode with an empty dict, is a no-op: app.enter_presentation_mode({}) @@ -619,8 +619,8 @@ def test_presentation_mode_no_op(event_loop): match="Presentation layout should be a list of windows, or a dict mapping windows to screens.", ): app.enter_presentation_mode(toga.Window()) - assert_action_not_performed(app, "enter presentation mode") - assert not app.in_presentation_mode + assert_action_not_performed(app, "enter presentation mode") + assert not app.in_presentation_mode def test_show_hide_cursor(app): diff --git a/docs/reference/api/window.rst b/docs/reference/api/window.rst index 605527edc5..df1e85bce6 100644 --- a/docs/reference/api/window.rst +++ b/docs/reference/api/window.rst @@ -101,11 +101,12 @@ Notes the main window, the request will be ignored. * On mobile platforms, a window's state cannot be :any:`WindowState.MINIMIZED` or - :any:`WindowState.MAXIMIZED` states, and requests for these states will be ignored. + :any:`WindowState.MAXIMIZED`. Any request to move to these states will be ignored. -* On Linux Wayland, window state request for :any:`WindowState.MINIMIZED` will be - ignored, and the window cannot be restored from MINIMIZED state into NORMAL state - with :any:`WindowState.NORMAL` due to Wayland security policies. +* On Linux, when using Wayland, a request to put a window into a + :any:`WindowState.MINIMIZED` state, or to restore from the + :any:`WindowState.MINIMIZED` state, will be ignored. This is due to + limitations in window management features that Wayland allows apps to use. Reference --------- From abfa7074474d2472494c5884c8002d28a89aff4e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 7 Oct 2024 05:25:35 -0400 Subject: [PATCH 214/248] corrected pre-commit issues --- docs/reference/api/window.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/api/window.rst b/docs/reference/api/window.rst index df1e85bce6..caf28e9899 100644 --- a/docs/reference/api/window.rst +++ b/docs/reference/api/window.rst @@ -105,7 +105,7 @@ Notes * On Linux, when using Wayland, a request to put a window into a :any:`WindowState.MINIMIZED` state, or to restore from the - :any:`WindowState.MINIMIZED` state, will be ignored. This is due to + :any:`WindowState.MINIMIZED` state, will be ignored. This is due to limitations in window management features that Wayland allows apps to use. Reference From 8f7c954a7ba1d92b78d90585dc025107cfd8bc95 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 7 Oct 2024 07:12:03 -0400 Subject: [PATCH 215/248] Moved implementation of app presentation mode to core from backends --- android/src/toga_android/app.py | 15 --------------- cocoa/src/toga_cocoa/app.py | 16 ---------------- cocoa/src/toga_cocoa/window.py | 9 ++++----- core/src/toga/app.py | 10 ++++++++-- core/src/toga/window.py | 5 +++-- gtk/src/toga_gtk/app.py | 16 ---------------- gtk/src/toga_gtk/window.py | 5 +---- iOS/src/toga_iOS/app.py | 10 ---------- iOS/src/toga_iOS/window.py | 5 +++-- winforms/src/toga_winforms/app.py | 17 ----------------- 10 files changed, 19 insertions(+), 89 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 81b8f8bc20..19671b70c4 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -11,7 +11,6 @@ import toga from toga.command import Group, Separator -from toga.constants import WindowState from toga.dialogs import InfoDialog from .libs import events @@ -314,20 +313,6 @@ def get_current_window(self): def set_current_window(self, window): pass - ###################################################################### - # Presentation mode controls - ###################################################################### - - def enter_presentation_mode(self, screen_window_dict): - for screen, window in screen_window_dict.items(): - # There is only a single window on android and moving between - # screens is not supported. - window._impl.set_window_state(WindowState.PRESENTATION) - - def exit_presentation_mode(self): - # There is only a single window on android. - self.interface.main_window._impl.set_window_state(WindowState.NORMAL) - ###################################################################### # Platform-specific APIs ###################################################################### diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 3b39a01fd8..3d5fce3ef7 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -13,7 +13,6 @@ import toga from toga.command import Command, Group, Separator -from toga.constants import WindowState from toga.handlers import NativeHandler from .command import Command as CommandImpl, submenu_for_group @@ -370,18 +369,3 @@ def get_current_window(self): def set_current_window(self, window): window._impl.native.makeKeyAndOrderFront(window._impl.native) - - ###################################################################### - # Presentation mode controls - ###################################################################### - - def enter_presentation_mode(self, screen_window_dict): - for screen, window in screen_window_dict.items(): - window._impl._before_presentation_mode_screen = window.screen - window.screen = screen - window._impl.set_window_state(WindowState.PRESENTATION) - - def exit_presentation_mode(self): - for window in self.interface.windows: - if window.state == WindowState.PRESENTATION: - window._impl.set_window_state(WindowState.NORMAL) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index af8986f6e4..00e21bcd52 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -351,7 +351,9 @@ def get_visible(self): # Window state ###################################################################### - def get_window_state(self): + def get_window_state(self, actual_state=True): + if not actual_state and self._pending_state_transition: + return self._pending_state_transition if bool(self.container.native.isInFullScreenMode()): return WindowState.PRESENTATION elif bool(self.native.styleMask & NSWindowStyleMask.FullScreen): @@ -390,11 +392,8 @@ def set_window_state(self, state): ): self.interface.app.exit_presentation_mode() - current_state = self.get_window_state() - if current_state == state: - return self._pending_state_transition = state - if current_state != WindowState.NORMAL: + if self.get_window_state() != WindowState.NORMAL: self._apply_state(WindowState.NORMAL) else: self._apply_state(state) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 12e3786f77..5f4fbf0f53 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -873,11 +873,17 @@ def enter_presentation_mode( raise ValueError( "Presentation layout should be a list of windows, or a dict mapping windows to screens." ) - self._impl.enter_presentation_mode(screen_window_dict) + + for screen, window in screen_window_dict.items(): + window._impl._before_presentation_mode_screen = window.screen + window.screen = screen + window._impl.set_window_state(WindowState.PRESENTATION) def exit_presentation_mode(self) -> None: """Exit presentation mode.""" - self._impl.exit_presentation_mode() + for window in self.windows: + if window.state == WindowState.PRESENTATION: + window._impl.set_window_state(WindowState.NORMAL) ###################################################################### # App events diff --git a/core/src/toga/window.py b/core/src/toga/window.py index dbf52a3637..9d65e06b67 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -473,7 +473,7 @@ def visible(self, visible: bool) -> None: @property def state(self) -> WindowState: """The current state of the window.""" - return self._impl.get_window_state() + return self._impl.get_window_state(actual_state=False) @state.setter def state(self, state: WindowState) -> None: @@ -492,7 +492,8 @@ def state(self, state: WindowState) -> None: # as non-blocking OS calls may not have completed at the time # the core checks the current state. This could lead to incorrect # assertions and potential glitches. - self._impl.set_window_state(state) + if not self.state == state: + self._impl.set_window_state(state) ###################################################################### # Window capabilities diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 42805d4581..ca6ff6eef7 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -3,7 +3,6 @@ from toga.app import App as toga_App from toga.command import Separator -from toga.constants import WindowState from .keys import gtk_accel from .libs import ( @@ -241,18 +240,3 @@ def get_current_window(self): # pragma: no-cover-if-linux-wayland def set_current_window(self, window): window._impl.native.present() - - ###################################################################### - # Presentation mode controls - ###################################################################### - - def enter_presentation_mode(self, screen_window_dict): - for screen, window in screen_window_dict.items(): - window._impl._before_presentation_mode_screen = window.screen - window.screen = screen - window._impl.set_window_state(WindowState.PRESENTATION) - - def exit_presentation_mode(self): - for window in self.interface.windows: - if window.state == WindowState.PRESENTATION: - window._impl.set_window_state(WindowState.NORMAL) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 0bf19bb7e9..5fd29edaae 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -205,11 +205,8 @@ def set_window_state(self, state): ): self.interface.app.exit_presentation_mode() - current_state = self.get_window_state() - if current_state == state: - return self._pending_state_transition = state - if current_state != WindowState.NORMAL: + if self.get_window_state() != WindowState.NORMAL: self._apply_state(WindowState.NORMAL) else: self._apply_state(state) diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index 6478a61394..f55e7df8d2 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -151,13 +151,3 @@ def get_current_window(self): def set_current_window(self, window): # iOS only has a main window, so this is a no-op pass - - ###################################################################### - # Presentation mode controls - ###################################################################### - - def enter_presentation_mode(self, screen_window_dict): - self.interface.factory.not_implemented("App.enter_presentation_mode()") - - def exit_presentation_mode(self): - self.interface.factory.not_implemented("App.exit_presentation_mode()") diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index b1303fa2c3..937d253ded 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -143,11 +143,12 @@ def hide(self): ###################################################################### def get_window_state(self): - # Windows are always normal. + # Windows are always in NORMAL state. return WindowState.NORMAL def set_window_state(self, state): - self.interface.factory.not_implemented(f"Window.set_window_state({state})") + # Window state setting is not implemented on iOS. + pass ###################################################################### # Window capabilities diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 8a2f38646f..3dd824c4e8 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -10,8 +10,6 @@ from System.Net import SecurityProtocolType, ServicePointManager from System.Windows.Threading import Dispatcher -from toga.constants import WindowState - from .libs.proactor import WinformsProactorEventLoop from .libs.wrapper import WeakrefCallable from .screens import Screen as ScreenImpl @@ -257,18 +255,3 @@ def get_current_window(self): def set_current_window(self, window): window._impl.native.Activate() - - ###################################################################### - # Presentation mode controls - ###################################################################### - - def enter_presentation_mode(self, screen_window_dict): - for screen, window in screen_window_dict.items(): - window._impl._before_presentation_mode_screen = window.screen - window.screen = screen - window._impl.set_window_state(WindowState.PRESENTATION) - - def exit_presentation_mode(self): - for window in self.interface.windows: - if window.state == WindowState.PRESENTATION: - window._impl.set_window_state(WindowState.NORMAL) From 379e00208658b299f0531f1b16206ab1c3d951e5 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 7 Oct 2024 07:32:12 -0400 Subject: [PATCH 216/248] Removed unused code from dummy backend --- android/src/toga_android/window.py | 2 +- dummy/src/toga_dummy/app.py | 17 -------------- dummy/src/toga_dummy/window.py | 10 +++------ gtk/src/toga_gtk/window.py | 33 +++++++++++++--------------- iOS/src/toga_iOS/window.py | 2 +- winforms/src/toga_winforms/window.py | 2 +- 6 files changed, 21 insertions(+), 45 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index a221542f45..62b08ee8a2 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -146,7 +146,7 @@ def get_visible(self): # Window state ###################################################################### - def get_window_state(self): + def get_window_state(self, actual_state=True): # `window.state` is called in `_close()`, which itself is # sometimes called during certain stages when the app # attribute may not exist. In such cases, return NORMAL. diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index 0494c0374f..fc3e2e459b 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -2,8 +2,6 @@ import sys from pathlib import Path -from toga.constants import WindowState - from .screens import Screen as ScreenImpl from .utils import LoggedObject @@ -131,21 +129,6 @@ def set_current_window(self, window): self._action("set_current_window", window=window) self._set_value("current_window", window._impl) - ###################################################################### - # Presentation mode control - ###################################################################### - - def enter_presentation_mode(self, screen_window_dict): - self._action("enter presentation mode", screen_window_dict=screen_window_dict) - for screen, window in screen_window_dict.items(): - window._impl.set_window_state(WindowState.PRESENTATION) - - def exit_presentation_mode(self): - self._action("exit presentation mode") - for window in self.interface.windows: - if window.state == WindowState.PRESENTATION: - window._impl.set_window_state(WindowState.NORMAL) - class DocumentApp(App): def create(self): diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index aad3828506..e63eb9be55 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -130,16 +130,12 @@ def hide(self): # Window state ###################################################################### - def set_full_screen(self, is_full_screen): - self._action("set full screen", full_screen=is_full_screen) - - def get_window_state(self): + def get_window_state(self, actual_state=True): return self._get_value("state", WindowState.NORMAL) def set_window_state(self, state): - if state != self.get_window_state(): - self._action(f"set window state to {state}", state=state) - self._set_value("state", state) + self._action(f"set window state to {state}", state=state) + self._set_value("state", state) ###################################################################### # Window capabilities diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 5fd29edaae..0cd33af928 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -32,11 +32,7 @@ def __init__(self, interface, title, position, size): self.native.connect("window-state-event", self.gtk_window_state_event) self._window_state_flags = None - - # Gdk.WindowState.FULLSCREEN is notified before it fully transitions to fullscreen, - # Use shadow variables to differentiate between the presentation & fullscreen state. self._in_presentation = False - # Pending Window state transition variable: self._pending_state_transition = None @@ -168,21 +164,22 @@ def hide(self): # Window state ###################################################################### - def get_window_state(self): + def get_window_state(self, actual_state=True): + if not actual_state and self._pending_state_transition: + return self._pending_state_transition window_state_flags = self._window_state_flags - if not window_state_flags: - return - if window_state_flags & Gdk.WindowState.MAXIMIZED: - return WindowState.MAXIMIZED - elif window_state_flags & Gdk.WindowState.ICONIFIED: - return WindowState.MINIMIZED # pragma: no-cover-if-linux-wayland - elif window_state_flags & Gdk.WindowState.FULLSCREEN: - if self._in_presentation: - return WindowState.PRESENTATION - else: - return WindowState.FULLSCREEN - else: - return WindowState.NORMAL + if window_state_flags: + if window_state_flags & Gdk.WindowState.MAXIMIZED: + return WindowState.MAXIMIZED + elif window_state_flags & Gdk.WindowState.ICONIFIED: + return WindowState.MINIMIZED # pragma: no-cover-if-linux-wayland + elif window_state_flags & Gdk.WindowState.FULLSCREEN: + return ( + WindowState.PRESENTATION + if self._in_presentation + else WindowState.FULLSCREEN + ) + return WindowState.NORMAL def set_window_state(self, state): if IS_WAYLAND and ( diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index 937d253ded..846429f88d 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -142,7 +142,7 @@ def hide(self): # Window state ###################################################################### - def get_window_state(self): + def get_window_state(self, actual_state=True): # Windows are always in NORMAL state. return WindowState.NORMAL diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 55c848b860..c625ea402c 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -189,7 +189,7 @@ def hide(self): # Window state ###################################################################### - def get_window_state(self): + def get_window_state(self, actual_state=True): window_state = self.native.WindowState if window_state == WinForms.FormWindowState.Maximized: if self.native.FormBorderStyle == getattr(WinForms.FormBorderStyle, "None"): From e3f31ff32e9412a5e5d3490e0524ef7289f06fc3 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 7 Oct 2024 09:17:25 -0400 Subject: [PATCH 217/248] corrected core tests --- core/src/toga/app.py | 4 +- core/src/toga/window.py | 6 +- core/tests/app/test_app.py | 186 ++++++++++++++++++++++++++----------- gtk/src/toga_gtk/window.py | 7 +- 4 files changed, 140 insertions(+), 63 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 5f4fbf0f53..533acd418a 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -959,7 +959,7 @@ def exit_full_screen(self) -> None: stacklevel=2, ) if self.in_presentation_mode: - self._impl.exit_presentation_mode() + self.exit_presentation_mode() @property def is_full_screen(self) -> bool: @@ -984,7 +984,7 @@ def set_full_screen(self, *windows: Window) -> None: ) self.exit_presentation_mode() if windows: - self.enter_presentation_mode([*windows]) + self.enter_presentation_mode(list(windows)) ###################################################################### # End backwards compatibility diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 9d65e06b67..a5737076a3 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -850,9 +850,9 @@ def full_screen(self, is_full_screen: bool) -> None: ("`Window.full_screen` is deprecated. Use `Window.state` instead."), DeprecationWarning, ) - self._impl.set_window_state( - WindowState.FULLSCREEN if is_full_screen else WindowState.NORMAL - ) + target_state = WindowState.FULLSCREEN if is_full_screen else WindowState.NORMAL + if self.state != target_state: + self._impl.set_window_state(target_state) ###################################################################### # End Backwards compatibility diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index a97694b8d5..9157cb425e 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -483,20 +483,21 @@ def test_presentation_mode_with_windows_list(event_loop, windows): # Enter presentation mode with 1 or more windows: app.enter_presentation_mode(windows_list) assert app.in_presentation_mode - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={ - app.screens[i]: window for i, window in enumerate(windows_list) - }, - ) + for window in windows_list: + assert_action_performed_with( + window, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) # Exit presentation mode: app.exit_presentation_mode() assert not app.in_presentation_mode - assert_action_performed( - app, - "exit presentation mode", - ) + for window in windows_list: + assert_action_performed_with( + window, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) @pytest.mark.parametrize( @@ -518,18 +519,22 @@ def test_presentation_mode_with_screen_window_dict(event_loop, windows): # Enter presentation mode with a 1 or more elements screen-window dict: app.enter_presentation_mode(screen_window_dict) assert app.in_presentation_mode - assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict=screen_window_dict, - ) + for screen, window in screen_window_dict.items(): + assert_action_performed_with( + window, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + # Exit presentation mode: app.exit_presentation_mode() assert not app.in_presentation_mode - assert_action_performed( - app, - "exit presentation mode", - ) + for screen, window in screen_window_dict.items(): + assert_action_performed_with( + window, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) def test_presentation_mode_with_excess_windows_list(event_loop): @@ -546,17 +551,36 @@ def test_presentation_mode_with_excess_windows_list(event_loop): app.enter_presentation_mode([window1, window2, window3]) assert app.in_presentation_mode assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, + window1, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + assert_action_performed_with( + window2, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + assert_action_not_performed( + window3, + "set window state to WindowState.PRESENTATION", ) # Exit presentation mode: app.exit_presentation_mode() assert not app.in_presentation_mode - assert_action_performed( - app, - "exit presentation mode", + assert_action_performed_with( + window1, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + assert_action_performed_with( + window2, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + assert_action_not_performed( + window3, + "set window state to WindowState.NORMAL", ) @@ -573,9 +597,13 @@ def test_presentation_mode_with_some_windows(event_loop): app.enter_presentation_mode([window1]) assert app.in_presentation_mode assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window1}, + window1, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + assert_action_not_performed( + window2, + "set window state to WindowState.PRESENTATION", ) assert window1.state == WindowState.PRESENTATION assert window2.state != WindowState.PRESENTATION @@ -583,9 +611,14 @@ def test_presentation_mode_with_some_windows(event_loop): # Exit presentation mode: app.exit_presentation_mode() assert not app.in_presentation_mode - assert_action_performed( - app, - "exit presentation mode", + assert_action_performed_with( + window1, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + assert_action_not_performed( + window2, + "set window state to WindowState.NORMAL", ) assert window1.state != WindowState.PRESENTATION assert window2.state != WindowState.PRESENTATION @@ -600,18 +633,24 @@ def test_presentation_mode_no_op(event_loop): # Entering presentation mode without any window is a no-op. with pytest.raises(TypeError): app.enter_presentation_mode() - assert_action_not_performed(app, "enter presentation mode") assert not app.in_presentation_mode + assert_action_not_performed( + app.main_window, "set window state to WindowState.PRESENTATION" + ) # Entering presentation mode with an empty dict, is a no-op: app.enter_presentation_mode({}) assert not app.in_presentation_mode - assert_action_not_performed(app, "enter presentation mode") + assert_action_not_performed( + app.main_window, "set window state to WindowState.PRESENTATION" + ) # Entering presentation mode with an empty windows list, is a no-op: app.enter_presentation_mode([]) assert not app.in_presentation_mode - assert_action_not_performed(app, "enter presentation mode") + assert_action_not_performed( + app.main_window, "set window state to WindowState.PRESENTATION" + ) # Entering presentation mode without proper type of parameter is a no-op. with pytest.raises( @@ -619,8 +658,10 @@ def test_presentation_mode_no_op(event_loop): match="Presentation layout should be a list of windows, or a dict mapping windows to screens.", ): app.enter_presentation_mode(toga.Window()) - assert_action_not_performed(app, "enter presentation mode") assert not app.in_presentation_mode + assert_action_not_performed( + app.main_window, "set window state to WindowState.PRESENTATION" + ) def test_show_hide_cursor(app): @@ -981,7 +1022,10 @@ def test_deprecated_full_screen(event_loop): match=is_full_screen_warning, ): assert not app.is_full_screen - assert_action_not_performed(app, "exit presentation mode") + assert_action_not_performed( + app.main_window, + "set window state to WindowState.NORMAL", + ) # Trying to enter full screen with no windows is a no-op with pytest.warns( @@ -995,7 +1039,10 @@ def test_deprecated_full_screen(event_loop): match=is_full_screen_warning, ): assert not app.is_full_screen - assert_action_not_performed(app, "enter presentation mode") + assert_action_not_performed( + app.main_window, + "set window state to WindowState.PRESENTATION", + ) # Enter full screen with 2 windows with pytest.warns( @@ -1009,9 +1056,14 @@ def test_deprecated_full_screen(event_loop): ): assert app.is_full_screen assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window2, app.screens[1]: app.main_window}, + window2, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + assert_action_performed_with( + app.main_window, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, ) # Change the screens that are full screen @@ -1026,11 +1078,20 @@ def test_deprecated_full_screen(event_loop): ): assert app.is_full_screen assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window2, app.screens[1]: app.main_window}, + app.main_window, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + assert_action_performed_with( + window1, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + assert_action_performed_with( + window2, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, ) - # Exit full screen mode with pytest.warns( DeprecationWarning, @@ -1042,9 +1103,15 @@ def test_deprecated_full_screen(event_loop): match=is_full_screen_warning, ): assert not app.is_full_screen - assert_action_performed( - app, - "exit presentation mode", + assert_action_performed_with( + app.main_window, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + assert_action_performed_with( + window1, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, ) @@ -1081,9 +1148,14 @@ def test_deprecated_set_empty_full_screen_window_list(event_loop): ): assert app.is_full_screen assert_action_performed_with( - app, - "enter presentation mode", - screen_window_dict={app.screens[0]: window1, app.screens[1]: window2}, + window1, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, + ) + assert_action_performed_with( + window2, + "set window state to WindowState.PRESENTATION", + state=WindowState.PRESENTATION, ) # Exit full screen mode by setting no windows full screen with pytest.warns( @@ -1096,7 +1168,13 @@ def test_deprecated_set_empty_full_screen_window_list(event_loop): match=is_full_screen_warning, ): assert not app.is_full_screen - assert_action_performed( - app, - "exit presentation mode", + assert_action_performed_with( + window1, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, + ) + assert_action_performed_with( + window2, + "set window state to WindowState.NORMAL", + state=WindowState.NORMAL, ) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 0cd33af928..d2506554d9 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -185,10 +185,9 @@ def set_window_state(self, state): if IS_WAYLAND and ( state == WindowState.MINIMIZED ): # pragma: no-cover-if-linux-x - # Not implemented on wayland due to wayland security policies. - self.interface.factory.not_implemented( - "Window.set_window_state(WindowState.MINIMIZED) on Wayland" - ) + # Not implemented on wayland due to wayland interpretation of an app's + # responsibility. + return else: if self._pending_state_transition: self._pending_state_transition = state From 383e027fb9a682255df6b45847bd08f6afeb3516 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Mon, 7 Oct 2024 06:50:42 -0700 Subject: [PATCH 218/248] Restart CI for intermittent Android failures From c986e3dc36bb82ed78198749033b034a81cc44ed Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 7 Oct 2024 23:39:10 -0400 Subject: [PATCH 219/248] Improved tests --- cocoa/src/toga_cocoa/window.py | 18 +++++++++-------- testbed/tests/app/test_desktop.py | 33 ++++++++++++++++++------------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 00e21bcd52..1f455ef16a 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -85,21 +85,23 @@ def windowDidDeminiaturize_(self, notification) -> None: @objc_method def windowDidEnterFullScreen_(self, notification) -> None: - # Directly doing post-fullscreen operations here will result in error: - # ````2024-08-09 15:46:39.050 python[2646:37395] not in fullscreen state```` - # and any subsequent window state calls to the OS will not work or will be glitchy. - self.performSelector(SEL("enteredFullScreen:"), withObject=None, afterDelay=0) - - @objc_method - def enteredFullScreen_(self, sender) -> None: if ( self.impl._pending_state_transition and self.impl._pending_state_transition != WindowState.FULLSCREEN ): - self.impl._apply_state(WindowState.NORMAL) + # Directly exiting fullscreen without a delay will result in error: + # ````2024-08-09 15:46:39.050 python[2646:37395] not in fullscreen state```` + # and any subsequent window state calls to the OS will not work or will be glitchy. + self.performSelector( + SEL("delayedFullScreenExit:"), withObject=None, afterDelay=0 + ) else: self.impl._pending_state_transition = None + @objc_method + def delayedFullScreenExit(self, sender) -> None: + self.impl._apply_state(WindowState.NORMAL) + @objc_method def windowDidExitFullScreen_(self, notification) -> None: self.impl._apply_state(self.impl._pending_state_transition) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 74a355f4f3..fee94c2217 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -1,10 +1,10 @@ -import random +import itertools from unittest.mock import Mock import pytest import toga -from toga.colors import CORNFLOWERBLUE, FIREBRICK, REBECCAPURPLE +from toga.colors import CORNFLOWERBLUE, FIREBRICK, GOLDENROD, REBECCAPURPLE from toga.constants import WindowState from toga.style.pack import Pack @@ -172,22 +172,21 @@ async def test_menu_minimize(app, app_probe): async def test_presentation_mode(app, app_probe, main_window, main_window_probe): """The app can enter presentation mode.""" + bg_colors = (CORNFLOWERBLUE, FIREBRICK, REBECCAPURPLE, GOLDENROD) + color_cycle = itertools.cycle(bg_colors) window_information_list = list() - windows_list = list() + screen_window_dict = dict() for i in range(len(app.screens)): window = toga.Window(title=f"Test Window {i}", size=(200, 200)) - r = random.randint(0, 255) - g = random.randint(0, 255) - b = random.randint(0, 255) - window_widget = toga.Box( - style=Pack(flex=1, background_color=f"#{r:02X}{g:02X}{b:02X}") - ) + window_widget = toga.Box(style=Pack(flex=1, background_color=next(color_cycle))) window.content = window_widget window.show() window_information = dict() window_information["window"] = window window_information["window_probe"] = window_probe(app, window) + window_information["initial_screen"] = window_information["window"].screen + window_information["paired_screen"] = app.screens[i] window_information["initial_content_size"] = window_information[ "window_probe" ].content_size @@ -197,15 +196,13 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) window_information["widget_probe"].height, ) window_information_list.append(window_information) - windows_list.append(window) + screen_window_dict[window_information["paired_screen"]] = window_information[ + "window" + ] # Add delay to ensure windows are visible after animation. await main_window_probe.wait_for_window("All Test Windows are visible") - screen_window_dict = dict() - for window, screen in zip(windows_list, app.screens): - screen_window_dict[screen] = window - # Enter presentation mode with a screen-window dict via the app app.enter_presentation_mode(screen_window_dict) # Add delay to ensure windows are visible after animation. @@ -218,6 +215,8 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) assert ( window_information["window"].state == WindowState.PRESENTATION ), f"{window_information['window'].title}:" + # 1000x700 is a size that is bigger that the original + # window size, while being smaller than any likely screen. assert ( window_information["window_probe"].content_size[0] > 1000 ), f"{window_information['window'].title}:" @@ -230,6 +229,9 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) and window_information["widget_probe"].height > window_information["initial_widget_size"][1] ), f"{window_information['window'].title}:" + assert ( + window_information["window"].screen == window_information["paired_screen"] + ), f"{window_information['window'].title}:" # Exit presentation mode app.exit_presentation_mode() @@ -252,6 +254,9 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) and window_information["widget_probe"].height == window_information["initial_widget_size"][1] ), f"{window_information['window'].title}:" + assert ( + window_information["window"].screen == window_information["initial_screen"] + ), f"{window_information['window'].title}:" @pytest.mark.parametrize( From 5f365c6e8f7c3cf4c59ae4c3cd267a533f643bff Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 8 Oct 2024 01:04:14 -0400 Subject: [PATCH 220/248] Improved tests --- testbed/tests/window/test_window.py | 155 +++++++--------------------- 1 file changed, 40 insertions(+), 115 deletions(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 46b045ff91..38c5db60ef 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -1,5 +1,4 @@ import gc -import itertools import re import weakref from importlib import import_module @@ -140,27 +139,30 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): finally: main_window.content = orig_content - @pytest.fixture(scope="session") - def intermediate_states_cycle(): - # Use a iterator cycling fixture to ensure each state is the initial - # intermediate state at least once and to test specific problematic - # combinations. This is to ensure full code coverage for all backends. - intermediate_states = [ + @pytest.mark.parametrize( + "initial_state, final_state", + [ + # Direct switch from NORMAL: + (WindowState.NORMAL, WindowState.FULLSCREEN), + (WindowState.NORMAL, WindowState.PRESENTATION), + # Direct switch from FULLSCREEN: + (WindowState.FULLSCREEN, WindowState.NORMAL), + (WindowState.FULLSCREEN, WindowState.PRESENTATION), + # Direct switch from PRESENTATION: + (WindowState.PRESENTATION, WindowState.NORMAL), + (WindowState.PRESENTATION, WindowState.FULLSCREEN), + ], + ) + @pytest.mark.parametrize( + "intermediate_states", + [ ( - WindowState.NORMAL, WindowState.FULLSCREEN, WindowState.PRESENTATION, WindowState.NORMAL, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - ), - ( WindowState.FULLSCREEN, WindowState.NORMAL, WindowState.PRESENTATION, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - WindowState.NORMAL, ), ( WindowState.PRESENTATION, @@ -170,45 +172,6 @@ def intermediate_states_cycle(): WindowState.NORMAL, WindowState.FULLSCREEN, ), - ( - WindowState.NORMAL, - WindowState.PRESENTATION, - WindowState.FULLSCREEN, - WindowState.NORMAL, - WindowState.PRESENTATION, - WindowState.FULLSCREEN, - ), - ( - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - WindowState.NORMAL, - WindowState.FULLSCREEN, - WindowState.NORMAL, - WindowState.PRESENTATION, - ), - ( - WindowState.PRESENTATION, - WindowState.NORMAL, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - WindowState.FULLSCREEN, - WindowState.NORMAL, - ), - ] - return itertools.cycle(intermediate_states) - - @pytest.mark.parametrize( - "initial_state, final_state", - [ - # Direct switch from NORMAL: - (WindowState.NORMAL, WindowState.FULLSCREEN), - (WindowState.NORMAL, WindowState.PRESENTATION), - # Direct switch from FULLSCREEN: - (WindowState.FULLSCREEN, WindowState.NORMAL), - (WindowState.FULLSCREEN, WindowState.PRESENTATION), - # Direct switch from PRESENTATION: - (WindowState.PRESENTATION, WindowState.NORMAL), - (WindowState.PRESENTATION, WindowState.FULLSCREEN), ], ) async def test_window_state_change_with_intermediate_states( @@ -217,7 +180,7 @@ async def test_window_state_change_with_intermediate_states( final_state, main_window, main_window_probe, - intermediate_states_cycle, + intermediate_states, ): """Window state can be directly changed to another state.""" if not main_window_probe.supports_fullscreen and WindowState.FULLSCREEN in { @@ -234,7 +197,6 @@ async def test_window_state_change_with_intermediate_states( } ): pytest.xfail("This backend doesn't support presentation window state.") - intermediate_states = next(intermediate_states_cycle) # Set to initial state main_window.state = initial_state @@ -725,63 +687,6 @@ async def test_move_and_resize(second_window, second_window_probe): assert second_window.size == (250 + extra_width, 210 + extra_height) assert second_window_probe.content_size == (250, 210) - @pytest.fixture(scope="session") - def intermediate_states_cycle(): - # Use a iterator cycling fixture to ensure each state is the initial - # intermediate state at least once and to test specific problematic - # combinations. This is to ensure full code coverage for all backends. - intermediate_states = [ - ( - WindowState.NORMAL, - WindowState.MINIMIZED, - WindowState.PRESENTATION, - WindowState.FULLSCREEN, - WindowState.MAXIMIZED, - WindowState.MINIMIZED, - ), - ( - WindowState.MINIMIZED, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - WindowState.NORMAL, - WindowState.FULLSCREEN, - WindowState.MAXIMIZED, - ), - ( - WindowState.MAXIMIZED, - WindowState.MINIMIZED, - WindowState.NORMAL, - WindowState.FULLSCREEN, - WindowState.MINIMIZED, - WindowState.PRESENTATION, - ), - ( - WindowState.FULLSCREEN, - WindowState.MAXIMIZED, - WindowState.MINIMIZED, - WindowState.PRESENTATION, - WindowState.FULLSCREEN, - WindowState.NORMAL, - ), - ( - WindowState.PRESENTATION, - WindowState.FULLSCREEN, - WindowState.NORMAL, - WindowState.MAXIMIZED, - WindowState.MINIMIZED, - WindowState.FULLSCREEN, - ), - ( - WindowState.MINIMIZED, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - WindowState.MAXIMIZED, - WindowState.MINIMIZED, - WindowState.FULLSCREEN, - ), - ] - return itertools.cycle(intermediate_states) - @pytest.mark.parametrize( "initial_state, final_state", [ @@ -817,6 +722,27 @@ def intermediate_states_cycle(): (WindowState.PRESENTATION, WindowState.PRESENTATION), ], ) + @pytest.mark.parametrize( + "intermediate_states", + [ + ( + WindowState.MINIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + WindowState.MAXIMIZED, + WindowState.MINIMIZED, + WindowState.FULLSCREEN, + ), + ( + WindowState.FULLSCREEN, + WindowState.MAXIMIZED, + WindowState.MINIMIZED, + WindowState.PRESENTATION, + WindowState.FULLSCREEN, + WindowState.MINIMIZED, + ), + ], + ) @pytest.mark.parametrize( "second_window_class, second_window_kwargs", [ @@ -833,7 +759,7 @@ async def test_window_state_change_with_intermediate_states( final_state, second_window, second_window_probe, - intermediate_states_cycle, + intermediate_states, ): """Window state can be changed to another state while passing through intermediate states with an expected OS delay.""" @@ -844,7 +770,6 @@ async def test_window_state_change_with_intermediate_states( pytest.xfail( "This backend doesn't reliably support minimized window state." ) - intermediate_states = next(intermediate_states_cycle) second_window.toolbar.add(app.cmd1) second_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) second_window.show() From 29615075023eb30227b93de0bb0d21b29712e762 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 8 Oct 2024 02:12:48 -0400 Subject: [PATCH 221/248] Moved same state transition tests to core --- core/src/toga/window.py | 8 +------- core/tests/window/test_window.py | 23 +++++++++++++++++++++++ dummy/src/toga_dummy/window.py | 9 +++++++-- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index a5737076a3..45f08caef4 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -486,13 +486,7 @@ def state(self, state: WindowState) -> None: f"A non-resizable window cannot be set to a state of {state}." ) else: - # State checks are handled by the backend to accommodate for - # non-blocking native OS calls(e.g., Cocoa). Performing these - # checks at the core level could result in incorrect inferences, - # as non-blocking OS calls may not have completed at the time - # the core checks the current state. This could lead to incorrect - # assertions and potential glitches. - if not self.state == state: + if self.state != state: self._impl.set_window_state(state) ###################################################################### diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index 44251aff4a..f0351538fe 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -360,6 +360,29 @@ def test_window_state(window, initial_state, final_state): ) +@pytest.mark.parametrize( + "state", + [ + WindowState.NORMAL, + WindowState.MINIMIZED, + WindowState.MAXIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + ], +) +def test_window_state_same_as_current(window, state): + """Setting window state the same as current is a no-op.""" + window.state = state + assert window.state == state + + # Reset the EventLog to check that the action was not re-performed. + EventLog.reset() + + window.state = state + assert window.state == state + assert_action_not_performed(window, f"set window state to {state}") + + @pytest.mark.parametrize( "state", [ diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index e63eb9be55..6b7c4a0e2b 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -57,6 +57,8 @@ def __init__(self, interface, title, position, size): self.set_position(position if position is not None else _initial_position()) self.set_size(size) + self._state = WindowState.NORMAL + ###################################################################### # Window properties ###################################################################### @@ -131,11 +133,14 @@ def hide(self): ###################################################################### def get_window_state(self, actual_state=True): - return self._get_value("state", WindowState.NORMAL) + return self._state def set_window_state(self, state): self._action(f"set window state to {state}", state=state) - self._set_value("state", state) + # We cannot store the state value on the EventLog, since the state + # value would be cleared on EventLog.reset(), thereby preventing us + # from testing no-op condition of assigning same state as current. + self._state = state ###################################################################### # Window capabilities From 9ca14edbf62f19ecc9bc3419e115f6c2fd89b4fb Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 8 Oct 2024 07:00:12 -0400 Subject: [PATCH 222/248] Improved tests --- cocoa/src/toga_cocoa/window.py | 4 +- testbed/tests/app/test_desktop.py | 65 +++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 1f455ef16a..0bce6da311 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -100,7 +100,9 @@ def windowDidEnterFullScreen_(self, notification) -> None: @objc_method def delayedFullScreenExit(self, sender) -> None: - self.impl._apply_state(WindowState.NORMAL) + # Marking as no cover, since this method is OS delay dependent and doesn't + # get covered in CI. However, it does get covered when testbed is run locally. + self.impl._apply_state(WindowState.NORMAL) # pragma: no cover @objc_method def windowDidExitFullScreen_(self, notification) -> None: diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index fee94c2217..83acc2dd28 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -259,6 +259,71 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) ), f"{window_information['window'].title}:" +async def test_window_presentation_exit_on_another_window_presentation( + app, main_window_probe +): + window1 = toga.Window(title="Test Window 1", size=(200, 200)) + window2 = toga.Window(title="Test Window 2", size=(200, 200)) + window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) + window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + window1.show() + window2.show() + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window("Test windows are shown") + + assert not app.in_presentation_mode + assert window1.state != WindowState.PRESENTATION + assert window2.state != WindowState.PRESENTATION + + # Enter presentation mode with window2 + app.enter_presentation_mode([window2]) + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window( + "App is in presentation mode", full_screen=True + ) + assert app.in_presentation_mode + assert window2.state == WindowState.PRESENTATION + assert window1.state != WindowState.PRESENTATION + + # Enter presentation mode with window1, window2 no longer in presentation + app.enter_presentation_mode([window1]) + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window( + "App is in presentation mode", full_screen=True + ) + assert app.in_presentation_mode + assert window1.state == WindowState.PRESENTATION + assert window2.state != WindowState.PRESENTATION + + # Exit presentation mode + app.exit_presentation_mode() + await main_window_probe.wait_for_window( + "App is not in presentation mode", full_screen=True + ) + assert not app.in_presentation_mode + assert window1.state != WindowState.PRESENTATION + assert window2.state != WindowState.PRESENTATION + + # Enter presentation mode again with window1 + app.enter_presentation_mode([window1]) + # Add delay to ensure windows are visible after animation. + await main_window_probe.wait_for_window( + "App is in presentation mode", full_screen=True + ) + assert app.in_presentation_mode + assert window1.state == WindowState.PRESENTATION + assert window2.state != WindowState.PRESENTATION + + # Exit presentation mode + app.exit_presentation_mode() + await main_window_probe.wait_for_window( + "App is not in presentation mode", full_screen=True + ) + assert not app.in_presentation_mode + assert window1.state != WindowState.PRESENTATION + assert window2.state != WindowState.PRESENTATION + + @pytest.mark.parametrize( "new_window_state", [ From b6edc2a2fbe35acc632e7d6373acae831f4a4485 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Tue, 8 Oct 2024 07:15:29 -0400 Subject: [PATCH 223/248] Restart CI for Intermittent Android failure From 589e3c7187a1bf08c11ba140e95370191eba60a2 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Sat, 12 Oct 2024 07:04:48 +0530 Subject: [PATCH 224/248] Restart CI for intermittent Android failures From 621c7234de45c087b3c8b4e85fbc2d4c49feb43a Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Fri, 11 Oct 2024 20:30:33 -0700 Subject: [PATCH 225/248] Restart CI for intermittent Android failures From 655a14692c3c5c9d663e5f08b37d776a717673c5 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Sat, 12 Oct 2024 16:34:02 -0700 Subject: [PATCH 226/248] Restart CI for intermittent Android failures --- testbed/tests/app/test_desktop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 83acc2dd28..9e4874a8b9 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -215,8 +215,8 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) assert ( window_information["window"].state == WindowState.PRESENTATION ), f"{window_information['window'].title}:" - # 1000x700 is a size that is bigger that the original - # window size, while being smaller than any likely screen. + # 1000x700 is bigger than the original window size, + # while being smaller than any likely screen. assert ( window_information["window_probe"].content_size[0] > 1000 ), f"{window_information['window'].title}:" From 4f2f2a5f00fe006285409d949ca17d03dc36f9a8 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 12 Oct 2024 17:14:12 -0700 Subject: [PATCH 227/248] Debugging android CI failure --- testbed/tests/window/test_window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 38c5db60ef..7c68d7d6d0 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -174,6 +174,7 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): ), ], ) + @pytest.mark.skip async def test_window_state_change_with_intermediate_states( app, initial_state, From 0d6732283b3ff0ffb5473cfceadfab9ef8c0ad48 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 12 Oct 2024 17:33:38 -0700 Subject: [PATCH 228/248] Debugging android CI failure --- testbed/tests/window/test_window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 7c68d7d6d0..a0d91d581f 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -174,7 +174,6 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): ), ], ) - @pytest.mark.skip async def test_window_state_change_with_intermediate_states( app, initial_state, @@ -211,7 +210,9 @@ async def test_window_state_change_with_intermediate_states( # Set to final state main_window.state = final_state - await main_window_probe.wait_for_window(f"Main window is in {final_state}") + await main_window_probe.wait_for_window( + f"Main window is in {final_state}", rapid_state_switching=True + ) assert main_window.state == final_state From 07e82f9bae326ea313cf484c1277bccee4e94362 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 15 Oct 2024 06:30:05 -0400 Subject: [PATCH 229/248] replaced with --- android/src/toga_android/window.py | 2 +- cocoa/src/toga_cocoa/window.py | 8 ++++---- core/src/toga/window.py | 2 +- dummy/src/toga_dummy/window.py | 2 +- gtk/src/toga_gtk/window.py | 9 ++++----- iOS/src/toga_iOS/window.py | 2 +- winforms/src/toga_winforms/window.py | 2 +- 7 files changed, 13 insertions(+), 14 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 62b08ee8a2..3ce7973255 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -146,7 +146,7 @@ def get_visible(self): # Window state ###################################################################### - def get_window_state(self, actual_state=True): + def get_window_state(self, in_progress_state=True): # `window.state` is called in `_close()`, which itself is # sometimes called during certain stages when the app # attribute may not exist. In such cases, return NORMAL. diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 0bce6da311..955309654a 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -355,8 +355,8 @@ def get_visible(self): # Window state ###################################################################### - def get_window_state(self, actual_state=True): - if not actual_state and self._pending_state_transition: + def get_window_state(self, in_progress_state=True): + if not in_progress_state and self._pending_state_transition: return self._pending_state_transition if bool(self.container.native.isInFullScreenMode()): return WindowState.PRESENTATION @@ -403,11 +403,11 @@ def set_window_state(self, state): self._apply_state(state) def _apply_state(self, target_state): - current_state = self.get_window_state() if target_state is None: return - elif target_state == current_state: + current_state = self.get_window_state() + if target_state == current_state: self._pending_state_transition = None return diff --git a/core/src/toga/window.py b/core/src/toga/window.py index ee10ea5509..7a2fa3685a 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -473,7 +473,7 @@ def visible(self, visible: bool) -> None: @property def state(self) -> WindowState: """The current state of the window.""" - return self._impl.get_window_state(actual_state=False) + return self._impl.get_window_state(in_progress_state=False) @state.setter def state(self, state: WindowState) -> None: diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 6b7c4a0e2b..8c0323b980 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -132,7 +132,7 @@ def hide(self): # Window state ###################################################################### - def get_window_state(self, actual_state=True): + def get_window_state(self, in_progress_state=True): return self._state def set_window_state(self, state): diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index d2506554d9..52bc14b86f 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -164,8 +164,8 @@ def hide(self): # Window state ###################################################################### - def get_window_state(self, actual_state=True): - if not actual_state and self._pending_state_transition: + def get_window_state(self, in_progress_state=True): + if not in_progress_state and self._pending_state_transition: return self._pending_state_transition window_state_flags = self._window_state_flags if window_state_flags: @@ -208,8 +208,6 @@ def set_window_state(self, state): self._apply_state(state) def _apply_state(self, target_state): - current_state = self.get_window_state() - if target_state is None: # pragma: no cover # This is OS delay related and is only sometimes triggered # when there is a delay in processing the states by the OS. @@ -217,7 +215,8 @@ def _apply_state(self, target_state): # testbed coverage. return - elif target_state == current_state: + current_state = self.get_window_state() + if target_state == current_state: self._pending_state_transition = None return diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index 846429f88d..b687251b66 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -142,7 +142,7 @@ def hide(self): # Window state ###################################################################### - def get_window_state(self, actual_state=True): + def get_window_state(self, in_progress_state=True): # Windows are always in NORMAL state. return WindowState.NORMAL diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index c625ea402c..8214ff6dae 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -189,7 +189,7 @@ def hide(self): # Window state ###################################################################### - def get_window_state(self, actual_state=True): + def get_window_state(self, in_progress_state=True): window_state = self.native.WindowState if window_state == WinForms.FormWindowState.Maximized: if self.native.FormBorderStyle == getattr(WinForms.FormBorderStyle, "None"): From 8be4d8d6e4482194dc040a89b2fd70013f26c324 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 15 Oct 2024 07:06:24 -0400 Subject: [PATCH 230/248] replaced with --- android/src/toga_android/window.py | 2 +- cocoa/src/toga_cocoa/window.py | 4 ++-- core/src/toga/window.py | 2 +- dummy/src/toga_dummy/window.py | 2 +- gtk/src/toga_gtk/window.py | 4 ++-- iOS/src/toga_iOS/window.py | 2 +- winforms/src/toga_winforms/window.py | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 3ce7973255..3136df1e08 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -146,7 +146,7 @@ def get_visible(self): # Window state ###################################################################### - def get_window_state(self, in_progress_state=True): + def get_window_state(self, in_progress_state=False): # `window.state` is called in `_close()`, which itself is # sometimes called during certain stages when the app # attribute may not exist. In such cases, return NORMAL. diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 955309654a..83ff221ea1 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -355,8 +355,8 @@ def get_visible(self): # Window state ###################################################################### - def get_window_state(self, in_progress_state=True): - if not in_progress_state and self._pending_state_transition: + def get_window_state(self, in_progress_state=False): + if in_progress_state and self._pending_state_transition: return self._pending_state_transition if bool(self.container.native.isInFullScreenMode()): return WindowState.PRESENTATION diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 7a2fa3685a..f6ba2d6a90 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -473,7 +473,7 @@ def visible(self, visible: bool) -> None: @property def state(self) -> WindowState: """The current state of the window.""" - return self._impl.get_window_state(in_progress_state=False) + return self._impl.get_window_state(in_progress_state=True) @state.setter def state(self, state: WindowState) -> None: diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 8c0323b980..2b7198aef9 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -132,7 +132,7 @@ def hide(self): # Window state ###################################################################### - def get_window_state(self, in_progress_state=True): + def get_window_state(self, in_progress_state=False): return self._state def set_window_state(self, state): diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 52bc14b86f..cc9af61bf5 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -164,8 +164,8 @@ def hide(self): # Window state ###################################################################### - def get_window_state(self, in_progress_state=True): - if not in_progress_state and self._pending_state_transition: + def get_window_state(self, in_progress_state=False): + if in_progress_state and self._pending_state_transition: return self._pending_state_transition window_state_flags = self._window_state_flags if window_state_flags: diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index b687251b66..6095c89b69 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -142,7 +142,7 @@ def hide(self): # Window state ###################################################################### - def get_window_state(self, in_progress_state=True): + def get_window_state(self, in_progress_state=False): # Windows are always in NORMAL state. return WindowState.NORMAL diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 8214ff6dae..792338dd00 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -189,7 +189,7 @@ def hide(self): # Window state ###################################################################### - def get_window_state(self, in_progress_state=True): + def get_window_state(self, in_progress_state=False): window_state = self.native.WindowState if window_state == WinForms.FormWindowState.Maximized: if self.native.FormBorderStyle == getattr(WinForms.FormBorderStyle, "None"): From 1692a0db1ca12869ba62efde6c7d4587a519bb99 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 15 Oct 2024 11:27:41 -0400 Subject: [PATCH 231/248] added new method on window probe --- android/src/toga_android/window.py | 5 +-- android/tests_backend/window.py | 4 +++ cocoa/src/toga_cocoa/window.py | 13 +++++--- cocoa/tests_backend/window.py | 4 +++ core/src/toga/window.py | 8 +++-- gtk/tests_backend/window.py | 4 +++ iOS/tests_backend/window.py | 4 +++ testbed/tests/app/test_desktop.py | 49 +++++++++++++++------------- testbed/tests/window/test_window.py | 49 ++++++++++++++-------------- winforms/src/toga_winforms/window.py | 5 +-- winforms/tests_backend/window.py | 4 +++ 11 files changed, 88 insertions(+), 61 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 3136df1e08..700734a41c 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -169,10 +169,7 @@ def set_window_state(self, state): current_state = self.get_window_state() decor_view = self.app.native.getWindow().getDecorView() - if current_state == state: - return - - elif current_state != WindowState.NORMAL: + if current_state != WindowState.NORMAL: if current_state == WindowState.FULLSCREEN: decor_view.setSystemUiVisibility(0) diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index 9ae999f776..67c03d454f 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -46,6 +46,10 @@ def top_bar_height(self): def _native_menu(self): return self.native.findViewById(appcompat_R.id.action_bar).getMenu() + @property + def instantaneous_state(self): + return self.impl.get_window_state(in_progress_state=False) + def _toolbar_items(self): result = [] prev_group = None diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 83ff221ea1..5079fd120a 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -74,7 +74,7 @@ def windowDidMiniaturize_(self, notification) -> None: and self.impl._pending_state_transition != WindowState.MINIMIZED ): # Marking as no cover, since the operation is native OS delay - # dependent and this doesn't get covered under macOS CI. + # dependent and this doesn't get consistently covered under macOS CI. self.impl._apply_state(WindowState.NORMAL) # pragma: no cover else: self.impl._pending_state_transition = None @@ -99,9 +99,9 @@ def windowDidEnterFullScreen_(self, notification) -> None: self.impl._pending_state_transition = None @objc_method - def delayedFullScreenExit(self, sender) -> None: - # Marking as no cover, since this method is OS delay dependent and doesn't - # get covered in CI. However, it does get covered when testbed is run locally. + def delayedFullScreenExit_(self, sender) -> None: + # Marking as no cover, since the operation is native OS delay + # dependent and this doesn't get consistently covered under macOS CI. self.impl._apply_state(WindowState.NORMAL) # pragma: no cover @objc_method @@ -407,6 +407,11 @@ def _apply_state(self, target_state): return current_state = self.get_window_state() + # Although same state check is done at the core, yet this is required + # Since, _apply_state() is called internally on the implementation + # side, after the completion of non-blocking APIs(setIsMiniaturized, + # toggleFullScreen), by the delegate. Then this same state check is + # used to terminate further processing. if target_state == current_state: self._pending_state_transition = None return diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 838c8255c1..656177b985 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -70,6 +70,10 @@ def minimize(self): def unminimize(self): self.native.deminiaturize(None) + @property + def instantaneous_state(self): + return self.impl.get_window_state(in_progress_state=False) + def has_toolbar(self): return self.native.toolbar is not None diff --git a/core/src/toga/window.py b/core/src/toga/window.py index f6ba2d6a90..5c092e1667 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -473,6 +473,10 @@ def visible(self, visible: bool) -> None: @property def state(self) -> WindowState: """The current state of the window.""" + # There are 2 types of window states that we can get: + # * The instantaneous state -- Used internally on implementation side + # * The in-progress state -- Used for same state checking on the core + # and for the public API. return self._impl.get_window_state(in_progress_state=True) @state.setter @@ -486,8 +490,8 @@ def state(self, state: WindowState) -> None: f"A non-resizable window cannot be set to a state of {state}." ) else: - if self.state != state: - self._impl.set_window_state(state) + # if self.state != state: + self._impl.set_window_state(state) ###################################################################### # Window capabilities diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index a41416bf7a..568752abd5 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -63,6 +63,10 @@ def minimize(self): def unminimize(self): self.native.deiconify() + @property + def instantaneous_state(self): + return self.impl.get_window_state(in_progress_state=False) + def has_toolbar(self): return self.impl.native_toolbar.get_n_items() > 0 diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py index ddddfb69c5..f8805aa8c1 100644 --- a/iOS/tests_backend/window.py +++ b/iOS/tests_backend/window.py @@ -46,5 +46,9 @@ def top_bar_height(self): + self.native.rootViewController.navigationBar.frame.size.height ) + @property + def instantaneous_state(self): + return self.impl.get_window_state(in_progress_state=False) + def has_toolbar(self): pytest.skip("Toolbars not implemented on iOS") diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 9e4874a8b9..faace31839 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -213,7 +213,8 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) # All the windows should be in presentation mode. for window_information in window_information_list: assert ( - window_information["window"].state == WindowState.PRESENTATION + window_information["window_probe"].instantaneous_state + == WindowState.PRESENTATION ), f"{window_information['window'].title}:" # 1000x700 is bigger than the original window size, # while being smaller than any likely screen. @@ -242,7 +243,7 @@ async def test_presentation_mode(app, app_probe, main_window, main_window_probe) assert not app.in_presentation_mode for window_information in window_information_list: assert ( - window_information["window"].state == WindowState.NORMAL + window_information["window_probe"].instantaneous_state == WindowState.NORMAL ), f"{window_information['window'].title}:" assert ( window_information["window_probe"].content_size @@ -264,6 +265,8 @@ async def test_window_presentation_exit_on_another_window_presentation( ): window1 = toga.Window(title="Test Window 1", size=(200, 200)) window2 = toga.Window(title="Test Window 2", size=(200, 200)) + window1_probe = window_probe(app, window1) + window2_probe = window_probe(app, window2) window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) window1.show() @@ -272,8 +275,8 @@ async def test_window_presentation_exit_on_another_window_presentation( await main_window_probe.wait_for_window("Test windows are shown") assert not app.in_presentation_mode - assert window1.state != WindowState.PRESENTATION - assert window2.state != WindowState.PRESENTATION + assert window1_probe.instantaneous_state != WindowState.PRESENTATION + assert window2_probe.instantaneous_state != WindowState.PRESENTATION # Enter presentation mode with window2 app.enter_presentation_mode([window2]) @@ -282,8 +285,8 @@ async def test_window_presentation_exit_on_another_window_presentation( "App is in presentation mode", full_screen=True ) assert app.in_presentation_mode - assert window2.state == WindowState.PRESENTATION - assert window1.state != WindowState.PRESENTATION + assert window2_probe.instantaneous_state == WindowState.PRESENTATION + assert window1_probe.instantaneous_state != WindowState.PRESENTATION # Enter presentation mode with window1, window2 no longer in presentation app.enter_presentation_mode([window1]) @@ -292,8 +295,8 @@ async def test_window_presentation_exit_on_another_window_presentation( "App is in presentation mode", full_screen=True ) assert app.in_presentation_mode - assert window1.state == WindowState.PRESENTATION - assert window2.state != WindowState.PRESENTATION + assert window1_probe.instantaneous_state == WindowState.PRESENTATION + assert window2_probe.instantaneous_state != WindowState.PRESENTATION # Exit presentation mode app.exit_presentation_mode() @@ -301,8 +304,8 @@ async def test_window_presentation_exit_on_another_window_presentation( "App is not in presentation mode", full_screen=True ) assert not app.in_presentation_mode - assert window1.state != WindowState.PRESENTATION - assert window2.state != WindowState.PRESENTATION + assert window1_probe.instantaneous_state != WindowState.PRESENTATION + assert window2_probe.instantaneous_state != WindowState.PRESENTATION # Enter presentation mode again with window1 app.enter_presentation_mode([window1]) @@ -311,8 +314,8 @@ async def test_window_presentation_exit_on_another_window_presentation( "App is in presentation mode", full_screen=True ) assert app.in_presentation_mode - assert window1.state == WindowState.PRESENTATION - assert window2.state != WindowState.PRESENTATION + assert window1_probe.instantaneous_state == WindowState.PRESENTATION + assert window2_probe.instantaneous_state != WindowState.PRESENTATION # Exit presentation mode app.exit_presentation_mode() @@ -320,8 +323,8 @@ async def test_window_presentation_exit_on_another_window_presentation( "App is not in presentation mode", full_screen=True ) assert not app.in_presentation_mode - assert window1.state != WindowState.PRESENTATION - assert window2.state != WindowState.PRESENTATION + assert window1_probe.instantaneous_state != WindowState.PRESENTATION + assert window2_probe.instantaneous_state != WindowState.PRESENTATION @pytest.mark.parametrize( @@ -343,6 +346,8 @@ async def test_presentation_mode_exit_on_window_state_change( window1 = toga.Window(title="Test Window 1", size=(200, 200)) window2 = toga.Window(title="Test Window 2", size=(200, 200)) + window1_probe = window_probe(app, window1) + window2_probe = window_probe(app, window2) window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) window1.show() @@ -357,7 +362,7 @@ async def test_presentation_mode_exit_on_window_state_change( ) assert app.in_presentation_mode - assert window1.state == WindowState.PRESENTATION + assert window1_probe.instantaneous_state == WindowState.PRESENTATION # Changing window state of main window should make the app exit presentation mode. window1.state = new_window_state @@ -369,7 +374,7 @@ async def test_presentation_mode_exit_on_window_state_change( ) assert not app.in_presentation_mode - assert window1.state == new_window_state + assert window1_probe.instantaneous_state == new_window_state # Reset window states window1.state = WindowState.NORMAL @@ -380,8 +385,8 @@ async def test_presentation_mode_exit_on_window_state_change( minimize=True if new_window_state == WindowState.MINIMIZED else False, full_screen=True if new_window_state == WindowState.FULLSCREEN else False, ) - assert window1.state == WindowState.NORMAL - assert window2.state == WindowState.NORMAL + assert window1_probe.instantaneous_state == WindowState.NORMAL + assert window2_probe.instantaneous_state == WindowState.NORMAL # Enter presentation mode again app.enter_presentation_mode([window1]) @@ -392,7 +397,7 @@ async def test_presentation_mode_exit_on_window_state_change( full_screen=True if new_window_state == WindowState.FULLSCREEN else False, ) assert app.in_presentation_mode - assert window1.state == WindowState.PRESENTATION + assert window1_probe.instantaneous_state == WindowState.PRESENTATION # Changing window state of extra window should make the app exit presentation mode. window2.state = new_window_state @@ -404,7 +409,7 @@ async def test_presentation_mode_exit_on_window_state_change( ) assert not app.in_presentation_mode - assert window2.state == new_window_state + assert window2_probe.instantaneous_state == new_window_state # Reset window states window1.state = WindowState.NORMAL @@ -415,8 +420,8 @@ async def test_presentation_mode_exit_on_window_state_change( minimize=True if new_window_state == WindowState.MINIMIZED else False, full_screen=True if new_window_state == WindowState.FULLSCREEN else False, ) - assert window1.state == WindowState.NORMAL - assert window2.state == WindowState.NORMAL + assert window1_probe.instantaneous_state == WindowState.NORMAL + assert window2_probe.instantaneous_state == WindowState.NORMAL async def test_show_hide_cursor(app, app_probe): diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index a0d91d581f..8489c0d825 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -156,14 +156,6 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): @pytest.mark.parametrize( "intermediate_states", [ - ( - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - WindowState.NORMAL, - WindowState.FULLSCREEN, - WindowState.NORMAL, - WindowState.PRESENTATION, - ), ( WindowState.PRESENTATION, WindowState.FULLSCREEN, @@ -202,7 +194,7 @@ async def test_window_state_change_with_intermediate_states( main_window.state = initial_state await main_window_probe.wait_for_window(f"Main window is in {initial_state}") - assert main_window.state == initial_state + assert main_window_probe.instantaneous_state == initial_state # Set to the intermediate states but don't wait for the OS delay. for state in intermediate_states: @@ -214,7 +206,7 @@ async def test_window_state_change_with_intermediate_states( f"Main window is in {final_state}", rapid_state_switching=True ) - assert main_window.state == final_state + assert main_window_probe.instantaneous_state == final_state @pytest.mark.parametrize( "state", @@ -243,11 +235,11 @@ async def test_window_state_same_as_current_without_intermediate_states( # Set the window state: main_window.state = state await main_window_probe.wait_for_window(f"Secondary window is in {state}") - assert main_window.state == state + assert main_window_probe.instantaneous_state == state # Set the window state the same as current: main_window.state = state - assert main_window.state == state + assert main_window_probe.instantaneous_state == state @pytest.mark.parametrize( "state", @@ -276,7 +268,7 @@ async def test_window_state_content_size_increase( # Add delay to ensure windows are visible after animation. await main_window_probe.wait_for_window("Main window is shown") - assert main_window.state == WindowState.NORMAL + assert main_window_probe.instantaneous_state == WindowState.NORMAL initial_content_size = main_window_probe.content_size main_window.state = state @@ -285,7 +277,7 @@ async def test_window_state_content_size_increase( f"Main window is in {state}", full_screen=True if state == WindowState.FULLSCREEN else False, ) - assert main_window.state == state + assert main_window_probe.instantaneous_state == state # At least one of the dimension should have increased. assert ( main_window_probe.content_size[0] > initial_content_size[0] @@ -297,7 +289,7 @@ async def test_window_state_content_size_increase( f"Main window is still in {state}", full_screen=True if state == WindowState.FULLSCREEN else False, ) - assert main_window.state == state + assert main_window_probe.instantaneous_state == state # At least one of the dimension should have increased. assert ( main_window_probe.content_size[0] > initial_content_size[0] @@ -310,7 +302,7 @@ async def test_window_state_content_size_increase( f"Main window is not in {state}", full_screen=True if state == WindowState.FULLSCREEN else False, ) - assert main_window.state == WindowState.NORMAL + assert main_window_probe.instantaneous_state == WindowState.NORMAL assert main_window_probe.content_size == initial_content_size @pytest.mark.parametrize( @@ -779,7 +771,7 @@ async def test_window_state_change_with_intermediate_states( # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window("Secondary window is visible") - assert second_window.state == WindowState.NORMAL + assert second_window_probe.instantaneous_state == WindowState.NORMAL # Set to initial state second_window.state = initial_state @@ -789,11 +781,17 @@ async def test_window_state_change_with_intermediate_states( minimize=True if initial_state == WindowState.MINIMIZED else False, full_screen=True if initial_state == WindowState.FULLSCREEN else False, ) - - assert second_window.state == initial_state + assert second_window_probe.instantaneous_state == initial_state # Set to the intermediate states but don't wait for the OS delay. for state in intermediate_states: + # If the state is not supported by the backend, then + # don't assign it, instead of skipping the whole test. + if ( + state == WindowState.MINIMIZED + and not second_window_probe.supports_minimize + ): + continue second_window.state = state # Set to final state @@ -802,6 +800,7 @@ async def test_window_state_change_with_intermediate_states( await second_window_probe.wait_for_window( f"Secondary window is in {final_state}", rapid_state_switching=True ) + assert second_window_probe.instantaneous_state == final_state @pytest.mark.parametrize( "state", @@ -845,12 +844,12 @@ async def test_window_state_same_as_current_without_intermediate_states( minimize=True if state == WindowState.MINIMIZED else False, full_screen=True if state == WindowState.FULLSCREEN else False, ) - assert second_window.state == state + assert second_window_probe.instantaneous_state == state # Set the window state the same as current: second_window.state = state # No need to wait for OS delay as the above operation should be a no-op. - assert second_window.state == state + assert second_window_probe.instantaneous_state == state @pytest.mark.parametrize( "state", @@ -879,7 +878,7 @@ async def test_window_state_content_size_increase( # Add delay to ensure windows are visible after animation. await second_window_probe.wait_for_window("Secondary window is shown") - assert second_window.state == WindowState.NORMAL + assert second_window_probe.instantaneous_state == WindowState.NORMAL assert second_window_probe.is_resizable initial_content_size = second_window_probe.content_size @@ -889,7 +888,7 @@ async def test_window_state_content_size_increase( f"Secondary window is in {state}", full_screen=True if state == WindowState.FULLSCREEN else False, ) - assert second_window.state == state + assert second_window_probe.instantaneous_state == state assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] @@ -898,7 +897,7 @@ async def test_window_state_content_size_increase( f"Secondary window is still in {state}", full_screen=True if state == WindowState.FULLSCREEN else False, ) - assert second_window.state == state + assert second_window_probe.instantaneous_state == state assert second_window_probe.content_size[0] > initial_content_size[0] assert second_window_probe.content_size[1] > initial_content_size[1] @@ -908,7 +907,7 @@ async def test_window_state_content_size_increase( f"Secondary window is not in {state}", full_screen=True if state == WindowState.FULLSCREEN else False, ) - assert second_window.state == WindowState.NORMAL + assert second_window_probe.instantaneous_state == WindowState.NORMAL assert second_window_probe.is_resizable assert second_window_probe.content_size == initial_content_size diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 792338dd00..3a1c04c0d9 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -214,10 +214,7 @@ def set_window_state(self, state): self.interface.app.exit_presentation_mode() current_state = self.get_window_state() - if current_state == state: - return - - elif current_state != WindowState.NORMAL: + if current_state != WindowState.NORMAL: if current_state == WindowState.PRESENTATION: if self.native.MainMenuStrip: self.native.MainMenuStrip.Visible = True diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 4190a54cac..401ac581ba 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -72,6 +72,10 @@ def minimize(self): def unminimize(self): self.native.WindowState = FormWindowState.Normal + @property + def instantaneous_state(self): + return self.impl.get_window_state(in_progress_state=False) + def _native_toolbar(self): for control in self.native.Controls: if isinstance(control, ToolStrip) and not isinstance(control, MenuStrip): From f8ee69d8417cb14465709d7da6036dcf8516295a Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 15 Oct 2024 11:31:21 -0400 Subject: [PATCH 232/248] Revert commented code --- core/src/toga/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 5c092e1667..9ccf58a8c3 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -490,8 +490,8 @@ def state(self, state: WindowState) -> None: f"A non-resizable window cannot be set to a state of {state}." ) else: - # if self.state != state: - self._impl.set_window_state(state) + if self.state != state: + self._impl.set_window_state(state) ###################################################################### # Window capabilities From b82b6d095e7eab184d4573358f590db36c7313bf Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 15 Oct 2024 13:27:06 -0400 Subject: [PATCH 233/248] Revert --- android/src/toga_android/window.py | 5 ++++- testbed/tests/window/test_window.py | 7 ------- winforms/src/toga_winforms/window.py | 5 ++++- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 700734a41c..3136df1e08 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -169,7 +169,10 @@ def set_window_state(self, state): current_state = self.get_window_state() decor_view = self.app.native.getWindow().getDecorView() - if current_state != WindowState.NORMAL: + if current_state == state: + return + + elif current_state != WindowState.NORMAL: if current_state == WindowState.FULLSCREEN: decor_view.setSystemUiVisibility(0) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 8489c0d825..bb04d2d2cc 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -785,13 +785,6 @@ async def test_window_state_change_with_intermediate_states( # Set to the intermediate states but don't wait for the OS delay. for state in intermediate_states: - # If the state is not supported by the backend, then - # don't assign it, instead of skipping the whole test. - if ( - state == WindowState.MINIMIZED - and not second_window_probe.supports_minimize - ): - continue second_window.state = state # Set to final state diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 3a1c04c0d9..792338dd00 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -214,7 +214,10 @@ def set_window_state(self, state): self.interface.app.exit_presentation_mode() current_state = self.get_window_state() - if current_state != WindowState.NORMAL: + if current_state == state: + return + + elif current_state != WindowState.NORMAL: if current_state == WindowState.PRESENTATION: if self.native.MainMenuStrip: self.native.MainMenuStrip.Visible = True From 247847e80bc3cb9c953a7650a06955fe0cb45f28 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 15 Oct 2024 13:57:15 -0400 Subject: [PATCH 234/248] Corrected probe method on android --- android/tests_backend/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index 67c03d454f..d0292dfdc9 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -13,6 +13,7 @@ def __init__(self, app, window): super().__init__(app) self.native = self.app._impl.native self.window = window + self.impl = self.window._impl async def wait_for_window( self, From 5560e3dedcbeca35964b1dab286319e2d01fee7f Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Tue, 15 Oct 2024 22:23:56 -0400 Subject: [PATCH 235/248] Diagnosing macOS-arm64 failures --- cocoa/tests_backend/window.py | 2 +- testbed/tests/window/test_window.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 656177b985..145ecf9e10 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -32,7 +32,7 @@ async def wait_for_window( await self.redraw( message, delay=( - 1.5 + 2.5 if rapid_state_switching else 0.75 if full_screen else 0.5 if minimize else 0.1 ), diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index bb04d2d2cc..5bb2ca938b 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -732,8 +732,8 @@ async def test_move_and_resize(second_window, second_window_probe): WindowState.MAXIMIZED, WindowState.MINIMIZED, WindowState.PRESENTATION, - WindowState.FULLSCREEN, - WindowState.MINIMIZED, + WindowState.MAXIMIZED, + WindowState.PRESENTATION, ), ], ) From 5f0c1a2a4c4f0860b6fb39e5bfaedb7b6d6b5579 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 18 Oct 2024 10:18:50 -0700 Subject: [PATCH 236/248] Correcting testbed failure --- cocoa/tests_backend/window.py | 2 +- core/src/toga/window.py | 7 ------ core/tests/window/test_window.py | 34 ++++++++++++++--------------- testbed/tests/conftest.py | 23 +++++++++---------- testbed/tests/window/test_window.py | 24 ++++++++++++-------- 5 files changed, 43 insertions(+), 47 deletions(-) diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 145ecf9e10..d1c425dd79 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -32,7 +32,7 @@ async def wait_for_window( await self.redraw( message, delay=( - 2.5 + 2 if rapid_state_switching else 0.75 if full_screen else 0.5 if minimize else 0.1 ), diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 9ccf58a8c3..17fbfdcc2c 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -323,13 +323,6 @@ def _close(self): # The actual logic for closing a window. This is abstracted so that the testbed # can monkeypatch this method, recording the close request without actually # closing the app. - - # Restore to normal state if in presentation mode. On some backends (e.g., Cocoa), - # the content is in presentation mode, not the window. Closing the window directly - # can cause rendering glitches. - if self.state == WindowState.PRESENTATION: - self.state = WindowState.NORMAL - if self.content: self.content.window = None self.app.windows.discard(self) diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index f0351538fe..828f81ed80 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -405,23 +405,23 @@ def test_non_resizable_window_state(state): non_resizable_window.close() -def test_close_direct_in_presentation(window, app): - """Directly closing a window in presentation mode restores to normal first.""" - window.state = WindowState.PRESENTATION - assert window.state == WindowState.PRESENTATION - assert_action_performed_with( - window, - "set window state to WindowState.PRESENTATION", - state=WindowState.PRESENTATION, - ) - - window.close() - assert window.state == WindowState.NORMAL - assert_action_performed_with( - window, - "set window state to WindowState.NORMAL", - state=WindowState.NORMAL, - ) +# def test_close_direct_in_presentation(window, app): +# """Directly closing a window in presentation mode restores to normal first.""" +# window.state = WindowState.PRESENTATION +# assert window.state == WindowState.PRESENTATION +# assert_action_performed_with( +# window, +# "set window state to WindowState.PRESENTATION", +# state=WindowState.PRESENTATION, +# ) + +# window.close() +# assert window.state == WindowState.NORMAL +# assert_action_performed_with( +# window, +# "set window state to WindowState.NORMAL", +# state=WindowState.NORMAL, +# ) def test_close_direct(window, app): diff --git a/testbed/tests/conftest.py b/testbed/tests/conftest.py index f4d0b70857..ed84665d71 100644 --- a/testbed/tests/conftest.py +++ b/testbed/tests/conftest.py @@ -71,19 +71,7 @@ def main_window(app): @fixture(autouse=True) -async def window_cleanup(app, app_probe, main_window, main_window_probe): - # After closing the window, the input focus might not be on main_window. - # Ensure that main_window is in NORMAL state and will be in focus for - # other tests. - app.current_window = main_window - main_window_state = main_window.state - main_window.state = WindowState.NORMAL - await main_window_probe.wait_for_window( - "main_window is now the current window and is in NORMAL state", - minimize=True if main_window_state == WindowState.MINIMIZED else False, - full_screen=True if main_window_state == WindowState.FULLSCREEN else False, - ) - +async def window_cleanup(app, main_window, main_window_probe): # Ensure that at the end of every test, all windows that aren't the # main window have been closed and deleted. This needs to be done in # 2 passes because we can't modify the list while iterating over it. @@ -102,6 +90,15 @@ async def window_cleanup(app, app_probe, main_window, main_window_probe): # minimize garbage collection on the test thread. gc.collect() + main_window_state = main_window.state + main_window.state = WindowState.NORMAL + app.current_window = main_window + await main_window_probe.wait_for_window( + "Resetting main_window", + minimize=True if main_window_state == WindowState.MINIMIZED else False, + full_screen=True if main_window_state == WindowState.FULLSCREEN else False, + ) + @fixture(scope="session") async def main_window_probe(app, main_window): diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 5bb2ca938b..6b6a6f4c4d 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -168,10 +168,10 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): ) async def test_window_state_change_with_intermediate_states( app, - initial_state, - final_state, main_window, main_window_probe, + initial_state, + final_state, intermediate_states, ): """Window state can be directly changed to another state.""" @@ -198,14 +198,13 @@ async def test_window_state_change_with_intermediate_states( # Set to the intermediate states but don't wait for the OS delay. for state in intermediate_states: - second_window.state = state + main_window.state = state # Set to final state main_window.state = final_state await main_window_probe.wait_for_window( f"Main window is in {final_state}", rapid_state_switching=True ) - assert main_window_probe.instantaneous_state == final_state @pytest.mark.parametrize( @@ -249,7 +248,7 @@ async def test_window_state_same_as_current_without_intermediate_states( ], ) async def test_window_state_content_size_increase( - main_window, main_window_probe, state + app, app_probe, main_window, main_window_probe, state ): """The size of the window content should increase when the window state is set to maximized, fullscreen or presentation.""" @@ -732,8 +731,8 @@ async def test_move_and_resize(second_window, second_window_probe): WindowState.MAXIMIZED, WindowState.MINIMIZED, WindowState.PRESENTATION, - WindowState.MAXIMIZED, - WindowState.PRESENTATION, + WindowState.FULLSCREEN, + WindowState.MINIMIZED, ), ], ) @@ -749,14 +748,18 @@ async def test_move_and_resize(second_window, second_window_probe): async def test_window_state_change_with_intermediate_states( app, app_probe, - initial_state, - final_state, second_window, second_window_probe, + initial_state, + final_state, intermediate_states, ): """Window state can be changed to another state while passing through intermediate states with an expected OS delay.""" + explicit_slow_mode = app.run_slow + if toga.platform.current_platform == "macOS": + app.run_slow = True + if ( WindowState.MINIMIZED in {initial_state, final_state} and not second_window_probe.supports_minimize @@ -795,6 +798,9 @@ async def test_window_state_change_with_intermediate_states( ) assert second_window_probe.instantaneous_state == final_state + if toga.platform.current_platform == "macOS" and not explicit_slow_mode: + app.run_slow = False + @pytest.mark.parametrize( "state", [ From 136cdcf8e47bd95421d1be055cf66ea408ef8165 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 21 Oct 2024 05:36:20 -0700 Subject: [PATCH 237/248] Removed unused android code --- android/src/toga_android/window.py | 5 ----- gtk/src/toga_gtk/window.py | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 3136df1e08..58f3aa79a0 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -147,11 +147,6 @@ def get_visible(self): ###################################################################### def get_window_state(self, in_progress_state=False): - # `window.state` is called in `_close()`, which itself is - # sometimes called during certain stages when the app - # attribute may not exist. In such cases, return NORMAL. - if getattr(self, "app", None) is None: - return WindowState.NORMAL decor_view = self.app.native.getWindow().getDecorView() system_ui_flags = decor_view.getSystemUiVisibility() if system_ui_flags & ( diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index cc9af61bf5..314d528a28 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -168,7 +168,7 @@ def get_window_state(self, in_progress_state=False): if in_progress_state and self._pending_state_transition: return self._pending_state_transition window_state_flags = self._window_state_flags - if window_state_flags: + if window_state_flags: # pragma: no branch if window_state_flags & Gdk.WindowState.MAXIMIZED: return WindowState.MAXIMIZED elif window_state_flags & Gdk.WindowState.ICONIFIED: From 8fcdbd60095534b697f5befef2920e0c7f3387b8 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 24 Oct 2024 21:58:52 -0700 Subject: [PATCH 238/248] Fixed testbed for macOS --- testbed/tests/conftest.py | 3 ++- testbed/tests/window/test_window.py | 23 ++++++++--------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/testbed/tests/conftest.py b/testbed/tests/conftest.py index ed84665d71..b665f5f4ae 100644 --- a/testbed/tests/conftest.py +++ b/testbed/tests/conftest.py @@ -71,7 +71,7 @@ def main_window(app): @fixture(autouse=True) -async def window_cleanup(app, main_window, main_window_probe): +async def window_cleanup(app, app_probe, main_window, main_window_probe): # Ensure that at the end of every test, all windows that aren't the # main window have been closed and deleted. This needs to be done in # 2 passes because we can't modify the list while iterating over it. @@ -84,6 +84,7 @@ async def window_cleanup(app, main_window, main_window_probe): while kill_list: window = kill_list.pop() window.close() + await main_window_probe.wait_for_window(f"Closing window: {window.title}") del window # Force a GC pass on the main thread. This isn't perfect, but it helps diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index 6b6a6f4c4d..be86dc9de7 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -718,14 +718,14 @@ async def test_move_and_resize(second_window, second_window_probe): @pytest.mark.parametrize( "intermediate_states", [ - ( - WindowState.MINIMIZED, - WindowState.FULLSCREEN, - WindowState.PRESENTATION, - WindowState.MAXIMIZED, - WindowState.MINIMIZED, - WindowState.FULLSCREEN, - ), + # ( + # WindowState.MINIMIZED, + # WindowState.FULLSCREEN, + # WindowState.PRESENTATION, + # WindowState.MAXIMIZED, + # WindowState.MINIMIZED, + # WindowState.FULLSCREEN, + # ), ( WindowState.FULLSCREEN, WindowState.MAXIMIZED, @@ -756,10 +756,6 @@ async def test_window_state_change_with_intermediate_states( ): """Window state can be changed to another state while passing through intermediate states with an expected OS delay.""" - explicit_slow_mode = app.run_slow - if toga.platform.current_platform == "macOS": - app.run_slow = True - if ( WindowState.MINIMIZED in {initial_state, final_state} and not second_window_probe.supports_minimize @@ -798,9 +794,6 @@ async def test_window_state_change_with_intermediate_states( ) assert second_window_probe.instantaneous_state == final_state - if toga.platform.current_platform == "macOS" and not explicit_slow_mode: - app.run_slow = False - @pytest.mark.parametrize( "state", [ From ef584b1a1a345277719d67dbd643fa50c08040d0 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 25 Oct 2024 03:09:21 -0700 Subject: [PATCH 239/248] Fixed testbed for Android --- android/tests_backend/window.py | 2 +- testbed/tests/conftest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index d0292dfdc9..db9e62569b 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -24,7 +24,7 @@ async def wait_for_window( ): await self.redraw( message, - delay=(0.1 if (full_screen or rapid_state_switching) else 0), + delay=(0.5 if (full_screen or rapid_state_switching) else 0), ) @property diff --git a/testbed/tests/conftest.py b/testbed/tests/conftest.py index b665f5f4ae..9c0141b018 100644 --- a/testbed/tests/conftest.py +++ b/testbed/tests/conftest.py @@ -84,7 +84,7 @@ async def window_cleanup(app, app_probe, main_window, main_window_probe): while kill_list: window = kill_list.pop() window.close() - await main_window_probe.wait_for_window(f"Closing window: {window.title}") + await main_window_probe.wait_for_window("Closing window") del window # Force a GC pass on the main thread. This isn't perfect, but it helps From 7f28348ac9efdbaf4504263c40ec2afe894df61b Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 25 Oct 2024 03:30:10 -0700 Subject: [PATCH 240/248] Enable additional test cases --- testbed/tests/window/test_window.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index be86dc9de7..f9f3befb7b 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -718,14 +718,14 @@ async def test_move_and_resize(second_window, second_window_probe): @pytest.mark.parametrize( "intermediate_states", [ - # ( - # WindowState.MINIMIZED, - # WindowState.FULLSCREEN, - # WindowState.PRESENTATION, - # WindowState.MAXIMIZED, - # WindowState.MINIMIZED, - # WindowState.FULLSCREEN, - # ), + ( + WindowState.MINIMIZED, + WindowState.FULLSCREEN, + WindowState.PRESENTATION, + WindowState.MAXIMIZED, + WindowState.MINIMIZED, + WindowState.FULLSCREEN, + ), ( WindowState.FULLSCREEN, WindowState.MAXIMIZED, From 6e0c0919b9f8b1268cab181ba1d9208e2f502907 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 25 Oct 2024 04:23:37 -0700 Subject: [PATCH 241/248] Removed stray comments --- cocoa/src/toga_cocoa/window.py | 14 +++++--------- core/src/toga/window.py | 2 +- core/tests/window/test_window.py | 19 ------------------- 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 5079fd120a..124993cd70 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -56,10 +56,11 @@ def windowWillClose_(self, notification) -> None: # Disconnect the window delegate. self.delegate = None - # Disconnecting the window delegate doesn't prevent custom - # methods which are performed with a delay i.e., having a - # delay != 0), from being triggered. Hence, check guards - # needs to be used in such custom methods. + # Disconnecting the window delegate doesn't prevent custom methods + # which are performed with a delay (i.e., having a delay != 0), + # from being triggered when `impl` & `interface` attributes are empty. + # Hence, check guards for empty `impl` and `interface` attributes need + # to be used in such custom methods. @objc_method def windowDidResize_(self, notification) -> None: @@ -208,11 +209,6 @@ def __init__(self, interface, title, position, size): if self.interface.minimizable: mask |= NSWindowStyleMask.Miniaturizable - # The objc callback methods can be called during the initialization - # of the NSWindow. So, any objc method referencing impl or interface - # will get an Attribute error. Hence, first allocate and assign the - # impl & interface to it. Then finally initialize the NSWindow. - # Create the window with a default frame; # we'll update size and position later. self.native = TogaWindow.alloc().initWithContentRect( diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 17fbfdcc2c..6d18be5a2f 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -824,7 +824,7 @@ def closeable(self) -> bool: ###################################################################### ###################################################################### - # 2024-07: Backwards compatibility + # 2024-10: Backwards compatibility ###################################################################### @property def full_screen(self) -> bool: diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index 828f81ed80..5a9e60803a 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -405,25 +405,6 @@ def test_non_resizable_window_state(state): non_resizable_window.close() -# def test_close_direct_in_presentation(window, app): -# """Directly closing a window in presentation mode restores to normal first.""" -# window.state = WindowState.PRESENTATION -# assert window.state == WindowState.PRESENTATION -# assert_action_performed_with( -# window, -# "set window state to WindowState.PRESENTATION", -# state=WindowState.PRESENTATION, -# ) - -# window.close() -# assert window.state == WindowState.NORMAL -# assert_action_performed_with( -# window, -# "set window state to WindowState.NORMAL", -# state=WindowState.NORMAL, -# ) - - def test_close_direct(window, app): """A window can be closed directly.""" on_close_handler = Mock(return_value=True) From f535cd6754652a8fc3540cf282f6b1cb534a891a Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 25 Oct 2024 06:00:13 -0700 Subject: [PATCH 242/248] Restart CI for intermittent Android failure From c175b6ab49511fc3b15dbd0bcc894e8223aa1dc8 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 25 Oct 2024 06:19:24 -0700 Subject: [PATCH 243/248] Fix Android delay --- android/tests_backend/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index db9e62569b..0cf69ee297 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -24,7 +24,7 @@ async def wait_for_window( ): await self.redraw( message, - delay=(0.5 if (full_screen or rapid_state_switching) else 0), + delay=(0.5 if (full_screen or rapid_state_switching) else 0.1), ) @property From bc1f902ac30660103b2303394c43f00a0c626acb Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 26 Oct 2024 20:09:15 -0700 Subject: [PATCH 244/248] Added note on state setter --- core/src/toga/window.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 6d18be5a2f..1b2094bd66 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -465,8 +465,12 @@ def visible(self, visible: bool) -> None: @property def state(self) -> WindowState: - """The current state of the window.""" - # There are 2 types of window states that we can get: + """The current state of the window. + + When the window is in transition, then this will return the state it + is transitioning towards, instead of the actual instantaneous state. + """ + # There are 2 types of window states that we can get from the backend: # * The instantaneous state -- Used internally on implementation side # * The in-progress state -- Used for same state checking on the core # and for the public API. From 4162e4bc6dd748658106e6440b9e75a78269bb8c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 7 Nov 2024 06:32:42 -0500 Subject: [PATCH 245/248] Fixed window size after restoration to NORMAL state on wayland --- gtk/src/toga_gtk/window.py | 6 ++++++ testbed/tests/window/test_window.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 314d528a28..1004cb6429 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -79,6 +79,8 @@ def gtk_window_state_event(self, widget, event): else: self._pending_state_transition = None else: + if IS_WAYLAND: # pragma: no-cover-if-linux-x + self._normal_state_size = self.get_size() self._apply_state(self._pending_state_transition) def gtk_delete_event(self, widget, data): @@ -114,6 +116,8 @@ def set_app(self, app): def show(self): self.native.show_all() + if IS_WAYLAND: # pragma: no-cover-if-linux-x + self._normal_state_size = self.get_size() ###################################################################### # Window content and resources @@ -260,6 +264,8 @@ def _apply_state(self, target_state): self.interface.screen = self._before_presentation_mode_screen del self._before_presentation_mode_screen self._in_presentation = False + if IS_WAYLAND: # pragma: no-cover-if-linux-x + self.set_size(self._normal_state_size) ###################################################################### # Window capabilities diff --git a/testbed/tests/window/test_window.py b/testbed/tests/window/test_window.py index f9f3befb7b..6765ee08e3 100644 --- a/testbed/tests/window/test_window.py +++ b/testbed/tests/window/test_window.py @@ -156,6 +156,10 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): @pytest.mark.parametrize( "intermediate_states", [ + # This set of intermediate states is randomly chosen and is not intended + # to check for specific problematic state transitions. Instead, it is used + # to test that rapidly passing new states to the window.state setter does + # not cause any unexpected glitches. ( WindowState.PRESENTATION, WindowState.FULLSCREEN, @@ -718,6 +722,9 @@ async def test_move_and_resize(second_window, second_window_probe): @pytest.mark.parametrize( "intermediate_states", [ + # These sets of intermediate states are specifically chosen to trigger cases that + # will cause test failures if the implementation is incorrect on certain backends, + # such as macOS. ( WindowState.MINIMIZED, WindowState.FULLSCREEN, From eb5173088099de61798c7ed8aeffdacebd057eee Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 7 Nov 2024 07:36:22 -0500 Subject: [PATCH 246/248] Fixed window size setting on wayland --- gtk/src/toga_gtk/window.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 1004cb6429..1a172bc728 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -136,7 +136,12 @@ def get_size(self) -> Size: return Size(size.width, size.height) def set_size(self, size: SizeT): - self.native.resize(size[0], size[1]) + if IS_WAYLAND: # pragma: no-cover-if-linux-x + # resize() doesn't work properly on wayland. Example: Doing + # resize(200,200) will actually set window size to (200,163). + self.native.set_default_size(size[0], size[1]) + else: # pragma: no-cover-if-linux-wayland + self.native.resize(size[0], size[1]) ###################################################################### # Window position From b25b90202d4bb715923e04eb9f62bdff609b1ebd Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 7 Nov 2024 08:56:30 -0500 Subject: [PATCH 247/248] Revert changes --- gtk/src/toga_gtk/window.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 1a172bc728..314d528a28 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -79,8 +79,6 @@ def gtk_window_state_event(self, widget, event): else: self._pending_state_transition = None else: - if IS_WAYLAND: # pragma: no-cover-if-linux-x - self._normal_state_size = self.get_size() self._apply_state(self._pending_state_transition) def gtk_delete_event(self, widget, data): @@ -116,8 +114,6 @@ def set_app(self, app): def show(self): self.native.show_all() - if IS_WAYLAND: # pragma: no-cover-if-linux-x - self._normal_state_size = self.get_size() ###################################################################### # Window content and resources @@ -136,12 +132,7 @@ def get_size(self) -> Size: return Size(size.width, size.height) def set_size(self, size: SizeT): - if IS_WAYLAND: # pragma: no-cover-if-linux-x - # resize() doesn't work properly on wayland. Example: Doing - # resize(200,200) will actually set window size to (200,163). - self.native.set_default_size(size[0], size[1]) - else: # pragma: no-cover-if-linux-wayland - self.native.resize(size[0], size[1]) + self.native.resize(size[0], size[1]) ###################################################################### # Window position @@ -269,8 +260,6 @@ def _apply_state(self, target_state): self.interface.screen = self._before_presentation_mode_screen del self._before_presentation_mode_screen self._in_presentation = False - if IS_WAYLAND: # pragma: no-cover-if-linux-x - self.set_size(self._normal_state_size) ###################################################################### # Window capabilities From 8df6c4b05ca4fd030c3290f5839e7fb696eff698 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 7 Nov 2024 09:37:00 -0500 Subject: [PATCH 248/248] Add delay to prevent glitching on wayland --- gtk/src/toga_gtk/window.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 314d528a28..f2a0e14481 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -1,5 +1,6 @@ from __future__ import annotations +from functools import partial from typing import TYPE_CHECKING from toga.command import Separator @@ -8,7 +9,7 @@ from toga.window import _initial_position from .container import TogaContainer -from .libs import IS_WAYLAND, Gdk, Gtk +from .libs import IS_WAYLAND, Gdk, GLib, Gtk from .screens import Screen as ScreenImpl if TYPE_CHECKING: # pragma: no cover @@ -75,11 +76,23 @@ def gtk_window_state_event(self, widget, event): current_state = self.get_window_state() if current_state != WindowState.NORMAL: if self._pending_state_transition != current_state: - self._apply_state(WindowState.NORMAL) + # Add slight delay to prevent glitching on wayland during rapid + # state switching. + if IS_WAYLAND: # pragma: no-cover-if-linux-x + GLib.timeout_add( + 10, partial(self._apply_state, WindowState.NORMAL) + ) + else: # pragma: no-cover-if-linux-wayland + self._apply_state(WindowState.NORMAL) else: self._pending_state_transition = None else: - self._apply_state(self._pending_state_transition) + if IS_WAYLAND: # pragma: no-cover-if-linux-x + GLib.timeout_add( + 10, partial(self._apply_state, self._pending_state_transition) + ) + else: # pragma: no-cover-if-linux-wayland + self._apply_state(self._pending_state_transition) def gtk_delete_event(self, widget, data): # Return value of the GTK on_close handler indicates whether the event has been