Repository: MyreMylar/pygame_gui (python)

Issue:
hide() has no effect on UIButton if the button's container is currently hidden
**Describe the bug**
I have a UIPanel that is serving as the container for a number of UIButton's.  show() and hide() are used to make the panel visible or not visible at various points in the game.  Before making the panel visible by calling show() against it, my code determines which buttons on the panel should be visible and which should be invisible.  If a button that was previously visible on the panel should be invisible when the panel is next shown, the code calls hide() against the button. Eventually the code calls show() against the panel to make the panel visible again. The button against which hide() was called is NOT hidden when the button becomes visible.

Note:  If I reverse the order of the steps, calling show against the UIPanel and then calling hide() against the UIButton, the button is correctly hidden.  But calling hide() against the UIButton while the UIPanel is itself hidden seems to have no effect.

**To Reproduce**
Steps to reproduce the behaviour:
1. Create a UIPanel
2. Create a UIButton setting the UIPanel as the button's container
3. Call hide() against the UIPanel
4. Call hide() against the UIButton
5. Call show() against the UIPanel
6. Observe that the button is NOT hidden despite the fact that hide() was called against it.

**Expected behaviour**
If hide() is called against a widget, the widget should remain hidden, regardless of changes to the visibility of the widget's container.  This should be true whether the container itself was visible or invisible when hide() was called against the widget.

**Platform and software (please complete the following information):**
 - OS: Mac (macOS Ventura 13.5.2)
 - Pygame GUI version 0.6.12
 - Pygame version: pygame-ce 2.5.1


Pull request description:
Add option to disable hiding/showing contained elements of containers
fixes #631 somewhat.

At least it gives you some more control over the system anyway,

Patch:
diff --git a/pygame_gui/core/ui_appearance_theme.py b/pygame_gui/core/ui_appearance_theme.py
index 2fe3d84..700e0bb 100644
--- a/pygame_gui/core/ui_appearance_theme.py
+++ b/pygame_gui/core/ui_appearance_theme.py
@@ -103,7 +103,7 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
             self.need_to_rebuild_data_manually_changed = False
             return True
         return False
-
+    
     @staticmethod
     def _json_to_dict(json_data):
         if isinstance(json_data, dict):
@@ -574,19 +574,16 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
 
         :return IGUIFontInterface: An interface to a pygame font object wrapper.
         """
-        font = None
-        
+        # set the default font as the final fall back
+        font = self.font_dict.get_default_font()
+
         for combined_element_id in combined_element_ids:
             if combined_element_id in self.ele_font_res:
                 if self._locale in self.ele_font_res[combined_element_id]:
-                    font = self.ele_font_res[combined_element_id][self._locale].loaded_font
+                    return self.ele_font_res[combined_element_id][self._locale].loaded_font
                 else:
-                    font = self.ele_font_res[combined_element_id]['en'].loaded_font
-                break
+                    return self.ele_font_res[combined_element_id]['en'].loaded_font
 
-        # set the default font as the final fall back
-        if font is None:
-            font = self.font_dict.get_default_font()
         return font
 
     def get_misc_data(self, misc_data_id: str, combined_element_ids: List[str]) -> Union[str, Dict]:
@@ -770,7 +767,7 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
                     self._load_element_font_data_from_theme(file_dict,
                                                             element_name)
 
-            if data_type == 'colours' or data_type == 'colors':
+            if data_type == 'colours':
                 self._load_element_colour_data_from_theme(data_type,
                                                           element_name,
                                                           element_theming)
@@ -1048,7 +1045,7 @@ class UIAppearanceTheme(IUIAppearanceThemeInterface):
         :param theme_dict: The data dictionary from the theming file to load data from.
         """
         for data_type in theme_dict['defaults']:
