Repository: ppizarror/pygame-menu

Issue:
widget_background_color apply twice
With the latest version 3.4.3 `widget_background_color` seems to cover both around the text and around the border which cause a layered effect.

See this:
![Capture d’écran (160)](https://user-images.githubusercontent.com/16426148/103669503-b2d67100-4f78-11eb-8a75-115ca9686900.png)


Discussion:

Patch:
diff --git a/pygame_menu/baseimage.py b/pygame_menu/baseimage.py
index 241c1ed8..c90a63d2 100644
--- a/pygame_menu/baseimage.py
+++ b/pygame_menu/baseimage.py
@@ -208,7 +208,8 @@ class BaseImage(object):
         """
         Apply a function to each pixel of the image. The function will receive the red, green, blue and alpha
         colors and must return the same values. The color pixel will be overriden by the function output.
-        See ``BaseImage.to_bw()`` method as an example.
+
+        .. note:: See ``BaseImage.to_bw()`` method as an example.
 
         :param image_function: Color function, takes colors as ``image_function=myfunc(r, g, b, a)``. Returns the same tuple *(r,g,b,a)*
         :type image_function: callable
@@ -224,6 +225,7 @@ class BaseImage(object):
                 g = int(max(0, min(g, 255)))
                 b = int(max(0, min(b, 255)))
                 a = int(max(0, min(a, 255)))
+                # noinspection PyArgumentList
                 self._surface.set_at((x, y), pygame.Color(r, g, b, a))
         return self
 
@@ -268,6 +270,7 @@ class BaseImage(object):
                     g = 0
                 if 'b' not in channels:
                     b = 0
+                # noinspection PyArgumentList
                 self._surface.set_at((x, y), pygame.Color(r, g, b, a))
         return self
 
diff --git a/pygame_menu/events.py b/pygame_menu/events.py
index 7623170b..b8b65f2f 100644
--- a/pygame_menu/events.py
+++ b/pygame_menu/events.py
@@ -51,6 +51,19 @@ class MenuAction(object):
         return False
 
 
+def is_event(event):
+    """
+    Check if event is pygame_menu event type.
+
+    :param event: Event
+    :type event: any
+    :return: True if it's event
+    :rtype: bool
+    """
+    return isinstance(event, MenuAction) or \
+           str(type(event)) == "<class 'pygame_menu.events.MenuAction'>"
+
+
 # Events
 BACK = MenuAction(0)  # Menu back
 CLOSE = MenuAction(1)  # Close menu
diff --git a/pygame_menu/examples/other/image_background.py b/pygame_menu/examples/other/image_background.py
index 7623b791..f1ac98cf 100644
--- a/pygame_menu/examples/other/image_background.py
+++ b/pygame_menu/examples/other/image_background.py
@@ -31,6 +31,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 """
 
 # Import libraries
+from __future__ import print_function
 import sys
 
 sys.path.insert(0, '../../')
@@ -111,8 +112,38 @@ def main(test=False):
         width=WINDOW_SIZE[0] * 0.8
     )
 
-    main_menu.add_button('Button 1', None)
-    main_menu.add_button('Button 2', None)
+    widget_colors_theme = pygame_menu.themes.THEME_ORANGE.copy()
+    widget_colors_theme.widget_font_size = 20
+    widget_colors_theme.set_background_color_opacity(0.5)  # 50% opacity
+
+    widget_colors = pygame_menu.Menu(
+        height=WINDOW_SIZE[1] * 0.7,
+        theme=widget_colors_theme,
+        title='Widget backgrounds',
+        width=WINDOW_SIZE[0] * 0.8
+    )
+
+    button_image = pygame_menu.baseimage.BaseImage(pygame_menu.baseimage.IMAGE_EXAMPLE_CARBON_FIBER)
+
+    widget_colors.add_button('Opaque color button', None,
+                             background_color=(100, 100, 100))
+    widget_colors.add_button('Transparent color button', None,
+                             background_color=(50, 50, 50, 200), font_size=40)
+    widget_colors.add_button('Transparent background inflate to selection effect', None,
+                             background_color=(50, 50, 50, 200), margin=(0, 15)
+                             ).expand_background_inflate_to_selection_effect()
+    widget_colors.add_button('Background inflate + font background color', None,
+                             background_color=(50, 50, 50, 200), font_background_color=(200, 200, 200)
+                             ).expand_background_inflate_to_selection_effect()
+    widget_colors.add_button('This inflates background to match selection effect', None,
+                             background_color=button_image, font_color=(255, 255, 255), font_size=15
+                             ).selection_expand_background = True
+    widget_colors.add_button('This is already inflated to match selection effect', None,
+                             background_color=button_image, font_color=(255, 255, 255), font_size=15
+                             ).expand_background_inflate_to_selection_effect()
+
+    main_menu.add_button('Test different widget colors', widget_colors)
+    main_menu.add_button('Another fancy button', lambda: print('This button has been pressed'))
     main_menu.add_button('Quit', pygame_menu.events.EXIT)
 
     # -------------------------------------------------------------------------
@@ -123,9 +154,6 @@ def main(test=False):
         # Tick
         clock.tick(FPS)
 
-        # Paint background
-        main_background()
-
         # Main menu
         main_menu.mainloop(surface, main_background, disable_loop=test, fps_limit=FPS)
 
diff --git a/pygame_menu/menu.py b/pygame_menu/menu.py
index 0ce7f197..cce75fc7 100644
--- a/pygame_menu/menu.py
+++ b/pygame_menu/menu.py
@@ -82,8 +82,8 @@ class Menu(object):
     :type mouse_motion_selection: bool
     :param mouse_visible: Set mouse visible on Menu
     :type mouse_visible: bool
-    :param onclose: Function applied when closing the Menu
-    :type onclose: callable, None
+    :param onclose: Event or function applied when closing the Menu
+    :type onclose: :py:class:`pygame_menu.events.MenuAction`, callable, None
     :param overflow: Enables overflow in x/y axes. If False then scrollbars will not work and the maximum width/height of the scrollarea is the same as the menu container. Style: (overflow_x, overflow_y)
     :type overflow: tuple, list
     :param rows: Number of rows of each column, None if there's only 1 column
