Repository: MyreMylar/pygame_gui (python)

Issue:
Changing object class and rebuild does not update object to new theme
**Describe the bug**
The following changes the theme class id of the object, but the visual display in the window display in the window does not update to reflect the new theme.

```
new_task_desc = pygame_gui.elements.UITextEntryBox(
    relative_rect=pygame.Rect((25, 92), (635, 32)),
    initial_text="",
    object_id=ObjectID(class_id="@add_ok"),
    manager=manager,
)

# Some event occurs and there is an error to be highlighted.

new_task_desc.class_ids = ["@add_error"]
# Rebuild the element to reflect the new class.
new_task_desc.rebuild()
```

Have checked that it is not being overidden by object level theme. Defaults removed from theme file and no other container that could be overriding theme class id identified.

**To Reproduce**
See example files attached.

[pygame_gui_class_bug.zip](https://github.com/MyreMylar/pygame_gui/files/11710375/pygame_gui_class_bug.zip)

**Expected behaviour**
When the class_id of a object changes, display should update to reflect new theme. Preferably without having to call a rebuild.

**Screenshots**
N/A - see sample files.

**Platform and software (please complete the following information):**
 - OS: Windows 10
 - Pygame GUI version 0.6.9
 - Pygame version 2.4.0

**Additional context**
It is quite possible that this is user-error and I haven't understood something. I have spent nearly three hours trying to solve this problem and have run out of ideas. Any assistance appreciated.



Pull request description:
Add method to change object ID
also some docs.

fixes #465

Patch:
diff --git a/docs/source/pygame_gui.core.rst b/docs/source/pygame_gui.core.rst
index 7019b9b..929ef63 100644
--- a/docs/source/pygame_gui.core.rst
+++ b/docs/source/pygame_gui.core.rst
@@ -22,7 +22,7 @@ pygame\_gui.core.colour\_gradient module
    :show-inheritance:
 
 pygame\_gui.core.colour\_parser module
-----------------------------------------
+--------------------------------------
 
 .. automodule:: pygame_gui.core.colour_parser
    :members:
@@ -30,7 +30,7 @@ pygame\_gui.core.colour\_parser module
    :show-inheritance:
 
 pygame\_gui.core.gui\_font\_pygame module
---------------------------------------
+-----------------------------------------
 
 .. automodule:: pygame_gui.core.gui_font_pygame
    :members:
@@ -38,7 +38,7 @@ pygame\_gui.core.gui\_font\_pygame module
    :show-inheritance:
 
 pygame\_gui.core.layered\_gui\_group module
---------------------------------------
+-------------------------------------------
 
 .. automodule:: pygame_gui.core.layered_gui_group
    :members:
@@ -46,7 +46,7 @@ pygame\_gui.core.layered\_gui\_group module
    :show-inheritance:
 
 pygame\_gui.core.object\_id module
---------------------------------------
+----------------------------------
 
 .. automodule:: pygame_gui.core.object_id
    :members:
@@ -54,7 +54,7 @@ pygame\_gui.core.object\_id module
    :show-inheritance:
 
 pygame\_gui.core.resource\_loaders module
---------------------------------------
+-----------------------------------------
 
 .. automodule:: pygame_gui.core.resource_loaders
    :members:
@@ -78,7 +78,7 @@ pygame\_gui.core.ui\_appearance\_theme module
    :show-inheritance:
 
 pygame\_gui.core.ui\_container module
----------------------------------------------
+-------------------------------------
 
 .. automodule:: pygame_gui.core.ui_container
    :members:
diff --git a/docs/source/pygame_gui.core.text.rst b/docs/source/pygame_gui.core.text.rst
new file mode 100644
index 0000000..d8720dd
--- /dev/null
+++ b/docs/source/pygame_gui.core.text.rst
@@ -0,0 +1,85 @@
+pygame\_gui.core.text package
+===================================
+
+Submodules
+----------
+
+pygame\_gui.core.text.horiz\_rule\_layout\_rect module
+------------------------------------------------------
+
+.. automodule:: pygame_gui.core.text.horiz_rule_layout_rect
+   :members:
+   :no-undoc-members:
+   :show-inheritance:
+
+pygame\_gui.core.text.html\_parser module
+-----------------------------------------
+
+.. automodule:: pygame_gui.core.text.html_parser
+   :members:
+   :no-undoc-members:
+   :show-inheritance:
+
+pygame\_gui.core.text.hyperlink\_text\_chunk module
+---------------------------------------------------
+
+.. automodule:: pygame_gui.core.text.hyperlink_text_chunk
+   :members:
+   :no-undoc-members:
+   :show-inheritance:
+
+pygame\_gui.core.text.image\_layout\_rect module
+------------------------------------------------
+
+.. automodule:: pygame_gui.core.text.image_layout_rect
+   :members:
+   :no-undoc-members:
+   :show-inheritance:
+
+pygame\_gui.core.text.line\_break\_layout\_rect module
+------------------------------------------------------
+
+.. automodule:: pygame_gui.core.text.line_break_layout_rect
+   :members:
+   :no-undoc-members:
+   :show-inheritance:
+
+pygame\_gui.core.text.text\_box\_layout module
+----------------------------------------------
+
+.. automodule:: pygame_gui.core.text.text_box_layout
+   :members:
+   :no-undoc-members:
+   :show-inheritance:
+
+pygame\_gui.core.text.text\_box\_layout\_row module
+---------------------------------------------------
+
+.. automodule:: pygame_gui.core.text.text_box_layout_row
+   :members:
+   :no-undoc-members:
+   :show-inheritance:
+
+pygame\_gui.core.text.text\_layout\_rect module
+-----------------------------------------------
+
+.. automodule:: pygame_gui.core.text.text_layout_rect
+   :members:
+   :no-undoc-members:
+   :show-inheritance:
+
+pygame\_gui.core.text.text\_line\_chunk module
+----------------------------------------------
+
+.. automodule:: pygame_gui.core.text.text_line_chunk
+   :members:
+   :no-undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: pygame_gui.core.text
+   :members:
+   :no-undoc-members:
+   :show-inheritance:
diff --git a/docs/source/pygame_gui.elements.rst b/docs/source/pygame_gui.elements.rst
index d297c10..73cc2c8 100644
--- a/docs/source/pygame_gui.elements.rst
+++ b/docs/source/pygame_gui.elements.rst
@@ -5,7 +5,7 @@ Submodules
 ----------
 
 pygame\_gui.elements.ui\_2d\_slider module
---------------------------------------
+------------------------------------------
 
 .. automodule:: pygame_gui.elements.ui_2d_slider
    :members:
@@ -13,9 +13,9 @@ pygame\_gui.elements.ui\_2d\_slider module
    :show-inheritance:
 
 pygame\_gui.elements.ui\_auto\_resizing\_container module
---------------------------------------
+---------------------------------------------------------
 
-.. automodule:: pygame_gui.elements.auto_resizing_container
+.. automodule:: pygame_gui.elements.ui_auto_resizing_container
    :members:
    :no-undoc-members:
    :show-inheritance:
@@ -37,7 +37,7 @@ pygame\_gui.elements.ui\_drop\_down\_menu module
    :show-inheritance:
 
 pygame\_gui.elements.ui\_horizontal\_scroll\_bar module
---------------------------------------------------
+-------------------------------------------------------
 
 .. automodule:: pygame_gui.elements.ui_horizontal_scroll_bar
    :members:
@@ -93,7 +93,7 @@ pygame\_gui.elements.ui\_screen\_space\_health\_bar module
    :show-inheritance:
 
 pygame\_gui.elements.ui\_scrolling\_container module
------------------------------------------------
+----------------------------------------------------
 
 .. automodule:: pygame_gui.elements.ui_scrolling_container
    :members:
@@ -109,7 +109,7 @@ pygame\_gui.elements.ui\_selection\_list module
    :show-inheritance:
 
 pygame\_gui.elements.ui\_status\_bar module
-----------------------------------------------------------
+-------------------------------------------
 
 .. automodule:: pygame_gui.elements.ui_status_bar
    :members:
@@ -125,7 +125,7 @@ pygame\_gui.elements.ui\_text\_box module
    :show-inheritance:
 
 pygame\_gui.elements.ui\_text\_entry\_box module
--------------------------------------------------
+------------------------------------------------
 
 .. automodule:: pygame_gui.elements.ui_text_entry_box
    :members:
diff --git a/docs/source/theme_guide.rst b/docs/source/theme_guide.rst
index 163be66..1cec86d 100644
--- a/docs/source/theme_guide.rst
+++ b/docs/source/theme_guide.rst
@@ -320,6 +320,27 @@ Once the ObjectID is in place in the code you can refer to it in a block in your
        }
    }
 