-            if data_type == 'colours' or data_type == 'colors':
+            if data_type == 'colours':
                 colours_dict = theme_dict['defaults'][data_type]
                 for colour_key in colours_dict:
                     colour = self._load_colour_or_gradient_from_theme(colours_dict,
diff --git a/pygame_gui/core/ui_container.py b/pygame_gui/core/ui_container.py
index 67a4ffc..2dbfc3a 100644
--- a/pygame_gui/core/ui_container.py
+++ b/pygame_gui/core/ui_container.py
@@ -352,29 +352,35 @@ class UIContainer(UIElement, IUIContainerInterface, IContainerLikeInterface):
             for element in self.elements:
                 element.enable()
 
-    def show(self):
+    def show(self, show_contents: bool = True):
         """
         Shows the container, which means the container will get drawn and will process events.
         Should also show all the children elements and containers.
         If the container was visible before - ignore.
+
+        :param show_contents: whether to also show the contents of the container. Defaults to True.
         """
         if not self.visible:
             self.visible = True
 
-            for element in self.elements:
-                if hasattr(element, 'show'):
-                    element.show()
+            if show_contents:
+                for element in self.elements:
+                    if hasattr(element, 'show'):
+                        element.show()
 
-    def hide(self):
+    def hide(self, hide_contents: bool = True):
         """
         Hides the container, which means the container will not get drawn and will not process
         events. Should also hide all the children elements and containers.
         If the container was hidden before - ignore.
+
+        :param hide_contents: whether to also hide the contents of the container. Defaults to True.
         """
         if self.visible:
-            for element in self.elements:
-                if hasattr(element, 'hide'):
-                    element.hide()
+            if hide_contents:
+                for element in self.elements:
+                    if hasattr(element, 'hide'):
+                        element.hide()
 
             self.visible = False
 
diff --git a/pygame_gui/elements/ui_image.py b/pygame_gui/elements/ui_image.py
index 22abbf4..232893e 100644
--- a/pygame_gui/elements/ui_image.py
+++ b/pygame_gui/elements/ui_image.py
@@ -37,8 +37,7 @@ class UIImage(UIElement):
                  anchors: Optional[Dict[str, Union[str, UIElement]]] = None,
                  visible: int = 1,
                  *,
-                 starting_height: int = 1,
-                 scale_func=pygame.transform.smoothscale):
+                 starting_height: int = 1,):
 
         super().__init__(relative_rect, manager, container,
                          starting_height=starting_height,
@@ -51,7 +50,7 @@ class UIImage(UIElement):
 
         self.original_image = None
 
-        self.set_image(image_surface, image_is_alpha_premultiplied, scale_func)
+        self.set_image(image_surface, image_is_alpha_premultiplied)
         self.rebuild_from_changed_theme_data()
 
     def rebuild_from_changed_theme_data(self):
@@ -80,8 +79,7 @@ class UIImage(UIElement):
 
     def set_image(self,
                   new_image: Union[pygame.surface.Surface, None],
-                  image_is_alpha_premultiplied: bool = False,
-                  scale_func = pygame.transform.smoothscale) -> None:
+                  image_is_alpha_premultiplied: bool = False) -> None:
         """
         Allows users to change the image displayed on a UIImage element during run time, without recreating
         the element.
@@ -99,6 +97,6 @@ class UIImage(UIElement):
         if (image_surface.get_width() != self.rect.width or
                 image_surface.get_height() != self.rect.height):
             self.original_image = image_surface
-            self._set_image(scale_func(self.original_image, self.rect.size))
+            self._set_image(pygame.transform.smoothscale(self.original_image, self.rect.size))
         else:
             self._set_image(image_surface)
diff --git a/pygame_gui/elements/ui_panel.py b/pygame_gui/elements/ui_panel.py
index 2dbfcef..842d6d6 100644
--- a/pygame_gui/elements/ui_panel.py
+++ b/pygame_gui/elements/ui_panel.py
@@ -287,23 +287,27 @@ class UIPanel(UIElement, IContainerLikeInterface):
             if self.panel_container is not None:
                 self.panel_container.enable()
 
-    def show(self):
+    def show(self, show_contents: bool = True):
         """
         In addition to the base UIElement.show() - call show() of owned container - panel_container.
+
+        :param show_contents: whether to also show the contents of the panel. Defaults to True.
         """
         super().show()
         if self.panel_container is not None:
-            self.panel_container.show()
+            self.panel_container.show(show_contents)
 
-    def hide(self):
+    def hide(self, hide_contents: bool = True):
         """
         In addition to the base UIElement.hide() - call hide() of owned container - panel_container.
+
+        :param hide_contents: whether to also hide the contents of the panel. Defaults to True.
         """
         if not self.visible:
             return
 
         if self.panel_container is not None:
-            self.panel_container.hide()
+            self.panel_container.hide(hide_contents)
         super().hide()
 
     def __iter__(self) -> Iterator[IUIElementInterface]:
diff --git a/pygame_gui/elements/ui_scrolling_container.py b/pygame_gui/elements/ui_scrolling_container.py
index dd05dae..381242d 100644
--- a/pygame_gui/elements/ui_scrolling_container.py
+++ b/pygame_gui/elements/ui_scrolling_container.py
@@ -513,29 +513,33 @@ class UIScrollingContainer(UIElement, IContainerLikeInterface):
             if self._root_container is not None:
                 self._root_container.enable()
 
-    def show(self):
+    def show(self, show_contents: bool = True):
         """
         In addition to the base UIElement.show() - call show() of owned container - _root_container.
         All other sub-elements (view_container, scrollbars) are children of _root_container, so
         it's visibility will propagate to them - there is no need to call their show() methods
         separately.