@@ -119,7 +119,7 @@ class Menu(object):
                  overflow=(True, True),
                  rows=None,
                  screen_dimension=None,
-                 theme=_themes.THEME_DEFAULT,
+                 theme=_themes.THEME_DEFAULT.copy(),
                  touchscreen_enabled=False,
                  touchscreen_motion_selection=False,
                  **kwargs
@@ -216,18 +216,22 @@ class Menu(object):
             menu_id = str(uuid4())
 
         # General properties of the Menu
+        self._attributes = {}
         self._background_function = None  # type: (None,callable)
         self._center_content = center_content
         self._clock = pygame.time.Clock()  # Inner clock
         self._height = float(height)
         self._id = menu_id
         self._index = -1  # Selected index, if -1 the widget does not have been selected yet
-        self._onclose = onclose  # Function that calls after closing Menu
+        self._onclose = None  # Function or event called on menu close
         self._sounds = Sound()  # type: Sound
         self._submenus = []  # type: list
         self._theme = theme
         self._width = float(width)
 
+        # Set onclose
+        self.set_onclose(onclose)
+
         # Menu links (pointer to previous and next menus in nested submenus), for public methods
         # accessing self should be through "_current", because user can move through submenus
         # and self pointer should target the current Menu object. Private methods access
@@ -372,6 +376,18 @@ class Menu(object):
             msg = 'menu constructor parameter {} does not exist'.format(invalid_keyword)
             raise ValueError(msg)
 
+    def set_onclose(self, onclose):
+        """
+        Set onclose callback.
+
+        :param onclose: Onclose callback, it can be a function, an event or None
+        :type onclose: :py:class:`pygame_menu.events.MenuAction`, callable, None
+        :return: None
+        """
+        assert _utils.is_callable(onclose) or _events.is_event(onclose) or onclose is None, \
+            'onclose must be a MenuAction, a function or None'
+        self._onclose = onclose
+
     def get_current(self):
         """
         Get current active Menu. If the user has not opened any submenu
@@ -409,7 +425,7 @@ class Menu(object):
             - ``font_color``            Widget font color (tuple, list)
             - ``font_name``             Widget font (str)
             - ``font_size``             Font size of the widget (int)
-            - ``margin``                (x,y) margin in px (tuple, list)
+            - ``margin``                *(left,bottom)* margin in px (tuple, list)
             - ``padding``               Widget padding according to CSS rules (int, float, list, tuple). General shape: *(top,right,bottom,left)*
             - ``selection_color``       Widget selection color (tuple, list)
             - ``selection_effect``      Widget selection effect (:py:class:`pygame_menu.widgets.core.Selection`)
@@ -439,9 +455,6 @@ class Menu(object):
         button_id = kwargs.pop('button_id', '')
         assert isinstance(button_id, str), 'id must be a string'
 
-        if action is None:
-            action = _events.NONE
-
         # If element is a Menu
         if isinstance(action, Menu):
 
@@ -457,7 +470,7 @@ class Menu(object):
             widget = _widgets.Button(title, button_id, self._open, action)
             widget.to_menu = True
 
-        # If element is a PyMenuAction
+        # If element is a MenuAction
         elif action == _events.BACK:  # Back to Menu
 
             widget = _widgets.Button(title, button_id, self.reset, total_back)  # reset is public, so no _current
@@ -474,17 +487,17 @@ class Menu(object):
 
             widget = _widgets.Button(title, button_id, self._exit)
 
-        elif action == _events.NONE:  # None action
+        elif action == _events.NONE or action is None:  # None action
 
             widget = _widgets.Button(title, button_id)
 
-        # If element is a function
+        # If element is a function or callable
         elif _utils.is_callable(action):
 
             widget = _widgets.Button(title, button_id, action, *args)
 
         else:
-            raise ValueError('element must be a Menu, a PymenuAction or a function')
+            raise ValueError('action must be a Menu, a MenuAction, a function or None')
 
         # Configure and add the button
         self._configure_widget(widget=widget, **attributes)
@@ -524,7 +537,7 @@ class Menu(object):
             - ``font_color``            Widget font color (tuple, list)
             - ``font_name``             Widget font (str)
             - ``font_size``             Font size of the widget (int)
-            - ``margin``                (x,y) margin in px (tuple, list)
+            - ``margin``                *(left,bottom)* margin in px (tuple, list)
             - ``padding``               Widget padding according to CSS rules (int, float, list, tuple). General shape: *(top,right,bottom,left)*
             - ``selection_color``       Widget selection color (tuple, list)
             - ``selection_effect``      Widget selection effect (:py:class:`pygame_menu.widgets.core.Selection`)
@@ -596,7 +609,7 @@ class Menu(object):
             - ``align``                 Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/create_menu.html#widgets-alignment>`_ (str)
             - ``background_color``      Color of the background (tuple, list, :py:class:`pygame_menu.baseimage.BaseImage`)
             - ``background_inflate``    Inflate background in *(x,y)* in px (tuple, list)
-            - ``margin``                (x,y) margin in px (tuple, list)
+            - ``margin``                *(left,bottom)* margin in px (tuple, list)
             - ``padding``               Widget padding according to CSS rules (int, float, list, tuple). General shape: (top, right, bottom, left)
             - ``selection_color``       Widget selection color (tuple, list)
             - ``selection_effect``      Widget selection effect (:py:class:`pygame_menu.widgets.core.Selection`)
@@ -649,12 +662,12 @@ class Menu(object):
         kwargs (Optional):
             - ``align``                 Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/create_menu.html#widgets-alignment>`_ (str)
             - ``background_color``      Color of the background (tuple, list, :py:class:`pygame_menu.baseimage.BaseImage`)
-            - ``background_inflate``    Inflate background in (x,y) in px (tuple, list)
+            - ``background_inflate``    Inflate background in *(x,y)* in px (tuple, list)
             - ``font_background_color`` Widget font background color (tuple, list, None)
             - ``font_color``            Widget font color (tuple, list)
             - ``font_name``             Widget font (str)
             - ``font_size``             Font size of the widget (int)
-            - ``margin``                *(x,y)* margin in px (tuple, list)
+            - ``margin``                *(left,bottom)* margin in px (tuple, list)
             - ``padding``               Widget padding according to CSS rules (int, float, list, tuple). General shape: *(top,right,bottom,left)*
             - ``shadow``                Shadow is enabled or disabled (bool)
             - ``shadow_color``          Text shadow color (tuple, list)
@@ -743,12 +756,12 @@ class Menu(object):
         kwargs (Optional):
             - ``align``                 Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/create_menu.html#widgets-alignment>`_ (str)
             - ``background_color``      Color of the background (tuple, list, :py:class:`pygame_menu.baseimage.BaseImage`)
-            - ``background_inflate``    Inflate background in (x,y) in px (tuple, list)
+            - ``background_inflate``    Inflate background in *(x,y)* in px (tuple, list)
             - ``font_background_color`` Widget font background color (tuple, list, None)
             - ``font_color``            Widget font color (tuple, list)
             - ``font_name``             Widget font (str)
             - ``font_size``             Font size of the widget (int)
-            - ``margin``                *(x,y)* margin in px (tuple, list)
+            - ``margin``                *(left,bottom)* margin in px (tuple, list)
             - ``padding``               Widget padding according to CSS rules (int, float, list, tuple). General shape: (top, right, bottom, left)
             - ``selection_color``       Widget selection color (tuple, list)
             - ``selection_effect``      Widget selection effect (:py:class:`pygame_menu.widgets.core.Selection`)
@@ -827,12 +840,12 @@ class Menu(object):
         kwargs (Optional):
             - ``align``                 Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/create_menu.html#widgets-alignment>`_ (str)
             - ``background_color``      Color of the background (tuple, list, :py:class:`pygame_menu.baseimage.BaseImage`)
-            - ``background_inflate``    Inflate background in (x,y) in px (tuple, list)
+            - ``background_inflate``    Inflate background in *(x,y)* in px (tuple, list)
             - ``font_background_color`` Widget font background color (tuple, list, None)
             - ``font_color``            Widget font color (tuple, list)
             - ``font_name``             Widget font (str)
             - ``font_size``             Font size of the widget (int)
-            - ``margin``                *(x,y)* margin in px (tuple, list)
+            - ``margin``                *(left,bottom)* margin in px (tuple, list)
             - ``padding``               Widget padding according to CSS rules (int, float, list, tuple). General shape: *(top,right,bottom,left)*
             - ``selection_color``       Widget selection color (tuple, list)
             - ``selection_effect``      Widget selection effect (:py:class:`pygame_menu.widgets.core.Selection`)
@@ -1079,7 +1092,7 @@ class Menu(object):
             - ``font_color``            Widget font color (tuple, list)
             - ``font_name``             Widget font (str)
             - ``font_size``             Font size of the widget (int)
-            - ``margin``                (x,y) margin in px (tuple)
+            - ``margin``                *(left,bottom)* margin in px (tuple)
             - ``padding``               Widget padding according to CSS rules (int, float, tuple)
             - ``selection_color``       Widget selection color (tuple, list)
             - ``selection_effect``      Widget selection effect (:py:class:`pygame_menu.widgets.core.Selection`)
@@ -1291,6 +1304,8 @@ class Menu(object):
         """
         Update the position dict for each widget.
 