+If you want to change a created element's Object ID later on, just use the `.change_object_id()` function. E.g.:
+
+.. code-block:: python
+   :caption: object_id.py
+   :linenos:
+   :emphasize-lines: 13,14
+
+   from pygame_gui.core import ObjectID
+   from pygame_gui.elements import UIButton
+
+   ...  # other code omitted here -
+        # see quick start guide for how to get up and running with a single button
+
+   hello_button = UIButton(relative_rect=pygame.Rect((350, 280), (-1, -1)),
+                           text='Hello',
+                           manager=manager,
+                           object_id=ObjectID(class_id='@friendly_buttons',
+                                              object_id='#hello_button'))
+
+   hello_button.change_object_id(ObjectID(class_id='@unfriendly_buttons',
+                                          object_id='#hello_button'))
 
 
 Theme block categories
diff --git a/pygame_gui/core/layered_gui_group.py b/pygame_gui/core/layered_gui_group.py
index 9d67126..1f2254c 100644
--- a/pygame_gui/core/layered_gui_group.py
+++ b/pygame_gui/core/layered_gui_group.py
@@ -29,9 +29,10 @@ class GUISprite:
             self.add(*groups)
 
     def add(self, *groups):
-        """add the sprite to groups
+        """
+        add the sprite to groups
 
