220 lines
7 KiB
Python
220 lines
7 KiB
Python
"""
|
|
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()
|