+
+        :param show_contents: whether to also show the contents of the container. Defaults to True.
         """
         super().show()
         if self._root_container is not None:
-            self._root_container.show()
+            self._root_container.show(show_contents)
 
-    def hide(self):
+    def hide(self, hide_contents: bool = True):
         """
         In addition to the base UIElement.hide() - call hide() of owned container - _root_container.
         All other sub-elements (view_container, scrollbars) are children of _root_container, so
         it's visibility will propagate to them - there is no need to call their hide() methods
         separately.
+
+        :param hide_contents: whether to also hide the contents of the container. Defaults to True.
         """
         if not self.visible:
             return
 
         if self._root_container is not None:
-            self._root_container.hide()
+            self._root_container.hide(hide_contents)
         super().hide()
 
     def __iter__(self) -> Iterator[IUIElementInterface]:
diff --git a/pygame_gui/elements/ui_tab_container.py b/pygame_gui/elements/ui_tab_container.py
index d1fa435..305b62c 100644
--- a/pygame_gui/elements/ui_tab_container.py
+++ b/pygame_gui/elements/ui_tab_container.py
@@ -170,25 +170,30 @@ class UITabContainer(UIElement):
         if not self.is_enabled:
             self._root_container.enable()
 
-    def show(self):
+    def show(self, show_contents: bool = True):
         """
         In addition to the base UIElement.show() - show the _window_root_container which will
         propagate and show all the children.
+
+        :param show_contents: whether to also show the contents of the tab container. Defaults to True.
+
         """
         super().show()
-        self._root_container.show()
+        self._root_container.show(show_contents)
 
-    def hide(self):
+    def hide(self, hide_contents: bool = True):
         """
         In addition to the base UIElement.hide() - hide the _window_root_container which will
         propagate and hide all the children.
+
+        :param hide_contents: whether to also hide the contents of the tab container. Defaults to True.
         """
         if not self.visible:
             return
 
         super().hide()
         if self._root_container is not None:
-            self._root_container.hide()
+            self._root_container.hide(hide_contents)
 
     def set_dimensions(self, dimensions: Coordinate, clamp_to_container: bool = False):
         """
diff --git a/pygame_gui/elements/ui_window.py b/pygame_gui/elements/ui_window.py
index 993e178..6da4711 100644
--- a/pygame_gui/elements/ui_window.py
+++ b/pygame_gui/elements/ui_window.py
@@ -750,26 +750,30 @@ class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
             if self._window_root_container is not None:
                 self._window_root_container.enable()
 
-    def show(self):
+    def show(self, show_contents: bool = True):
         """
         In addition to the base UIElement.show() - show the _window_root_container which will
         propagate and show all the children.
+
+        :param show_contents: whether to also show the contents of the window. Defaults to True.
         """
         super().show()
         if self._window_root_container is not None:
             self._window_root_container.show()
 
-    def hide(self):
+    def hide(self, hide_contents: bool = True):
         """
         In addition to the base UIElement.hide() - hide the _window_root_container which will
         propagate and hide all the children.
+
+        :param hide_contents: whether to also hide the contents of the window. Defaults to True.
         """
         if not self.visible:
             return
 
         super().hide()
         if self._window_root_container is not None:
-            self._window_root_container.hide()
+            self._window_root_container.hide(hide_contents)
 
     def get_relative_mouse_pos(self):
         """


Test patch:
diff --git a/tests/test_core/test_ui_appearance_theme.py b/tests/test_core/test_ui_appearance_theme.py
index 3920cbe..2d2c206 100644
--- a/tests/test_core/test_ui_appearance_theme.py
+++ b/tests/test_core/test_ui_appearance_theme.py
@@ -192,11 +192,8 @@ class TestUIAppearanceTheme:
         assert shadow_width == '0'
 
     def test_get_font(self, _init_pygame, _display_surface_return_none: None):
-        resource_loader = BlockingThreadedResourceLoader()
-        theme = UIAppearanceTheme(resource_loader, locale='nonsense_locale')
+        theme = UIAppearanceTheme(BlockingThreadedResourceLoader(), locale='nonsense_locale')
         theme.load_theme(os.path.join("tests", "data", "themes", "ui_button_non_default.json"))
-        resource_loader.start()
-        resource_loader.update()
         assert theme.get_font_info(['button']) == theme.ui_element_fonts_info['button']['en']
         assert theme.get_font(['button']) == theme.ele_font_res['button']['en'].loaded_font