Skip to content

Commit

Permalink
Merge pull request #580 from MyreMylar/add-always-on-top-to-windows
Browse files Browse the repository at this point in the history
Add always on top feature to windows
  • Loading branch information
MyreMylar authored Apr 13, 2024
2 parents 7a5f9ef + 2921cde commit 7c83ef8
Show file tree
Hide file tree
Showing 11 changed files with 373 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
143 changes: 129 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,88 @@ 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 = []

# first clear out the top stack
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

# 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()

window.change_layer(new_layer)
self.stack.append(window)
window.on_moved_to_front()
# 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)

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)

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.
Expand All @@ -56,18 +122,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 = []
Expand All @@ -85,7 +189,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 +198,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.window_stack.remove_window(self)
self._always_on_top = value
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.
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 7c83ef8

Please sign in to comment.