Version

Quick search

Table Of Contents

Source code for kivy.uix.bubble

'''
Bubble
======

.. versionadded:: 1.1.0

.. image:: images/bubble.jpg
    :align: right

The :class:`Bubble` widget is a form of menu or a small popup with an arrow
arranged on one side of it's content.

The :class:`Bubble` contains an arrow attached to the content
(e.g., :class:`BubbleContent`) pointing in the direction you choose. It can
be placed either at a predefined location or flexibly by specifying a relative
position on the border of the widget.

The :class:`BubbleContent` is a styled BoxLayout and is thought to be added to
the :class:`Bubble` as a child widget. The :class:`Bubble` will then arrange
an arrow around the content as desired. Instead of the class:`BubbleContent`,
you can theoretically use any other :class:`Widget` as well as long as it
supports the 'bind' and 'unbind' function of the :class:`EventDispatcher` and
is compatible with Kivy to be placed inside a :class:`BoxLayout`.

The :class:`BubbleButton`is a styled Button. It suits to the style of
:class:`Bubble` and :class:`BubbleContent`. Feel free to place other Widgets
inside the 'content' of the :class:`Bubble`.


.. versionchanged:: 2.2.0
The properties :attr:`background_image`, :attr:`background_color`,
:attr:`border` and :attr:`border_auto_scale` were removed from :class:`Bubble`.
These properties had only been used by the content widget that now uses it's
own properties instead. The color of the arrow is now changed with
:attr:`arrow_color` instead of :attr:`background_color`.
These changes makes the :class:`Bubble` transparent to use with other layouts
as content without any side-effects due to property inheritance.

The property :attr:`flex_arrow_pos` has been added to allow further
customization of the arrow positioning.

The properties :attr:`arrow_margin`, :attr:`arrow_margin_x`,
:attr:`arrow_margin_y`, :attr:`content_size`, :attr:`content_width` and
:attr:`content_height` have been added to ease proper sizing of a
:class:`Bubble` e.g., based on it's content size.

BubbleContent
=============

The :class:`BubbleContent` is a styled BoxLayout that can be used to
add e.g., :class:`BubbleButtons` as menu items.

.. versionchanged:: 2.2.0
The properties :attr:`background_image`, :attr:`background_color`,
:attr:`border` and :attr:`border_auto_scale` were added to the
:class:`BubbleContent`. The :class:`BubbleContent` does no longer rely on these
properties being present in the parent class.

BubbleButton
============

The :class:`BubbleButton` is a styled :class:`Button` that can be used to be
added to the :class:`BubbleContent`.

Simple example
--------------

.. include:: ../../examples/widgets/bubble_test.py
    :literal:

Customize the Bubble
--------------------

You can choose the direction in which the arrow points::

    Bubble(arrow_pos='top_mid')
    or
    Bubble(size=(200, 40), flex_arrow_pos=(175, 40))

    Similarly, the corresponding properties in the '.kv' language can be used
    as well.

You can change the appearance of the bubble::

    Bubble(
        arrow_image='/path/to/arrow/image',
        arrow_color=(1, 0, 0, .5)),
    )
    BubbleContent(
        background_image='/path/to/background/image',
        background_color=(1, 0, 0, .5),  # 50% translucent red
        border=(0,0,0,0),
    )

    Similarly, the corresponding properties in the '.kv' language can be used
    as well.

-----------------------------
'''

__all__ = ('Bubble', 'BubbleButton', 'BubbleContent')

from kivy.uix.image import Image
from kivy.uix.scatter import Scatter
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.button import Button
from kivy.properties import ObjectProperty
from kivy.properties import StringProperty
from kivy.properties import OptionProperty
from kivy.properties import ListProperty
from kivy.properties import BooleanProperty
from kivy.properties import ColorProperty
from kivy.properties import NumericProperty
from kivy.properties import ReferenceListProperty
from kivy.base import EventLoop
from kivy.metrics import dp


class BubbleException(Exception):
    pass