+        .. note:: Selection effect does not affect widget positioning.
+
         :return: None
         """
         self._update_column_width()
@@ -1319,16 +1334,15 @@ class Menu(object):
 
             # Calculate X position
             column_width = self._column_widths[col]
-            _, sel_left, sel_bottom, sel_right = selection.get_margin()
             selection_margin = 0
             align = widget.get_alignment()
             if align == _locals.ALIGN_CENTER:
                 dx = -float(rect.width) / 2
             elif align == _locals.ALIGN_LEFT:
-                selection_margin = sel_left
+                selection_margin = selection.get_margin()[1]  # left
                 dx = -column_width / 2 + selection_margin
             elif align == _locals.ALIGN_RIGHT:
-                selection_margin = sel_right
+                selection_margin = selection.get_margin()[3]  # right
                 dx = column_width / 2 - rect.width - selection_margin
             else:
                 dx = 0
@@ -1341,8 +1355,9 @@ class Menu(object):
             for r in range(row):
                 rwidget = self._widgets[int(self._rows * col + r)]  # type: _widgets.core.Widget
                 if rwidget.visible:
-                    ysum += widget_rects[rwidget.get_id()].height + rwidget.get_margin()[1]
-            y_coord = max(1, self._widget_offset[1]) + ysum + sel_bottom + widget.get_padding()[0]
+                    ysum += widget_rects[rwidget.get_id()].height  # Height
+                    ysum += rwidget.get_margin()[1]  # Vertical margin (bottom)
+            y_coord = max(1, self._widget_offset[1]) + ysum + widget.get_padding()[0]
 
             # Update the position of the widget
             widget.set_position(x_coord, y_coord)
@@ -1437,7 +1452,7 @@ class Menu(object):
         assert isinstance(widget_id, str)
         for widget in self._widgets:  # type: _widgets.core.Widget
             if widget.get_id() == widget_id:
-                raise IndexError('widget ID="{0}" is duplicated'.format(widget_id))
+                raise IndexError('widget ID="{0}" already exists on the menu'.format(widget_id))
 
     def _close(self):
         """
@@ -1455,8 +1470,7 @@ class Menu(object):
             close = True
 
             # If action is an event
-            if isinstance(onclose, _events.MenuAction) or \
-                    str(type(onclose)) == "<class 'pygame_menu.events.PymenuAction'>":
+            if _events.is_event(onclose):
 
                 if onclose == _events.DISABLE_CLOSE:
                     close = False
@@ -2358,6 +2372,17 @@ class Menu(object):
                     return True
         return False
 
