test-kivy-app/kivy_venv/lib/python3.11/site-packages/kivy/uix/bubble.py

577 lines
21 KiB
Python
Raw Normal View History

2024-09-15 12:12:16 +00:00
'''
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
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
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'.
'''
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()
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"
)
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)