-        Sprite.add(*groups): return None
+        :param groups: sprite groups to add to.
 
         Any number of Group instances can be passed as arguments. The
         Sprite will be added to the Groups it is not already a member of.
@@ -49,7 +50,7 @@ class GUISprite:
     def remove(self, *groups):
         """remove the sprite from groups
 
-        Sprite.remove(*groups): return None
+        :param groups: sprite groups to remove from.
 
         Any number of Group instances can be passed as arguments. The Sprite
         will be removed from the Groups it is currently a member of.
@@ -127,7 +128,7 @@ class GUISprite:
         """
         A stub to override.
 
-        :param time_delta the time passed in seconds between calls to this function.
+        :param time_delta: the time passed in seconds between calls to this function.
         """
 
     @property
@@ -270,6 +271,11 @@ class LayeredGUIGroup(LayeredUpdates):
         surface.blits(self.visible)
 
     def update(self, *args, **kwargs) -> None:
+        """
+
+        :param args:
+        :param kwargs:
+        """
         super().update(*args, **kwargs)
         if self.should_update_visibility:
             self.should_update_visibility = False
diff --git a/pygame_gui/core/text/html_parser.py b/pygame_gui/core/text/html_parser.py
index 1d5d85d..39b4c96 100644
--- a/pygame_gui/core/text/html_parser.py
+++ b/pygame_gui/core/text/html_parser.py
@@ -362,12 +362,12 @@ class HTMLParser(html.parser.HTMLParser):
         The eventual style of a character/bit of text is built up by evaluating all styling
         elements currently on the stack when we parse that bit of text.
 
-        Styles on top of the stack will be evaluated last so they can overwrite elements earlier
+        Styles on top of the stack will be evaluated last, so they can overwrite elements earlier
         in the stack (i.e. a later 'font_size' of 5 will overwrite an earlier 'font_size' of 3).
 
-        :param key: Name for this styling element so we can identify when to remove it when the
-        styling block is closed
+        :param key: Name for this styling element so, we can identify when to remove it
         :param styles: The styling dictionary that contains the actual styling.
+
         """
         old_styles = {name: self.current_style.get(name) for name in styles}
         self.style_stack.append((key, old_styles))
diff --git a/pygame_gui/core/text/text_line_chunk.py b/pygame_gui/core/text/text_line_chunk.py
index e12ab54..4230fe6 100644
--- a/pygame_gui/core/text/text_line_chunk.py
+++ b/pygame_gui/core/text/text_line_chunk.py
@@ -817,8 +817,9 @@ class TextLineChunkFTFont(TextLayoutRect):
         """
         Grab the 'pre effect' surface, used to get a 'normal' pre-effect surface to apply effects
         to.
-        TODO: Could be a memory hog on large text surfaces as we are currently holding the whole
-              text surface again. Perhaps only hold the chunk's part?
+
         """
