Skip to content

Commit

Permalink
Add always on top feature to windows
Browse files Browse the repository at this point in the history
fixes #564
  • Loading branch information
MyreMylar committed Apr 13, 2024
1 parent 7a5f9ef commit a3c35e8
Show file tree
Hide file tree
Showing 11 changed files with 305 additions and 33 deletions.
36 changes: 33 additions & 3 deletions pygame_gui/core/interfaces/window_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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?
"""

Expand Down Expand Up @@ -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
"""
14 changes: 12 additions & 2 deletions pygame_gui/core/interfaces/window_stack_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
4 changes: 2 additions & 2 deletions pygame_gui/core/ui_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down
136 changes: 122 additions & 14 deletions pygame_gui/core/ui_window_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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()

Check warning on line 74 in pygame_gui/core/ui_window_stack.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/core/ui_window_stack.py#L73-L74

Added lines #L73 - L74 were not covered by tests
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

Check warning on line 83 in pygame_gui/core/ui_window_stack.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/core/ui_window_stack.py#L78-L83

Added lines #L78 - L83 were not covered by tests
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)

Check warning on line 97 in pygame_gui/core/ui_window_stack.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/core/ui_window_stack.py#L91-L97

Added lines #L91 - L97 were not covered by tests

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)

Check warning on line 101 in pygame_gui/core/ui_window_stack.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/core/ui_window_stack.py#L99-L101

Added lines #L99 - L101 were not covered by tests

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.
Expand All @@ -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()

Check warning on line 159 in pygame_gui/core/ui_window_stack.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/core/ui_window_stack.py#L153-L159

Added lines #L153 - L159 were not covered by tests

popped_windows_to_add_back.reverse()
for old_window in popped_windows_to_add_back:
self.add_new_window(old_window)

Check warning on line 163 in pygame_gui/core/ui_window_stack.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/core/ui_window_stack.py#L161-L163

Added lines #L161 - L163 were not covered by tests

self.add_new_window(window_to_front)
window_to_front.on_moved_to_front()

Check warning on line 166 in pygame_gui/core/ui_window_stack.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/core/ui_window_stack.py#L165-L166

Added lines #L165 - L166 were not covered by tests

if window_to_front not in self.stack or window_to_front == self.stack[-1]:
return
popped_windows_to_add_back = []
Expand All @@ -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.
Expand All @@ -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
26 changes: 24 additions & 2 deletions pygame_gui/elements/ui_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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)

Check warning on line 113 in pygame_gui/elements/ui_window.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/elements/ui_window.py#L110-L113

Added lines #L110 - L113 were not covered by tests

def set_blocking(self, state: bool):
"""
Sets whether this window being open should block clicks to the rest of the UI or not.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand All @@ -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)

Expand Down Expand Up @@ -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
6 changes: 4 additions & 2 deletions pygame_gui/windows/ui_colour_picker_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
Loading

0 comments on commit a3c35e8

Please sign in to comment.