globalforest/kivymd/vendor/circleLayout/__init__.py

221 lines
7 KiB
Python
Raw Normal View History

2020-10-14 00:19:43 -04:00
"""
CircularLayout
==============
CircularLayout is a special layout that places widgets around a circle.
size_hint
---------
size_hint_x is used as an angle-quota hint (widget with higher
size_hint_x will be farther from each other, and vice versa), while
size_hint_y is used as a widget size hint (widgets with a higher size
hint will be bigger).size_hint_x cannot be None.
Widgets are all squares, unless you set size_hint_y to None (in that
case you'll be able to specify your own size), and their size is the
difference between the outer and the inner circle's radii. To make the
widgets bigger you can just decrease inner_radius_hint.
"""
__all__ = ("CircularLayout",)
from math import cos, pi, radians, sin
from kivy.properties import (
AliasProperty,
BoundedNumericProperty,
NumericProperty,
OptionProperty,
ReferenceListProperty,
VariableListProperty,
)
from kivy.uix.layout import Layout
try:
xrange(1, 2)
except NameError:
def xrange(first, second, third=None):
if third:
return range(first, second, third)
else:
return range(first, second)
class CircularLayout(Layout):
"""
Circular layout class. See module documentation for more information.
"""
padding = VariableListProperty([0, 0, 0, 0])
"""Padding between the layout box and it's children: [padding_left,
padding_top, padding_right, padding_bottom].
padding also accepts a two argument form [padding_horizontal,
padding_vertical] and a one argument form [padding].
.. version changed:: 1.7.0
Replaced NumericProperty with VariableListProperty.
:attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and
defaults to [0, 0, 0, 0].
"""
start_angle = NumericProperty(0)
"""Angle (in degrees) at which the first widget will be placed.
Start counting angles from the X axis, going counterclockwise.
:attr:`start_angle` is a :class:`~kivy.properties.NumericProperty` and
defaults to 0 (start from the right).
"""
circle_quota = BoundedNumericProperty(360, min=0, max=360)
"""Size (in degrees) of the part of the circumference that will actually
be used to place widgets.
:attr:`circle_quota` is a :class:`~kivy.properties.BoundedNumericProperty`
and defaults to 360 (all the circumference).
"""
direction = OptionProperty("ccw", options=("cw", "ccw"))
"""Direction of widgets in the circle.
:attr:`direction` is an :class:`~kivy.properties.OptionProperty` and
defaults to 'ccw'. Can be 'ccw' (counterclockwise) or 'cw' (clockwise).
"""
outer_radius_hint = NumericProperty(1)
"""Sets the size of the outer circle. A number greater than 1 will make the
widgets larger than the actual widget, a number smaller than 1 will leave
a gap.
:attr:`outer_radius_hint` is a :class:`~kivy.properties.NumericProperty`
and defaults to 1.
"""
inner_radius_hint = NumericProperty(0.6)
"""Sets the size of the inner circle. A number greater than
:attr:`outer_radius_hint` will cause glitches. The closest it is to
:attr:`outer_radius_hint`, the smallest will be the widget in the layout.
:attr:`outer_radius_hint` is a :class:`~kivy.properties.NumericProperty`
and defaults to 1.
"""
radius_hint = ReferenceListProperty(inner_radius_hint, outer_radius_hint)
"""Combined :attr:`outer_radius_hint` and :attr:`inner_radius_hint`
in a list for convenience. See their documentation for more details.
:attr:`radius_hint` is a :class:`~kivy.properties.ReferenceListProperty`.
"""
def _get_delta_radii(self):
radius = (
min(
self.width - self.padding[0] - self.padding[2],
self.height - self.padding[1] - self.padding[3],
)
/ 2.0
)
outer_r = radius * self.outer_radius_hint
inner_r = radius * self.inner_radius_hint
return outer_r - inner_r
delta_radii = AliasProperty(
_get_delta_radii, None, bind=("radius_hint", "padding", "size")
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(
start_angle=self._trigger_layout,
parent=self._trigger_layout,
# padding=self._trigger_layout,
children=self._trigger_layout,
size=self._trigger_layout,
radius_hint=self._trigger_layout,
pos=self._trigger_layout,
)
def do_layout(self, *largs):
# optimize layout by preventing looking at the same attribute in a loop
len_children = len(self.children)
if len_children == 0:
return
selfcx = self.center_x
selfcy = self.center_y
direction = self.direction
cquota = radians(self.circle_quota)
start_angle_r = radians(self.start_angle)
padding_left = self.padding[0]
padding_top = self.padding[1]
padding_right = self.padding[2]
padding_bottom = self.padding[3]
padding_x = padding_left + padding_right
padding_y = padding_top + padding_bottom
radius = min(self.width - padding_x, self.height - padding_y) / 2.0
outer_r = radius * self.outer_radius_hint
inner_r = radius * self.inner_radius_hint
middle_r = radius * sum(self.radius_hint) / 2.0
delta_r = outer_r - inner_r
stretch_weight_angle = 0.0
for w in self.children:
sha = w.size_hint_x
if sha is None:
raise ValueError(
"size_hint_x cannot be None in a CircularLayout"
)
else:
stretch_weight_angle += sha
sign = +1.0
angle_offset = start_angle_r
if direction == "cw":
angle_offset = 2 * pi - start_angle_r
sign = -1.0
for c in reversed(self.children):
sha = c.size_hint_x
shs = c.size_hint_y
angle_quota = cquota / stretch_weight_angle * sha
angle = angle_offset + (sign * angle_quota / 2)
angle_offset += sign * angle_quota
# kived: looking it up, yes. x = cos(angle) * radius + centerx;
# y = sin(angle) * radius + centery
ccx = cos(angle) * middle_r + selfcx + padding_left - padding_right
ccy = sin(angle) * middle_r + selfcy + padding_bottom - padding_top
c.center_x = ccx
c.center_y = ccy
if shs:
s = delta_r * shs
c.width = s
c.height = s
if __name__ == "__main__":
from kivymd.app import MDApp
from kivy.uix.button import Button
class CircLayoutApp(MDApp):
def build(self):
cly = CircularLayout(
direction="cw",
start_angle=-75,
inner_radius_hint=0.7,
padding="20dp",
)
for i in xrange(1, 13):
cly.add_widget(Button(text=str(i), font_size="30dp"))
return cly
CircLayoutApp().run()