+    def get_theme(self):
+        """
+        Returns the menu theme.
+
+        .. warning:: Use with caution.
+
+        :return: Menu theme
+        :rtype: :py:class:`pygame_menu.themes.Theme`
+        """
+        return self._theme
+
     def get_clock(self):
         """
         Returns the pygame menu timer.
@@ -2383,9 +2408,65 @@ class Menu(object):
         :return: Widget object, None if no widget is selected
         :rtype: :py:class:`pygame_menu.widgets.core.widget.Widget`, None
         """
+        if not isinstance(self._index, int):
+            self._index = 0
+            return None
         if self._index < 0:
             return None
         try:
             return self._widgets[self._index % len(self._widgets)]
         except (IndexError, ZeroDivisionError):
             return None
+
+    def set_attribute(self, key, value):
+        """
+        Set menu attribute.
+
+        :param key: Key of the attribute
+        :type key: str
+        :param value: Value of the attribute
+        :type value: any
+        :return: None
+        """
+        assert isinstance(key, str)
+        self._attributes[key] = value
+
+    def get_attribute(self, key, default=None):
+        """
+        Get attribute value.
+
+        :param key: Key of the attribute
+        :type key: str
+        :param default: Value if does not exists
+        :type default: any
+        :return: Attribute data
+        :rtype: any
+        """
+        assert isinstance(key, str)
+        if not self.has_attribute(key):
+            return default
+        return self._attributes[key]
+
+    def has_attribute(self, key):
+        """
+        Returns true if widget has the given attribute.
+
+        :param key: Key of the attribute
+        :type key: str
+        :return: True if exists
+        :rtype: bool
+        """
+        assert isinstance(key, str)
+        return key in self._attributes.keys()
+
+    def remove_attribute(self, key):
+        """
+        Removes the given attribute from the menu. Throws ``IndexError`` if given key does not exist.
+
+        :param key: Key of the attribute
+        :type key: str
+        :return: None
+        """
+        if not self.has_attribute(key):
+            raise IndexError('attribute "{0}" does not exists on menu'.format(key))
+        del self._attributes[key]
diff --git a/pygame_menu/themes.py b/pygame_menu/themes.py
index c583eadc..5db8b88c 100644
--- a/pygame_menu/themes.py
+++ b/pygame_menu/themes.py
@@ -35,7 +35,6 @@ import pygame_menu.utils as _utils
 import pygame_menu.widgets as _widgets
 from pygame_menu.baseimage import BaseImage
 
-from pygame.version import vernum as pygame_vernum
 import copy
 
 
@@ -125,7 +124,7 @@ class Theme(object):
     :type widget_font_antialias: bool
     :param widget_font_background_color: Widget font background color. By default it is None. If None the value will be the same as ``background_color`` if it's is a color object and if ``widget_font_background_color_from_menu`` is True and ``widget_background_color`` is None
     :type widget_font_background_color: tuple, list, None
-    :param widget_font_background_color_from_menu: Use menu background color as font background color, True by default in pygame v2
+    :param widget_font_background_color_from_menu: Use menu background color as font background color; disabled by default
     :type widget_font_background_color_from_menu: bool
     :param widget_font_color: Color of the font
     :type widget_font_color: tuple, list
@@ -151,7 +150,6 @@ class Theme(object):
 
     def __init__(self, **kwargs):
 
-        # Set independant parameters
         self.background_color = self._get(kwargs, 'background_color', 'color_image',
                                           (220, 220, 220))  # type: tuple
         self.cursor_color = self._get(kwargs, 'cursor_color', 'color',
@@ -212,12 +210,14 @@ class Theme(object):
                                           pygame_menu.locals.ALIGN_CENTER)  # type: str
         self.widget_background_color = self._get(kwargs, 'widget_background_color', 'color_image_none',
                                                  )  # type: (tuple, type(None))