[docs]class BubbleButton(Button): '''A button intended for use in a BubbleContent widget. You can use a "normal" button class, but it will not look good unless the background is changed. Rather use this BubbleButton widget that is already defined and provides a suitable background for you. ''' pass
[docs]class BubbleContent(BoxLayout): '''A styled BoxLayout that can be used as the content widget of a Bubble. .. versionchanged:: 2.2.0 The graphical appearance of :class:`BubbleContent` is now based on it's own properties :attr:`background_image`, :attr:`background_color`, :attr:`border` and :attr:`border_auto_scale`. The parent widget properties are no longer considered. This makes the BubbleContent a standalone themed BoxLayout. ''' background_color = ColorProperty([1, 1, 1, 1]) '''Background color, in the format (r, g, b, a). To use it you have to set :attr:`background_image` first. .. versionadded:: 2.2.0 :attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and defaults to [1, 1, 1, 1]. ''' background_image = StringProperty('atlas://data/images/defaulttheme/bubble') '''Background image of the bubble. .. versionadded:: 2.2.0 :attr:`background_image` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/bubble'. ''' border = ListProperty([16, 16, 16, 16]) '''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage` graphics instruction. Used with the :attr:`background_image`. It should be used when using custom backgrounds. It must be a list of 4 values: (bottom, right, top, left). Read the BorderImage instructions for more information about how to use it. .. versionadded:: 2.2.0 :attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults to (16, 16, 16, 16) ''' border_auto_scale = OptionProperty( 'both_lower', options=[ 'off', 'both', 'x_only', 'y_only', 'y_full_x_lower', 'x_full_y_lower', 'both_lower' ] ) '''Specifies the :attr:`kivy.graphics.BorderImage.auto_scale` value on the background BorderImage. .. versionadded:: 2.2.0 :attr:`border_auto_scale` is a :class:`~kivy.properties.OptionProperty` and defaults to 'both_lower'. '''
[docs]class Bubble(BoxLayout): '''Bubble class. See module documentation for more information. ''' content = ObjectProperty(allownone=True) '''This is the object where the main content of the bubble is held. The content of the Bubble set by 'add_widget' and removed with 'remove_widget' similarly to the :class:`ActionView` which is placed into a class:`ActionBar` :attr:`content` is a :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' arrow_image = StringProperty( 'atlas://data/images/defaulttheme/bubble_arrow' ) ''' Image of the arrow pointing to the bubble. :attr:`arrow_image` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/bubble_arrow'. ''' arrow_color = ColorProperty([1, 1, 1, 1]) '''Arrow color, in the format (r, g, b, a). To use it you have to set :attr:`arrow_image` first. .. versionadded:: 2.2.0 :attr:`arrow_color` is a :class:`~kivy.properties.ColorProperty` and defaults to [1, 1, 1, 1]. ''' show_arrow = BooleanProperty(True) ''' Indicates whether to show arrow. .. versionadded:: 1.8.0 :attr:`show_arrow` is a :class:`~kivy.properties.BooleanProperty` and defaults to `True`. ''' arrow_pos = OptionProperty( 'bottom_mid', options=( 'left_top', 'left_mid', 'left_bottom', 'top_left', 'top_mid', 'top_right', 'right_top', 'right_mid', 'right_bottom', 'bottom_left', 'bottom_mid', 'bottom_right', ) ) '''Specifies the position of the arrow as predefined relative position to the bubble. Can be one of: left_top, left_mid, left_bottom top_left, top_mid, top_right right_top, right_mid, right_bottom bottom_left, bottom_mid, bottom_right. :attr:`arrow_pos` is a :class:`~kivy.properties.OptionProperty` and defaults to 'bottom_mid'. ''' flex_arrow_pos = ListProperty(None) '''Specifies the position of the arrow as flex coordinate around the border of the :class:`Bubble` Widget. If this property is set to a proper position (relative pixel coordinates within the :class:`Bubble` widget, it overwrites the setting :attr:`arrow_pos`. .. versionadded:: 2.2.0 :attr:`flex_arrow_pos` is a :class:`~kivy.properties.ListProperty` and defaults to None. ''' limit_to = ObjectProperty(None, allownone=True) '''Specifies the widget to which the bubbles position is restricted. .. versionadded:: 1.6.0 :attr:`limit_to` is a :class:`~kivy.properties.ObjectProperty` and defaults to 'None'. ''' arrow_margin_x = NumericProperty(0) '''Automatically computed margin in x direction that the arrow widget occupies in pixel. In combination with the :attr:`content_width`, this property can be used to determine the correct width of the Bubble to exactly enclose the arrow + content by adding :attr:`content_width` and :attr:`arrow_margin_x` .. versionadded:: 2.2.0 :attr:`arrow_margin_x` is a :class:`~kivy.properties.NumericProperty` and represents the added margin in x direction due to the arrow widget. It defaults to 0 and is read only. ''' arrow_margin_y = NumericProperty(0) '''Automatically computed margin in y direction that the arrow widget occupies in pixel. In combination with the :attr:`content_height`, this property can be used to determine the correct height of the Bubble to exactly enclose the arrow + content by adding :attr:`content_height` and :attr:`arrow_margin_y` .. versionadded:: 2.2.0 :attr:`arrow_margin_y` is a :class:`~kivy.properties.NumericProperty` and represents the added margin in y direction due to the arrow widget. It defaults to 0 and is read only. ''' arrow_margin = ReferenceListProperty(arrow_margin_x, arrow_margin_y) '''Automatically computed margin that the arrow widget occupies in x and y direction in pixel. Check the description of :attr:`arrow_margin_x` and :attr:`arrow_margin_y`. .. versionadded:: 2.2.0 :attr:`arrow_margin` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`arrow_margin_x`, :attr:`arrow_margin_y`) properties. It is read only. ''' content_width = NumericProperty(0) '''The width of the content Widget. .. versionadded:: 2.2.0 :attr:`content_width` is a :class:`~kivy.properties.NumericProperty` and is the same as self.content.width if content is not None, else it defaults to 0. It is read only. ''' content_height = NumericProperty(0) '''The height of the content Widget. .. versionadded:: 2.2.0 :attr:`content_height` is a :class:`~kivy.properties.NumericProperty` and is the same as self.content.height if content is not None, else it defaults to 0. It is read only. ''' content_size = ReferenceListProperty(content_width, content_height) ''' The size of the content Widget. .. versionadded:: 2.2.0 :attr:`content_size` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`content_width`, :attr:`content_height`) properties. It is read only. ''' # Internal map that specifies the different parameters for fixed arrow # position layouts. The flex_arrow_pos uses these parameter sets # as a template. # 0: orientation of the children of Bubble ([content, arrow]) # 1: order of widgets to add to the BoxLayout (default: [content, arrow]) # 2: size_hint of _arrow_image_layout # 3: rotation of the _arrow_image # 4: pos_hint of the _arrow_image_layout ARROW_LAYOUTS = { "bottom_left": ( "vertical", 1, ( 1, None), 0, { "top": 1.0, "x": 0.05}), # noqa: E201,E241,E501 "bottom_mid": ( "vertical", 1, ( 1, None), 0, { "top": 1.0, "center_x": 0.50}), # noqa: E201,E241,E501 "bottom_right": ( "vertical", 1, ( 1, None), 0, { "top": 1.0, "right": 0.95}), # noqa: E201,E241,E501 "right_bottom": ( "horizontal", 1, (None, 1), 90, { "left": 0.0, "y": 0.05}), # noqa: E201,E241,E501 "right_mid": ( "horizontal", 1, (None, 1), 90, { "left": 0.0, "center_y": 0.50}), # noqa: E201,E241,E501 "right_top": ( "horizontal", 1, (None, 1), 90, { "left": 0.0, "top": 0.95}), # noqa: E201,E241,E501 "top_left": ( "vertical", -1, ( 1, None), 180, {"bottom": 0.0, "x": 0.05}), # noqa: E201,E241,E501 "top_mid": ( "vertical", -1, ( 1, None), 180, {"bottom": 0.0, "center_x": 0.50}), # noqa: E201,E241,E501 "top_right": ( "vertical", -1, ( 1, None), 180, {"bottom": 0.0, "right": 0.95}), # noqa: E201,E241,E501 "left_bottom": ( "horizontal", -1, (None, 1), -90, {"right": 1.0, "y": 0.05}), # noqa: E201,E241,E501 "left_mid": ( "horizontal", -1, (None, 1), -90, {"right": 1.0, "center_y": 0.50}), # noqa: E201,E241,E501 "left_top": ( "horizontal", -1, (None, 1), -90, {"right": 1.0, "top": 0.95}), # noqa: E201,E241,E501 } def __init__(self, **kwargs): self.content = None self._flex_arrow_layout_params = None self._temporarily_ignore_limits = False self._arrow_image = Image( source=self.arrow_image, fit_mode="scale-down", color=self.arrow_color ) self._arrow_image.width = self._arrow_image.texture_size[0] self._arrow_image.height = dp(self._arrow_image.texture_size[1]) self._arrow_image_scatter = Scatter( size_hint=(None, None), do_scale=False, do_rotation=False, do_translation=False, ) self._arrow_image_scatter.add_widget(self._arrow_image) self._arrow_image_scatter.size = self._arrow_image.texture_size self._arrow_image_scatter_wrapper = BoxLayout( size_hint=(None, None), ) self._arrow_image_scatter_wrapper.add_widget(self._arrow_image_scatter) self._arrow_image_layout = RelativeLayout() self._arrow_image_layout.add_widget(self._arrow_image_scatter_wrapper) self._arrow_layout = None super().__init__(**kwargs) self.reposition_inner_widgets()
[docs] def add_widget(self, widget, *args, **kwargs): if self.content is None: self.content = widget self.content_size = widget.size self.content.bind(size=self.update_content_size) self.reposition_inner_widgets() else: raise BubbleException( "Bubble can only contain a single Widget or Layout" )
[docs] def remove_widget(self, widget, *args, **kwargs): if widget == self.content: self.content.unbind(size=self.update_content_size) self.content = None self.content_size = [0, 0] self.reposition_inner_widgets() return super().remove_widget(widget, *args, **kwargs)
def on_content_size(self, instance, value): self.adjust_position() def on_limit_to(self, instance, value): self.adjust_position() def on_pos(self, instance, value): self.adjust_position() def on_size(self, instance, value): self.reposition_inner_widgets() def on_arrow_image(self, instance, value): self._arrow_image.source = self.arrow_image self._arrow_image.width = self._arrow_image.texture_size[0] self._arrow_image.height = dp(self._arrow_image.texture_size[1]) self._arrow_image_scatter.size = self._arrow_image.texture_size self.reposition_inner_widgets() def on_arrow_color(self, instance, value): self._arrow_image.color = self.arrow_color def on_arrow_pos(self, instance, value): self.reposition_inner_widgets() def on_flex_arrow_pos(self, instance, value): self._flex_arrow_layout_params = self.get_flex_arrow_layout_params() self.reposition_inner_widgets() def get_flex_arrow_layout_params(self): pos = self.flex_arrow_pos if pos is None: return None x, y = pos if not (0 <= x <= self.width and 0 <= y <= self.height): return None # the order of the following list defines the side that the arrow # will be attached to in case of ambiguity (same distances) base_layouts_map = [ ("bottom_mid", y), ("top_mid", self.height - y), ("left_mid", x), ("right_mid", self.width - x), ] base_layout_key = min(base_layouts_map, key=lambda val: val[1])[0] arrow_layout = list(Bubble.ARROW_LAYOUTS[base_layout_key]) arrow_width = self._arrow_image.width # This function calculates the proper value for pos_hint, i.e., the # arrow texture does not 'overflow' and stays entirely connected to # the side of the content. def calc_x0(x, length): return x * (length - arrow_width) / (length * length) if base_layout_key == "bottom_mid": arrow_layout[-1] = {"top": 1.0, "x": calc_x0(x, self.width)} elif base_layout_key == "top_mid": arrow_layout[-1] = {"bottom": 0.0, "x": calc_x0(x, self.width)} elif base_layout_key == "left_mid": arrow_layout[-1] = {"right": 1.0, "y": calc_x0(y, self.height)} elif base_layout_key == "right_mid": arrow_layout[-1] = {"left": 0.0, "y": calc_x0(y, self.height)} return arrow_layout def update_content_size(self, instance, value): self.content_size = self.content.size def adjust_position(self): if self.limit_to is not None and not self._temporarily_ignore_limits: if self.limit_to is EventLoop.window: lim_x, lim_y = 0, 0 lim_top, lim_right = self.limit_to.size else: lim_x = self.limit_to.x lim_y = self.limit_to.y lim_top = self.limit_to.top lim_right = self.limit_to.right self._temporarily_ignore_limits = True if not (lim_x > self.x and lim_right < self.right): self.x = max(lim_x, min(lim_right - self.width, self.x)) if not (lim_y > self.y and lim_right < self.right): self.y = min(lim_top - self.height, max(lim_y, self.y)) self._temporarily_ignore_limits = False def reposition_inner_widgets(self): arrow_image_layout = self._arrow_image_layout arrow_image_scatter = self._arrow_image_scatter arrow_image_scatter_wrapper = self._arrow_image_scatter_wrapper content = self.content # Remove the children of the Bubble (BoxLayout) as a first step for child in list(self.children): super().remove_widget(child) if self.canvas is None or content is None: return # find the layout parameters that define a specific bubble setup if self._flex_arrow_layout_params is not None: layout_params = self._flex_arrow_layout_params else: layout_params = Bubble.ARROW_LAYOUTS[self.arrow_pos] (bubble_orientation, widget_order, arrow_size_hint, arrow_rotation, arrow_pos_hint) = layout_params # rotate the arrow, place it at the right pos and setup the size # of the widget, so the BoxLayout can do the rest. arrow_image_scatter.rotation = arrow_rotation arrow_image_scatter_wrapper.size = arrow_image_scatter.bbox[1] arrow_image_scatter_wrapper.pos_hint = arrow_pos_hint arrow_image_layout.size_hint = arrow_size_hint arrow_image_layout.size = arrow_image_scatter.bbox[1] # set the orientation of the Bubble (BoxLayout) self.orientation = bubble_orientation # Add the updated children of the Bubble (BoxLayout) and update # properties widgets_to_add = [content, arrow_image_layout] # Set the arrow_margin, so we can use this property for proper sizing # of the Bubble Widget. # Determine whether to add the arrow_image_layout to the # Bubble (BoxLayout) or not. arrow_margin_x, arrow_margin_y = (0, 0) if self.show_arrow: if bubble_orientation[0] == "h": arrow_margin_x = arrow_image_layout.width elif bubble_orientation[0] == "v": arrow_margin_y = arrow_image_layout.height else: widgets_to_add.pop(1) for widget in widgets_to_add[::widget_order]: super().add_widget(widget) self.arrow_margin = (arrow_margin_x, arrow_margin_y)