From a3c35e886fd2609a828d6392c7525a246485847c Mon Sep 17 00:00:00 2001 From: Dan Lawrence Date: Sat, 13 Apr 2024 19:59:37 +0100 Subject: [PATCH 1/3] Add always on top feature to windows fixes #564 --- .../core/interfaces/window_interface.py | 36 ++++- .../core/interfaces/window_stack_interface.py | 14 +- pygame_gui/core/ui_container.py | 4 +- pygame_gui/core/ui_window_stack.py | 136 ++++++++++++++++-- pygame_gui/elements/ui_window.py | 26 +++- pygame_gui/windows/ui_colour_picker_dialog.py | 6 +- pygame_gui/windows/ui_confirmation_dialog.py | 6 +- pygame_gui/windows/ui_console_window.py | 6 +- pygame_gui/windows/ui_file_dialog.py | 6 +- pygame_gui/windows/ui_message_window.py | 6 +- tests/test_core/test_window_stack.py | 92 ++++++++++++ 11 files changed, 305 insertions(+), 33 deletions(-) diff --git a/pygame_gui/core/interfaces/window_interface.py b/pygame_gui/core/interfaces/window_interface.py index ea724455..2ed61623 100644 --- a/pygame_gui/core/interfaces/window_interface.py +++ b/pygame_gui/core/interfaces/window_interface.py @@ -7,13 +7,37 @@ class IWindowInterface(metaclass=ABCMeta): """ - A meta class that defines the interface that the window stack uses to interface with the + A metaclass that defines the interface that the window stack uses to interface with the UIWindow class. Interfaces like this help us evade cyclical import problems by allowing us to define the actual window class later on and have it make use of the window stack. """ + @property + @abstractmethod + def layer(self) -> int: + """ + The layer of this window (read-only) + """ + + @property + @abstractmethod + def always_on_top(self) -> bool: + """ + Whether the window is always above normal windows or not. + :return: + """ + + @always_on_top.setter + @abstractmethod + def always_on_top(self, value: bool): + """ + Sets whether the window is always above normal windows or not. + :param value: the value to set + + """ + @abstractmethod def set_blocking(self, state: bool): """ @@ -79,7 +103,7 @@ def process_event(self, event: pygame.event.Event) -> bool: @abstractmethod def check_clicked_inside_or_blocking(self, event: pygame.event.Event) -> bool: """ - A quick event check outside of the normal event processing so that this window is brought + A quick event check outside the normal event processing so that this window is brought to the front of the window stack if we click on any of the elements contained within it. :param event: The event to check. @@ -112,7 +136,7 @@ def check_hover(self, time_delta: float, hovered_higher_element: bool) -> bool: For the window the only hovering we care about is the edges if this is a resizable window. :param time_delta: time passed in seconds between one call to this method and the next. - :param hovered_higher_element: Have we already hovered an element/window above this one. + :param hovered_higher_element: Have we already hovered an element/window above this one? """ @@ -186,3 +210,9 @@ def set_display_title(self, new_title: str): :param new_title: The title to set. """ + + def get_layer_thickness(self) -> int: + """ + The layer 'thickness' of this window/ + :return: an integer + """ diff --git a/pygame_gui/core/interfaces/window_stack_interface.py b/pygame_gui/core/interfaces/window_stack_interface.py index a95802a3..ef4578ae 100644 --- a/pygame_gui/core/interfaces/window_stack_interface.py +++ b/pygame_gui/core/interfaces/window_stack_interface.py @@ -57,10 +57,20 @@ def is_window_at_top(self, window: IWindowInterface) -> bool: """ + def is_window_at_top_of_top(self, window: IWindowInterface) -> bool: + """ + Checks if a window is at the top of the top window stack or not. + + :param window: The window to check. + + :return: returns True if this window is at the top of the stack. + + """ + @abstractmethod - def get_stack(self) -> List[IWindowInterface]: + def get_full_stack(self) -> List[IWindowInterface]: """ - Return the internal window stack directly. + Returns the full stack of normal and always on top windows. :return: a list of Windows """ diff --git a/pygame_gui/core/ui_container.py b/pygame_gui/core/ui_container.py index 978b844c..67a4ffc1 100644 --- a/pygame_gui/core/ui_container.py +++ b/pygame_gui/core/ui_container.py @@ -124,7 +124,7 @@ def recalculate_container_layer_thickness(self): max_element_top_layer = self._layer for element in self.elements: if (element.get_top_layer() > max_element_top_layer - and element not in self.ui_manager.get_window_stack().get_stack() + and element not in self.ui_manager.get_window_stack().get_full_stack() and (not isinstance(element, UIContainer) or not element.is_window_root_container)): max_element_top_layer = element.get_top_layer() @@ -143,7 +143,7 @@ def calc_add_element_changes_thickness(self, element: IUIElementInterface): :param element: the element to check. """ if (element.get_top_layer() > self.max_element_top_layer - and element not in self.ui_manager.get_window_stack().get_stack() + and element not in self.ui_manager.get_window_stack().get_full_stack() and (not isinstance(element, UIContainer) or not element.is_window_root_container)): self.max_element_top_layer = element.get_top_layer() diff --git a/pygame_gui/core/ui_window_stack.py b/pygame_gui/core/ui_window_stack.py index 8b3d5364..ee519cde 100644 --- a/pygame_gui/core/ui_window_stack.py +++ b/pygame_gui/core/ui_window_stack.py @@ -15,7 +15,10 @@ class UIWindowStack(IUIWindowStackInterface): """ def __init__(self, window_resolution: Tuple[int, int], root_container: IUIContainerInterface): self.window_resolution = window_resolution - self.stack = [] # type: List[IWindowInterface] + self.stack: List[IWindowInterface] = [] # the main stack + # a second stack that sits above the first, + # contains 'always_on_top' windows + self.top_stack: List[IWindowInterface] = [] self.root_container = root_container def clear(self): @@ -26,25 +29,81 @@ def clear(self): self.stack.pop().kill() self.stack.clear() + while len(self.top_stack) != 0: + self.top_stack.pop().kill() + self.top_stack.clear() + def add_new_window(self, window: IWindowInterface): """ - Adds a window to the top of the stack. + Adds a new window to the top of the stack. :param window: The window to add. """ - new_layer = (self.stack[-1].get_top_layer() + 1 - if len(self.stack) > 0 - else self.root_container.get_top_layer() + 1) + if window.always_on_top: + new_layer = (self.top_stack[-1].get_top_layer() + 1 + if len(self.top_stack) > 0 + else (self.stack[-1].get_top_layer() + 1 + if len(self.stack) > 0 + else self.root_container.get_top_layer() + 1)) + + window.change_layer(new_layer) + self.top_stack.append(window) + window.on_moved_to_front() + else: + new_layer = (self.stack[-1].get_top_layer() + 1 + if len(self.stack) > 0 + else self.root_container.get_top_layer() + 1) + + window.change_layer(new_layer) + self.stack.append(window) + window.on_moved_to_front() + + increased_height = window.get_layer_thickness() + + # need to bump up the layers of everything in the top stack as the main stack just got bigger + for window in self.top_stack: + window.change_layer(window.layer + increased_height) + + def refresh_window_stack_from_window(self, window_to_refresh_from: IWindowInterface): + if window_to_refresh_from in self.stack: + popped_windows_to_add_back = [] + window = self.stack.pop() + while window != window_to_refresh_from: + popped_windows_to_add_back.append(window) + window = self.stack.pop() + popped_windows_to_add_back.append(window_to_refresh_from) + try: + top_window = self.top_stack.pop() + while top_window is not None: + popped_windows_to_add_back.append(top_window) + try: + top_window = self.top_stack.pop() + except IndexError: + top_window = None + except IndexError: + pass + + popped_windows_to_add_back.reverse() + for old_window in popped_windows_to_add_back: + self.add_new_window(old_window) + + elif window_to_refresh_from in self.top_stack: + popped_windows_to_add_back = [] + window = self.top_stack.pop() + while window != window_to_refresh_from: + popped_windows_to_add_back.append(window) + window = self.top_stack.pop() + popped_windows_to_add_back.append(window_to_refresh_from) - window.change_layer(new_layer) - self.stack.append(window) - window.on_moved_to_front() + popped_windows_to_add_back.reverse() + for old_window in popped_windows_to_add_back: + self.add_new_window(old_window) def remove_window(self, window_to_remove: IWindowInterface): """ Removes a window from the stack and resorts the remaining windows to adjust for - it's absence. + its absence. :param window_to_remove: the window to remove. @@ -56,18 +115,56 @@ def remove_window(self, window_to_remove: IWindowInterface): popped_windows_to_add_back.append(window) window = self.stack.pop() + try: + top_window = self.top_stack.pop() + while top_window is not None: + popped_windows_to_add_back.append(top_window) + try: + top_window = self.top_stack.pop() + except IndexError: + top_window = None + except IndexError: + pass + + popped_windows_to_add_back.reverse() + for old_window in popped_windows_to_add_back: + self.add_new_window(old_window) + + elif window_to_remove in self.top_stack: + popped_windows_to_add_back = [] + window = self.top_stack.pop() + while window != window_to_remove: + popped_windows_to_add_back.append(window) + window = self.top_stack.pop() + popped_windows_to_add_back.reverse() for old_window in popped_windows_to_add_back: self.add_new_window(old_window) def move_window_to_front(self, window_to_front: IWindowInterface): """ - Moves the passed in window to the top of the window stack and resorts the other windows + Moves the passed in window to the top of its stack and resorts the other windows to deal with the change. :param window_to_front: the window to move to the front. """ + if window_to_front in self.top_stack: + if window_to_front == self.top_stack[-1]: + return # already at top of top stack + popped_windows_to_add_back = [] + window = self.top_stack.pop() + while window != window_to_front: + popped_windows_to_add_back.append(window) + window = self.top_stack.pop() + + popped_windows_to_add_back.reverse() + for old_window in popped_windows_to_add_back: + self.add_new_window(old_window) + + self.add_new_window(window_to_front) + window_to_front.on_moved_to_front() + if window_to_front not in self.stack or window_to_front == self.stack[-1]: return popped_windows_to_add_back = [] @@ -85,7 +182,7 @@ def move_window_to_front(self, window_to_front: IWindowInterface): def is_window_at_top(self, window: IWindowInterface) -> bool: """ - Checks if a window is at the top of the window stack or not. + Checks if a window is at the top of the normal window stack or not. :param window: The window to check. @@ -94,10 +191,21 @@ def is_window_at_top(self, window: IWindowInterface) -> bool: """ return window is self.stack[-1] - def get_stack(self) -> List[IWindowInterface]: + def is_window_at_top_of_top(self, window: IWindowInterface) -> bool: + """ + Checks if a window is at the top of the top window stack or not. + + :param window: The window to check. + + :return: returns True if this window is at the top of the stack. + + """ + return window is self.top_stack[-1] + + def get_full_stack(self) -> List[IWindowInterface]: """ - Return the internal window stack directly. + Returns the full stack of normal and always on top windows. :return: a list of Windows """ - return self.stack + return self.stack + self.top_stack diff --git a/pygame_gui/elements/ui_window.py b/pygame_gui/elements/ui_window.py index b26280d8..5c81c4fc 100644 --- a/pygame_gui/elements/ui_window.py +++ b/pygame_gui/elements/ui_window.py @@ -44,12 +44,14 @@ def __init__(self, visible: int = 1, draggable: bool = True, *, - ignore_shadow_for_initial_size_and_pos: bool = True): + ignore_shadow_for_initial_size_and_pos: bool = True, + always_on_top: bool = False): self.window_display_title = window_display_title self._window_root_container = None # type: Optional[UIContainer] self.resizable = resizable self.draggable = draggable + self._always_on_top = always_on_top self.edge_hovering = [False, False, False, False] @@ -99,6 +101,17 @@ def __init__(self, self.window_stack = self.ui_manager.get_window_stack() self.window_stack.add_new_window(self) + @property + def always_on_top(self) -> bool: + return self._always_on_top + + @always_on_top.setter + def always_on_top(self, value: bool): + if value != self._always_on_top: + self._always_on_top = value + self.window_stack.remove_window(self) + self.window_stack.add_new_window(self) + def set_blocking(self, state: bool): """ Sets whether this window being open should block clicks to the rest of the UI or not. @@ -252,6 +265,7 @@ def update(self, time_delta: float): # This is needed to keep the window in sync with the container after adding elements to it if self._window_root_container.layer_thickness != self.layer_thickness: self.layer_thickness = self._window_root_container.layer_thickness + self.window_stack.refresh_window_stack_from_window(self) if self.title_bar is not None: if self.title_bar.held: mouse_x, mouse_y = self.ui_manager.get_mouse_position() @@ -677,7 +691,7 @@ def get_hovering_edge_id(self) -> str: def on_moved_to_front(self): """ - Called when a window is moved to the front of the stack. + Called when a window is moved to the front of its stack. """ # old event - to be removed in 0.8.0 window_front_event = pygame.event.Event(pygame.USEREVENT, @@ -689,6 +703,7 @@ def on_moved_to_front(self): # new event window_front_event = pygame.event.Event(UI_WINDOW_MOVED_TO_FRONT, {'ui_element': self, + 'always_on_top': int(self.always_on_top), 'ui_object_id': self.most_specific_combined_id}) pygame.event.post(window_front_event) @@ -786,3 +801,10 @@ def are_contents_hovered(self) -> bool: if any_hovered: break return any_hovered + + def get_layer_thickness(self) -> int: + """ + The layer 'thickness' of this window/ + :return: an integer + """ + return self.layer_thickness diff --git a/pygame_gui/windows/ui_colour_picker_dialog.py b/pygame_gui/windows/ui_colour_picker_dialog.py index a6b6257c..9f7d3657 100644 --- a/pygame_gui/windows/ui_colour_picker_dialog.py +++ b/pygame_gui/windows/ui_colour_picker_dialog.py @@ -311,14 +311,16 @@ def __init__(self, rect: RectLike, initial_colour: pygame.Color = pygame.Color(0, 0, 0, 255), window_title: str = "pygame-gui.colour_picker_title_bar", object_id: Union[ObjectID, str] = ObjectID('#colour_picker_dialog', None), - visible: int = 1): + visible: int = 1, + always_on_top: bool = False): super().__init__(rect, manager, window_display_title=window_title, element_id=['colour_picker_dialog'], object_id=object_id, resizable=True, - visible=visible) + visible=visible, + always_on_top=always_on_top) minimum_dimensions = (390, 390) if self.relative_rect.width < minimum_dimensions[0] or self.relative_rect.height < minimum_dimensions[1]: diff --git a/pygame_gui/windows/ui_confirmation_dialog.py b/pygame_gui/windows/ui_confirmation_dialog.py index fcf06e79..5ec7a50d 100644 --- a/pygame_gui/windows/ui_confirmation_dialog.py +++ b/pygame_gui/windows/ui_confirmation_dialog.py @@ -43,7 +43,8 @@ def __init__(self, rect: RectLike, blocking: bool = True, object_id: Union[ObjectID, str] = ObjectID('#confirmation_dialog', None), visible: int = 1, - action_long_desc_text_kwargs: Optional[Dict[str, str]] = None + action_long_desc_text_kwargs: Optional[Dict[str, str]] = None, + always_on_top: bool = False ): super().__init__(rect, manager, @@ -51,7 +52,8 @@ def __init__(self, rect: RectLike, element_id=['confirmation_dialog'], object_id=object_id, resizable=True, - visible=visible) + visible=visible, + always_on_top=always_on_top) minimum_dimensions = (260, 200) if self.relative_rect.width < minimum_dimensions[0] or self.relative_rect.height < minimum_dimensions[1]: diff --git a/pygame_gui/windows/ui_console_window.py b/pygame_gui/windows/ui_console_window.py index f7aa8799..cee37f20 100644 --- a/pygame_gui/windows/ui_console_window.py +++ b/pygame_gui/windows/ui_console_window.py @@ -35,13 +35,15 @@ def __init__(self, window_title: str = 'pygame-gui.console_title_bar', object_id: Union[ObjectID, str] = ObjectID('#console_window', None), visible: int = 1, - preload_bold_log_font: bool = True): + preload_bold_log_font: bool = True, + always_on_top: bool = False): super().__init__(rect, manager, window_display_title=window_title, element_id=['console_window'], object_id=object_id, resizable=True, - visible=visible) + visible=visible, + always_on_top=always_on_top) self.default_log_prefix = '> ' self.log_prefix = self.default_log_prefix diff --git a/pygame_gui/windows/ui_file_dialog.py b/pygame_gui/windows/ui_file_dialog.py index e1152889..f250fc65 100644 --- a/pygame_gui/windows/ui_file_dialog.py +++ b/pygame_gui/windows/ui_file_dialog.py @@ -45,7 +45,8 @@ def __init__(self, object_id: Union[ObjectID, str] = ObjectID('#file_dialog', None), allow_existing_files_only: bool = False, allow_picking_directories: bool = False, - visible: int = 1 + visible: int = 1, + always_on_top: bool = False ): super().__init__(rect, manager, @@ -53,7 +54,8 @@ def __init__(self, element_id=['file_dialog'], object_id=object_id, resizable=True, - visible=visible) + visible=visible, + always_on_top=always_on_top) locale.setlocale(locale.LC_ALL, "") diff --git a/pygame_gui/windows/ui_message_window.py b/pygame_gui/windows/ui_message_window.py index a373de1e..74dfd48d 100644 --- a/pygame_gui/windows/ui_message_window.py +++ b/pygame_gui/windows/ui_message_window.py @@ -29,14 +29,16 @@ def __init__(self, rect: RectLike, window_title: str = 'pygame-gui.message_window_title_bar', object_id: Union[ObjectID, str] = ObjectID('#message_window', None), visible: int = 1, - html_message_text_kwargs: Optional[Dict[str, str]] = None): + html_message_text_kwargs: Optional[Dict[str, str]] = None, + always_on_top: bool = False): super().__init__(rect, manager, window_display_title=window_title, element_id=['message_window'], object_id=object_id, resizable=True, - visible=visible) + visible=visible, + always_on_top=always_on_top) minimum_dimensions = (250, 160) if self.relative_rect.width < minimum_dimensions[0] or self.relative_rect.height < minimum_dimensions[1]: diff --git a/tests/test_core/test_window_stack.py b/tests/test_core/test_window_stack.py index b98eabc2..469fc544 100644 --- a/tests/test_core/test_window_stack.py +++ b/tests/test_core/test_window_stack.py @@ -88,6 +88,98 @@ def test_is_window_at_top(self, _init_pygame, default_ui_manager, assert stack.is_window_at_top(window) is False assert stack.is_window_at_top(window_3) is False + def test_add_window_always_on_top(self, _init_pygame, default_ui_manager, + _display_surface_return_none): + stack = UIWindowStack((800, 600), default_ui_manager.get_root_container()) + window = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window') + window_always_on_top = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window', + always_on_top=True) + stack.add_new_window(window) + stack.add_new_window(window_always_on_top) + + assert len(stack.stack) == 1 + assert len(stack.top_stack) == 1 + + def test_clear_with_always_on_top(self, _init_pygame, default_ui_manager, + _display_surface_return_none): + stack = UIWindowStack((800, 600), default_ui_manager.get_root_container()) + window = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window') + window_always_on_top = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window', + always_on_top=True) + stack.add_new_window(window) + stack.add_new_window(window_always_on_top) + + assert len(stack.stack) == 1 + assert len(stack.top_stack) == 1 + + stack.clear() + + assert len(stack.stack) == 0 + assert len(stack.top_stack) == 0 + + def test_remove_window_with_always_on_top(self, _init_pygame, default_ui_manager, + _display_surface_return_none): + stack = UIWindowStack((800, 600), default_ui_manager.get_root_container()) + window_always_on_top = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window', + always_on_top=True) + window = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window') + window_2 = UIWindow(pygame.Rect(50, 50, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window') + window_3 = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window') + window_always_on_top_2 = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window', + always_on_top=True) + stack.add_new_window(window_always_on_top) + stack.add_new_window(window) + stack.add_new_window(window_2) + stack.add_new_window(window_3) + stack.add_new_window(window_always_on_top_2) + assert len(stack.stack) == 3 + assert len(stack.top_stack) == 2 + assert stack.is_window_at_top(window_3) + assert stack.is_window_at_top_of_top(window_always_on_top_2) + stack.remove_window(window) + stack.remove_window(window_always_on_top) + stack.remove_window(window_2) + stack.remove_window(window_3) + stack.remove_window(window_always_on_top_2) + + assert len(stack.top_stack) == 0 + assert len(stack.stack) == 0 + + def test_move_window_to_front_with_always_on_top(self, _init_pygame, default_ui_manager, + _display_surface_return_none): + stack = UIWindowStack((800, 600), default_ui_manager.get_root_container()) + window_always_on_top = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window', + always_on_top=True) + window = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window') + window_2 = UIWindow(pygame.Rect(50, 50, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window') + window_3 = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window') + + stack.add_new_window(window_always_on_top) + stack.add_new_window(window) + stack.add_new_window(window_2) + stack.add_new_window(window_3) + stack.move_window_to_front(window) + stack.move_window_to_front(window_3) + stack.move_window_to_front(window_2) + + assert stack.stack[0] == window + assert stack.stack[2] == window_2 + assert stack.top_stack[0] == window_always_on_top + assert window_always_on_top.layer > window_2.layer + if __name__ == '__main__': pytest.console_main() From 88720808a29e36f700b7602b26e625e2d0a3f52d Mon Sep 17 00:00:00 2001 From: Dan Lawrence Date: Sat, 13 Apr 2024 21:18:41 +0100 Subject: [PATCH 2/3] More always on top tests --- pygame_gui/core/ui_window_stack.py | 17 +++++++--- pygame_gui/elements/ui_window.py | 2 +- tests/test_core/test_window_stack.py | 48 ++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/pygame_gui/core/ui_window_stack.py b/pygame_gui/core/ui_window_stack.py index ee519cde..1401107e 100644 --- a/pygame_gui/core/ui_window_stack.py +++ b/pygame_gui/core/ui_window_stack.py @@ -68,11 +68,8 @@ def add_new_window(self, window: IWindowInterface): def refresh_window_stack_from_window(self, window_to_refresh_from: IWindowInterface): if window_to_refresh_from in self.stack: popped_windows_to_add_back = [] - window = self.stack.pop() - while window != window_to_refresh_from: - popped_windows_to_add_back.append(window) - window = self.stack.pop() - popped_windows_to_add_back.append(window_to_refresh_from) + + # first clear out the top stack try: top_window = self.top_stack.pop() while top_window is not None: @@ -84,6 +81,16 @@ def refresh_window_stack_from_window(self, window_to_refresh_from: IWindowInterf except IndexError: pass + # then clear out everything above our target + window = self.stack.pop() + while window != window_to_refresh_from: + popped_windows_to_add_back.append(window) + window = self.stack.pop() + + # add back our target as well + popped_windows_to_add_back.append(window_to_refresh_from) + + # reverse the list and re-add them popped_windows_to_add_back.reverse() for old_window in popped_windows_to_add_back: self.add_new_window(old_window) diff --git a/pygame_gui/elements/ui_window.py b/pygame_gui/elements/ui_window.py index 5c81c4fc..bc121aee 100644 --- a/pygame_gui/elements/ui_window.py +++ b/pygame_gui/elements/ui_window.py @@ -108,8 +108,8 @@ def always_on_top(self) -> bool: @always_on_top.setter def always_on_top(self, value: bool): if value != self._always_on_top: - self._always_on_top = value self.window_stack.remove_window(self) + self._always_on_top = value self.window_stack.add_new_window(self) def set_blocking(self, state: bool): diff --git a/tests/test_core/test_window_stack.py b/tests/test_core/test_window_stack.py index 469fc544..db3c11b2 100644 --- a/tests/test_core/test_window_stack.py +++ b/tests/test_core/test_window_stack.py @@ -3,6 +3,8 @@ from pygame_gui.core.ui_window_stack import UIWindowStack from pygame_gui.elements.ui_window import UIWindow +from pygame_gui.elements.ui_drop_down_menu import UIDropDownMenu +from pygame_gui.ui_manager import UIManager class TestWindowStack: @@ -180,6 +182,52 @@ def test_move_window_to_front_with_always_on_top(self, _init_pygame, default_ui_ assert stack.top_stack[0] == window_always_on_top assert window_always_on_top.layer > window_2.layer + def test_swap_window_always_on_top(self, _init_pygame, default_ui_manager, + _display_surface_return_none): + manager = UIManager((800, 600)) + stack = manager.ui_window_stack + window = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", + manager=manager, element_id='test_window') + window_always_on_top = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", + manager=manager, element_id='test_window', + always_on_top=True) + + window_always_on_top.always_on_top = False + window.always_on_top = True + + assert len(stack.stack) == 1 + assert len(stack.top_stack) == 1 + assert window == stack.top_stack[0] + assert window_always_on_top == stack.stack[0] + + def test_update_always_on_top(self, _init_pygame, default_ui_manager, + _display_surface_return_none): + manager = UIManager((800, 600)) + + window = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", + manager=manager, element_id='test_window') + assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 5 + UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", + manager=manager, element_id='test_window') + assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 8 + UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", + manager=manager, element_id='test_window', always_on_top=True) + assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 11 + window_always_on_top_2 = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", + manager=manager, element_id='test_window', always_on_top=True) + + assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 14 + + assert window_always_on_top_2.get_top_layer() == 14 + assert window.layer_thickness == 2 + + UIDropDownMenu(["Test", "Test"], "Test", (50, 50, 120, 30), container=window, manager=manager) + assert window.layer_thickness == 2 + window.update(0.4) + assert window.layer_thickness == 4 + assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 16 + assert window_always_on_top_2.get_top_layer() == 16 + if __name__ == '__main__': pytest.console_main() From 2921cdebb16d9072bd30bc82e92673fe22180a14 Mon Sep 17 00:00:00 2001 From: Dan Lawrence Date: Sat, 13 Apr 2024 21:27:20 +0100 Subject: [PATCH 3/3] More always on top tests --- tests/test_core/test_window_stack.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/test_core/test_window_stack.py b/tests/test_core/test_window_stack.py index db3c11b2..c97a369c 100644 --- a/tests/test_core/test_window_stack.py +++ b/tests/test_core/test_window_stack.py @@ -162,6 +162,9 @@ def test_move_window_to_front_with_always_on_top(self, _init_pygame, default_ui_ window_always_on_top = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window", manager=default_ui_manager, element_id='test_window', always_on_top=True) + window_always_on_top_2 = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window", + manager=default_ui_manager, element_id='test_window', + always_on_top=True) window = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", manager=default_ui_manager, element_id='test_window') window_2 = UIWindow(pygame.Rect(50, 50, 200, 200), window_display_title="Test Window", @@ -170,17 +173,20 @@ def test_move_window_to_front_with_always_on_top(self, _init_pygame, default_ui_ manager=default_ui_manager, element_id='test_window') stack.add_new_window(window_always_on_top) + stack.add_new_window(window_always_on_top_2) stack.add_new_window(window) stack.add_new_window(window_2) stack.add_new_window(window_3) stack.move_window_to_front(window) stack.move_window_to_front(window_3) stack.move_window_to_front(window_2) + stack.move_window_to_front(window_always_on_top) assert stack.stack[0] == window assert stack.stack[2] == window_2 - assert stack.top_stack[0] == window_always_on_top - assert window_always_on_top.layer > window_2.layer + assert stack.top_stack[0] == window_always_on_top_2 + assert stack.top_stack[1] == window_always_on_top + assert window_always_on_top_2.layer > window_2.layer def test_swap_window_always_on_top(self, _init_pygame, default_ui_manager, _display_surface_return_none): @@ -210,8 +216,8 @@ def test_update_always_on_top(self, _init_pygame, default_ui_manager, UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", manager=manager, element_id='test_window') assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 8 - UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", - manager=manager, element_id='test_window', always_on_top=True) + window_always_on_top = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", + manager=manager, element_id='test_window', always_on_top=True) assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 11 window_always_on_top_2 = UIWindow(pygame.Rect(100, 100, 200, 200), window_display_title="Test Window", manager=manager, element_id='test_window', always_on_top=True) @@ -228,6 +234,13 @@ def test_update_always_on_top(self, _init_pygame, default_ui_manager, assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 16 assert window_always_on_top_2.get_top_layer() == 16 + UIDropDownMenu(["Test", "Test"], "Test", (50, 50, 120, 30), container=window_always_on_top, manager=manager) + assert window_always_on_top.layer_thickness == 2 + window_always_on_top.update(0.4) + assert window_always_on_top.layer_thickness == 4 + assert manager.get_window_stack().get_full_stack()[-1].get_top_layer() == 18 + assert window_always_on_top_2.get_top_layer() == 18 + if __name__ == '__main__': pytest.console_main()