+        self.widget_background_inflate = self._get(kwargs, 'background_inflate', 'tuple2',
+                                                   (0, 0))  # type: tuple
         self.widget_font_antialias = self._get(kwargs, 'widget_font_antialias', bool,
                                                True)  # type: bool
         self.widget_font_background_color = self._get(kwargs, 'widget_font_background_color', 'color_none',
                                                       )  # type: tuple
         self.widget_font_background_color_from_menu = self._get(kwargs, 'widget_font_background_color_from_menu',
-                                                                bool, pygame_vernum[0] == 2)  # type: bool
+                                                                bool, False)  # type: bool
         self.widget_font_color = self._get(kwargs, 'widget_font_color', 'color',
                                            (70, 70, 70))  # type: tuple
         self.widget_font_size = self._get(kwargs, 'widget_font_size', int,
@@ -239,10 +239,6 @@ class Theme(object):
         self.widget_shadow_position = self._get(kwargs, 'widget_shadow_position', 'position',
                                                 pygame_menu.locals.POSITION_NORTHWEST)  # type: str
 
-        # Set dependant parameters
-        self.widget_background_inflate = self._get(kwargs, 'background_inflate', 'tuple2',
-                                                   self.widget_selection_effect.get_xy_margin())  # type: tuple
-
         # Upon this, no more kwargs should exist, raise exception if there's more
         for invalid_keyword in kwargs.keys():
             msg = 'parameter Theme.{} does not exist'.format(invalid_keyword)
diff --git a/pygame_menu/utils.py b/pygame_menu/utils.py
index 1dac1d80..f75571f3 100644
--- a/pygame_menu/utils.py
+++ b/pygame_menu/utils.py
@@ -64,7 +64,7 @@ def assert_color(color):
     :type color: tuple, list
     :return: None
     """
-    assert isinstance(color, (tuple, list))
+    assert isinstance(color, (tuple, list)), 'color must be a tuple or list'
     assert 4 >= len(color) >= 3, 'color must be a tuple or list of 3 or 4 numbers'
     for i in range(3):
         assert isinstance(color[i], int), \
@@ -72,7 +72,7 @@ def assert_color(color):
         assert 0 <= color[i] <= 255, \
             '"{0}" in element color {1} must be a number between 0 and 255'.format(color[i], color)
     if len(color) == 4:
-        assert isinstance(color[3], int)
+        assert isinstance(color[3], int), 'alpha channel must be an integer between 0 and 255'
         assert 0 <= color[3] <= 255, \
             'opacity of color {0} must be an integer between 0 and 255, ' \
             '0 is transparent, 255 is opaque'.format(color)
@@ -86,7 +86,7 @@ def assert_orientation(orientation):
     :type orientation: str
     :return: None
     """
-    assert isinstance(orientation, str)
+    assert isinstance(orientation, str), 'orientation must be a string'
     assert orientation in [_locals.ORIENTATION_HORIZONTAL, _locals.ORIENTATION_VERTICAL], \
         'invalid orientation value "{0}"'.format(orientation)
 
@@ -100,9 +100,9 @@ def assert_vector2(num_vector):
     :return: None
     """
     assert isinstance(num_vector, (tuple, list)), \
-        'object "{0}" must be a list or tuple of 2 items'.format(num_vector)
+        'vector "{0}" must be a list or tuple of 2 items'.format(num_vector)
     assert len(num_vector) == 2, \
-        'object list "{0}" must contain 2 items only'.format(num_vector)
+        'vector "{0}" must contain 2 items only'.format(num_vector)
     vector_numeric = isinstance(num_vector[0], (int, float)) and \
                      isinstance(num_vector[1], (int, float))
     assert vector_numeric, \
@@ -117,7 +117,7 @@ def assert_position(position):
     :type position: str
     :return: None
     """
-    assert isinstance(position, str)
+    assert isinstance(position, str), 'position must be a string'
     assert position in [_locals.POSITION_WEST, _locals.POSITION_SOUTHWEST,
                         _locals.POSITION_SOUTH, _locals.POSITION_SOUTHEAST,
                         _locals.POSITION_EAST, _locals.POSITION_NORTH,
@@ -179,6 +179,7 @@ def make_surface(width, height, alpha=False, fill_color=None):
         'surface width and height must be equal or greater than zero'
     surface = pygame.Surface((int(width), int(height)), pygame.SRCALPHA, 32)  # lgtm [py/call/wrong-arguments]
     if alpha:
+        # noinspection PyArgumentList
         surface = pygame.Surface.convert_alpha(surface)
     if fill_color is not None:
         assert_color(fill_color)
diff --git a/pygame_menu/version.py b/pygame_menu/version.py
index 22b6feab..2bcfab5e 100644
--- a/pygame_menu/version.py
+++ b/pygame_menu/version.py
@@ -54,6 +54,6 @@ class Version(tuple):
     patch = property(lambda self: self[2])
 
 
-vernum = Version(3, 4, 3)
+vernum = Version(3, 4, 4)
 ver = str(vernum)
 rev = ''
diff --git a/pygame_menu/widgets/core/widget.py b/pygame_menu/widgets/core/widget.py
index d3385035..28b9aa53 100644
--- a/pygame_menu/widgets/core/widget.py
+++ b/pygame_menu/widgets/core/widget.py
@@ -70,7 +70,7 @@ class Widget(object):
                  args=None,
                  kwargs=None
                  ):
-        assert isinstance(widget_id, str)
+        assert isinstance(widget_id, str), 'widget id must be a string'
         if onchange:
             assert is_callable(onchange), 'onchange must be callable or None'
         if onreturn:
@@ -158,6 +158,7 @@ class Widget(object):
         self.mouse_enabled = True
         self.selected = False
         self.selection_effect_enabled = True  # Some widgets cannot have selection effect
+        self.selection_expand_background = False  # If True, the widget background will inflate to match selection margin if selected
         self.sound = Sound()  # type: Sound
         self.touchscreen_enabled = True
         self.visible = True  # Use .show() or .hide() to modify this status
@@ -288,9 +289,23 @@ class Widget(object):
         assert inflate[0] >= 0 and inflate[1] >= 0, \
             'widget background inflate must be equal or greater than zero in both axis'
         self._background_color = color
-        self._background_inflate = inflate
+        self._background_inflate = tuple(inflate)
         self._last_render_hash = 0  # Force widget render
 
+    def expand_background_inflate_to_selection_effect(self):
+        """
+        Expand background inflate to match the selection effect
+        (the widget don't require to be selected).
+
+        This is a permanent change; for dynamic purpuoses, depending if the widget
+        is selected or not, setting ``widget.selection_expand_background`` to ``True`` may help.
+
+        .. note:: This method may have unexpected results with certain selection effects.
+
+        :return: None
+        """
+        self._background_inflate = self._selection_effect.get_xy_margin()
+
     def _fill_background_color(self, surface):
         """
         Fill a surface with the widget background color.
@@ -301,9 +316,11 @@ class Widget(object):
         """
         if self._background_color is None:
             return
-        rect = self.get_rect(
-            inflate=(self._background_inflate[0] / 2, self._background_inflate[1] / 2)
-        )
+        if not (self.selection_expand_background and self.selected):
+            inflate = self._background_inflate
+        else:
+            inflate = self._selection_effect.get_xy_margin()
+        rect = self.get_rect(inflate=inflate)
         if isinstance(self._background_color, _baseimage.BaseImage):
             self._background_color.draw(
                 surface=surface,
@@ -421,25 +438,25 @@ class Widget(object):
         :return: None
         """
         if width is not None:
-            assert isinstance(width, (int, float))
+            assert isinstance(width, (int, float)), 'width must be numeric'
         self._max_width = width
 
     def get_margin(self):
         """
         Return the widget margin.
 
-        :return: Widget margin (left, top)
+        :return: Widget margin *(left,bottom)*
         :rtype: tuple
         """
         return self._margin
 
     def set_margin(self, x, y):
         """
-        Set Widget margin (left, top).
+        Set Widget margin (left, bottom).
 
         :param x: Margin on x axis (left)
         :type x: int, float
-        :param y: Margin on y axis (top)
+        :param y: Margin on y axis (bottom)
         :type y: int, float
         :return: None
         """
@@ -512,10 +529,10 @@ class Widget(object):
             inflate = (0, 0)
         apply_padding = 1
 
-        pad_top = self._padding[0] * apply_padding + inflate[1]
-        pad_right = self._padding[1] * apply_padding + inflate[0]
-        pad_bottom = self._padding[2] * apply_padding + inflate[1]
-        pad_left = self._padding[3] * apply_padding + inflate[0]
+        pad_top = self._padding[0] * apply_padding + inflate[1] / 2
+        pad_right = self._padding[1] * apply_padding + inflate[0] / 2
+        pad_bottom = self._padding[2] * apply_padding + inflate[1] / 2
+        pad_left = self._padding[3] * apply_padding + inflate[0] / 2
 
         return pygame.Rect(int(self._rect.x - pad_left),
                            int(self._rect.y - pad_top),
@@ -573,7 +590,7 @@ class Widget(object):
         Render text. If the font is not defined returns a zero-width surface.
 
         :param text: Text to render
-        :type text: str
+        :type text: str, None
         :param color: Text color
         :type color: tuple
         :param use_background_color: Use default background color
@@ -582,24 +599,21 @@ class Widget(object):
         :rtype: :py:class:`pygame.Surface`
         """
         # assert isinstance(text, str)
-        assert isinstance(color, tuple)
-        assert isinstance(use_background_color, bool)
+        assert isinstance(color, tuple), 'invalid color'
+        assert isinstance(use_background_color, bool), 'use_background_color must be boolean'
         bgcolor = self._font_background_color
 
-        # Background color must be opaque, otherwise the results are quite bad
-        if isinstance(bgcolor, (tuple, list)) and len(bgcolor) == 4 and bgcolor[3] != 255:
-            bgcolor = None
-
         # Disable
         if not use_background_color:
             bgcolor = None
 
         if self._font is None:
             return make_surface(0, 0)
+
         surface = self._font.render(text, self._font_antialias, color, bgcolor)
         return surface
 
-    def _render_string(self, string, color, enable_fill=True):
+    def _render_string(self, string, color):
         """
         Render text and turn it into a surface.
 
@@ -607,22 +621,16 @@ class Widget(object):
         :type string: str
         :param color: Text color
         :type color: tuple
-        :param enable_fill: Enable font fill
-        :type enable_fill: bool
         :return: Text surface
         :rtype: :py:class:`pygame.Surface`
         """
         text = self._font_render_string(string, color)
 
         # Create surface
-        fill_color = self._background_color
-        if not isinstance(fill_color, tuple) or not enable_fill:
-            fill_color = None
         surface = make_surface(
             width=text.get_width(),
             height=text.get_height(),
-            alpha=True,
-            fill_color=fill_color
+            alpha=True
         )
 
         # Draw shadow first
@@ -706,6 +714,13 @@ class Widget(object):
         assert isinstance(background_color, (tuple, type(None)))
         assert isinstance(antialias, bool)
 
+        # If background is a color and it's transparent raise a warning
+        # Font background color must be opaque, otherwise the results are quite bad
+        if isinstance(background_color, (tuple, list)) and \
+                len(background_color) == 4 and background_color[3] != 255:
+            background_color = None
+            warnings.warn('font background color must be opaque, alpha channel must be 255')
+
         self._font = _fonts.get_font(font, font_size)
         self._font_antialias = antialias
         self._font_background_color = background_color
diff --git a/pygame_menu/widgets/examples/scrollbar.py b/pygame_menu/widgets/examples/scrollbar.py
index 7aa8fd0d..e3ffa3de 100644
--- a/pygame_menu/widgets/examples/scrollbar.py
+++ b/pygame_menu/widgets/examples/scrollbar.py
@@ -64,8 +64,10 @@ def make_world(width, height):
         for y in range(100, height, 200):
             if numberx in (0, maxx - 1) or numbery in (0, maxy - 1):
                 # White circles to delimit world boundaries
+                # noinspection PyArgumentList
                 pygame.draw.circle(world, (255, 255, 255), (x, y), 100, 10)
             else:
+                # noinspection PyArgumentList
                 pygame.draw.circle(world, color, (x, y), 100, 10)
                 if color[0] + 15 < 255:
                     color[0] += 15
diff --git a/pygame_menu/widgets/examples/scrollbar_area.py b/pygame_menu/widgets/examples/scrollbar_area.py
index 534ad35b..f92812df 100644
--- a/pygame_menu/widgets/examples/scrollbar_area.py
+++ b/pygame_menu/widgets/examples/scrollbar_area.py
@@ -69,6 +69,7 @@ WORLDS = {
 }
 
 
+# noinspection PyArgumentList
 def make_world(width, height, text=''):
     """
     Create a test surface.
diff --git a/pygame_menu/widgets/selection/highlight.py b/pygame_menu/widgets/selection/highlight.py
index a1c8b63b..4480fb58 100644
--- a/pygame_menu/widgets/selection/highlight.py
+++ b/pygame_menu/widgets/selection/highlight.py
@@ -38,6 +38,8 @@ class HighlightSelection(Selection):
     """
     Widget selection highlight class.
 
+    .. note:: Widget background color may not reach the entire selection area
+
     :param border_width: Border width of the highlight box
     :type border_width: int
     :param margin_x: X margin of selected highlight box
@@ -66,6 +68,7 @@ class HighlightSelection(Selection):
 
     # noinspection PyMissingOrEmptyDocstring
     def draw(self, surface, widget):
+        # noinspection PyArgumentList
         pygame.draw.rect(
             surface,
             self.color,
diff --git a/pygame_menu/widgets/widget/button.py b/pygame_menu/widgets/widget/button.py
index 507b7e2a..4f6c7471 100644
--- a/pygame_menu/widgets/widget/button.py
+++ b/pygame_menu/widgets/widget/button.py
@@ -144,7 +144,7 @@ class Button(Widget):
                 self.sound.play_click_mouse()
                 window_size = self.get_menu().get_window_size()
                 finger_pos = (event.x * window_size[0], event.y * window_size[1])
-                if rect.collidepoint(finger_pos):
+                if rect.collidepoint(*finger_pos):
                     self.apply()
                     updated = True
 
diff --git a/pygame_menu/widgets/widget/menubar.py b/pygame_menu/widgets/widget/menubar.py
index e3101f6c..eb6d4276 100644
--- a/pygame_menu/widgets/widget/menubar.py
+++ b/pygame_menu/widgets/widget/menubar.py
@@ -103,6 +103,7 @@ class MenuBar(Widget):
         )
 
         self._backbox = back_box
+        self._backbox_border_width = 1  # px
         self._backbox_pos = None  # type: (tuple,None)
         self._backbox_rect = None  # type: (pygame.rect.Rect,None)
         self._background_color = background_color
@@ -125,6 +126,18 @@ class MenuBar(Widget):
     def scale(self, width, height, smooth=True):  # Widget don't support scalling (yet)
         pass
 
+    def set_backbox_border_width(self, width):
+        """
+        Set backbox border width in px.
+
+        :param width: Width in px
+        :type width: int
+        :return: None
+        """
+        assert isinstance(width, int)
+        assert width > 0
+        self._backbox_border_width = width
+
     # noinspection PyMissingOrEmptyDocstring
     def draw(self, surface):
         self._render()
@@ -142,7 +155,8 @@ class MenuBar(Widget):
             if self._box_mode == _MODE_CLOSE and self.get_menu()._onclose is None:
                 pass
             else:
-                pygame.draw.rect(surface, self._font_selected_color, self._backbox_rect, 1)
+                # noinspection PyArgumentList
+                pygame.draw.rect(surface, self._font_selected_color, self._backbox_rect, self._backbox_border_width)
                 pygame.draw.polygon(surface, self._font_selected_color, self._backbox_pos)
 
         surface.blit(self._surface,
@@ -163,7 +177,7 @@ class MenuBar(Widget):
         else:
             self._box_mode = _MODE_BACK
 
-        self._surface = self._render_string(self._title, self._font_selected_color, enable_fill=False)
+        self._surface = self._render_string(self._title, self._font_selected_color)
         self._rect.width, self._rect.height = self._surface.get_size()
         self._apply_surface_transforms()  # Rotation does not affect rect size
 
diff --git a/pygame_menu/widgets/widget/selector.py b/pygame_menu/widgets/widget/selector.py
index fe6c5d26..dd8c09b3 100644
--- a/pygame_menu/widgets/widget/selector.py
+++ b/pygame_menu/widgets/widget/selector.py
@@ -276,7 +276,7 @@ class Selector(Widget):
             elif self.touchscreen_enabled and event.type == pygame.FINGERUP:
                 window_size = self.get_menu().get_window_size()
                 finger_pos = (event.x * window_size[0], event.y * window_size[1])
-                if rect.collidepoint(finger_pos):
+                if rect.collidepoint(*finger_pos):
                     # Check if mouse collides left or right as percentage, use only X coordinate
                     mousex, _ = finger_pos
                     topleft, _ = rect.topleft


Test patch:
diff --git a/test/_utils.py b/test/_utils.py
index 68401fc5..ff6ced1b 100644
--- a/test/_utils.py
+++ b/test/_utils.py
@@ -370,6 +370,7 @@ class MenuUtils(object):
             center_content=True,
             columns=1,
             height=400,
+            onclose=None,
             position_x=50,
             position_y=50,
             rows=None,
@@ -388,6 +389,8 @@ class MenuUtils(object):
         :type columns: int
         :param height: Menu height (px)
         :type height: int
+        :param onclose: Event or function applied when closing the Menu
+        :type onclose: :py:class:`pygame_menu.events.MenuAction`, callable, None
         :param position_x: X position of the menu
         :type position_x: int, float
         :param position_y: Y position of the menu
@@ -413,9 +416,10 @@ class MenuUtils(object):
             enabled=False,
             height=height,
             menu_position=(position_x, position_y),
+            onclose=onclose,
             rows=rows,
-            title=title,
             theme=theme,
+            title=title,
             width=width,
             *args,
             **kwargs
diff --git a/test/test_menu.py b/test/test_menu.py
index 30314a7b..27f3fd6f 100644
--- a/test/test_menu.py
+++ b/test/test_menu.py
@@ -47,6 +47,7 @@ class MenuTest(unittest.TestCase):
         """
         Test setup.
         """
+        test_reset_surface()
         self.menu = MenuUtils.generic_menu(title='mainmenu')
         self.menu.mainloop(surface, bgfun=dummy_function)
 
@@ -94,11 +95,45 @@ class MenuTest(unittest.TestCase):
         menu = MenuUtils.generic_menu(position_x=0, position_y=0)
         menu.set_relative_position(20, 40)
 
+    def test_attributes(self):
+        """
+        Test menu attributes.
+        """
+        menu = MenuUtils.generic_menu()
+        self.assertFalse(menu.has_attribute('epic'))
+        self.assertRaises(IndexError, lambda: menu.remove_attribute('epic'))
+        menu.set_attribute('epic', True)
+        self.assertTrue(menu.has_attribute('epic'))
+        self.assertTrue(menu.get_attribute('epic'))
+        menu.set_attribute('epic', False)
+        self.assertFalse(menu.get_attribute('epic'))
+        menu.remove_attribute('epic')
+        self.assertFalse(menu.has_attribute('epic'))
+        self.assertEqual(menu.get_attribute('epic', 420), 420)
+
+    def test_close(self):
+        """
+        Test menu close.
+        """
+        menu = MenuUtils.generic_menu()
+        menu.set_attribute('epic', False)
+        menu._back()
+
+        def test_close():
+            menu.set_attribute('epic', True)
+
+        menu.set_onclose(test_close)
+        self.assertTrue(not menu.is_enabled())
+        menu.enable()
+        self.assertFalse(menu.get_attribute('epic'))
+        menu._close()
+        self.assertTrue(menu.get_attribute('epic'))
+
     def test_enabled(self):
         """
         Test menu enable/disable feature.
         """
-        menu = MenuUtils.generic_menu()
+        menu = MenuUtils.generic_menu(onclose=events.NONE)
         self.assertTrue(not menu.is_enabled())
         menu.enable()
         self.assertTrue(menu.is_enabled())
@@ -193,8 +228,14 @@ class MenuTest(unittest.TestCase):
         """
         Test generic widget.
         """
+        self.menu.clear()
+        menu = MenuUtils.generic_menu()
+        btn = menu.add_button('nice', None)
         w = Button('title')
         self.menu.add_generic_widget(w)
+        self.assertRaises(ValueError, lambda: menu.add_generic_widget(w))
+        btn._menu = None
+        self.menu.add_generic_widget(btn)
 
     # noinspection PyArgumentEqualDefault
     def test_get_selected_widget(self):
@@ -292,8 +333,14 @@ class MenuTest(unittest.TestCase):
 
         # Clear all widgets and get index
         self.menu._widgets = []
+        self._index = 100
         self.assertEqual(self.menu.get_selected_widget(), None)
 
+        # Destroy index
+        self.menu._index = '0'
+        self.assertEqual(None, self.menu.get_selected_widget())
+        self.assertEqual(self.menu._index, 0)
+
     def test_submenu(self):
         """
         Test submenus.
@@ -321,11 +368,11 @@ class MenuTest(unittest.TestCase):
         """
         Test centering menu.
         """
-
         # Vertical offset disables centering
         theme = pygame_menu.themes.THEME_BLUE.copy()
         theme.widget_offset = (0, 100)
         menu = MenuUtils.generic_menu(theme=theme)
+        self.assertEqual(menu.get_theme(), theme)
         self.assertFalse(menu._center_content)
 
         # Outer scrollarea margin disables centering
@@ -339,6 +386,21 @@ class MenuTest(unittest.TestCase):
         menu = MenuUtils.generic_menu(theme=theme)
         self.assertTrue(menu._center_content)
 
+    def test_getters(self):
+        """
+        Test other getters.
+        """
+        self.assertTrue(self.menu.get_menubar_widget() is not None)
+        self.assertTrue(self.menu.get_scrollarea() is not None)
+
+        w, h = self.menu.get_size()
+        self.assertEqual(int(w), 600)
+        self.assertEqual(int(h), 400)
+
+        w, h = self.menu.get_window_size()
+        self.assertEqual(int(w), 600)
+        self.assertEqual(int(h), 600)
+
     def test_generic_events(self):
         """
         Test key events.
@@ -521,3 +583,5 @@ class MenuTest(unittest.TestCase):
         self.assertTrue(event_val[0])
         event_val[0] = False
         self.assertEqual(menu.get_selected_widget().get_id(), button.get_id())
+        btn = menu.get_selected_widget()  # type: Button
+        self.assertTrue(btn.get_selected_time() >= 0)
diff --git a/test/test_widgets.py b/test/test_widgets.py
index 76abe5d7..7f5f85fd 100644
--- a/test/test_widgets.py
+++ b/test/test_widgets.py
@@ -46,6 +46,7 @@ class WidgetsTest(unittest.TestCase):
         """
         Setup sound engine.
         """
+        test_reset_surface()
         self.menu = MenuUtils.generic_menu()
 
     def test_nonascii(self):
@@ -66,6 +67,20 @@ class WidgetsTest(unittest.TestCase):
         self.menu.enable()
         self.menu.draw(surface)
 
+    def test_background(self):
+        """
+        Test widget background.
+        """
+        self.menu.clear()
+        self.menu.enable()
+        w = self.menu.add_label('Text')  # type: Label
+        w.set_background_color((255, 255, 255), (10, 10))
+        w.draw(surface)
+        self.assertEqual(w._background_inflate[0], 10)
+        self.assertEqual(w._background_inflate[1], 10)
+        w.set_background_color(pygame_menu.baseimage.BaseImage(pygame_menu.baseimage.IMAGE_EXAMPLE_GRAY_LINES))
+        w.draw(surface)
+
     def test_transform(self):
         """
         Transform widgets.
@@ -76,6 +91,7 @@ class WidgetsTest(unittest.TestCase):
         w.rotate(45)
         w.translate(10, 10)
         w.scale(1, 1)
+        w.set_max_width(150)
         self.assertFalse(w._scale[0])  # Scalling is disabled
         w.scale(1.5, 1)
         self.assertTrue(w._scale[0])  # Scalling is enabled
@@ -191,6 +207,12 @@ class WidgetsTest(unittest.TestCase):
                      MENUBAR_STYLE_TITLE_ONLY_DIAGONAL]:
             mb = MenuBar('Menu', 500, (0, 0, 0), True, mode=mode)
             self.menu.add_generic_widget(mb)
+        mb = MenuBar('Menu', 500, (0, 0, 0), True)
+        mb.set_backbox_border_width(2)
+        self.assertRaises(AssertionError, lambda: mb.set_backbox_border_width(1.5))
+        self.assertRaises(AssertionError, lambda: mb.set_backbox_border_width(0))
+        self.assertRaises(AssertionError, lambda: mb.set_backbox_border_width(-1))
+        self.assertEqual(mb._backbox_border_width, 2)
         self.menu.draw(surface)
         self.menu.disable()
 
@@ -618,6 +640,22 @@ class WidgetsTest(unittest.TestCase):
         # Invalid recursive menu
         self.assertRaises(ValueError, lambda: menu.add_button('bt', menu))
 
+    def test_attributes(self):
+        """
+        Test widget attributes.
+        """
+        widget = self.menu.add_label('epic')
+        self.assertFalse(widget.has_attribute('epic'))
+        self.assertRaises(IndexError, lambda: widget.remove_attribute('epic'))
+        widget.set_attribute('epic', True)
+        self.assertTrue(widget.has_attribute('epic'))
+        self.assertTrue(widget.get_attribute('epic'))
+        widget.set_attribute('epic', False)
+        self.assertFalse(widget.get_attribute('epic'))
+        widget.remove_attribute('epic')
+        self.assertFalse(widget.has_attribute('epic'))
+        self.assertEqual(widget.get_attribute('epic', 420), 420)
+
     def test_draw_callback(self):
         """
         Test drawing callback.