+        # TODO: Could be a memory hog on large text surfaces as we are currently holding the whole
+        #       text surface again. Perhaps only hold the chunk's part?
         if self.target_surface is not None:
             self.pre_effect_target_surface = self.target_surface.copy()
diff --git a/pygame_gui/core/ui_element.py b/pygame_gui/core/ui_element.py
index 6541a65..84dfffb 100644
--- a/pygame_gui/core/ui_element.py
+++ b/pygame_gui/core/ui_element.py
@@ -54,6 +54,7 @@ class UIElement(GUISprite, IUIElementInterface):
         self._layer = 0
         self.ui_manager = manager
         self.ui_container: Optional[IContainerLikeInterface] = None
+        self.parent_element = parent_element
         if self.ui_manager is None:
             self.ui_manager = get_default_manager()
         if self.ui_manager is None:
@@ -474,6 +475,23 @@ class UIElement(GUISprite, IUIElementInterface):
 
         self.most_specific_combined_id = self.combined_element_ids[0]
 
+    def change_object_id(self, new_object_id: Union[ObjectID, str, None]):
+        """
+        Allows for easy switching of an element's ObjectID used for theming and events.
+
+        Will rebuild the element after switching the ID
+
+        :param new_object_id: The new ID to use for this element.
+        :return:
+        """
+        self._create_valid_ids(container=self.ui_container,
+                               parent_element=self.parent_element,
+                               object_id=new_object_id,
+                               element_id=self.element_ids[-1],
+                               element_base_id=self.element_base_ids[-1])
+
+        self.rebuild_from_changed_theme_data()
+
     @staticmethod
     def _calc_top_offset(container: Optional[IContainerLikeInterface],
                          anchors: Dict[str, Union[str, IUIElementInterface]]) -> int:


Test patch:
diff --git a/tests/data/themes/ui_button_non_default.json b/tests/data/themes/ui_button_non_default.json
index 9d978a7..c199a28 100644
--- a/tests/data/themes/ui_button_non_default.json
+++ b/tests/data/themes/ui_button_non_default.json
@@ -71,5 +71,16 @@
                 "hovered_normal": "0.5"
             }
          }
-     }
- }
\ No newline at end of file
+     },
+    "@rounded_button": {
+        "misc": {
+            "shape": "rounded_rectangle",
+            "shape_corner_radius": "5,5,5,5"
+        }
+    },
+    "@rectangular_button": {
+        "misc": {
+            "shape": "rectangle"
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_elements/test_ui_button.py b/tests/test_elements/test_ui_button.py
index 75869e4..b4dc6da 100644
--- a/tests/test_elements/test_ui_button.py
+++ b/tests/test_elements/test_ui_button.py
@@ -10,7 +10,7 @@ from tests.shared_comparators import compare_surfaces
 
 from pygame_gui.ui_manager import UIManager
 from pygame_gui.elements.ui_button import UIButton
-from pygame_gui.core.ui_container import UIContainer
+from pygame_gui.core.ui_container import UIContainer, ObjectID
 from pygame_gui import PackageResource
 
 
@@ -778,6 +778,20 @@ class TestUIButton:
 
         assert button.image is not None
 
+    def test_switching_object_ids(self, _init_pygame, _display_surface_return_none):
+        manager = UIManager((800, 600), os.path.join("tests", "data", "themes", "ui_button_non_default.json"))
+        button = UIButton(relative_rect=pygame.Rect(10, 10, 150, 30),
+                          text="Test Button",
+                          manager=manager,
+                          object_id=ObjectID(object_id=None, class_id="@rounded_button"))
+        assert button.image is not None
+        assert button.drawable_shape.shape_corner_radius == [5, 5, 5, 5]
+
+        button.change_object_id(ObjectID(object_id=None, class_id="@rectangular_button"))
+
+        assert button.image is not None
+        assert button.drawable_shape.shape_corner_radius == [0, 0, 0, 0]
+
     @pytest.mark.filterwarnings("ignore:Invalid value")
     @pytest.mark.filterwarnings("ignore:Colour hex code")
     @pytest.mark.filterwarnings("ignore:Invalid gradient")