Esempio n. 1
0
    def test_reference_child_update(self):
        from kivy.properties import NumericProperty, ReferenceListProperty

        x = NumericProperty(0)
        x.link(wid, 'x')
        x.link_deps(wid, 'x')
        y = NumericProperty(0)
        y.link(wid, 'y')
        y.link_deps(wid, 'y')
        pos = ReferenceListProperty(x, y)
        pos.link(wid, 'pos')
        pos.link_deps(wid, 'pos')

        pos.get(wid)[0] = 10
        self.assertEqual(pos.get(wid), [10, 0])

        pos.get(wid)[:] = (20, 30)
        self.assertEqual(pos.get(wid), [20, 30])
Esempio n. 2
0
class CommonElevationBehavior(Widget):
    """Common base class for rectangular and circular elevation behavior."""

    elevation = BoundedNumericProperty(0, min=0)
    """
    Elevation of the widget

    .. note::

        However this value does not represent the current elevation of the
        widget. `_elevation` can be used to animate the current elevation and
        come back using the `elevation` property directly.

        For example:

        .. code-block:: python

            from kivy.lang import Builder
            from kivy.uix.behaviors import ButtonBehavior

            from kivymd.app import MDApp
            from kivymd.uix.behaviors import CircularElevationBehavior, CircularRippleBehavior
            from kivymd.uix.boxlayout import MDBoxLayout

            KV = '''
            #:import Animation kivy.animation.Animation


            MDScreen:

                CircularRippleButton:
                    size_hint: None, None
                    size: "50dp", "50dp"
                    md_bg_color: .2, .2, .2, 1
                    pos_hint: {"center_x": .5, "center_y": .5}
                    on_press: Animation(_elevation=40, d=1).start(self)
                    on_release: Animation(_elevation=6, d=1).start(self)
            '''


            class CircularRippleButton(
                CircularElevationBehavior,
                CircularRippleBehavior,
                ButtonBehavior,
                MDBoxLayout,
            ):
                def __init__(self, **kv):
                    super().__init__(**kv)
                    self.elevation = 6
            
                def on_size(self, *args):
                    self.radius = [self.size[0] / 2]


            class Example(MDApp):
                def build(self):
                    return Builder.load_string(KV)


            Example().run()
    """

    # Shadow rendering porpoerties
    # Shadow rotation meomry - SHARED ACROSS OTHER CLASSES.

    angle = NumericProperty(0)
    """
    Angle of rotation in degrees of the current shadow.
    This value is shared across different widgets.

    .. note::

        This value will affect both, hard and soft shadows.
        Each shadow has his own origin point that's computed every time the
        elevation changes.

    :attr:`angle` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    radius = VariableListProperty([0])
    """
    Radius of the Corners of the shadow. This values represents each corner of
    the shadow, starting from top-left corner and going clockwise.

    .. code-block:: python

        radius = [
            "top-left",
            "top-right",
            "bottom-right",
            "bottom-left",
        ]

    This value can be expanded thus allowing this settings to be valid:

    .. code-block:: python

        widget.radius = [0]  # Translates to [0, 0, 0, 0]
        widget.radius = [10, 3]  # Translates to [10, 3, 10, 3]
        widget.radius = [7.0, 8.7, 1.5, 3.0]  # Translates to [7, 8, 1, 3]

    .. note::

        This value will affect both, hard and soft shadows.
        this value only affects RoundedRectangularElevationBehavior for now,
        but can be stored and used by custom shadow Draw functions.

    :attr:`radius` is an :class:`~kivy.properties.VariableListProperty`
    and defaults to `[0, 0, 0, 0]`.
    """

    # Position of the shadow
    _shadow_origin_x = NumericProperty(0)
    """
    Shadow origin x position for the rotation origin.
    Managed by `_shadow_origin`.

    :attr:`_shadow_origin_x` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.

    .. note::
        This property is automatically processed. by _shadow_origin
    """

    _shadow_origin_y = NumericProperty(0)
    """
    Shadow origin y position for the rotation origin.
    Managed by `_shadow_origin`.

    :attr:`_shadow_origin_y` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.

    .. note::
        This property is automatically processed.
    """

    _shadow_origin = ReferenceListProperty(_shadow_origin_x, _shadow_origin_y)
    """
    Soft Shadow Rotation origin point.

    :attr:`_shadow_origin` is an :class:`~kivy.properties.ReferenceListProperty`
    and defaults to `[0, 0]`.

    .. note::

        This property is automatically processed and relative to the canvas center.
    """

    _shadow_pos = ListProperty([0, 0])  # custom offset
    """
    Soft Shadow origin point.

    :attr:`_shadow_pos` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0]`.

    .. note::
        This property is automatically processed and relative to the Widget's
        canvas center.
    """

    shadow_pos = ListProperty([0, 0])  # bottom left corner
    """
    Custom shadow Origin point. if this property is set, _shadow_pos will be
    ommited.

    This property allows userts to fake light source.

    :attr:`shadow_pos` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0]`.

    .. note::
        this value overwrite the _shadow_pos processing
    """

    # Shadow Group shared memory
    __shadow_groups = {"global": []}

    shadow_group = StringProperty("global")
    """
    Widget's shadow Group.
    By default every widget with a shadow is saved inside the memory
    `__shadow_groups` as a weakref. This means that you can have multiple light
    sources, one for every shadow group.

    To fake a light source use force_shadow_pos.

    :attr:`shadow_group` is an :class:`~kivy.properties.StringProperty`
    and defaults to `"global"`.
    """

    _elevation = NumericProperty(0)
    """
    inner memory for the elevation.

    .. warning:: This property is the current elevation of the widget, do not
        use this property directly, instead, use `CommonElevationBehavior`
        elevation.

    :attr:`_elevation` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    # soft shadow
    _soft_shadow_texture = ObjectProperty()
    """
    Texture of the Soft Shadow texture for the canvas.

    :attr:`_soft_shadow_texture` is an :class:`~kivy.core.image.Image`
    and defaults to `None`.

    .. note::
        This property is automatically processed.
    """

    soft_shadow_size = ListProperty([0, 0])
    """
    Size of the soft Shadow texture over the canvas.

    :attr:`soft_shadow_size` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0]`.

    .. note::
        This property is automatically processed.
    """

    soft_shadow_pos = ListProperty([0, 0])
    """
    Position of the Hard Shadow texture over the canvas.

    :attr:`soft_shadow_pos` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0]`.

    .. note::
        This property is automatically processed.
    """

    soft_shadow_cl = ListProperty([0, 0, 0, 0.50])
    """
    Color of the soft Shadow

    :attr:`soft_shadow_cl` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0, 0, 0.15]`.
    """

    # hard shadow
    hard_shadow_texture = ObjectProperty()
    """
    Texture of the Hard Shadow texture for the canvas.

    :attr:`hard_shadow_texture` is an :class:`~kivy.core.image.Image`
    and defaults to `None`.

    .. note::
        This property is automatically processed when elevation is changed.
    """

    hard_shadow_size = ListProperty([0, 0])
    """
    Size of the Hard Shadow texture over the canvas.

    :attr:`hard_shadow_size` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0]`.

    .. note::
        This property is automatically processed when elevation is changed.
    """

    hard_shadow_pos = ListProperty([0, 0])
    """
    Position of the Hard Shadow texture over the canvas.

    :attr:`hard_shadow_pos` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0]`.

    .. note::
        This property is automatically processed when elevation is changed.
    """

    hard_shadow_cl = ListProperty([0, 0, 0, 0.15])
    """
    Color of the Hard Shadow.

    .. note::
        :attr:`hard_shadow_cl` is an :class:`~kivy.properties.ListProperty`
        and defaults to `[0, 0, 0, 0.15]`.
    """

    # Shared property for some calculations.
    hard_shadow_offset = BoundedNumericProperty(2,
                                                min=0,
                                                errorhandler=lambda x: 0
                                                if x < 0 else x)
    """
    This value sets a special offset to the shadow canvas, this offset allows a
    correct draw of the canvas size. allowing the effect to correctly blur the
    image in the given space.

    :attr:`hard_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to `2`.
    """

    soft_shadow_offset = BoundedNumericProperty(4,
                                                min=0,
                                                errorhandler=lambda x: 0
                                                if x < 0 else x)
    """
    This value sets a special offset to the shadow canvas, this offset allows a
    correct draw of the canvas size. allowing the effect to correctly blur the
    image in the given space.

    :attr:`soft_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to `4`.
    """

    draw_shadow = ObjectProperty(None)
    """
    This property controls the draw call of the context.

    This property is automatically set to `__draw_shadow__` inside the
    `super().__init__ call.` unless the property is different of None.

    To set a different drawing instruction function, set this property before the
    `super(),__init__` call inside the `__init__` definition of the new class.

    You can use the source for this clases as example of how to draw over
    with the context:

        #. "RectangularElevationBehavior"
        #. "CircularElevationBehavior"
        #. "RoundedRectangularElevationBehavior"

    :attr:`draw_shadow` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to None.

    .. note:: If this property is left to None the CommonElevationBehavior will
        set to a function that will raise a `NotImplementedError` inside
        `super().__init__`.

    Follow the next example to set a new draw instruction for the class
    inside `__init__`:

    .. code-block:: python

        class RoundedRectangularElevationBehavior(CommonElevationBehavior):
            '''
            Shadow class for the RoundedRectangular shadow behavior.
            Controls the size and position of the shadow.'''

            def __init__(self, **kwargs):
                self._draw_shadow = WeakMethod(self.__draw_shadow__)
                super().__init__(**kwargs)

            def __draw_shadow__(self, origin, end, context=None):
                context.draw(...)

    Context is a Pillow `ImageDraw` class. For more information check the
    [Pillow official documentation](https://github.com/python-pillow/Pillow/).
    """
    def __init__(self, **kwargs):
        if self.draw_shadow is None:
            self.draw_shadow = WeakMethod(self.__draw_shadow__)
        self.prev_shadow_group = None
        im = BytesIO()
        Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im, format="png")
        im.seek(0)
        self._soft_shadow_texture = self.hard_shadow_texture = CoreImage(
            im, ext="png").texture
        Clock.schedule_once(self.shadow_preset, -1)
        self.on_shadow_group(self, self.shadow_group)

        self.bind(
            pos=self._update_shadow,
            size=self._update_shadow,
            radius=self._update_shadow,
        )
        super().__init__(**kwargs)

    def on_shadow_group(self, instance, value):
        """
        This function controls the shadow group of the widget.
        Do not use Directly to change the group. instead, use the shadow_group
        :attr:`property`.
        """

        groups = CommonElevationBehavior.__shadow_groups
        if self.prev_shadow_group:
            group = groups[self.prev_shadow_group]
            for widget in group[:]:
                if widget() is self:
                    group.remove(widget)
        group = self.prev_shadow_group = self.shadow_group
        if group not in groups:
            groups[group] = []
        r = ref(self, CommonElevationBehavior._clear_shadow_groups)
        groups[group].append(r)

    @staticmethod
    def _clear_shadow_groups(wk):
        # auto flush the element when the weak reference have been deleted
        groups = CommonElevationBehavior.__shadow_groups
        for group in list(groups.values()):
            if not group:
                break
            if wk in group:
                group.remove(wk)
                break

    def force_shadow_pos(self, shadow_pos):
        """
        This property forces the shadow position in every widget inside the
        widget. The argument :attr:`shadow_pos` is expected as a <class 'list'>
        or <class 'tuple'>.
        """

        if self.shadow_group is None:
            return
        group = CommonElevationBehavior.__shadow_groups[self.shadow_group]
        for wk in group[:]:
            widget = wk()
            if widget is None:
                group.remove(wk)
            widget.shadow_pos = shadow_pos
        del group

    def update_group_property(self, property_name, value):
        """
        This functions allows to change properties of every widget inside the
        shadow group.
        """

        if self.shadow_group is None:
            return
        group = CommonElevationBehavior.__shadow_groups[self.shadow_group]
        for wk in group[:]:
            widget = wk()
            if widget is None:
                group.remove(wk)
            setattr(widget, property_name, value)
        del group

    def shadow_preset(self, *dt):
        """
        This function is meant to set the default configuration of the
        elevation.

        After a new instance is created, the elevation property will be launched
        and thus this function will update the elevation if the KV lang have not
        done it already.

        Works similar to an `__after_init__` call inside a widget.
        """

        if self.elevation is None:
            self.elevation = 10
        self._update_shadow(self, self.elevation)
        self.bind(
            pos=self._update_shadow,
            size=self._update_shadow,
            _elevation=self._update_shadow,
        )

    def on_elevation(self, instance, value):
        """
        Elevation event that sets the current elevation value to _elevation
        """

        if value is not None:
            self._elevation = value

    def _set_soft_shadow_a(self, value):
        value = 0 if value < 0 else (1 if value > 1 else value)
        self.soft_shadow_cl[-1] = value
        return True

    def _set_hard_shadow_a(self, value):
        value = 0 if value < 0 else (1 if value > 1 else value)
        self.hard_shadow_cl[-1] = value
        return True

    def _get_soft_shadow_a(self):
        return self.soft_shadow_cl[-1]

    def _get_hard_shadow_a(self):
        return self.hard_shadow_cl[-1]

    _soft_shadow_a = AliasProperty(_get_soft_shadow_a,
                                   _set_soft_shadow_a,
                                   bind=["soft_shadow_cl"])
    _hard_shadow_a = AliasProperty(_get_hard_shadow_a,
                                   _set_hard_shadow_a,
                                   bind=["hard_shadow_cl"])

    def on_disabled(self, instance, value):
        """
        This function hides the shadow when the widget is disabled.
        it sets the shadow to 0.
        """

        if self.disabled is True:
            self._elevation = 0
        else:
            self._elevation = 0 if self.elevation is None else self.elevation
        self._update_shadow(self, self._elevation)
        try:
            super().on_disabled(instance, value)
        except Exception:
            pass

    def _update_elevation(self, instance, value):
        self._elevation = value
        self._update_shadow(instance, value)

    def _update_shadow_pos(self, instance, value):
        self.hard_shadow_pos = [
            self.x - dp(self.hard_shadow_offset),  # + self.shadow_pos[0],
            self.y - dp(self.hard_shadow_offset),  # + self.shadow_pos[1],
        ]
        if self.shadow_pos == [0, 0]:
            self.soft_shadow_pos = [
                self.x + self._shadow_pos[0] - self._elevation -
                dp(self.soft_shadow_offset),
                self.y + self._shadow_pos[1] - self._elevation -
                dp(self.soft_shadow_offset),
            ]
        else:
            self.soft_shadow_pos = [
                self.x + self.shadow_pos[0] - self._elevation -
                dp(self.soft_shadow_offset),
                self.y + self.shadow_pos[1] - self._elevation -
                dp(self.soft_shadow_offset),
            ]
        self._shadow_origin = [
            self.soft_shadow_pos[0] + self.soft_shadow_size[0] / 2,
            self.soft_shadow_pos[1] + self.soft_shadow_size[1] / 2,
        ]

    def on__shadow_pos(self, ins, val):
        """
        Updates the shadow with the computed value.

        Call this function every time you need to force a shadow update.
        """

        self._update_shadow_pos(ins, val)

    def on_shadow_pos(self, ins, val):
        """
        Updates the shadow with the fixed value.

        Call this function every time you need to force a shadow update.
        """

        self._update_shadow_pos(ins, val)

    def _update_shadow(self, instance, value):
        self._update_shadow_pos(instance, value)
        if self._elevation > 0:
            # dynamic elecation position for the shadow
            if self.shadow_pos == [0, 0]:
                self._shadow_pos = [0, -self._elevation * 0.4]
            # HARD Shadow
            offset = int(dp(self.hard_shadow_offset))
            size = [
                int(self.size[0] + (offset * 2)),
                int(self.size[1] + (offset * 2)),
            ]
            im = BytesIO()
            # Context
            img = Image.new("RGBA", tuple(size), color=(0, 0, 0, 0))
            # Draw context
            shadow = ImageDraw.Draw(img)
            self.draw_shadow()([offset, offset], [
                int(size[0] - 1 - offset),
                int(size[1] - 1 - offset),
            ],
                               context=shadow
                               # context=ref(shadow)
                               )
            img = img.filter(
                ImageFilter.GaussianBlur(
                    radius=int(dp(1 + self.hard_shadow_offset / 3))))
            img.save(im, format="png")
            im.seek(0)
            self.hard_shadow_size = size
            self.hard_shadow_texture = CoreImage(im, ext="png").texture

            # soft shadow
            offset = dp(self.soft_shadow_offset)
            size = [
                int(self.size[0] + dp(self._elevation * 2) + (offset * 2)),
                int(self.size[1] + dp(self._elevation * 2) + (offset * 2)),
                # ((self._elevation)*2) + x + (offset*2)) for x in self.size
            ]
            im = BytesIO()
            img = Image.new("RGBA", tuple(size), color=((0, ) * 4))
            shadow = ImageDraw.Draw(img)
            _offset = int(dp(self._elevation + offset))
            self.draw_shadow()([
                _offset,
                _offset,
            ], [int(size[0] - _offset - 1),
                int(size[1] - _offset - 1)],
                               context=shadow
                               # context=ref(shadow)
                               )
            img = img.filter(
                ImageFilter.GaussianBlur(radius=self._elevation // 2))
            shadow = ImageDraw.Draw(img)

            img.save(im, format="png")
            im.seek(0)
            self.soft_shadow_size = size
            self._soft_shadow_texture = CoreImage(im, ext="png").texture
        else:
            im = BytesIO()
            Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im,
                                                               format="png")
            im.seek(0)
            #
            self._soft_shadow_texture = self.hard_shadow_texture = CoreImage(
                im, ext="png").texture
            return

    def __draw_shadow__(self, origin, end, context=None):
        raise NotImplementedError(
            "KivyMD:\n"
            "If you see this error, this means that either youre using "
            "CommonElevationBehavior directly or your 'shader' dont have a "
            "_draw_shadow instruction, remember to overwrite this function to "
            "draw over the image context. the figure you would like.")
Esempio n. 3
0
class Agent(Widget):

    angle = NumericProperty(0)
    rotation = NumericProperty(0)
    velocity_x = NumericProperty(0)
    velocity_y = NumericProperty(0)
    velocity = ReferenceListProperty(velocity_x, velocity_y)
    sensor1_x = NumericProperty(0)
    sensor1_y = NumericProperty(0)
    sensor1 = ReferenceListProperty(sensor1_x, sensor1_y)
    sensor2_x = NumericProperty(0)
    sensor2_y = NumericProperty(0)
    sensor2 = ReferenceListProperty(sensor2_x, sensor2_y)
    sensor3_x = NumericProperty(0)
    sensor3_y = NumericProperty(0)
    sensor3 = ReferenceListProperty(sensor3_x, sensor3_y)
    signal1 = NumericProperty(0)
    signal2 = NumericProperty(0)
    signal3 = NumericProperty(0)

    def move(self, rotation):
        self.pos = Vector(*self.velocity) + self.pos
        self.rotation = rotation
        self.angle = self.angle + self.rotation
        self.sensor1 = Vector(30, 0).rotate(self.angle) + self.pos
        self.sensor2 = Vector(30, 0).rotate((self.angle + 30) % 360) + self.pos
        self.sensor3 = Vector(30, 0).rotate((self.angle - 30) % 360) + self.pos

        self.signal1 = 1 if (
            int(
                np.sum(white_space[
                    int(self.sensor1_x) - 10:int(self.sensor1_x) + 10,
                    int(self.sensor1_y) - 10:int(self.sensor1_y) + 10])) /
            400 > 0.5) else int(
                np.sum(white_space[int(self.sensor1_x) -
                                   10:int(self.sensor1_x) + 10,
                                   int(self.sensor1_y) -
                                   10:int(self.sensor1_y) + 10])) / 400.
        self.signal2 = 1 if (
            int(
                np.sum(white_space[
                    int(self.sensor2_x) - 10:int(self.sensor2_x) + 10,
                    int(self.sensor2_y) - 10:int(self.sensor2_y) + 10])) /
            400 > 0.5) else int(
                np.sum(white_space[int(self.sensor2_x) -
                                   10:int(self.sensor2_x) + 10,
                                   int(self.sensor2_y) -
                                   10:int(self.sensor2_y) + 10])) / 400.
        self.signal3 = 1 if (
            int(
                np.sum(white_space[
                    int(self.sensor3_x) - 10:int(self.sensor3_x) + 10,
                    int(self.sensor3_y) - 10:int(self.sensor3_y) + 10])) /
            400 > 0.5) else int(
                np.sum(white_space[int(self.sensor3_x) -
                                   10:int(self.sensor3_x) + 10,
                                   int(self.sensor3_y) -
                                   10:int(self.sensor3_y) + 10])) / 400.

        if self.sensor1_x > maze_width - 10 or self.sensor1_x < 10 or self.sensor1_y > maze_height - 10 or self.sensor1_y < 10:
            self.signal1 = 1.
        if self.sensor2_x > maze_width - 10 or self.sensor2_x < 10 or self.sensor2_y > maze_height - 10 or self.sensor2_y < 10:
            self.signal2 = 1.
        if self.sensor3_x > maze_width - 10 or self.sensor3_x < 10 or self.sensor3_y > maze_height - 10 or self.sensor3_y < 10:
            self.signal3 = 1.
Esempio n. 4
0
class CropScatter(ScatterLayout):
    image_width = NumericProperty(1)
    image_height = NumericProperty(1)
    image_size = ReferenceListProperty(image_width, image_height)
    image_x = NumericProperty(0)
    image_y = NumericProperty(0)
    image_pos = ReferenceListProperty(image_x, image_y)

    crop_width = NumericProperty(0)
    crop_height = NumericProperty(0)
    crop_size = ReferenceListProperty(crop_width, crop_height)
    crop_x = NumericProperty(0)
    crop_y = NumericProperty(0)
    crop_pos = ReferenceListProperty(crop_x, crop_y)
    frame_width = NumericProperty(0)
    frame_height = NumericProperty(0)
    frame_size = ReferenceListProperty(frame_width, frame_height)

    texture = ObjectProperty(None)
    source = StringProperty(None)

    zoom_delta = NumericProperty(0.05)

    def on_touch_down(self, touch):
        result = super().on_touch_down(touch)
        if result:
            if touch.button == 'scrollup':
                self.apply_image_transform(scale=self.clamp_scale(1 - self.zoom_delta), anchor=(touch.x, touch.y))
            elif touch.button == 'scrolldown':
                self.apply_image_transform(scale=self.clamp_scale(1 + self.zoom_delta), anchor=(touch.x, touch.y))
        return result

    def clamp_scale(self, scale):
        new_scale = scale * self.scale
        if new_scale < self.scale_min:
            scale = self.scale_min / self.scale
        elif new_scale > self.scale_max:
            scale = self.scale_max / self.scale
        return scale

    def transform_with_touch(self, touch):
        # just do a simple one finger drag
        changed = False
        if len(self._touches) == self.translation_touches:
            # _last_touch_pos has last pos in correct parent space,
            # just like incoming touch
            dx = (touch.x - self._last_touch_pos[touch][0]) * self.do_translation_x
            dy = (touch.y - self._last_touch_pos[touch][1]) * self.do_translation_y
            dx = dx / self.translation_touches
            dy = dy / self.translation_touches
            self.apply_image_transform(x=dx, y=dy)
            changed = True

        if len(self._touches) == 1:
            # If we only have one touch, we want to scale on a scroll
            return changed

        # we have more than one touch... list of last known pos
        points = [Vector(self._last_touch_pos[t]) for t in self._touches if t is not touch]
        # add current touch last
        points.append(Vector(touch.pos))

        # we only want to transform if the touch is part of the two touches
        # farthest apart! So first we find anchor, the point to transform
        # around as another touch farthest away from current touch's pos
        anchor = max(points[:-1], key=lambda p: p.distance(touch.pos))

        # now we find the touch farthest away from anchor, if its not the
        # same as touch. Touch is not one of the two touches used to transform
        farthest = max(points, key=anchor.distance)
        if farthest is not points[-1]:
            return changed

        # ok, so we have touch, and anchor, so we can actually compute the
        # transformation
        old_line = Vector(*touch.ppos) - anchor
        new_line = Vector(*touch.pos) - anchor
        if not old_line.length():   # div by zero
            return changed

        angle = radians(new_line.angle(old_line)) * self.do_rotation
        if angle:
            changed = True

        if self.do_scale:
            scale = new_line.length() / old_line.length()
            self.apply_image_transform(scale=self.clamp_scale(scale), anchor=anchor)
            changed = True
        return changed

    def apply_image_transform(self, x=0, y=0, scale=1, post_multiply=False, anchor=(0, 0)):
        # Test if the x move is valid
        t = Matrix()
        t = t.translate(x, 0, 0)

        if not self.test_valid(t.multiply(self.transform)):
            x = 0

        # Test if the y move is valid
        t = Matrix()
        t = t.translate(0, y, 0)

        if not self.test_valid(t.multiply(self.transform)):
            y = 0

        # Test if the scale is valid
        t = Matrix().translate(anchor[0], anchor[1], 0)
        t = t.scale(scale, scale, scale)
        t = t.multiply(Matrix().translate(-anchor[0], -anchor[1], 0))

        if not self.test_valid(t.multiply(self.transform)):
            scale = 1

        # Compile final matrix
        t = Matrix().translate(anchor[0], anchor[1], 0)
        t = t.translate(x, y, 0)
        t = t.scale(scale, scale, scale)
        t = t.multiply(Matrix().translate(-anchor[0], -anchor[1], 0))

        if post_multiply:
            self.transform = self.transform.multiply(t)
        else:
            self.transform = t.multiply(self.transform)

    def test_valid(self, matrix):
        crop_x, crop_y = (self.width - self.crop_width) / 2, (self.height - self.crop_height) / 2
        x, y, z = matrix.transform_point(self.image_x, self.image_y, 0)
        mx, my, mz = matrix.transform_point(self.image_x + self.image_width, self.image_y + self.image_height, 0)

        if x > crop_x:
            return False
        if y > crop_y:
            return False
        if mx < crop_x + self.crop_width:
            return False
        if my < crop_y + self.crop_height:
            return False
        return True

    def on_texture(self, instance, value):
        self.calculate()
        self.reset()

    def on_size(self, *args):
        self.calculate()

    def reset(self):
        self.matrix = Matrix()

    def calculate(self):
        if 'image' not in self.ids or self.ids.image.texture is None:
            return
        width, height = self.ids.image.texture.size
        frame_width, frame_height = self.width, self.height
        if width > height:
            self.image_width = frame_width
            self.image_height = height / width * frame_height
            self.crop_size = self.image_height, self.image_height
            self.frame_size = frame_width, frame_width
            self.image_y = (self.height - self.image_height) / 2
        else:
            self.image_width = width / height * frame_width
            self.image_height = frame_height
            self.frame_size = frame_height, frame_height
            self.crop_size = self.image_width, self.image_width
            self.image_x = (self.width - self.image_width) / 2

    def get_image_crop(self):
        x, y, z = self.transform.transform_point(self.image_x, self.image_y, 0)
        mx, my, mz = self.transform.transform_point(self.image_x + self.image_width, self.image_y + self.image_height, 0)
        ccenx, cceny = (self.width - self.crop_width) / 2 + self.crop_width / 2, (self.height - self.crop_height) / 2 + self.crop_height / 2

        cx, cy = (self.width - self.crop_width) / 2, (self.height - self.crop_height) / 2
        cmx, cmy = cx + self.crop_width, cy + self.crop_height

        cw = int(round(self.texture.width * ((cmx - cx) / (mx - x))))
        ch = int(round(self.texture.height * ((cmy - cy) / (my - y))))
        cenx, ceny = int(round((ccenx - x) * (self.texture.width / (mx - x)))), int(round((cceny - y) * (self.texture.height / (my - y))))

        return cw, ch, cenx, ceny
Esempio n. 5
0
class CircularNumberPicker(CircularLayout):
    """A circular number picker based on CircularLayout. A selector will
    help you pick a number. You can also set :attr:`multiples_of` to make
    it show only some numbers and use the space in between for the other
    numbers.
    """

    min = NumericProperty(0)
    """The first value of the range.

    :attr:`min` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.
    """

    max = NumericProperty(0)
    """The last value of the range. Note that it behaves like xrange, so
    the actual last displayed value will be :attr:`max` - 1.

    :attr:`max` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.
    """

    range = ReferenceListProperty(min, max)
    """Packs :attr:`min` and :attr:`max` into a list for convenience. See
    their documentation for further information.

    :attr:`range` is a :class:`~kivy.properties.ReferenceListProperty`.
    """

    multiples_of = NumericProperty(1)
    """Only show numbers that are multiples of this number. The other numbers
    will be selectable, but won't have their own label.

    :attr:`multiples_of` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1.
    """

    # selector_color = ListProperty([.337, .439, .490])
    selector_color = ListProperty([1, 1, 1])
    """Color of the number selector. RGB.

    :attr:`selector_color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [.337, .439, .490] (material green).
    """

    color = ListProperty([0, 0, 0])
    """Color of the number labels and of the center dot. RGB.

    :attr:`color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [1, 1, 1] (white).
    """

    selector_alpha = BoundedNumericProperty(0.3, min=0, max=1)
    """Alpha value for the transparent parts of the selector.

    :attr:`selector_alpha`
    is a :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to 0.3 (min=0, max=1).
    """

    selected = NumericProperty(None)
    """Currently selected number.

    :attr:`selected` is a :class:`~kivy.properties.NumericProperty` and
    defaults to :attr:`min`.
    """

    number_size_factor = NumericProperty(0.5)
    """Font size scale factor for the :class:`~Number`.

    :attr:`number_size_factor`
    is a :class:`~kivy.properties.NumericProperty` and defaults to 0.5.
    """

    number_format_string = StringProperty("{}")
    """String that will be formatted with the selected number as the
    first argument.
    Can be anything supported by :meth:`str.format` (es. "{:02d}").

    :attr:`number_format_string`
    is a :class:`~kivy.properties.StringProperty` and defaults to "{}".
    """

    scale = NumericProperty(1)
    """Canvas scale factor. Used in :class:`CircularTimePicker` transitions.

    :attr:`scale` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1.
    """

    _selection_circle = ObjectProperty(None)
    _selection_line = ObjectProperty(None)
    _selection_dot = ObjectProperty(None)
    _selection_dot_color = ObjectProperty(None)
    _selection_color = ObjectProperty(None)
    _center_dot = ObjectProperty(None)
    _center_color = ObjectProperty(None)

    def _get_items(self):
        return self.max - self.min

    items = AliasProperty(_get_items, None)

    def _get_shown_items(self):
        sh = 0
        for i in xrange(*self.range):
            if i % self.multiples_of == 0:
                sh += 1
        return sh

    shown_items = AliasProperty(_get_shown_items, None)

    def __init__(self, **kw):
        self._trigger_genitems = Clock.create_trigger(self._genitems, -1)
        self.bind(
            min=self._trigger_genitems,
            max=self._trigger_genitems,
            multiples_of=self._trigger_genitems,
        )
        super().__init__(**kw)
        self.selected = self.min
        self.bind(
            selected=self.on_selected,
            pos=self.on_selected,
            size=self.on_selected,
        )

        cx = self.center_x + self.padding[0] - self.padding[2]
        cy = self.center_y + self.padding[3] - self.padding[1]
        sx, sy = self.pos_for_number(self.selected)
        epos = [
            i - (self.delta_radii * self.number_size_factor) for i in (sx, sy)
        ]
        esize = [self.delta_radii * self.number_size_factor * 2] * 2
        dsize = [i * 0.3 for i in esize]
        dpos = [i + esize[0] / 2.0 - dsize[0] / 2.0 for i in epos]
        csize = [i * 0.05 for i in esize]
        cpos = [i - csize[0] / 2.0 for i in (cx, cy)]
        dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1
        color = list(self.selector_color)

        with self.canvas:
            self._selection_color = Color(*(color + [self.selector_alpha]))
            self._selection_circle = Ellipse(pos=epos, size=esize)
            self._selection_line = Line(points=[cx, cy, sx, sy],
                                        width=dp(1.25))
            self._selection_dot_color = Color(*(color + [dot_alpha]))
            self._selection_dot = Ellipse(pos=dpos, size=dsize)
            self._center_color = Color(*self.color)
            self._center_dot = Ellipse(pos=cpos, size=csize)

        self.bind(selector_color=lambda ign, u: setattr(
            self._selection_color, "rgba", u + [self.selector_alpha]))
        self.bind(selector_color=lambda ign, u: setattr(
            self._selection_dot_color, "rgb", u))
        self.bind(selector_color=lambda ign, u: self.dot_is_none())
        self.bind(color=lambda ign, u: setattr(self._center_color, "rgb", u))
        Clock.schedule_once(self._genitems)
        # Just to make sure pos/size are set
        Clock.schedule_once(self.on_selected)

    def dot_is_none(self, *args):
        dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1
        if self._selection_dot_color:
            self._selection_dot_color.a = dot_alpha

    def _genitems(self, *a):
        self.clear_widgets()
        for i in xrange(*self.range):
            if i % self.multiples_of != 0:
                continue
            n = Number(
                text=self.number_format_string.format(i),
                size_factor=self.number_size_factor,
                color=self.color,
            )
            self.bind(color=n.setter("color"))
            self.add_widget(n)

    def on_touch_down(self, touch):
        if not self.collide_point(*touch.pos):
            return
        touch.grab(self)
        self.selected = self.number_at_pos(*touch.pos)
        if self.selected == 60:
            self.selected = 0

    def on_touch_move(self, touch):
        if touch.grab_current is not self:
            return super().on_touch_move(touch)
        self.selected = self.number_at_pos(*touch.pos)
        if self.selected == 60:
            self.selected = 0

    def on_touch_up(self, touch):
        if touch.grab_current is not self:
            return super().on_touch_up(touch)
        touch.ungrab(self)

    def on_selected(self, *a):
        cx = self.center_x + self.padding[0] - self.padding[2]
        cy = self.center_y + self.padding[3] - self.padding[1]
        sx, sy = self.pos_for_number(self.selected)
        epos = [
            i - (self.delta_radii * self.number_size_factor) for i in (sx, sy)
        ]
        esize = [self.delta_radii * self.number_size_factor * 2] * 2
        dsize = [i * 0.3 for i in esize]
        dpos = [i + esize[0] / 2.0 - dsize[0] / 2.0 for i in epos]
        csize = [i * 0.05 for i in esize]
        cpos = [i - csize[0] / 2.0 for i in (cx, cy)]
        dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1

        if self._selection_circle:
            self._selection_circle.pos = epos
            self._selection_circle.size = esize
        if self._selection_line:
            self._selection_line.points = [cx, cy, sx, sy]
        if self._selection_dot:
            self._selection_dot.pos = dpos
            self._selection_dot.size = dsize
        if self._selection_dot_color:
            self._selection_dot_color.a = dot_alpha
        if self._center_dot:
            self._center_dot.pos = cpos
            self._center_dot.size = csize

    def pos_for_number(self, n):
        """Returns the center x, y coordinates for a given number.
        """

        if self.items == 0:
            return 0, 0
        radius = (min(
            self.width - self.padding[0] - self.padding[2],
            self.height - self.padding[1] - self.padding[3],
        ) / 2.0)
        middle_r = radius * sum(self.radius_hint) / 2.0
        cx = self.center_x + self.padding[0] - self.padding[2]
        cy = self.center_y + self.padding[3] - self.padding[1]
        sign = +1.0
        angle_offset = radians(self.start_angle)
        if self.direction == "cw":
            angle_offset = 2 * pi - angle_offset
            sign = -1.0
        quota = 2 * pi / self.items
        mult_quota = 2 * pi / self.shown_items
        angle = angle_offset + n * sign * quota

        if self.items == self.shown_items:
            angle += quota / 2
        else:
            angle -= mult_quota / 2

        # kived: looking it up, yes. x = cos(angle) * radius + centerx;
        # y = sin(angle) * radius + centery
        x = cos(angle) * middle_r + cx
        y = sin(angle) * middle_r + cy

        return x, y

    def number_at_pos(self, x, y):
        """Returns the number at a given x, y position. The number is found
        using the widget's center as a starting point for angle calculations.

        Not thoroughly tested, may yield wrong results.
        """
        if self.items == 0:
            return self.min
        cx = self.center_x + self.padding[0] - self.padding[2]
        cy = self.center_y + self.padding[3] - self.padding[1]
        lx = x - cx
        ly = y - cy
        quota = 2 * pi / self.items
        mult_quota = 2 * pi / self.shown_items
        if lx == 0 and ly > 0:
            angle = pi / 2
        elif lx == 0 and ly < 0:
            angle = 3 * pi / 2
        else:
            angle = atan(ly / lx)
            if lx < 0 < ly:
                angle += pi
            if lx > 0 > ly:
                angle += 2 * pi
            if lx < 0 and ly < 0:
                angle += pi
        angle += radians(self.start_angle)
        if self.direction == "cw":
            angle = 2 * pi - angle
        if mult_quota != quota:
            angle -= mult_quota / 2
        if angle < 0:
            angle += 2 * pi
        elif angle > 2 * pi:
            angle -= 2 * pi

        return int(angle / quota) + self.min
Esempio n. 6
0
class Car(Widget):
    angle = NumericProperty(0)
    rotation = NumericProperty(0)

    velocity_x = NumericProperty(0)
    velocity_y = NumericProperty(0)
    velocity = ReferenceListProperty(velocity_x, velocity_y)

    signal1 = NumericProperty(0)
    signal2 = NumericProperty(0)
    signal3 = NumericProperty(0)

    sand = None

    sand_width = 0
    sand_height = 0

    points = None

    red = None
    blue = None
    yellow = None

    def move(self, rotation):

        self.pos = Vector(*self.velocity) + self.pos
        self.rotation = rotation
        self.angle = self.angle + self.rotation

        self.red = Vector(25, 0).rotate(self.angle) + self.pos
        self.blue = Vector(25, 0).rotate((self.angle + 30) % 360) + self.pos
        self.yellow = Vector(25, 0).rotate((self.angle - 30) % 360) + self.pos
        if self.sensor_onborder(self.red):
            self.signal1 = 1.
        else:
            self.signal1 = self.sensor_sanddensity(self.red)

        if self.sensor_onborder(self.blue):
            self.signal2 = 1.
        else:
            self.signal2 = self.sensor_sanddensity(self.blue)

        if self.sensor_onborder(self.yellow):
            self.signal3 = 1.
        else:
            self.signal3 = self.sensor_sanddensity(self.yellow)

    def sensor_sanddensity(self, vector):
        int_sensorx = int(vector[0])
        int_sensory = int(vector[1])

        return int(np.sum(self.sand[int_sensorx:int_sensorx + 10, int_sensory:int_sensory + 10])) / 100.

    def get_vec_corners(self, vector):
        return {
            'bottom_left': vector,
            'top_left': Vector(vector[0], vector[1] + 10),
            'top_right': Vector(vector[0] + 10, vector[1] + 10),
            'bottom_right': Vector(vector[0] + 10, vector[1]),
        }

    def sensor_onborder(self, vector):
        corners = self.get_vec_corners(vector)
        margin = 2
        # left border
        for corner in corners.values():
            x_diff = margin - corner.x
            if x_diff > 0:
                return True

        # right border
        for corner in corners.values():
            x_diff = corner.x - (self.sand_width - margin)
            if x_diff > 0:
                return True

        # top border
        for corner in corners.values():
            y_diff = corner.y - (self.sand_height - margin)
            if y_diff > 0:
                return True

        # bottom border
        for corner in corners.values():
            y_diff = margin - corner.y
            if y_diff > 0:
                return True

        return False

    def get_carfront(self):
        return Vector(self.pos[0] + self.width, self.center[1])

    def get_corners(self):
        return {
            'bottom_left': Vector(*self.pos),
            'top_left': Vector(self.pos[0], self.pos[1] + self.height),
            'top_right': Vector(self.pos[0] + self.width, self.pos[1] + self.height),
            'bottom_right': Vector(self.pos[0] + self.width, self.pos[1]),
        }
Esempio n. 7
0
class Knob(Widget):
    """Class for creating a Knob widget."""

    min = NumericProperty(0)
    '''Minimum value for value :attr:`value`.
    :attr:`min` is a :class:`~kivy.properties.NumericProperty` and defaults
    to 0.
    '''

    max = NumericProperty(100)
    '''Maximum value for value :attr:`value`.
    :attr:`max` is a :class:`~kivy.properties.NumericProperty` and defaults
    to 100.
    '''

    range = ReferenceListProperty(min, max)
    ''' Range of the values for Knob.
    :attr:`range` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`min`, :attr:`max`).
    '''

    value = NumericProperty(0)
    '''Current value of the knob. Set :attr:`value` when creating a knob to
    set its initial position. An internal :attr:`_angle` is calculated to set
    the position of the knob.
    :attr:`value` is a :class:`~kivy.properties.NumericProperty` and defaults
    to 0.
    '''

    step = BoundedNumericProperty(1, min=0)
    '''Step interval of knob to go from min to max. An internal
    :attr:`_angle_step` is calculated to set knob step in degrees.
    :attr:`step` is a :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to 1 (min=0).
    '''

    curve = BoundedNumericProperty(1, min=1)
    '''This parameter determines the shape of the map function. It represent the
    reciprocal of a power function's exponent used to map the input value.
    So, for higher values of curve the contol is more reactive, and conversely.
    '''

    knobimg_source = StringProperty("")
    '''Path of texture image that visually represents the knob. Use PNG for
    transparency support. The texture is rendered on a centered rectangle of
    size = :attr:`size` * :attr:`knobimg_size`.
    :attr:`knobimg_source` is a :class:`~kivy.properties.StringProperty`
    and defaults to empty string.
    '''

    knobimg_color = ListProperty([1, 1, 1, 1])
    '''Color to apply to :attr:`knobimg_source` texture when loaded.
    :attr:`knobimg_color` is a :class:`~kivy.properties.ListProperty`
    and defaults to [1,1,1,1].
    '''

    knobimg_size = BoundedNumericProperty(0.9, max=1.0, min=0.1)
    ''' Internal proportional size of rectangle that holds
    :attr:`knobimg_source` texture.
    :attr:`knobimg_size` is a :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to 0.9.
    '''

    show_marker = BooleanProperty(True)
    ''' Shows/hides marker surrounding knob. use :attr:`knob_size` < 1.0 to
    leave space to marker.
    :attr:`show_marker` is a :class:`~kivy.properties.BooleanProperty`
    and defaults to True.
    '''

    marker_img = StringProperty("")
    '''Path of texture image that visually represents the knob marker. The
    marker is rendered in a centered Ellipse (Circle) with the same size of
    the widget and goes from angle_start=:attr:`marker_startangle` to
    angle_end=:attr:`_angle`.
    :attr:`marker_img` is a :class:`~kivy.properties.StringProperty` and
    defaults to "".
    '''

    marker_color = ListProperty([1, 1, 1, 1])
    '''Color to apply to :attr:`marker_img` texture when loaded.
    :attr:`marker_color` is a :class:`~kivy.properties.ListProperty`
    and defaults to [1,1,1,1].
    '''

    knobimg_bgcolor = ListProperty([0, 0, 0, 1])
    ''' Background color behind :attr:`knobimg_source` texture.
    :attr:`value` is a :class:`~kivy.properties.ListProperty` and defaults
    to [0,0,0,1].
    '''

    markeroff_img = StringProperty("")
    '''Path of texture image that visually represents the knob marker where
    it's off, that is, parts of the marker that haven't been reached yet by
    the knob (:attr:`value`).
    :attr:`markeroff_img` is a :class:`~kivy.properties.StringProperty`
    and defaults to "".
    '''

    markeroff_color = ListProperty([0, 0, 0, 0])
    '''Color applied to :attr:`markeroff_img` int the Canvas.
    :attr:`markeroff_color` is a :class:`~kivy.properties.ListProperty`
    and defaults to [0,0,0,0].
    '''

    marker_startangle = NumericProperty(0)
    '''Starting angle of Ellipse where :attr:`marker_img` is rendered.
    :attr:`value` is a :class:`~kivy.properties.NumericProperty` and defaults
    to 0.
    '''

    marker_ahead = NumericProperty(0)
    ''' Adds degrees to angle_end of marker (except when :attr:`value` == 0).
    :attr:`marker_ahead` is a :class:`~kivy.properties.NumericProperty`
    and defaults to 0.
    '''

    _angle          = NumericProperty(0)            # Internal angle calculated from value.
    _angle_step     = NumericProperty(0)            # Internal angle_step calculated from step.

    def __init__(self, *args, **kwargs):
        super(Knob, self).__init__(*args, **kwargs)
        self.bind(show_marker   =   self._show_marker)
        self.bind(value         =   self._value)

    def _value(self, instance, value):
        self._angle     =   pow( (value - self.min)/(self.max - self.min), 1./self.curve) * 360.
        self.on_knob(value)

    def _show_marker(self, instance, flag):
        # "show/hide" marker.
        if flag:
            self.knobimg_bgcolor[3] = 1
            self.marker_color[3] = 1
            self.markeroff_color[3] = 1
        else:
            self.knobimg_bgcolor[3] = 0
            self.marker_color[3] = 0
            self.markeroff_color[3] = 0


    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            self.update_angle(touch)


    def on_touch_move(self, touch):
        if self.collide_point(*touch.pos):
            self.update_angle(touch)


    def update_angle(self, touch):
        posx, posy          =   touch.pos
        cx, cy              =   self.center
        rx, ry              =   posx - cx, posy - cy

        if ry >= 0:                                 # Quadrants are clockwise.
            quadrant = 1 if rx >= 0 else 4
        else:
            quadrant = 3 if rx <= 0 else 2

        try:
            angle    = math.atan(rx / ry) * (180./math.pi)
            if quadrant == 2 or quadrant == 3:
                angle = 180 + angle
            elif quadrant == 4:
                angle = 360 + angle

        except:                                   # atan not def for angle 90 and 270
            angle = 90 if quadrant <= 2 else 270

        self._angle_step    =   (self.step*360)/(self.max - self.min)
        self._angle         =   self._angle_step
        while self._angle < angle:
            self._angle     =   self._angle + self._angle_step

        relativeValue   =   pow((angle/360.), 1./self.curve)
        self.value      =   (relativeValue * (self.max - self.min)) + self.min


    #TO OVERRIDE
    def on_knob(self, value):
        pass #Knob values listenerr
Esempio n. 8
0
class ScrollView(StencilView):
    '''ScrollView class. See module documentation for more information.

    :Events:
        `on_scroll_start`
            Generic event fired when scrolling starts from touch.
        `on_scroll_move`
            Generic event fired when scrolling move from touch.
        `on_scroll_stop`
            Generic event fired when scrolling stops from touch.

    .. versionchanged:: 1.9.0
        `on_scroll_start`, `on_scroll_move` and `on_scroll_stop` events are
        now dispatched when scrolling to handle nested ScrollViews.

    .. versionchanged:: 1.7.0
        `auto_scroll`, `scroll_friction`, `scroll_moves`, `scroll_stoptime' has
        been deprecated, use :attr:`effect_cls` instead.
    '''

    scroll_distance = NumericProperty(_scroll_distance)
    '''Distance to move before scrolling the :class:`ScrollView`, in pixels. As
    soon as the distance has been traveled, the :class:`ScrollView` will start
    to scroll, and no touch event will go to children.
    It is advisable that you base this value on the dpi of your target device's
    screen.

    :attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 20 (pixels), according to the default value in user
    configuration.
    '''

    scroll_wheel_distance = NumericProperty('20sp')
    '''Distance to move when scrolling with a mouse wheel.
    It is advisable that you base this value on the dpi of your target device's
    screen.

    .. versionadded:: 1.8.0

    :attr:`scroll_wheel_distance` is a
    :class:`~kivy.properties.NumericProperty` , defaults to 20 pixels.
    '''

    scroll_timeout = NumericProperty(_scroll_timeout)
    '''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds.
    If the user has not moved :attr:`scroll_distance` within the timeout,
    the scrolling will be disabled, and the touch event will go to the
    children.

    :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 55 (milliseconds) according to the default value in user
    configuration.

    .. versionchanged:: 1.5.0
        Default value changed from 250 to 55.
    '''

    scroll_x = NumericProperty(0.)
    '''X scrolling value, between 0 and 1. If 0, the content's left side will
    touch the left side of the ScrollView. If 1, the content's right side will
    touch the right side.

    This property is controled by :class:`ScrollView` only if
    :attr:`do_scroll_x` is True.

    :attr:`scroll_x` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.
    '''

    scroll_y = NumericProperty(1.)
    '''Y scrolling value, between 0 and 1. If 0, the content's bottom side will
    touch the bottom side of the ScrollView. If 1, the content's top side will
    touch the top side.

    This property is controled by :class:`ScrollView` only if
    :attr:`do_scroll_y` is True.

    :attr:`scroll_y` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1.
    '''

    do_scroll_x = BooleanProperty(True)
    '''Allow scroll on X axis.

    :attr:`do_scroll_x` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to True.
    '''

    do_scroll_y = BooleanProperty(True)
    '''Allow scroll on Y axis.

    :attr:`do_scroll_y` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to True.
    '''
    def _get_do_scroll(self):
        return (self.do_scroll_x, self.do_scroll_y)

    def _set_do_scroll(self, value):
        if type(value) in (list, tuple):
            self.do_scroll_x, self.do_scroll_y = value
        else:
            self.do_scroll_x = self.do_scroll_y = bool(value)

    do_scroll = AliasProperty(_get_do_scroll,
                              _set_do_scroll,
                              bind=('do_scroll_x', 'do_scroll_y'))
    '''Allow scroll on X or Y axis.

    :attr:`do_scroll` is a :class:`~kivy.properties.AliasProperty` of
    (:attr:`do_scroll_x` + :attr:`do_scroll_y`)
    '''

    def _get_vbar(self):
        # must return (y, height) in %
        # calculate the viewport size / scrollview size %
        if self._viewport is None:
            return 0, 1.
        vh = self._viewport.height
        h = self.height
        if vh < h or vh == 0:
            return 0, 1.
        ph = max(0.01, h / float(vh))
        sy = min(1.0, max(0.0, self.scroll_y))
        py = (1. - ph) * sy
        return (py, ph)

    vbar = AliasProperty(_get_vbar,
                         None,
                         bind=('scroll_y', '_viewport', 'viewport_size'))
    '''Return a tuple of (position, size) of the vertical scrolling bar.

    .. versionadded:: 1.2.0

    The position and size are normalized between 0-1, and represent a
    percentage of the current scrollview height. This property is used
    internally for drawing the little vertical bar when you're scrolling.

    :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly.
    '''

    def _get_hbar(self):
        # must return (x, width) in %
        # calculate the viewport size / scrollview size %
        if self._viewport is None:
            return 0, 1.
        vw = self._viewport.width
        w = self.width
        if vw < w or vw == 0:
            return 0, 1.
        pw = max(0.01, w / float(vw))
        sx = min(1.0, max(0.0, self.scroll_x))
        px = (1. - pw) * sx
        return (px, pw)

    hbar = AliasProperty(_get_hbar,
                         None,
                         bind=('scroll_x', '_viewport', 'viewport_size'))
    '''Return a tuple of (position, size) of the horizontal scrolling bar.

    .. versionadded:: 1.2.0

    The position and size are normalized between 0-1, and represent a
    percentage of the current scrollview height. This property is used
    internally for drawing the little horizontal bar when you're scrolling.

    :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly.
    '''

    bar_color = ListProperty([.7, .7, .7, .9])
    '''Color of horizontal / vertical scroll bar, in RGBA format.

    .. versionadded:: 1.2.0

    :attr:`bar_color` is a :class:`~kivy.properties.ListProperty` and defaults
    to [.7, .7, .7, .9].
    '''

    bar_inactive_color = ListProperty([.7, .7, .7, .2])
    '''Color of horizontal / vertical scroll bar (in RGBA format), when no
    scroll is happening.

    .. versionadded:: 1.9.0

    :attr:`bar_inactive_color` is a
    :class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .2].
    '''

    bar_width = NumericProperty('2dp')
    '''Width of the horizontal / vertical scroll bar. The width is interpreted
    as a height for the horizontal bar.

    .. versionadded:: 1.2.0

    :attr:`bar_width` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 2.
    '''

    bar_pos_x = OptionProperty('bottom', options=('top', 'bottom'))
    '''Which side of the ScrollView the horizontal scroll bar should go
    on. Possible values are 'top' and 'bottom'.

    .. versionadded:: 1.8.0

    :attr:`bar_pos_x` is an :class:`~kivy.properties.OptionProperty`,
    defaults to 'bottom'.

    '''

    bar_pos_y = OptionProperty('right', options=('left', 'right'))
    '''Which side of the ScrollView the vertical scroll bar should go
    on. Possible values are 'left' and 'right'.

    .. versionadded:: 1.8.0

    :attr:`bar_pos_y` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'right'.

    '''

    bar_pos = ReferenceListProperty(bar_pos_x, bar_pos_y)
    '''Which side of the scroll view to place each of the bars on.

    :attr:`bar_pos` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`bar_pos_x`, :attr:`bar_pos_y`)
    '''

    bar_margin = NumericProperty(0)
    '''Margin between the bottom / right side of the scrollview when drawing
    the horizontal / vertical scroll bar.

    .. versionadded:: 1.2.0

    :attr:`bar_margin` is a :class:`~kivy.properties.NumericProperty`, default
    to 0
    '''

    effect_cls = ObjectProperty(DampedScrollEffect, allownone=True)
    '''Class effect to instantiate for X and Y axis.

    .. versionadded:: 1.7.0

    :attr:`effect_cls` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to :class:`DampedScrollEffect`.

    .. versionchanged:: 1.8.0
        If you set a string, the :class:`~kivy.factory.Factory` will be used to
        resolve the class.

    '''

    effect_x = ObjectProperty(None, allownone=True)
    '''Effect to apply for the X axis. If None is set, an instance of
    :attr:`effect_cls` will be created.

    .. versionadded:: 1.7.0

    :attr:`effect_x` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to None.
    '''

    effect_y = ObjectProperty(None, allownone=True)
    '''Effect to apply for the Y axis. If None is set, an instance of
    :attr:`effect_cls` will be created.

    .. versionadded:: 1.7.0

    :attr:`effect_y` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to None, read-only.
    '''

    viewport_size = ListProperty([0, 0])
    '''(internal) Size of the internal viewport. This is the size of your only
    child in the scrollview.
    '''

    scroll_type = OptionProperty(['content'],
                                 options=(['content'], ['bars'],
                                          ['bars',
                                           'content'], ['content', 'bars']))
    '''Sets the type of scrolling to use for the content of the scrollview.
    Available options are: ['content'], ['bars'], ['bars', 'content'].

    .. versionadded:: 1.8.0

    :attr:`scroll_type` is a :class:`~kivy.properties.OptionProperty`, defaults
    to ['content'].
    '''

    # private, for internal use only

    _viewport = ObjectProperty(None, allownone=True)
    _bar_color = ListProperty([0, 0, 0, 0])
    _effect_x_start_width = None
    _effect_y_start_height = None
    _update_effect_bounds_ev = None
    _bind_inactive_bar_color_ev = None

    def _set_viewport_size(self, instance, value):
        self.viewport_size = value

    def on__viewport(self, instance, value):
        if value:
            value.bind(size=self._set_viewport_size)
            self.viewport_size = value.size

    __events__ = ('on_scroll_start', 'on_scroll_move', 'on_scroll_stop')

    def __init__(self, **kwargs):
        self._touch = None
        self._trigger_update_from_scroll = Clock.create_trigger(
            self.update_from_scroll, -1)
        # create a specific canvas for the viewport
        from kivy.graphics import PushMatrix, Translate, PopMatrix, Canvas
        self.canvas_viewport = Canvas()
        self.canvas = Canvas()
        with self.canvas_viewport.before:
            PushMatrix()
            self.g_translate = Translate(0, 0)
        with self.canvas_viewport.after:
            PopMatrix()

        super(ScrollView, self).__init__(**kwargs)

        self.register_event_type('on_scroll_start')
        self.register_event_type('on_scroll_move')
        self.register_event_type('on_scroll_stop')

        # now add the viewport canvas to our canvas
        self.canvas.add(self.canvas_viewport)

        effect_cls = self.effect_cls
        if isinstance(effect_cls, string_types):
            effect_cls = Factory.get(effect_cls)
        if self.effect_x is None and effect_cls is not None:
            self.effect_x = effect_cls(target_widget=self._viewport)
        if self.effect_y is None and effect_cls is not None:
            self.effect_y = effect_cls(target_widget=self._viewport)

        trigger_update_from_scroll = self._trigger_update_from_scroll
        update_effect_widget = self._update_effect_widget
        update_effect_x_bounds = self._update_effect_x_bounds
        update_effect_y_bounds = self._update_effect_y_bounds
        fbind = self.fbind
        fbind('width', update_effect_x_bounds)
        fbind('height', update_effect_y_bounds)
        fbind('viewport_size', self._update_effect_bounds)
        fbind('_viewport', update_effect_widget)
        fbind('scroll_x', trigger_update_from_scroll)
        fbind('scroll_y', trigger_update_from_scroll)
        fbind('pos', trigger_update_from_scroll)
        fbind('size', trigger_update_from_scroll)

        update_effect_widget()
        update_effect_x_bounds()
        update_effect_y_bounds()

    def on_effect_x(self, instance, value):
        if value:
            value.bind(scroll=self._update_effect_x)
            value.target_widget = self._viewport

    def on_effect_y(self, instance, value):
        if value:
            value.bind(scroll=self._update_effect_y)
            value.target_widget = self._viewport

    def on_effect_cls(self, instance, cls):
        if isinstance(cls, string_types):
            cls = Factory.get(cls)
        self.effect_x = cls(target_widget=self._viewport)
        self.effect_x.bind(scroll=self._update_effect_x)
        self.effect_y = cls(target_widget=self._viewport)
        self.effect_y.bind(scroll=self._update_effect_y)

    def _update_effect_widget(self, *args):
        if self.effect_x:
            self.effect_x.target_widget = self._viewport
        if self.effect_y:
            self.effect_y.target_widget = self._viewport

    def _update_effect_x_bounds(self, *args):
        if not self._viewport or not self.effect_x:
            return
        self.effect_x.min = -(self.viewport_size[0] - self.width)
        self.effect_x.max = 0
        self.effect_x.value = self.effect_x.min * self.scroll_x

    def _update_effect_y_bounds(self, *args):
        if not self._viewport or not self.effect_y:
            return
        self.effect_y.min = -(self.viewport_size[1] - self.height)
        self.effect_y.max = 0
        self.effect_y.value = self.effect_y.min * self.scroll_y

    def _update_effect_bounds(self, *args):
        if not self._viewport:
            return
        if self.effect_x:
            self._update_effect_x_bounds()
        if self.effect_y:
            self._update_effect_y_bounds()

    def _update_effect_x(self, *args):
        vp = self._viewport
        if not vp or not self.effect_x:
            return

        if self.effect_x.is_manual:
            sw = vp.width - self._effect_x_start_width
        else:
            sw = vp.width - self.width
        if sw < 1:
            return
        sx = self.effect_x.scroll / float(sw)
        self.scroll_x = -sx
        self._trigger_update_from_scroll()

    def _update_effect_y(self, *args):
        vp = self._viewport
        if not vp or not self.effect_y:
            return
        if self.effect_y.is_manual:
            sh = vp.height - self._effect_y_start_height
        else:
            sh = vp.height - self.height
        if sh < 1:
            return
        sy = self.effect_y.scroll / float(sh)
        self.scroll_y = -sy
        self._trigger_update_from_scroll()

    def to_local(self, x, y, **k):
        tx, ty = self.g_translate.xy
        return x - tx, y - ty

    def to_parent(self, x, y, **k):
        tx, ty = self.g_translate.xy
        return x + tx, y + ty

    def _apply_transform(self, m, pos=None):
        tx, ty = self.g_translate.xy
        m.translate(tx, ty, 0)
        return super(ScrollView, self)._apply_transform(m, (0, 0))

    def simulate_touch_down(self, touch):
        # at this point the touch is in parent coords
        touch.push()
        touch.apply_transform_2d(self.to_local)
        ret = super(ScrollView, self).on_touch_down(touch)
        touch.pop()
        return ret

    def on_touch_down(self, touch):
        if self.dispatch('on_scroll_start', touch):
            self._touch = touch
            touch.grab(self)
            return True

    def _touch_in_handle(self, pos, size, touch):
        x, y = pos
        width, height = size
        return x <= touch.x <= x + width and y <= touch.y <= y + height

    def on_scroll_start(self, touch, check_children=True):
        if check_children:
            touch.push()
            touch.apply_transform_2d(self.to_local)
            if self.dispatch_children('on_scroll_start', touch):
                touch.pop()
                return True
            touch.pop()

        if not self.collide_point(*touch.pos):
            touch.ud[self._get_uid('svavoid')] = True
            return
        if self.disabled:
            return True
        if self._touch or (not (self.do_scroll_x or self.do_scroll_y)):
            return self.simulate_touch_down(touch)

        # handle mouse scrolling, only if the viewport size is bigger than the
        # scrollview size, and if the user allowed to do it
        vp = self._viewport
        if not vp:
            return True
        scroll_type = self.scroll_type
        ud = touch.ud
        scroll_bar = 'bars' in scroll_type

        # check if touch is in bar_x(horizontal) or bay_y(bertical)
        ud['in_bar_x'] = ud['in_bar_y'] = False
        width_scrollable = vp.width > self.width
        height_scrollable = vp.height > self.height
        bar_pos_x = self.bar_pos_x[0]
        bar_pos_y = self.bar_pos_y[0]

        d = {
            'b': True if touch.y < self.y + self.bar_width else False,
            't': True if touch.y > self.top - self.bar_width else False,
            'l': True if touch.x < self.x + self.bar_width else False,
            'r': True if touch.x > self.right - self.bar_width else False
        }
        if scroll_bar:
            if (width_scrollable and d[bar_pos_x]):
                ud['in_bar_x'] = True
            if (height_scrollable and d[bar_pos_y]):
                ud['in_bar_y'] = True

        if vp and 'button' in touch.profile and \
                touch.button.startswith('scroll'):
            btn = touch.button
            m = self.scroll_wheel_distance
            e = None

            if ((btn == 'scrolldown' and self.scroll_y >= 1)
                    or (btn == 'scrollup' and self.scroll_y <= 0)
                    or (btn == 'scrollleft' and self.scroll_x >= 1)
                    or (btn == 'scrollright' and self.scroll_x <= 0)):
                return False

            if (self.effect_x and self.do_scroll_y and height_scrollable
                    and btn in ('scrolldown', 'scrollup')):
                e = self.effect_x if ud['in_bar_x'] else self.effect_y

            elif (self.effect_y and self.do_scroll_x and width_scrollable
                  and btn in ('scrollleft', 'scrollright')):
                e = self.effect_y if ud['in_bar_y'] else self.effect_x

            if e:
                if btn in ('scrolldown', 'scrollleft'):
                    e.value = max(e.value - m, e.min)
                    e.velocity = 0
                elif btn in ('scrollup', 'scrollright'):
                    e.value = min(e.value + m, e.max)
                    e.velocity = 0
                touch.ud[self._get_uid('svavoid')] = True
                e.trigger_velocity_update()
            return True

        in_bar = ud['in_bar_x'] or ud['in_bar_y']
        if scroll_type == ['bars'] and not in_bar:
            return self.simulate_touch_down(touch)

        if in_bar:
            if (ud['in_bar_y'] and not self._touch_in_handle(
                    self._handle_y_pos, self._handle_y_size, touch)):
                self.scroll_y = (touch.y - self.y) / self.height
            elif (ud['in_bar_x'] and not self._touch_in_handle(
                    self._handle_x_pos, self._handle_x_size, touch)):
                self.scroll_x = (touch.x - self.x) / self.width

        # no mouse scrolling, so the user is going to drag the scrollview with
        # this touch.
        self._touch = touch
        uid = self._get_uid()

        ud[uid] = {
            'mode': 'unknown',
            'dx': 0,
            'dy': 0,
            'user_stopped': in_bar,
            'frames': Clock.frames,
            'time': touch.time_start
        }

        if self.do_scroll_x and self.effect_x and not ud['in_bar_x']:
            self._effect_x_start_width = self.width
            self.effect_x.start(touch.x)
            self._scroll_x_mouse = self.scroll_x
        if self.do_scroll_y and self.effect_y and not ud['in_bar_y']:
            self._effect_y_start_height = self.height
            self.effect_y.start(touch.y)
            self._scroll_y_mouse = self.scroll_y

        if not in_bar:
            Clock.schedule_once(self._change_touch_mode,
                                self.scroll_timeout / 1000.)
        return True

    def on_touch_move(self, touch):
        if self._touch is not touch:
            # touch is in parent
            touch.push()
            touch.apply_transform_2d(self.to_local)
            super(ScrollView, self).on_touch_move(touch)
            touch.pop()
            return self._get_uid() in touch.ud
        if touch.grab_current is not self:
            return True

        if touch.ud.get(self._get_uid()) is None:
            return super(ScrollView, self).on_touch_move(touch)

        touch.ud['sv.handled'] = {'x': False, 'y': False}
        if self.dispatch('on_scroll_move', touch):
            return True

    def on_scroll_move(self, touch):
        if self._get_uid('svavoid') in touch.ud:
            return False

        touch.push()
        touch.apply_transform_2d(self.to_local)
        if self.dispatch_children('on_scroll_move', touch):
            touch.pop()
            return True
        touch.pop()

        rv = True

        # By default this touch can be used to defocus currently focused
        # widget, like any touch outside of ScrollView.
        touch.ud['sv.can_defocus'] = True

        uid = self._get_uid()
        if not uid in touch.ud:
            self._touch = False
            return self.on_scroll_start(touch, False)
        ud = touch.ud[uid]
        mode = ud['mode']

        # check if the minimum distance has been travelled
        if mode == 'unknown' or mode == 'scroll':
            if not touch.ud['sv.handled']['x'] and self.do_scroll_x \
                    and self.effect_x:
                width = self.width
                if touch.ud.get('in_bar_x', False):
                    dx = touch.dx / float(width - width * self.hbar[1])
                    self.scroll_x = min(max(self.scroll_x + dx, 0.), 1.)
                    self._trigger_update_from_scroll()
                else:
                    if self.scroll_type != ['bars']:
                        self.effect_x.update(touch.x)
                if self.scroll_x < 0 or self.scroll_x > 1:
                    rv = False
                else:
                    touch.ud['sv.handled']['x'] = True
                # Touch resulted in scroll should not defocus focused widget
                touch.ud['sv.can_defocus'] = False
            if not touch.ud['sv.handled']['y'] and self.do_scroll_y \
                    and self.effect_y:
                height = self.height
                if touch.ud.get('in_bar_y', False):
                    dy = touch.dy / float(height - height * self.vbar[1])
                    self.scroll_y = min(max(self.scroll_y + dy, 0.), 1.)
                    self._trigger_update_from_scroll()
                else:
                    if self.scroll_type != ['bars']:
                        self.effect_y.update(touch.y)
                if self.scroll_y < 0 or self.scroll_y > 1:
                    rv = False
                else:
                    touch.ud['sv.handled']['y'] = True
                # Touch resulted in scroll should not defocus focused widget
                touch.ud['sv.can_defocus'] = False

        if mode == 'unknown':
            ud['dx'] += abs(touch.dx)
            ud['dy'] += abs(touch.dy)
            if ((ud['dx'] > self.scroll_distance)
                    or (ud['dy'] > self.scroll_distance)):
                if not self.do_scroll_x and not self.do_scroll_y:
                    # touch is in parent, but _change expects window coords
                    touch.push()
                    touch.apply_transform_2d(self.to_local)
                    touch.apply_transform_2d(self.to_window)
                    self._change_touch_mode()
                    touch.pop()
                    return
                mode = 'scroll'
            ud['mode'] = mode

        if mode == 'scroll':
            ud['dt'] = touch.time_update - ud['time']
            ud['time'] = touch.time_update
            ud['user_stopped'] = True

        return rv

    def on_touch_up(self, touch):
        if self._touch is not touch and self._get_uid(
                'svavoid') not in touch.ud:
            # touch is in parents
            touch.push()
            touch.apply_transform_2d(self.to_local)
            if super(ScrollView, self).on_touch_up(touch):
                touch.pop()
                return True
            touch.pop()
            return False

        if self.dispatch('on_scroll_stop', touch):
            touch.ungrab(self)
            if not touch.ud.get('sv.can_defocus', True):
                # Focused widget should stay focused
                FocusBehavior.ignored_touch.append(touch)
            return True

    def on_scroll_stop(self, touch, check_children=True):
        self._touch = None

        if check_children:
            touch.push()
            touch.apply_transform_2d(self.to_local)
            if self.dispatch_children('on_scroll_stop', touch):
                touch.pop()
                return True
            touch.pop()

        if self._get_uid('svavoid') in touch.ud:
            return
        if self._get_uid() not in touch.ud:
            return False

        self._touch = None
        uid = self._get_uid()
        ud = touch.ud[uid]
        if self.do_scroll_x and self.effect_x:
            if not touch.ud.get('in_bar_x', False) and\
                    self.scroll_type != ['bars']:
                self.effect_x.stop(touch.x)
        if self.do_scroll_y and self.effect_y and\
                self.scroll_type != ['bars']:
            if not touch.ud.get('in_bar_y', False):
                self.effect_y.stop(touch.y)
        if ud['mode'] == 'unknown':
            # we must do the click at least..
            # only send the click if it was not a click to stop
            # autoscrolling
            if not ud['user_stopped']:
                self.simulate_touch_down(touch)
            Clock.schedule_once(partial(self._do_touch_up, touch), .2)

        ev = self._update_effect_bounds_ev
        if ev is None:
            ev = self._update_effect_bounds_ev = Clock.create_trigger(
                self._update_effect_bounds)
        ev()

        # if we do mouse scrolling, always accept it
        if 'button' in touch.profile and touch.button.startswith('scroll'):
            return True

        return self._get_uid() in touch.ud

    def scroll_to(self, widget, padding=10, animate=True):
        '''Scrolls the viewport to ensure that the given widget is visible,
        optionally with padding and animation. If animate is True (the
        default), then the default animation parameters will be used.
        Otherwise, it should be a dict containing arguments to pass to
        :class:`~kivy.animation.Animation` constructor.

        .. versionadded:: 1.9.1
        '''
        if not self.parent:
            return

        if isinstance(padding, (int, float)):
            padding = (padding, padding)

        pos = self.parent.to_widget(*widget.to_window(*widget.pos))
        cor = self.parent.to_widget(
            *widget.to_window(widget.right, widget.top))

        dx = dy = 0

        if pos[1] < self.y:
            dy = self.y - pos[1] + dp(padding[1])
        elif cor[1] > self.top:
            dy = self.top - cor[1] - dp(padding[1])

        if pos[0] < self.x:
            dx = self.x - pos[0] + dp(padding[0])
        elif cor[0] > self.right:
            dx = self.right - cor[0] - dp(padding[0])

        dsx, dsy = self.convert_distance_to_scroll(dx, dy)
        sxp = min(1, max(0, self.scroll_x - dsx))
        syp = min(1, max(0, self.scroll_y - dsy))

        if animate:
            if animate is True:
                animate = {'d': 0.2, 't': 'out_quad'}
            Animation.stop_all(self, 'scroll_x', 'scroll_y')
            Animation(scroll_x=sxp, scroll_y=syp, **animate).start(self)
        else:
            self.scroll_x = sxp
            self.scroll_y = syp

    def convert_distance_to_scroll(self, dx, dy):
        '''Convert a distance in pixels to a scroll distance, depending on the
        content size and the scrollview size.

        The result will be a tuple of scroll distance that can be added to
        :data:`scroll_x` and :data:`scroll_y`
        '''
        if not self._viewport:
            return 0, 0
        vp = self._viewport
        if vp.width > self.width:
            sw = vp.width - self.width
            sx = dx / float(sw)
        else:
            sx = 0
        if vp.height > self.height:
            sh = vp.height - self.height
            sy = dy / float(sh)
        else:
            sy = 1
        return sx, sy

    def update_from_scroll(self, *largs):
        '''Force the reposition of the content, according to current value of
        :attr:`scroll_x` and :attr:`scroll_y`.

        This method is automatically called when one of the :attr:`scroll_x`,
        :attr:`scroll_y`, :attr:`pos` or :attr:`size` properties change, or
        if the size of the content changes.
        '''
        if not self._viewport:
            return
        vp = self._viewport

        # update from size_hint
        if vp.size_hint_x is not None:
            vp.width = vp.size_hint_x * self.width
        if vp.size_hint_y is not None:
            vp.height = vp.size_hint_y * self.height

        if vp.width > self.width:
            sw = vp.width - self.width
            x = self.x - self.scroll_x * sw
        else:
            x = self.x
        if vp.height > self.height:
            sh = vp.height - self.height
            y = self.y - self.scroll_y * sh
        else:
            y = self.top - vp.height

        # from 1.8.0, we now use a matrix by default, instead of moving the
        # widget position behind. We set it here, but it will be a no-op most of
        # the time.
        vp.pos = 0, 0
        self.g_translate.xy = x, y

        # New in 1.2.0, show bar when scrolling happens and (changed in 1.9.0)
        # fade to bar_inactive_color when no scroll is happening.
        ev = self._bind_inactive_bar_color_ev
        if ev is None:
            ev = self._bind_inactive_bar_color_ev = Clock.create_trigger(
                self._bind_inactive_bar_color, .5)
        self.funbind('bar_inactive_color', self._change_bar_color)
        Animation.stop_all(self, '_bar_color')
        self.fbind('bar_color', self._change_bar_color)
        self._bar_color = self.bar_color
        ev()

    def _bind_inactive_bar_color(self, *l):
        self.funbind('bar_color', self._change_bar_color)
        self.fbind('bar_inactive_color', self._change_bar_color)
        Animation(_bar_color=self.bar_inactive_color, d=.5,
                  t='out_quart').start(self)

    def _change_bar_color(self, inst, value):
        self._bar_color = value

    #
    # Private
    #
    def add_widget(self, widget, index=0):
        if self._viewport:
            raise Exception('ScrollView accept only one widget')
        canvas = self.canvas
        self.canvas = self.canvas_viewport
        super(ScrollView, self).add_widget(widget, index)
        self.canvas = canvas
        self._viewport = widget
        widget.bind(size=self._trigger_update_from_scroll)
        self._trigger_update_from_scroll()

    def remove_widget(self, widget):
        canvas = self.canvas
        self.canvas = self.canvas_viewport
        super(ScrollView, self).remove_widget(widget)
        self.canvas = canvas
        if widget is self._viewport:
            self._viewport = None

    def _get_uid(self, prefix='sv'):
        return '{0}.{1}'.format(prefix, self.uid)

    def _change_touch_mode(self, *largs):
        if not self._touch:
            return
        uid = self._get_uid()
        touch = self._touch
        if uid not in touch.ud:
            self._touch = False
            return
        ud = touch.ud[uid]
        if ud['mode'] != 'unknown' or ud['user_stopped']:
            return
        diff_frames = Clock.frames - ud['frames']

        # in order to be able to scroll on very slow devices, let at least 3
        # frames displayed to accumulate some velocity. And then, change the
        # touch mode. Otherwise, we might never be able to compute velocity, and
        # no way to scroll it. See #1464 and #1499
        if diff_frames < 3:
            Clock.schedule_once(self._change_touch_mode, 0)
            return

        if self.do_scroll_x and self.effect_x:
            self.effect_x.cancel()
        if self.do_scroll_y and self.effect_y:
            self.effect_y.cancel()
        # XXX the next line was in the condition. But this stop
        # the possibility to "drag" an object out of the scrollview in the
        # non-used direction: if you have an horizontal scrollview, a
        # vertical gesture will not "stop" the scroll view to look for an
        # horizontal gesture, until the timeout is done.
        # and touch.dx + touch.dy == 0:
        touch.ungrab(self)
        self._touch = None
        # touch is in window coords
        touch.push()
        touch.apply_transform_2d(self.to_widget)
        touch.apply_transform_2d(self.to_parent)
        self.simulate_touch_down(touch)
        touch.pop()
        return

    def _do_touch_up(self, touch, *largs):
        # touch is in window coords
        touch.push()
        touch.apply_transform_2d(self.to_widget)
        super(ScrollView, self).on_touch_up(touch)
        touch.pop()
        # don't forget about grab event!
        for x in touch.grab_list[:]:
            touch.grab_list.remove(x)
            x = x()
            if not x:
                continue
            touch.grab_current = x
            # touch is in window coords
            touch.push()
            touch.apply_transform_2d(self.to_widget)
            super(ScrollView, self).on_touch_up(touch)
            touch.pop()
        touch.grab_current = None
Esempio n. 9
0
class Label(Widget):
    '''Label class, see module documentation for more information.

    :Events:
        `on_ref_press`
            Fired when the user clicks on a word referenced with a
            ``[ref]`` tag in a text markup.
    '''

    __events__ = ['on_ref_press']

    _font_properties = ('text', 'font_size', 'font_name', 'bold', 'italic',
                        'halign', 'valign', 'padding_x', 'padding_y',
                        'text_size', 'shorten', 'mipmap', 'markup',
                        'line_height', 'max_lines', 'strip', 'shorten_from',
                        'split_str')

    def __init__(self, **kwargs):
        self._trigger_texture = Clock.create_trigger(self.texture_update, -1)
        super(Label, self).__init__(**kwargs)

        # bind all the property for recreating the texture
        d = Label._font_properties
        dkw = {}
        for x in d:
            dkw[x] = partial(self._trigger_texture_update, x)
        self.bind(**dkw)

        self._label = None
        self._create_label()

        # force the texture creation
        self._trigger_texture()

    def _create_label(self):
        # create the core label class according to markup value
        if self._label is not None:
            cls = self._label.__class__
        else:
            cls = None
        markup = self.markup
        if (markup and cls is not CoreMarkupLabel) or \
           (not markup and cls is not CoreLabel):
            # markup have change, we need to change our rendering method.
            d = Label._font_properties
            dkw = dict(list(zip(d, [getattr(self, x) for x in d])))
            if markup:
                self._label = CoreMarkupLabel(**dkw)
            else:
                self._label = CoreLabel(**dkw)

    def _trigger_texture_update(self, name=None, source=None, value=None):
        # check if the label core class need to be switch to a new one
        if name == 'markup':
            self._create_label()
        if source:
            if name == 'text':
                self._label.text = value
            elif name == 'text_size':
                self._label.usersize = value
            elif name == 'font_size':
                self._label.options[name] = value
            else:
                self._label.options[name] = value
        self._trigger_texture()

    def texture_update(self, *largs):
        '''Force texture recreation with the current Label properties.

        After this function call, the :attr:`texture` and :attr:`texture_size`
        will be updated in this order.
        '''
        mrkup = self._label.__class__ is CoreMarkupLabel
        self.texture = None

        if (not self._label.text or (self.halign[-1] == 'y' or self.strip)
                and not self._label.text.strip()):
            self.texture_size = (0, 0)
            if mrkup:
                self.refs, self._label._refs = {}, {}
                self.anchors, self._label._anchors = {}, {}
        else:
            if mrkup:
                text = self._label.text
                # we must strip here, otherwise, if the last line is empty,
                # markup will retain the last empty line since it only strips
                # line by line within markup
                if self.halign[-1] == 'y' or self.strip:
                    text = text.strip()
                self._label.text = ''.join(
                    ('[color=', get_hex_from_color(self.color), ']', text,
                     '[/color]'))
                self._label.refresh()
                # force the rendering to get the references
                if self._label.texture:
                    self._label.texture.bind()
                self.refs = self._label.refs
                self.anchors = self._label.anchors
            else:
                self._label.refresh()
            texture = self._label.texture
            if texture is not None:
                self.texture = self._label.texture
                self.texture_size = list(self.texture.size)

    def on_touch_down(self, touch):
        if super(Label, self).on_touch_down(touch):
            return True
        if not len(self.refs):
            return False
        tx, ty = touch.pos
        tx -= self.center_x - self.texture_size[0] / 2.
        ty -= self.center_y - self.texture_size[1] / 2.
        ty = self.texture_size[1] - ty
        for uid, zones in self.refs.items():
            for zone in zones:
                x, y, w, h = zone
                if x <= tx <= w and y <= ty <= h:
                    self.dispatch('on_ref_press', uid)
                    return True
        return False

    def on_ref_press(self, ref):
        pass

    #
    # Properties
    #

    disabled_color = ListProperty([1, 1, 1, .3])
    '''Text color, in the format (r, g, b, a)

    .. versionadded:: 1.8.0

    :attr:`disabled_color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [1, 1, 1, .5].
    '''

    text = StringProperty('')
    '''Text of the label.

    Creation of a simple hello world::

        widget = Label(text='Hello world')

    If you want to create the widget with an unicode string, use::

        widget = Label(text=u'My unicode string')

    :attr:`text` is a :class:`~kivy.properties.StringProperty` and defaults to
    ''.
    '''

    text_size = ListProperty([None, None])
    '''By default, the label is not constrained to any bounding box.
    You can set the size constraint of the label with this property.
    The text will autoflow into the constrains. So although the font size
    will not be reduced, the text will be arranged to fit into the box as best
    as possible, with any text still outside the box clipped.

    This sets and clips :attr:`texture_size` to text_size if not None.

    .. versionadded:: 1.0.4

    For example, whatever your current widget size is, if you want the label to
    be created in a box with width=200 and unlimited height::

        Label(text='Very big big line', text_size=(200, None))

    .. note::

        This text_size property is the same as the
        :attr:`~kivy.core.text.Label.usersize` property in the
        :class:`~kivy.core.text.Label` class. (It is named size= in the
        constructor.)

    :attr:`text_size` is a :class:`~kivy.properties.ListProperty` and
    defaults to (None, None), meaning no size restriction by default.
    '''

    font_name = StringProperty('DroidSans')
    '''Filename of the font to use. The path can be absolute or relative.
    Relative paths are resolved by the :func:`~kivy.resources.resource_find`
    function.

    .. warning::

        Depending of your text provider, the font file can be ignored. However,
        you can mostly use this without problems.

        If the font used lacks the glyphs for the particular language/symbols
        you are using, you will see '[]' blank box characters instead of the
        actual glyphs. The solution is to use a font that has the glyphs you
        need to display. For example, to display |unicodechar|, use a font such
        as freesans.ttf that has the glyph.

        .. |unicodechar| image:: images/unicode-char.png

    :attr:`font_name` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'DroidSans'.
    '''

    font_size = NumericProperty('15sp')
    '''Font size of the text, in pixels.

    :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 12dp.
    '''

    line_height = NumericProperty(1.0)
    '''Line Height for the text. e.g. line_height = 2 will cause the spacing
    between lines to be twice the size.

    :attr:`line_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1.0.

    .. versionadded:: 1.5.0
    '''

    bold = BooleanProperty(False)
    '''Indicates use of the bold version of your font.

    .. note::

        Depending of your font, the bold attribute may have no impact on your
        text rendering.

    :attr:`bold` is a :class:`~kivy.properties.BooleanProperty` and defaults to
    False.
    '''

    italic = BooleanProperty(False)
    '''Indicates use of the italic version of your font.

    .. note::

        Depending of your font, the italic attribute may have no impact on your
        text rendering.

    :attr:`italic` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    padding_x = NumericProperty(0)
    '''Horizontal padding of the text inside the widget box.

    :attr:`padding_x` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.

    .. versionchanged:: 1.9.0
        `padding_x` has been fixed to work as expected.
        In the past, the text was padded by the negative of its values.
    '''

    padding_y = NumericProperty(0)
    '''Vertical padding of the text inside the widget box.

    :attr:`padding_y` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.

    .. versionchanged:: 1.9.0
        `padding_y` has been fixed to work as expected.
        In the past, the text was padded by the negative of its values.
    '''

    padding = ReferenceListProperty(padding_x, padding_y)
    '''Padding of the text in the format (padding_x, padding_y)

    :attr:`padding` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`padding_x`, :attr:`padding_y`) properties.
    '''

    halign = OptionProperty('left',
                            options=['left', 'center', 'right', 'justify'])
    '''Horizontal alignment of the text.

    :attr:`halign` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'left'. Available options are : left, center, right and
    justify.

    .. warning::

        This doesn't change the position of the text texture of the Label
        (centered), only the position of the text in this texture. You probably
        want to bind the size of the Label to the :attr:`texture_size` or set a
        :attr:`text_size`.

    .. versionchanged:: 1.6.0
        A new option was added to :attr:`halign`, namely `justify`.
    '''

    valign = OptionProperty('bottom', options=['bottom', 'middle', 'top'])
    '''Vertical alignment of the text.

    :attr:`valign` is an :class:`~kivy.properties.OptionProperty` and defaults
    to 'bottom'. Available options are : bottom, middle and top.

    .. warning::

        This doesn't change the position of the text texture of the Label
        (centered), only the position of the text within this texture. You
        probably want to bind the size of the Label to the :attr:`texture_size`
        or set a :attr:`text_size` to change this behavior.
    '''

    color = ListProperty([1, 1, 1, 1])
    '''Text color, in the format (r, g, b, a)

    :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to
    [1, 1, 1, 1].
    '''

    texture = ObjectProperty(None, allownone=True)
    '''Texture object of the text.
    The text is rendered automatically when a property changes. The OpenGL
    texture created in this operation is stored in this property. You can use
    this :attr:`texture` for any graphics elements.

    Depending on the texture creation, the value will be a
    :class:`~kivy.graphics.texture.Texture` or
    :class:`~kivy.graphics.texture.TextureRegion` object.

    .. warning::

        The :attr:`texture` update is scheduled for the next frame. If you need
        the texture immediately after changing a property, you have to call
        the :meth:`texture_update` method before accessing :attr:`texture`::

            l = Label(text='Hello world')
            # l.texture is good
            l.font_size = '50sp'
            # l.texture is not updated yet
            l.texture_update()
            # l.texture is good now.

    :attr:`texture` is an :class:`~kivy.properties.ObjectProperty` and defaults
    to None.
    '''

    texture_size = ListProperty([0, 0])
    '''Texture size of the text. The size is determined by the font size and
    text. If :attr:`text_size` is [None, None], the texture will be the size
    required to fit the text, otherwise it's clipped to fit :attr:`text_size`.

    When :attr:`text_size` is [None, None], one can bind to texture_size
    and rescale it proportionally to fit the size of the label in order to
    make the text fit maximally in the label.

    .. warning::

        The :attr:`texture_size` is set after the :attr:`texture`
        property. If you listen for changes to :attr:`texture`,
        :attr:`texture_size` will not be up-to-date in your callback.
        Bind to :attr:`texture_size` instead.
    '''

    mipmap = BooleanProperty(False)
    '''Indicates whether OpenGL mipmapping is applied to the texture or not.
    Read :ref:`mipmap` for more information.

    .. versionadded:: 1.0.7

    :attr:`mipmap` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    shorten = BooleanProperty(False)
    '''
    Indicates whether the label should attempt to shorten its textual contents
    as much as possible if a :attr:`text_size` is given. Setting this to True
    without an appropriately set :attr:`text_size` will lead to unexpected
    results.

    :attr:`shorten_from` and :attr:`split_str` control the direction from
    which the :attr:`text` is split, as well as where in the :attr:`text` we
    are allowed to split.

    :attr:`shorten` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    shorten_from = OptionProperty('center',
                                  options=['left', 'center', 'right'])
    '''The side from which we should shorten the text from, can be left,
    right, or center.

    For example, if left, the ellipsis will appear towards the left side and we
    will display as much text starting from the right as possible. Similar to
    :attr:`shorten`, this option only applies when :attr:`text_size` [0] is
    not None, In this case, the string is shortened to fit within the specified
    width.

    .. versionadded:: 1.9.0

    :attr:`shorten_from` is a :class:`~kivy.properties.OptionProperty` and
    defaults to `center`.
    '''

    split_str = StringProperty('')
    '''The string used to split the :attr:`text` while shortening the string
    when :attr:`shorten` is True.

    For example, if it's a space, the string will be broken into words and as
    many whole words that can fit into a single line will be displayed. If
    :attr:`shorten_from` is the empty string, `''`, we split on every character
    fitting as much text as possible into the line.

    .. versionadded:: 1.9.0

    :attr:`split_str` is a :class:`~kivy.properties.StringProperty` and
    defaults to `''` (the empty string).
    '''

    markup = BooleanProperty(False)
    '''
    .. versionadded:: 1.1.0

    If True, the text will be rendered using the
    :class:`~kivy.core.text.markup.MarkupLabel`: you can change the
    style of the text using tags. Check the
    :doc:`api-kivy.core.text.markup` documentation for more information.

    :attr:`markup` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    refs = DictProperty({})
    '''
    .. versionadded:: 1.1.0

    List of ``[ref=xxx]`` markup items in the text with the bounding box of
    all the words contained in a ref, available only after rendering.

    For example, if you wrote::

        Check out my [ref=hello]link[/ref]

    The refs will be set with::

        {'hello': ((64, 0, 78, 16), )}

    The references marked "hello" have a bounding box at (x1, y1, x2, y2).
    These co-ordinates are relative to the top left corner of the text, with
    the y value increasing downwards. You can define multiple refs with the same
    name: each occurence will be added as another (x1, y1, x2, y2) tuple to
    this list.

    The current Label implementation uses these references if they exist in
    your markup text, automatically doing the collision with the touch and
    dispatching an `on_ref_press` event.

    You can bind a ref event like this::

        def print_it(instance, value):
            print('User click on', value)
        widget = Label(text='Hello [ref=world]World[/ref]', markup=True)
        widget.on_ref_press(print_it)

    .. note::

        This works only with markup text. You need :attr:`markup` set to
        True.
    '''

    anchors = DictProperty({})
    '''
    .. versionadded:: 1.1.0

    Position of all the ``[anchor=xxx]`` markup in the text.
    These co-ordinates are relative to the top left corner of the text, with
    the y value increasing downwards. Anchors names should be unique and only
    the first occurence of any duplicate anchors will be recorded.


    You can place anchors in your markup text as follows::

        text = """
            [anchor=title1][size=24]This is my Big title.[/size]
            [anchor=content]Hello world
        """

    Then, all the ``[anchor=]`` references will be removed and you'll get all
    the anchor positions in this property (only after rendering)::

        >>> widget = Label(text=text, markup=True)
        >>> widget.texture_update()
        >>> widget.anchors
        {"content": (20, 32), "title1": (20, 16)}

    .. note::

        This works only with markup text. You need :attr:`markup` set to
        True.

    '''

    max_lines = NumericProperty(0)
    '''Maximum number of lines to use, defaults to 0, which means unlimited.
    Please note that :attr:`shorten` take over this property. (with
    shorten, the text is always one line.)

    .. versionadded:: 1.8.0

    :attr:`max_lines` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.
    '''

    strip = BooleanProperty(False)
    '''Whether leading and trailing spaces and newlines should be stripped from
Esempio n. 10
0
class ColorWheel(Widget):
    '''Chromatic wheel for the ColorPicker.

    .. versionchanged:: 1.7.1
        `font_size`, `font_name` and `foreground_color` have been removed. The
        sizing is now the same as others widget, based on 'sp'. Orientation is
        also automatically determined according to the width/height ratio.

    '''

    r = BoundedNumericProperty(0, min=0, max=1)
    '''The Red value of the color currently selected.

    :attr:`r` is a :class:`~kivy.properties.BoundedNumericProperty` and
    can be a value from 0 to 1. It defaults to 0.
    '''

    g = BoundedNumericProperty(0, min=0, max=1)
    '''The Green value of the color currently selected.

    :attr:`g` is a :class:`~kivy.properties.BoundedNumericProperty`
    and can be a value from 0 to 1.
    '''

    b = BoundedNumericProperty(0, min=0, max=1)
    '''The Blue value of the color currently selected.

    :attr:`b` is a :class:`~kivy.properties.BoundedNumericProperty` and
    can be a value from 0 to 1.
    '''

    a = BoundedNumericProperty(0, min=0, max=1)
    '''The Alpha value of the color currently selected.

    :attr:`a` is a :class:`~kivy.properties.BoundedNumericProperty` and
    can be a value from 0 to 1.
    '''

    color = ReferenceListProperty(r, g, b, a)
    '''The holds the color currently selected.

    :attr:`color` is a :class:`~kivy.properties.ReferenceListProperty` and
    contains a list of `r`, `g`, `b`, `a` values.
    '''

    _origin = ListProperty((100, 100))
    _radius = NumericProperty(100)

    _piece_divisions = NumericProperty(10)
    _pieces_of_pie = NumericProperty(16)

    _inertia_slowdown = 1.25
    _inertia_cutoff = .25

    _num_touches = 0
    _pinch_flag = False

    _hsv = ListProperty([1, 1, 1, 0])

    def __init__(self, **kwargs):
        super(ColorWheel, self).__init__(**kwargs)

        pdv = self._piece_divisions
        self.sv_s = [(float(x) / pdv, 1) for x in range(pdv)] + [
            (1, float(y) / pdv) for y in reversed(range(pdv))]

    def on__origin(self, instance, value):
        self.init_wheel(None)

    def on__radius(self, instance, value):
        self.init_wheel(None)

    def init_wheel(self, dt):
        # initialize list to hold all meshes
        self.canvas.clear()
        self.arcs = []
        self.sv_idx = 0
        pdv = self._piece_divisions
        ppie = self._pieces_of_pie

        for r in range(pdv):
            for t in range(ppie):
                self.arcs.append(
                    _ColorArc(
                        self._radius * (float(r) / float(pdv)),
                        self._radius * (float(r + 1) / float(pdv)),
                        2 * pi * (float(t) / float(ppie)),
                        2 * pi * (float(t + 1) / float(ppie)),
                        origin=self._origin,
                        color=(float(t) / ppie,
                               self.sv_s[self.sv_idx + r][0],
                               self.sv_s[self.sv_idx + r][1],
                               1)))

                self.canvas.add(self.arcs[-1])

    def recolor_wheel(self):
        ppie = self._pieces_of_pie
        for idx, segment in enumerate(self.arcs):
            segment.change_color(
                sv=self.sv_s[int(self.sv_idx + idx / ppie)])

    def change_alpha(self, val):
        for idx, segment in enumerate(self.arcs):
            segment.change_color(a=val)

    def inertial_incr_sv_idx(self, dt):
        # if its already zoomed all the way out, cancel the inertial zoom
        if self.sv_idx == len(self.sv_s) - self._piece_divisions:
            return False

        self.sv_idx += 1
        self.recolor_wheel()
        if dt * self._inertia_slowdown > self._inertia_cutoff:
            return False
        else:
            Clock.schedule_once(self.inertial_incr_sv_idx,
                                dt * self._inertia_slowdown)

    def inertial_decr_sv_idx(self, dt):
        # if its already zoomed all the way in, cancel the inertial zoom
        if self.sv_idx == 0:
            return False
        self.sv_idx -= 1
        self.recolor_wheel()
        if dt * self._inertia_slowdown > self._inertia_cutoff:
            return False
        else:
            Clock.schedule_once(self.inertial_decr_sv_idx,
                                dt * self._inertia_slowdown)

    def on_touch_down(self, touch):
        r = self._get_touch_r(touch.pos)
        if r > self._radius:
            return False

        # code is still set up to allow pinch to zoom, but this is
        # disabled for now since it was fiddly with small wheels.
        # Comment out these lines and  adjust on_touch_move to reenable
        # this.
        if self._num_touches != 0:
            return False

        touch.grab(self)
        self._num_touches += 1
        touch.ud['anchor_r'] = r
        touch.ud['orig_sv_idx'] = self.sv_idx
        touch.ud['orig_time'] = Clock.get_time()

    def on_touch_move(self, touch):
        if touch.grab_current is not self:
            return
        r = self._get_touch_r(touch.pos)
        goal_sv_idx = (touch.ud['orig_sv_idx'] -
                       int((r - touch.ud['anchor_r']) /
                            (float(self._radius) / self._piece_divisions)))

        if (
            goal_sv_idx != self.sv_idx and
            goal_sv_idx >= 0 and
            goal_sv_idx <= len(self.sv_s) - self._piece_divisions
        ):
            # this is a pinch to zoom
            self._pinch_flag = True
            self.sv_idx = goal_sv_idx
            self.recolor_wheel()

    def on_touch_up(self, touch):
        if touch.grab_current is not self:
            return
        touch.ungrab(self)
        self._num_touches -= 1
        if self._pinch_flag:
            if self._num_touches == 0:
                # user was pinching, and now both fingers are up. Return
                # to normal
                if self.sv_idx > touch.ud['orig_sv_idx']:
                    Clock.schedule_once(
                        self.inertial_incr_sv_idx,
                        (Clock.get_time() - touch.ud['orig_time']) /
                        (self.sv_idx - touch.ud['orig_sv_idx']))

                if self.sv_idx < touch.ud['orig_sv_idx']:
                    Clock.schedule_once(
                        self.inertial_decr_sv_idx,
                        (Clock.get_time() - touch.ud['orig_time']) /
                        (self.sv_idx - touch.ud['orig_sv_idx']))

                self._pinch_flag = False
                return
            else:
                # user was pinching, and at least one finger remains. We
                # don't want to treat the remaining fingers as touches
                return
        else:
            r, theta = rect_to_polar(self._origin, *touch.pos)
            # if touch up is outside the wheel, ignore
            if r >= self._radius:
                return
            # compute which ColorArc is being touched (they aren't
            # widgets so we don't get collide_point) and set
            # _hsv based on the selected ColorArc
            piece = int((theta / (2 * pi)) * self._pieces_of_pie)
            division = int((r / self._radius) * self._piece_divisions)
            self._hsv = \
                self.arcs[self._pieces_of_pie * division + piece].color

    def on__hsv(self, instance, value):
        c_hsv = Color(*value, mode='hsv')
        self.r = c_hsv.r
        self.g = c_hsv.g
        self.b = c_hsv.b
        self.a = c_hsv.a
        self.rgba = (self.r, self.g, self.b, self.a)

    def _get_touch_r(self, pos):
        return distance(pos, self._origin)
Esempio n. 11
0
class Text(Widget):
    widget_type_name = 'Text'
    var_finder = re.compile("(?<=\()[a-zA-Z_0-9|]+(?=\))")
    string_finder = re.compile("(?<=\$)[a-zA-Z_0-9]+")
    merge_settings = ('font_name', 'font_size', 'bold', 'italic', 'halign',
                      'valign', 'padding_x', 'padding_y', 'text_size',
                      'shorten', 'mipmap', 'markup', 'line_height',
                      'max_lines', 'strip', 'shorten_from', 'split_str',
                      'unicode_errors', 'color')
    animation_properties = ('x', 'y', 'font_size', 'color', 'opacity', 'rotation', 'scale')

    def __init__(self, mc: "MpfMc", config: dict, key: Optional[str]=None,
                 play_kwargs: Optional[dict]=None, **kwargs) -> None:
        self._label = CustomLabel(mc, config)
        self._label.fbind('texture', self.on_label_texture)

        super().__init__(mc=mc, config=config, key=key)

        # Special handling for baseline anchor
        if self.config['anchor_y'] == 'baseline':
            self.anchor_y = 'bottom'
            self.adjust_bottom = self._label._label.get_descent() * -1

        self.original_text = self._get_text_string(config.get('text', ''))

        self.text_variables = dict()
        if play_kwargs:
            self.event_replacements = play_kwargs
        else:
            self.event_replacements = kwargs
        self._process_text(self.original_text)

        # Bind to all properties that when changed need to force
        # the widget to be redrawn
        self.bind(pos=self._draw_widget,
                  size=self._draw_widget,
                  color=self._draw_widget,
                  rotation=self._draw_widget,
                  scale=self._draw_widget)

    def __repr__(self) -> str:
        if hasattr(self, '_label') and self._label:
            return '<Text Widget text={}>'.format(self._label.text)
        else:
            return '<Text Widget text=None>'

    def _draw_widget(self, *args):
        """Draws the image (draws a rectangle using the image texture)"""
        del args

        anchor = (self.x - self.anchor_offset_pos[0], self.y - self.anchor_offset_pos[1])
        self.canvas.clear()

        if len(self._label.text) > 0:
            with self.canvas:
                Color(*self.color)
                Rotate(angle=self.rotation, origin=anchor)
                Scale(self.scale).origin = anchor
                Rectangle(pos=self.pos, size=self.size, texture=self._label.texture)

    def on_label_texture(self, instance, texture):
        del instance
        if texture:
            self.size = texture.size

            if self.config['anchor_y'] == 'baseline':
                self.adjust_bottom = self._label._label.get_descent() * -1

    def update_kwargs(self, **kwargs) -> None:
        self.event_replacements.update(kwargs)
        self._process_text(self.original_text)

    def _get_text_string(self, text: str) -> str:
        if '$' not in text:
            return text

        for text_string in Text.string_finder.findall(text):
            text = text.replace('${}'.format(text_string),
                                self._do_get_text_string(text_string))

        return text

    def _do_get_text_string(self, text_string: str) -> str:
        try:
            return str(self.mc.machine_config['text_strings'][text_string])
        except KeyError:
            # if the text string is not found, put the $ back on
            return '${}'.format(text_string)

    def _get_text_vars(self, text: str):
        return Text.var_finder.findall(text)

    def _process_text(self, text: str) -> None:
        for var_string in self._get_text_vars(text):
            if var_string in self.event_replacements:
                text = text.replace('({})'.format(var_string),
                                    str(self.event_replacements[var_string]))

        if self._get_text_vars(text):
            # monitors won't be added twice, so it's ok to blindly call this
            self._setup_variable_monitors(text)

        self.update_vars_in_text(text)

    def update_vars_in_text(self, text: str) -> None:
        for var_string in self._get_text_vars(text):
            if var_string.startswith('machine|'):
                try:
                    text = text.replace('(' + var_string + ')',
                                        str(self.mc.machine_vars[
                                                var_string.split('|')[1]]))
                except KeyError:
                    text = ''

            elif self.mc.player:
                if var_string.startswith('player|'):
                    text = text.replace('(' + var_string + ')',
                                        str(self.mc.player[
                                                var_string.split('|')[1]]))
                    continue
                elif var_string.startswith('player') and '|' in var_string:
                    player_num, var_name = var_string.lstrip('player').split(
                            '|')
                    try:
                        value = self.mc.player_list[int(player_num) - 1][
                            var_name]

                        if value is not None:
                            text = text.replace('(' + var_string + ')',
                                                str(value))
                        else:
                            text = ''
                    except IndexError:
                        text = ''
                    continue
                elif self.mc.player.is_player_var(var_string):
                    text = text.replace('(' + var_string + ')',
                                        str(self.mc.player[var_string]))
                    continue

            if var_string in self.event_replacements:
                text = text.replace('({})'.format(var_string),
                    str(self.event_replacements[var_string]))

        self.update_text(text)

    def update_text(self, text: str) -> None:
        if text:
            if self.config['min_digits']:
                text = text.zfill(self.config['min_digits'])

            if self.config['number_grouping']:

                # find the numbers in the string
                number_list = [s for s in text.split() if s.isdigit()]

                # group the numbers and replace them in the string
                for item in number_list:
                    grouped_item = Text.group_digits(item)
                    text = text.replace(str(item), grouped_item)
        self._label.text = text
        self._label.texture_update()

    def _player_var_change(self, **kwargs) -> None:
        del kwargs

        self.update_vars_in_text(self.original_text)

    def _machine_var_change(self, **kwargs) -> None:
        del kwargs
        self.update_vars_in_text(self.original_text)

    def _setup_variable_monitors(self, text: str) -> None:
        for var_string in self._get_text_vars(text):
            if '|' not in var_string:
                self.add_player_var_handler(name=var_string)
                self.add_current_player_handler()
            else:
                source, variable_name = var_string.split('|')
                if source.lower().startswith('player'):

                    if source.lstrip('player'):  # we have player num
                        self.add_player_var_handler(name=variable_name)
                    else:  # no player num
                        self.add_player_var_handler(name=var_string)
                        self.add_current_player_handler()
                elif source.lower() == 'machine':
                    self.add_machine_var_handler(name=variable_name)

    def add_player_var_handler(self, name: str) -> None:
        self.mc.events.add_handler('player_{}'.format(name),
                                   self._player_var_change)

    def add_current_player_handler(self) -> None:
        self.mc.events.add_handler('player_turn_start',
                                   self._player_var_change)

    def add_machine_var_handler(self, name: str) -> None:
        self.mc.events.add_handler('machine_var_{}'.format(name),
                                   self._machine_var_change)

    def prepare_for_removal(self) -> None:
        self.mc.events.remove_handler(self._player_var_change)
        self.mc.events.remove_handler(self._machine_var_change)

    @staticmethod
    def group_digits(text: str, separator: str=',', group_size: int=3) -> str:
        """Enables digit grouping (i.e. adds comma separators between
        thousands digits).

        Args:
            text: The incoming string of text
            separator: String of the character(s) you'd like to add between the
                digit groups. Default is a comma. (",")
            group_size: How many digits you want in each group. Default is 3.

        Returns: A string with the separator added.

        MPF uses this method instead of the Python locale settings because the
        locale settings are a mess. They're set system-wide and it's really
        hard
        to make them work cross-platform and there are all sorts of external
        dependencies, so this is just way easier.

        """
        digit_list = list(text.split('.')[0])

        for i in range(len(digit_list))[::-group_size][1:]:
            digit_list.insert(i + 1, separator)

        return ''.join(digit_list)

    #
    # Properties
    #

    disabled_color = ListProperty([1, 1, 1, .3])
    '''The color of the text when the widget is disabled, in the (r, g, b, a)
    format.

    .. versionadded:: 1.8.0

    :attr:`disabled_color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [1, 1, 1, .3].
    '''

    def _get_text(self) -> str:
        return self._label.text

    def _set_text(self, text: str) -> None:
        self._label.text = text

    text = AliasProperty(_get_text, _set_text)
    '''Text of the label.
    '''

    def _get_text_size(self) -> list:
        return self._label.text_size

    def _set_text_size(self, text_size: list) -> None:
        self._label.text_size = text_size

    text_size = AliasProperty(_get_text_size, _set_text_size)
    '''By default, the label is not constrained to any bounding box.
    You can set the size constraint of the label with this property.
    The text will autoflow into the constraints. So although the font size
    will not be reduced, the text will be arranged to fit into the box as best
    as possible, with any text still outside the box clipped.

    This sets and clips :attr:`texture_size` to text_size if not None.

    .. versionadded:: 1.0.4

    For example, whatever your current widget size is, if you want the label to
    be created in a box with width=200 and unlimited height::

        Label(text='Very big big line', text_size=(200, None))

    .. note::

        This text_size property is the same as the
        :attr:`~kivy.core.text.Label.usersize` property in the
        :class:`~kivy.core.text.Label` class. (It is named size= in the
        constructor.)

    :attr:`text_size` is a :class:`~kivy.properties.ListProperty` and
    defaults to (None, None), meaning no size restriction by default.
    '''

    def _get_font_name(self) -> str:
        return self._label.font_name

    def _set_font_name(self, font_name: str) -> None:
        self._label.font_name = font_name

    font_name = AliasProperty(_get_font_name, _set_font_name)
    '''Filename of the font to use. The path can be absolute or relative.
    Relative paths are resolved by the :func:`~kivy.resources.resource_find`
    function.

    .. warning::

        Depending of your text provider, the font file can be ignored. However,
        you can mostly use this without problems.

        If the font used lacks the glyphs for the particular language/symbols
        you are using, you will see '[]' blank box characters instead of the
        actual glyphs. The solution is to use a font that has the glyphs you
        need to display. For example, to display |unicodechar|, use a font such
        as freesans.ttf that has the glyph.

        .. |unicodechar| image:: images/unicode-char.png

    :attr:`font_name` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'Roboto'. This value is taken
    from :class:`~kivy.config.Config`.
    '''

    def _get_font_size(self):
        return self._label.font_size

    def _set_font_size(self, font_size) -> None:
        self._label.font_size = font_size

    font_size = AliasProperty(_get_font_size, _set_font_size)
    '''Font size of the text, in pixels.

    :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 15sp.
    '''

    def _get_line_height(self) -> float:
        return self._label.line_height

    def _set_line_height(self, line_height: float) -> None:
        self._label.line_height = line_height

    bitmap_font = BooleanProperty(False)
    '''Flag indicating whether or not the font_name attribute refers to a 
    bitmap font.'''

    line_height = AliasProperty(_get_line_height, _set_line_height)
    '''Line Height for the text. e.g. line_height = 2 will cause the spacing
    between lines to be twice the size.

    :attr:`line_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1.0.

    .. versionadded:: 1.5.0
    '''

    def _get_bold(self) -> bool:
        return self._label.bold

    def _set_bold(self, bold: bool) -> None:
        self._label.bold = bold

    bold = AliasProperty(_get_bold, _set_bold)
    '''Indicates use of the bold version of your font.

    .. note::

        Depending of your font, the bold attribute may have no impact on your
        text rendering.

    :attr:`bold` is a :class:`~kivy.properties.BooleanProperty` and defaults to
    False.
    '''

    def _get_italic(self) -> bool:
        return self._label.italic

    def _set_italic(self, italic: bool) -> None:
        self._label.italic = italic

    italic = AliasProperty(_get_italic, _set_italic)
    '''Indicates use of the italic version of your font.

    .. note::

        Depending of your font, the italic attribute may have no impact on your
        text rendering.

    :attr:`italic` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    def _get_underline(self) -> bool:
        return self._label.underline

    def _set_underline(self, underline: bool) -> None:
        self._label.underline = underline

    underline = AliasProperty(_get_underline, _set_underline)
    '''Adds an underline to the text.

    .. note::
        This feature requires the SDL2 text provider.

    .. versionadded:: 1.10.0

    :attr:`underline` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    def _get_strikethrough(self) -> bool:
        return self._label.strikethrough

    def _set_strikethrough(self, strikethrough: bool) -> None:
        self._label.strikethrough = strikethrough

    strikethrough = AliasProperty(_get_strikethrough, _set_strikethrough)
    '''Adds a strikethrough line to the text.

    .. note::
        This feature requires the SDL2 text provider.
    '''

    def _get_padding_x(self):
        return self._label.padding_x

    def _set_padding_x(self, padding_x):
        self._label.padding_x = padding_x

    padding_x = AliasProperty(_get_padding_x, _set_padding_x)
    '''Horizontal padding of the text inside the widget box.
    '''

    def _get_padding_y(self):
        return self._label.padding_y

    def _set_padding_y(self, padding_y):
        self._label.padding_y = padding_y

    padding_y = AliasProperty(_get_padding_y, _set_padding_y)
    '''Vertical padding of the text inside the widget box.
    '''

    padding = ReferenceListProperty(padding_x, padding_y)
    '''Padding of the text in the format (padding_x, padding_y)

    :attr:`padding` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`padding_x`, :attr:`padding_y`) properties.
    '''

    def _get_halign(self) -> str:
        return self._label.halign

    def _set_halign(self, halign: str) -> None:
        self._label.halign = halign

    halign = AliasProperty(_get_halign, _set_halign)
    '''Horizontal alignment of the text. Available options are : left, center, 
    right and justify.

    .. warning::

        This doesn't change the position of the text texture of the Label
        (centered), only the position of the text in this texture. You probably
        want to bind the size of the Label to the :attr:`texture_size` or set a
        :attr:`text_size`.
    '''

    def _get_valign(self) -> str:
        return self._label.valign

    def _set_valign(self, valign: str) -> None:
        self._label.valign = valign

    valign = AliasProperty(_get_valign, _set_valign)
    '''Vertical alignment of the text. Available options are : `'bottom'`,
    `'middle'` (or `'center'`) and `'top'`.

    .. warning::

        This doesn't change the position of the text texture of the Label
        (centered), only the position of the text within this texture. You
        probably want to bind the size of the Label to the :attr:`texture_size`
        or set a :attr:`text_size` to change this behavior.
    '''

    def _get_outline_width(self) -> Optional[int]:
        return self._label.outline_width

    def _set_outline_width(self, outline_width: Optional[int]) -> None:
        self._label.outline_width = outline_width

    outline_width = AliasProperty(_get_outline_width, _set_outline_width)
    '''Width in pixels for the outline around the text. No outline will be
    rendered if the value is None.

    .. note::
        This feature requires the SDL2 text provider.
    '''

    def _get_outline_color(self) -> list:
        return self._label.outline_color

    def _set_outline_color(self, outline_color: list) -> None:
        self._label.outline_color = outline_color

    outline_color = AliasProperty(_get_outline_color, _set_outline_color)
    '''The color of the text outline, in the (r, g, b) format.

    .. note::
        This feature requires the SDL2 text provider.
    '''

    def _get_disabled_outline_color(self) -> list:
        return self._label.disabled_outline_color

    def _set_disabled_outline_color(self, disabled_outline_color: list) -> None:
        self._label.disabled_outline_color = disabled_outline_color

    disabled_outline_color = AliasProperty(_get_disabled_outline_color, _set_disabled_outline_color)
    '''The color of the text outline when the widget is disabled, in the
    (r, g, b) format.

    .. note::
        This feature requires the SDL2 text provider.
    '''

    def _get_mipmap(self) -> bool:
        return self._label.mipmap

    def _set_mipmap(self, mipmap: bool) -> None:
        self._label.mipmap = mipmap

    mipmap = AliasProperty(_get_mipmap, _set_mipmap)
    '''Indicates whether OpenGL mipmapping is applied to the texture or not.
    Read :ref:`mipmap` for more information.
    '''

    def _get_shorten(self) -> bool:
        return self._label.shorten

    def _set_shorten(self, shorten: bool) -> None:
        self._label.shorten = shorten

    shorten = AliasProperty(_get_shorten, _set_shorten)
    '''
    Indicates whether the label should attempt to shorten its textual contents
    as much as possible if a :attr:`text_size` is given. Setting this to True
    without an appropriately set :attr:`text_size` will lead to unexpected
    results.

    :attr:`shorten_from` and :attr:`split_str` control the direction from
    which the :attr:`text` is split, as well as where in the :attr:`text` we
    are allowed to split.
    '''

    def _get_shorten_from(self) -> str:
        return self._label.shorten_from

    def _set_shorten_from(self, shorten_from: str) -> None:
        self._label.shorten_from = shorten_from

    shorten_from = AliasProperty(_get_shorten_from, _set_shorten_from)
    '''The side from which we should shorten the text from, can be left,
    right, or center.

    For example, if left, the ellipsis will appear towards the left side and we
    will display as much text starting from the right as possible. Similar to
    :attr:`shorten`, this option only applies when :attr:`text_size` [0] is
    not None, In this case, the string is shortened to fit within the specified
    width.
    '''

    def _get_is_shortened(self) -> bool:
        return self._label.is_shortened

    is_shortened = AliasProperty(_get_is_shortened, None)
    '''This property indicates if :attr:`text` was rendered with or without
    shortening when :attr:`shorten` is True.
    '''

    def _get_split_str(self) -> str:
        return self._label.split_str

    def _set_split_str(self, split_str: str) -> None:
        self._label.split_str = split_str

    split_str = AliasProperty(_get_split_str, _set_split_str)
    '''The string used to split the :attr:`text` while shortening the string
    when :attr:`shorten` is True.

    For example, if it's a space, the string will be broken into words and as
    many whole words that can fit into a single line will be displayed. If
    :attr:`split_str` is the empty string, `''`, we split on every character
    fitting as much text as possible into the line.
    '''

    def _get_ellipsis_options(self) -> dict:
        return self._label.ellipsis_options

    def _set_ellipsis_options(self, ellipsis_options: dict) -> None:
        self._label.ellipsis_options = ellipsis_options

    ellipsis_options = AliasProperty(_get_ellipsis_options, _set_ellipsis_options)
    '''Font options for the ellipsis string('...') used to split the text.

    Accepts a dict as option name with the value. Only applied when
    :attr:`markup` is true and text is shortened. All font options which work
    for :class:`Label` will work for :attr:`ellipsis_options`. Defaults for
    the options not specified are taken from the surronding text.

    .. code-block:: kv

        Label:
            text: 'Some very long line which will be cut'
            markup: True
            shorten: True
            ellipsis_options: {'color':(1,0.5,0.5,1),'underline':True}
    '''

    def _get_unicode_errors(self) -> str:
        return self._label.unicode_errors

    def _set_unicode_errors(self, unicode_errors: str) -> None:
        self._label.unicode_errors = unicode_errors

    unicode_errors = AliasProperty(_get_unicode_errors, _set_unicode_errors)
    '''How to handle unicode decode errors. Can be `'strict'`, `'replace'` or
    `'ignore'`.
    '''

    def _get_markup(self) -> bool:
        return self._label.markup

    def _set_markup(self, markup: bool) -> None:
        self._label.markup = markup

    markup = AliasProperty(_get_markup, _set_markup)
    '''If True, the text will be rendered using the
    :class:`~kivy.core.text.markup.MarkupLabel`: you can change the
    style of the text using tags. Check the
    :doc:`api-kivy.core.text.markup` documentation for more information.
    '''

    def _get_refs(self) -> dict:
        return self._label.refs

    refs = AliasProperty(_get_refs, None)
    '''List of ``[ref=xxx]`` markup items in the text with the bounding box of
    all the words contained in a ref, available only after rendering.

    For example, if you wrote::

        Check out my [ref=hello]link[/ref]

    The refs will be set with::

        {'hello': ((64, 0, 78, 16), )}

    The references marked "hello" have a bounding box at (x1, y1, x2, y2).
    These co-ordinates are relative to the top left corner of the text, with
    the y value increasing downwards. You can define multiple refs with the
    same name: each occurrence will be added as another (x1, y1, x2, y2) tuple
    to this list.

    The current Label implementation uses these references if they exist in
    your markup text, automatically doing the collision with the touch and
    dispatching an `on_ref_press` event.

    You can bind a ref event like this::

        def print_it(instance, value):
            print('User click on', value)
        widget = Label(text='Hello [ref=world]World[/ref]', markup=True)
        widget.on_ref_press(print_it)

    .. note::

        This works only with markup text. You need :attr:`markup` set to
        True.
    '''

    def _get_max_lines(self) -> Optional[int]:
        return self._label.max_lines

    def _set_max_lines(self, max_lines: Optional[int]) -> None:
        self._label.max_lines = max_lines

    max_lines = AliasProperty(_get_max_lines, _set_max_lines)
    '''Maximum number of lines to use, defaults to 0, which means unlimited.
    Please note that :attr:`shorten` take over this property. (with
    shorten, the text is always one line.)
    '''

    def _get_strip(self) -> bool:
        return self._label.strip

    def _set_strip(self, strip: bool) -> None:
        self._label.strip = strip

    strip = AliasProperty(_get_strip, _set_strip)
    '''Whether leading and trailing spaces and newlines should be stripped from
    each displayed line. If True, every line will start at the right or left
    edge, depending on :attr:`halign`. If :attr:`halign` is `justify` it is
    implicitly True.

    .. versionadded:: 1.9.0

    :attr:`strip` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    def _get_font_hinting(self) -> Optional[str]:
        return self._label.font_hinting

    def _set_font_hinting(self, font_hinting: Optional[str]):
        self._label.font_hinting = font_hinting

    font_hinting = AliasProperty(_get_font_hinting, _set_font_hinting)
    '''What hinting option to use for font rendering.
    Can be one of `'normal'`, `'light'`, `'mono'` or None.

    .. note::
        This feature requires the SDL2 text provider.
    '''

    def _get_font_kerning(self) -> bool:
        return self._label.font_kerning

    def _set_font_kerning(self, font_kerning: bool) -> None:
        self._label.font_kerning = font_kerning

    font_kerning = AliasProperty(_get_font_kerning, _set_font_kerning)
    '''Whether kerning is enabled for font rendering.

    .. note::
        This feature requires the SDL2 text provider.
    '''

    def _get_font_blended(self) -> bool:
        return self._label.font_blended

    def _set_font_blended(self, font_blended: bool) -> None:
        self._label.font_blended = font_blended

    font_blended = AliasProperty(_get_font_blended, _set_font_blended)
    '''Whether blended or solid font rendering should be used.

    .. note::
        This feature requires the SDL2 text provider.
    '''

    rotation = NumericProperty(0)
    '''Rotation angle value of the widget.

    :attr:`rotation` is an :class:`~kivy.properties.NumericProperty` and defaults to
    0.
    '''

    scale = NumericProperty(1.0)
    '''Scale value of the widget.
Esempio n. 12
0
class Dummy(ImageStack):
    """A widget that looks like the ones on the board, which, when dragged
    onto the board, creates one of them.

    """
    _touch = ObjectProperty(None, allownone=True)
    name = StringProperty()
    prefix = StringProperty()
    num = NumericProperty()
    x_start = NumericProperty(0)
    y_start = NumericProperty(0)
    pos_start = ReferenceListProperty(x_start, y_start)
    x_down = NumericProperty(0)
    y_down = NumericProperty(0)
    pos_down = ReferenceListProperty(x_down, y_down)
    x_up = NumericProperty(0)
    y_up = NumericProperty(0)
    pos_up = ReferenceListProperty(x_up, y_up)
    x_center_up = NumericProperty(0)
    y_center_up = NumericProperty(0)
    center_up = ReferenceListProperty(x_center_up, y_center_up)
    right_up = NumericProperty(0)
    top_up = NumericProperty(0)

    def on_paths(self, *args, **kwargs):
        super().on_paths(*args, **kwargs)
        Logger.debug("Dummy: {} got paths {}".format(self.name, self.paths))

    def on_touch_down(self, touch):
        """If hit, record my starting position, that I may return to it in
        ``on_touch_up`` after creating a real :class:`board.Spot` or
        :class:`board.Pawn` instance.

        """
        if not self.collide_point(*touch.pos):
            return False
        self.pos_start = self.pos
        self.pos_down = (self.x - touch.x, self.y - touch.y)
        touch.grab(self)
        self._touch = touch
        return True

    def on_touch_move(self, touch):
        """Follow the touch"""
        if touch is not self._touch:
            return False
        self.pos = (touch.x + self.x_down, touch.y + self.y_down)
        return True

    def on_touch_up(self, touch):
        """Return to ``pos_start``, but first, save my current ``pos`` into
        ``pos_up``, so that the layout knows where to put the real
        :class:`board.Spot` or :class:`board.Pawn` instance.

        """
        if touch is not self._touch:
            return False
        self.pos_up = self.pos
        self.pos = self.pos_start
        self._touch = None
        return True
Esempio n. 13
0
class FlexSlider(Widget):
    value = NumericProperty(0.)
    min = NumericProperty(0.)
    max = NumericProperty(100.)
    padding = NumericProperty('16sp')  # default: 16sp
    orientation = OptionProperty('horizontal',
                                 options=('vertical', 'horizontal'))
    range = ReferenceListProperty(min, max)
    step = BoundedNumericProperty(0, min=0)
    background_horizontal = StringProperty(
        'atlas://data/images/defaulttheme/sliderh_background')
    background_disabled_horizontal = StringProperty(
        'atlas://data/images/defaulttheme/sliderh_background_disabled')
    background_vertical = StringProperty(
        'atlas://data/images/defaulttheme/sliderv_background')
    background_disabled_vertical = StringProperty(
        'atlas://data/images/defaulttheme/sliderv_background_disabled')
    background_width = NumericProperty('36sp')
    cursor_image = StringProperty(
        'atlas://data/images/defaulttheme/slider_cursor')
    cursor_disabled_image = StringProperty(
        'atlas://data/images/defaulttheme/slider_cursor_disabled')
    cursor_width = NumericProperty('32sp')
    cursor_height = NumericProperty('32sp')
    cursor_size = ReferenceListProperty(cursor_width, cursor_height)
    border_horizontal = ListProperty([0, 18, 0, 18])
    border_vertical = ListProperty([18, 0, 18, 0])
    value_track = BooleanProperty(False)
    value_track_color = ListProperty([1, 1, 1, 1])
    value_track_width = NumericProperty('3dp')
    sensitivity = OptionProperty('all', options=('all', 'handle'))

    def on_min(self, *largs):
        self.value = min(self.max, max(self.min, self.value))

    def on_max(self, *largs):
        self.value = min(self.max, max(self.min, self.value))

    def get_norm_value(self):
        vmin = self.min
        d = self.max - vmin
        if d == 0:
            return 0
        return (self.value - vmin) / float(d)

    def set_norm_value(self, value):
        vmin = self.min
        vmax = self.max
        step = self.step
        val = min(value * (vmax - vmin) + vmin, vmax)
        if step == 0:
            self.value = val
        else:
            self.value = min(round((val - vmin) / step) * step + vmin, vmax)

    value_normalized = AliasProperty(get_norm_value,
                                     set_norm_value,
                                     bind=('value', 'min', 'max'),
                                     cache=True)

    def get_value_pos(self):
        padding = self.padding
        x = self.x
        y = self.y
        nval = self.value_normalized
        if self.orientation == 'horizontal':
            return (x + padding + nval * (self.width - 2 * padding), y)
        else:
            return (x, y + padding + nval * (self.height - 2 * padding))

    def set_value_pos(self, pos):
        padding = self.padding
        x = min(self.right - padding, max(pos[0], self.x + padding))
        y = min(self.top - padding, max(pos[1], self.y + padding))
        if self.orientation == 'horizontal':
            if self.width == 0:
                self.value_normalized = 0
            else:
                self.value_normalized = (
                    x - self.x - padding) / float(self.width - 2 * padding)
        else:
            if self.height == 0:
                self.value_normalized = 0
            else:
                self.value_normalized = (
                    y - self.y - padding) / float(self.height - 2 * padding)

    value_pos = AliasProperty(get_value_pos,
                              set_value_pos,
                              bind=('pos', 'size', 'min', 'max', 'padding',
                                    'value_normalized', 'orientation'),
                              cache=True)

    def on_touch_down(self, touch):
        if self.disabled or not self.collide_point(*touch.pos):
            return
        if touch.is_mouse_scrolling:
            if 'down' in touch.button or 'left' in touch.button:
                if self.step:
                    self.value = min(self.max, self.value + self.step)
                else:
                    self.value = min(self.max,
                                     self.value + (self.max - self.min) / 20)
            if 'up' in touch.button or 'right' in touch.button:
                if self.step:
                    self.value = max(self.min, self.value - self.step)
                else:
                    self.value = max(self.min,
                                     self.value - (self.max - self.min) / 20)
        elif self.sensitivity == 'handle':
            if self.children[0].collide_point(*touch.pos):
                touch.grab(self)
        else:
            touch.grab(self)
            self.value_pos = touch.pos
        return True

    def on_touch_move(self, touch):
        if touch.grab_current == self:
            self.value_pos = touch.pos
            self.loading_value_pos = touch.pos[0] - 10, touch.pos[1]
            return True

    def on_touch_up(self, touch):
        if touch.grab_current == self:
            self.value_pos = touch.pos
            return True
Esempio n. 14
0
class Slider(Widget):
    """Class for creating a Slider widget.

    Check module documentation for more details.
    """

    value = NumericProperty(0.)
    '''Current value used for the slider.

    :attr:`value` is a :class:`~kivy.properties.NumericProperty` and defaults
    to 0.'''

    min = NumericProperty(0.)
    '''Minimum value allowed for :attr:`value`.

    :attr:`min` is a :class:`~kivy.properties.NumericProperty` and defaults to
    0.'''

    max = NumericProperty(100.)
    '''Maximum value allowed for :attr:`value`.

    :attr:`max` is a :class:`~kivy.properties.NumericProperty` and defaults to
    100.'''

    padding = NumericProperty(sp(16))
    '''Padding of the slider. The padding is used for graphical representation
    and interaction. It prevents the cursor from going out of the bounds of the
    slider bounding box.

    By default, padding is sp(16). The range of the slider is reduced from
    padding \*2 on the screen. It allows drawing the default cursor of sp(32)
    width without having the cursor go out of the widget.

    :attr:`padding` is a :class:`~kivy.properties.NumericProperty` and defaults
    to sp(16).'''

    orientation = OptionProperty('horizontal',
                                 options=('vertical', 'horizontal'))
    '''Orientation of the slider.

    :attr:`orientation` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'horizontal'. Can take a value of 'vertical' or 'horizontal'.
    '''

    range = ReferenceListProperty(min, max)
    '''Range of the slider in the format (minimum value, maximum value)::

        >>> slider = Slider(min=10, max=80)
        >>> slider.range
        [10, 80]
        >>> slider.range = (20, 100)
        >>> slider.min
        20
        >>> slider.max
        100

    :attr:`range` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`min`, :attr:`max`) properties.
    '''

    step = BoundedNumericProperty(0, min=0)
    '''Step size of the slider.

    .. versionadded:: 1.4.0

    Determines the size of each interval or step the slider takes between
    min and max. If the value range can't be evenly divisible by step the
    last step will be capped by slider.max

    :attr:`step` is a :class:`~kivy.properties.NumericProperty` and defaults
    to 1.'''

    # The following two methods constrain the slider's value
    # to range(min,max). Otherwise it may happen that self.value < self.min
    # at init.

    def on_min(self, *largs):
        self.value = min(self.max, max(self.min, self.value))

    def on_max(self, *largs):
        self.value = min(self.max, max(self.min, self.value))

    def get_norm_value(self):
        vmin = self.min
        d = self.max - vmin
        if d == 0:
            return 0
        return (self.value - vmin) / float(d)

    def set_norm_value(self, value):
        vmin = self.min
        vmax = self.max
        step = self.step
        val = min(value * (vmax - vmin) + vmin, vmax)
        if step == 0:
            self.value = val
        else:
            self.value = min(round((val - vmin) / step) * step + vmin, vmax)

    value_normalized = AliasProperty(get_norm_value,
                                     set_norm_value,
                                     bind=('value', 'min', 'max', 'step'))
    '''Normalized value inside the :attr:`range` (min/max) to 0-1 range::

        >>> slider = Slider(value=50, min=0, max=100)
        >>> slider.value
        50
        >>> slider.value_normalized
        0.5
        >>> slider.value = 0
        >>> slider.value_normalized
        0
        >>> slider.value = 100
        >>> slider.value_normalized
        1

    You can also use it for setting the real value without knowing the minimum
    and maximum::

        >>> slider = Slider(min=0, max=200)
        >>> slider.value_normalized = .5
        >>> slider.value
        100
        >>> slider.value_normalized = 1.
        >>> slider.value
        200

    :attr:`value_normalized` is an :class:`~kivy.properties.AliasProperty`.
    '''

    def get_value_pos(self):
        padding = self.padding
        x = self.x
        y = self.y
        nval = self.value_normalized
        if self.orientation == 'horizontal':
            return (x + padding + nval * (self.width - 2 * padding), y)
        else:
            return (x, y + padding + nval * (self.height - 2 * padding))

    def set_value_pos(self, pos):
        padding = self.padding
        x = min(self.right - padding, max(pos[0], self.x + padding))
        y = min(self.top - padding, max(pos[1], self.y + padding))
        if self.orientation == 'horizontal':
            if self.width == 0:
                self.value_normalized = 0
            else:
                self.value_normalized = (
                    x - self.x - padding) / float(self.width - 2 * padding)
        else:
            if self.height == 0:
                self.value_normalized = 0
            else:
                self.value_normalized = (
                    y - self.y - padding) / float(self.height - 2 * padding)

    value_pos = AliasProperty(get_value_pos,
                              set_value_pos,
                              bind=('x', 'y', 'width', 'height', 'min', 'max',
                                    'value_normalized', 'orientation'))
    '''Position of the internal cursor, based on the normalized value.

    :attr:`value_pos` is an :class:`~kivy.properties.AliasProperty`.
    '''

    def on_touch_down(self, touch):
        if self.disabled or not self.collide_point(*touch.pos):
            return
        if touch.is_mouse_scrolling:
            if 'down' in touch.button or 'left' in touch.button:
                if self.step:
                    self.value = min(self.max, self.value + self.step)
                else:
                    self.value = min(self.max,
                                     self.value + (self.max - self.min) / 20)
            if 'up' in touch.button or 'right' in touch.button:
                if self.step:
                    self.value = max(self.min, self.value - self.step)
                else:
                    self.value = max(self.min,
                                     self.value - (self.max - self.min) / 20)
        else:
            touch.grab(self)
            self.value_pos = touch.pos
        return True

    def on_touch_move(self, touch):
        if touch.grab_current == self:
            self.value_pos = touch.pos
            return True

    def on_touch_up(self, touch):
        if touch.grab_current == self:
            self.value_pos = touch.pos
            return True
class Car(Widget):

    angle = NumericProperty(0)  # inicialização do ângulo do carro
    rotation = NumericProperty(
        0
    )  # inicialização da última rotação do carro (depois de uma ação, o caro faz uma rotação de 0, 20 ou -20 graus)
    velocity_x = NumericProperty(
        0)  # inicialização da coordenada de velocidade x
    velocity_y = NumericProperty(
        0)  # inicialização da coordenada de velocidade y
    velocity = ReferenceListProperty(
        velocity_x, velocity_y)  # vetor com a velocidade x e y
    sensor1_x = NumericProperty(
        0)  # inicialização da coordenada x do primeiro sensor (frente)
    sensor1_y = NumericProperty(
        0)  # inicialização da coordenadao y do primeiro sensor (frente)
    sensor1 = ReferenceListProperty(sensor1_x, sensor1_y)  # primeiro sensor
    sensor2_x = NumericProperty(
        0
    )  # inicialização da coordenada x do segundo sensor (30 graus para a esquerda)
    sensor2_y = NumericProperty(
        0
    )  # inicialização da coordenada y do segundo sensor (30 graus para a esquerda)
    sensor2 = ReferenceListProperty(sensor2_x, sensor2_y)  # segundo sensor
    sensor3_x = NumericProperty(
        0
    )  # inicialização da coordenada x do terceiro sensor (30 graus para a direita)
    sensor3_y = NumericProperty(
        0
    )  # inicialização da coordenada y do terceiro sensor (30 graus para a direita)
    sensor3 = ReferenceListProperty(sensor3_x, sensor3_y)  # terceiro sensor
    signal1 = NumericProperty(
        0)  # inicialização do sinal recebido pelo sensor 1
    signal2 = NumericProperty(
        0)  # inicialização do sinal recebido pelo sensor 2
    signal3 = NumericProperty(
        0)  # inicialização do sinal recebido pelo sensor 3

    def move(self, rotation):
        self.pos = Vector(
            *self.velocity
        ) + self.pos  # atualiza a posição do carro de acordo com sua última posição e velocidade
        self.rotation = rotation  # busca a rotação do carro
        self.angle = self.angle + self.rotation  # atualiza o ângulo
        self.sensor1 = Vector(30, 0).rotate(
            self.angle) + self.pos  # atualiza a posição do sensor 1
        self.sensor2 = Vector(30, 0).rotate(
            (self.angle + 30) %
            360) + self.pos  # atualiza a posição do sensor 2
        self.sensor3 = Vector(30, 0).rotate(
            (self.angle - 30) %
            360) + self.pos  # atualiza a posição do sensor 3
        self.signal1 = int(
            np.sum(sand[int(self.sensor1_x) - 10:int(self.sensor1_x) + 10,
                        int(self.sensor1_y) - 10:int(self.sensor1_y) + 10])
        ) / 400.  # calcula o sinal recebido do sensor 1 (densidade de areia ao redor do sensor 1)
        self.signal2 = int(
            np.sum(sand[int(self.sensor2_x) - 10:int(self.sensor2_x) + 10,
                        int(self.sensor2_y) - 10:int(self.sensor2_y) + 10])
        ) / 400.  # calcula o sinal recebido do sensor 1 (densidade de areia ao redor do sensor 2)
        self.signal3 = int(
            np.sum(sand[int(self.sensor3_x) - 10:int(self.sensor3_x) + 10,
                        int(self.sensor3_y) - 10:int(self.sensor3_y) + 10])
        ) / 400.  # calcula o sinal recebido do sensor 1 (densidade de areia ao redor do sensor 3)
        if self.sensor1_x > longueur - 10 or self.sensor1_x < 10 or self.sensor1_y > largeur - 10 or self.sensor1_y < 10:  # se o sensor 1 saiu do mapa
            self.signal1 = 1.  # sensor 1 detecta areia
        if self.sensor2_x > longueur - 10 or self.sensor2_x < 10 or self.sensor2_y > largeur - 10 or self.sensor2_y < 10:  # se o sensor 2 saiu do mapa
            self.signal2 = 1.  # sensor 2 detecta areia
        if self.sensor3_x > longueur - 10 or self.sensor3_x < 10 or self.sensor3_y > largeur - 10 or self.sensor3_y < 10:  # se o sensor 3 saiu do mapa
            self.signal3 = 1.  # sensor 3 detecta areia
Esempio n. 16
0
class BackgroundColorBehavior(CommonElevationBehavior):
    background = StringProperty()
    """
    Background image path.

    :attr:`background` is a :class:`~kivy.properties.StringProperty`
    and defaults to `None`.
    """

    r = BoundedNumericProperty(1.0, min=0.0, max=1.0)
    """
    The value of ``red`` in the ``rgba`` palette.

    :attr:`r` is an :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to `1.0`.
    """

    g = BoundedNumericProperty(1.0, min=0.0, max=1.0)
    """
    The value of ``green`` in the ``rgba`` palette.

    :attr:`g` is an :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to `1.0`.
    """

    b = BoundedNumericProperty(1.0, min=0.0, max=1.0)
    """
    The value of ``blue`` in the ``rgba`` palette.

    :attr:`b` is an :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to `1.0`.
    """

    a = BoundedNumericProperty(0.0, min=0.0, max=1.0)
    """
    The value of ``alpha channel`` in the ``rgba`` palette.

    :attr:`a` is an :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to `0.0`.
    """

    radius = VariableListProperty([0], length=4)
    """
    Canvas radius.

    .. code-block:: python

        # Top left corner slice.
        MDBoxLayout:
            md_bg_color: app.theme_cls.primary_color
            radius: [25, 0, 0, 0]

    :attr:`radius` is an :class:`~kivy.properties.VariableListProperty`
    and defaults to `[0, 0, 0, 0]`.
    """

    md_bg_color = ReferenceListProperty(r, g, b, a)
    """
    The background color of the widget (:class:`~kivy.uix.widget.Widget`)
    that will be inherited from the :attr:`BackgroundColorBehavior` class.

    For example:

    .. code-block:: kv

        Widget:
            canvas:
                Color:
                    rgba: 0, 1, 1, 1
                Rectangle:
                    size: self.size
                    pos: self.pos

    similar to code:

    .. code-block:: kv

        <MyWidget@BackgroundColorBehavior>
            md_bg_color: 0, 1, 1, 1

    :attr:`md_bg_color` is an :class:`~kivy.properties.ReferenceListProperty`
    and defaults to :attr:`r`, :attr:`g`, :attr:`b`, :attr:`a`.
    """

    line_color = ColorProperty([0, 0, 0, 0])
    """
    If a custom value is specified for the `line_color parameter`, the border
    of the specified color will be used to border the widget:

    .. code-block:: kv

        MDBoxLayout:
            size_hint: .5, .2
            md_bg_color: 0, 1, 1, .5
            line_color: 0, 0, 1, 1
            radius: [24, ]

    .. versionadded:: 0.104.2

    :attr:`line_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `[0, 0, 0, 0]`.
    """

    line_width = NumericProperty(1)
    """
    Border of the specified width will be used to border the widget.

    .. versionadded:: 1.0.0

    :attr:`line_width` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `1`.
    """

    angle = NumericProperty(0)
    background_origin = ListProperty(None)

    _background_x = NumericProperty(0)
    _background_y = NumericProperty(0)
    _background_origin = ReferenceListProperty(
        _background_x,
        _background_y,
    )

    def __init__(self, **kwarg):
        super().__init__(**kwarg)
        self.bind(pos=self.update_background_origin)

    def update_background_origin(self, instance_md_widget,
                                 pos: List[float]) -> None:
        if self.background_origin:
            self._background_origin = self.background_origin
        else:
            self._background_origin = self.center
Esempio n. 17
0
class Pallet(StackLayout):
    """Many :class:`SwatchButton`, gathered from an :class:`kivy.atlas.Atlas`."""
    atlas = ObjectProperty()
    """:class:`kivy.atlas.Atlas` object I'll make :class:`SwatchButton` from."""
    filename = StringProperty()
    """Path to an atlas; will construct :class:`kivy.atlas.Atlas` when set"""
    swatches = DictProperty({})
    """:class:`SwatchButton` widgets here, keyed by name of their graphic"""
    swatch_width = NumericProperty(100)
    """Width of each and every :class:`SwatchButton` here"""
    swatch_height = NumericProperty(75)
    """Height of each and every :class:`SwatchButton` here"""
    swatch_size = ReferenceListProperty(swatch_width, swatch_height)
    """Size of each and every :class:`SwatchButton` here"""
    selection = ListProperty([])
    """List of :class:`SwatchButton`s that are selected"""
    selection_mode = OptionProperty('single', options=['single', 'multiple'])
    """Whether to allow only a 'single' selected :class:`SwatchButton` (default), or 'multiple'"""

    def on_selection(self, *args):
        Logger.debug(
            'Pallet: {} got selection {}'.format(
                self.filename, self.selection
            )
        )

    def on_filename(self, *args):
        if not self.filename:
            return
        resource = resource_find(self.filename)
        if not resource:
            raise ValueError("Couldn't find atlas: {}".format(self.filename))
        self.atlas = Atlas(resource)

    def on_atlas(self, *args):
        if self.atlas is None:
            return
        self.upd_textures()
        self.atlas.bind(textures=self._trigger_upd_textures)

    def upd_textures(self, *args):
        """Create one :class:`SwatchButton` for each texture"""
        if self.canvas is None:
            Clock.schedule_once(self.upd_textures, 0)
            return
        swatches = self.swatches
        atlas_textures = self.atlas.textures
        remove_widget = self.remove_widget
        add_widget = self.add_widget
        swatch_size = self.swatch_size
        for name, swatch in list(swatches.items()):
            if name not in atlas_textures:
                remove_widget(swatch)
                del swatches[name]
        for (name, tex) in atlas_textures.items():
            if name in swatches and swatches[name] != tex:
                remove_widget(swatches[name])
            if name not in swatches or swatches[name] != tex:
                swatches[name] = SwatchButton(
                    text=name,
                    tex=tex,
                    size_hint=(None, None),
                    size=swatch_size
                )
                add_widget(swatches[name])

    def _trigger_upd_textures(self, *args):
        Clock.unschedule(self.upd_textures)
        Clock.schedule_once(self._trigger_upd_textures)
Esempio n. 18
0
class Piece(RelativeLayout, GridEntry):

    name = StringProperty('L')
    mode = OptionProperty('up', options=['up', 'dn', 'lt', 'rt'])
    brick_a = ObjectProperty(None)
    brick_b = ObjectProperty(None)
    brick_c = ObjectProperty(None)
    brick_0 = ObjectProperty(None)
    bricks = ReferenceListProperty(brick_a, brick_b, brick_c, brick_0)
    color = ListProperty([1, 1, 1, 1])

    def on_grid_pos(self, *args, **kwargs):
        self.pos_hint = {
            'x': self.size_hint[0] * self.column,
            'y': self.size_hint[1] * self.row
        }

    def fall(self, *args):
        self.row = self.row - 1

    def strifel(self):
        self.column = self.column - 1

    def strifer(self):
        self.column = self.column + 1

    def shift_left(self):
        if self.mode is 'up':
            self.mode = 'lt'
        elif self.mode is 'dn':
            self.mode = 'rt'
        elif self.mode is 'rt':
            self.mode = 'up'
        elif self.mode is 'lt':
            self.mode = 'dn'

    def shift_right(self):
        if self.mode is 'up':
            self.mode = 'rt'
        elif self.mode is 'dn':
            self.mode = 'lt'
        elif self.mode is 'rt':
            self.mode = 'dn'
        elif self.mode is 'lt':
            self.mode = 'up'

    def release_bricks(self):
        for brick in self.bricks:
            self.remove_widget(brick)

    def factory(name):
        if name is 'L':
            return LPiece()
        if name is 'J':
            return JPiece()
        if name is 'S':
            return SPiece()
        if name is 'O':
            return OPiece()
        if name is 'T':
            return TPiece()
        if name is 'I':
            return IPiece()
        if name is 'Z':
            return ZPiece()

    factory = staticmethod(factory)
Esempio n. 19
0
class CommonElevationBehavior(Widget):
    """Common base class for rectangular and circular elevation behavior."""

    elevation = BoundedNumericProperty(0, min=0, errorvalue=0)
    """
    Elevation of the widget.

    .. note::
        Although, this value does not represent the current elevation of the
        widget. :attr:`~CommonElevationBehavior._elevation` can be used to
        animate the current elevation and come back using the
        :attr:`~CommonElevationBehavior.elevation` property directly.

        For example:

        .. code-block:: python

            from kivy.lang import Builder
            from kivy.uix.behaviors import ButtonBehavior

            from kivymd.app import MDApp
            from kivymd.uix.behaviors import CircularElevationBehavior, CircularRippleBehavior
            from kivymd.uix.boxlayout import MDBoxLayout

            KV = '''
            #:import Animation kivy.animation.Animation


            <WidgetWithShadow>
                size_hint: [None, None]
                elevation: 6
                animation_: None
                md_bg_color: [1] * 4
                on_size:
                    self.radius = [self.height / 2] * 4
                on_press:
                    if self.animation_: \
                    self.animation_.cancel(self); \
                    self.animation_ = Animation(_elevation=self.elevation + 6, d=0.08); \
                    self.animation_.start(self)
                on_release:
                    if self.animation_: \
                    self.animation_.cancel(self); \
                    self.animation_ = Animation(_elevation = self.elevation, d=0.08); \
                    self.animation_.start(self)

            MDFloatLayout:

                WidgetWithShadow:
                    size: [root.size[1] / 2] * 2
                    pos_hint: {"center": [0.5, 0.5]}
            '''


            class WidgetWithShadow(
                CircularElevationBehavior,
                CircularRippleBehavior,
                ButtonBehavior,
                MDBoxLayout,
            ):
                def __init__(self, **kwargs):
                    # always set the elevation before the super().__init__ call
                    # self.elevation = 6
                    super().__init__(**kwargs)

                def on_size(self, *args):
                    self.radius = [self.size[0] / 2]


            class Example(MDApp):
                def build(self):
                    return Builder.load_string(KV)


            Example().run()

    :attr:`elevation` is an :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to `0`.
    """

    # Shadow rendering properties.
    # Shadow rotation memory - SHARED ACROSS OTHER CLASSES.
    angle = NumericProperty(0)
    """
    Angle of rotation in degrees of the current shadow.
    This value is shared across different widgets.

    .. note::
        This value will affect both, hard and soft shadows.
        Each shadow has his own origin point that's computed every time the
        elevation changes.

    .. warning::
        Do not add `PushMatrix` inside the canvas before and add `PopMatrix`
        in the next layer, this will cause visual errors, because the stack
        used will clip the push and pop matrix already inside the canvas.before
        canvas layer.

        Incorrect:

        .. code-block:: kv

            <TiltedWidget>
                canvas.before:
                    PushMatrix
                    [...]
                canvas:
                    PopMatrix

        Correct:

        .. code-block:: kv

            <TiltedWidget>
                canvas.before:
                    PushMatrix
                    [...]
                    PopMatrix



    :attr:`angle` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    radius = VariableListProperty([0])
    """
    Radius of the corners of the shadow.
    This values represents each corner of the shadow, starting from `top-left`
    corner and going clockwise.

    .. code-block:: python

        radius = [
            "top-left",
            "top-right",
            "bottom-right",
            "bottom-left",
        ]

    This value can be expanded thus allowing this settings to be valid:

    .. code-block:: python

        widget.radius=[0]  # Translates to [0, 0, 0, 0]
        widget.radius=[10, 3]  # Translates to [10, 3, 10, 3]
        widget.radius=[7.0, 8.7, 1.5, 3.0]  # Translates to [7, 8, 1, 3]

    .. note::
        This value will affect both, hard and soft shadows.
        This value only affects :class:`~RoundedRectangularElevationBehavior`
        for now, but can be stored and used by custom shadow draw functions.

    :attr:`radius` is an :class:`~kivy.properties.VariableListProperty`
    and defaults to `[0, 0, 0, 0]`.
    """

    # Position of the shadow.
    _shadow_origin_x = NumericProperty(0)
    """
    Shadow origin `x` position for the rotation origin.

    Managed by `_shadow_origin`.

    :attr:`_shadow_origin_x` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.

    .. note::
        This property is automatically processed. by _shadow_origin.
    """

    _shadow_origin_y = NumericProperty(0)
    """
    Shadow origin y position for the rotation origin.

    Managed by :attr:`_shadow_origin`.

    :attr:`_shadow_origin_y` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.

    .. note::
        This property is automatically processed.
    """

    _shadow_origin = ReferenceListProperty(_shadow_origin_x, _shadow_origin_y)
    """
    Soft shadow rotation origin point.

    :attr:`_shadow_origin` is an :class:`~kivy.properties.ReferenceListProperty`
    and defaults to `[0, 0]`.

    .. note::
        This property is automatically processed and relative to the canvas center.
    """

    _shadow_pos = ListProperty([0, 0])  # custom offset
    """
    Soft shadow origin point.

    :attr:`_shadow_pos` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0]`.

    .. note::
        This property is automatically processed and relative to the widget's
        canvas center.
    """

    shadow_pos = ListProperty([0, 0])  # bottom left corner
    """
    Custom shadow origin point. If this property is set, :attr:`_shadow_pos`
    will be ommited.

    This property allows users to fake light source.

    :attr:`shadow_pos` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0]`.

    .. note::
        this value overwrite the :attr:`_shadow_pos` processing.
    """

    # Shadow Group shared memory
    __shadow_groups = {"global": []}

    shadow_group = StringProperty("global")
    """
    Widget's shadow group.
    By default every widget with a shadow is saved inside the memory
    :attr:`__shadow_groups` as a weakref. This means that you can have multiple
    light sources, one for every shadow group.

    To fake a light source use :attr:`force_shadow_pos`.

    :attr:`shadow_group` is an :class:`~kivy.properties.StringProperty`
    and defaults to `"global"`.
    """

    _elevation = BoundedNumericProperty(0, min=0, errorvalue=0)
    """
    Current elevation of the widget.

    .. warning::
        This property is the current elevation of the widget, do not
        use this property directly, instead, use :class:`~CommonElevationBehavior`
        elevation.

    :attr:`_elevation` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    # soft shadow
    _soft_shadow_texture = ObjectProperty()
    """
    Texture of the soft shadow texture for the canvas.

    :attr:`_soft_shadow_texture` is an :class:`~kivy.core.image.Image`
    and defaults to `None`.

    .. note::
        This property is automatically processed.
    """

    soft_shadow_size = ListProperty([0, 0])
    """
    Size of the soft shadow texture over the canvas.

    :attr:`soft_shadow_size` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0]`.

    .. note::
        This property is automatically processed.
    """

    soft_shadow_pos = ListProperty([0, 0])
    """
    Position of the hard shadow texture over the canvas.

    :attr:`soft_shadow_pos` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0]`.

    .. note::
        This property is automatically processed.
    """

    soft_shadow_cl = ListProperty([0, 0, 0, 0.50])
    """
    Color of the soft shadow.

    :attr:`soft_shadow_cl` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0, 0, 0.15]`.
    """

    # hard shadow
    hard_shadow_texture = ObjectProperty()
    """
    Texture of the hard shadow texture for the canvas.

    :attr:`hard_shadow_texture` is an :class:`~kivy.core.image.Image`
    and defaults to `None`.

    .. note::
        This property is automatically processed when elevation is changed.
    """

    hard_shadow_size = ListProperty([0, 0])
    """
    Size of the hard shadow texture over the canvas.

    :attr:`hard_shadow_size` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0]`.

    .. note::
        This property is automatically processed when elevation is changed.
    """

    hard_shadow_pos = ListProperty([0, 0])
    """
    Position of the hard shadow texture over the canvas.

    :attr:`hard_shadow_pos` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0]`.

    .. note::
        This property is automatically processed when elevation is changed.
    """

    hard_shadow_cl = ListProperty([0, 0, 0, 0.15])
    """
    Color of the hard shadow.

    .. note::
        :attr:`hard_shadow_cl` is an :class:`~kivy.properties.ListProperty`
        and defaults to `[0, 0, 0, 0.15]`.
    """

    # Shared property for some calculations.
    # This values are used to improve the gaussain blur and avoid that
    # the blur goes outside the texture.
    hard_shadow_offset = BoundedNumericProperty(2,
                                                min=0,
                                                errorhandler=lambda x: 0
                                                if x < 0 else x)
    """
    This value sets a special offset to the shadow canvas, this offset allows a
    correct draw of the canvas size. allowing the effect to correctly blur the
    image in the given space.

    :attr:`hard_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to `2`.
    """

    soft_shadow_offset = BoundedNumericProperty(4,
                                                min=0,
                                                errorhandler=lambda x: 0
                                                if x < 0 else x)
    """
    This value sets a special offset to the shadow canvas, this offset allows a
    correct draw of the canvas size. allowing the effect to correctly blur the
    image in the given space.

    :attr:`soft_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to `4`.
    """

    draw_shadow = ObjectProperty(None)
    """
    This property controls the draw call of the context.

    This property is automatically set to :attr:`__draw_shadow__` inside the
    `super().__init__ call.` unless the property is different of None.

    To set a different drawing instruction function, set this property before the
    `super(),__init__` call inside the `__init__` definition of the new class.

    You can use the source for this classes as example of how to draw over
    with the context:

    Real time shadows:
        #. :class:`~RectangularElevationBehavior`
        #. :class:`~CircularElevationBehavior`
        #. :class:`~RoundedRectangularElevationBehavior`
        #. :class:`~ObservableShadow`


    Fake shadows (d`ont use this property):
        #. :class:`~FakeRectangularElevationBehavior`
        #. :class:`~FakeCircularElevationBehavior`

    :attr:`draw_shadow` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to `None`.

    .. note:: If this property is left to `None` the
        :class:`~CommonElevationBehavior` will set to a function that will
        raise a `NotImplementedError` inside `super().__init__`.

    Follow the next example to set a new draw instruction for the class
    inside `__init__`:

    .. code-block:: python

        class RoundedRectangularElevationBehavior(CommonElevationBehavior):
            '''
            Shadow class for the RoundedRectangular shadow behavior.
            Controls the size and position of the shadow.
            '''

            def __init__(self, **kwargs):
                self._draw_shadow = WeakMethod(self.__draw_shadow__)
                super().__init__(**kwargs)

            def __draw_shadow__(self, origin, end, context=None):
                context.draw(...)

    Context is a `Pillow` `ImageDraw` class. For more information check the
    [Pillow official documentation](https://github.com/python-pillow/Pillow/).
    """

    # All classes that uses a fake shadow shall set this value as `True`
    # for performance.
    _fake_elevation = BooleanProperty(False)

    def __init__(self, **kwargs):
        if self.draw_shadow is None:
            self.draw_shadow = WeakMethod(self.__draw_shadow__)
        self.prev_shadow_group = None
        im = BytesIO()
        Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im, format="png")
        im.seek(0)
        # Setting a empty image as texture, improves performance.
        self._soft_shadow_texture = self.hard_shadow_texture = CoreImage(
            im, ext="png").texture
        Clock.schedule_once(self.shadow_preset, -1)
        self.on_shadow_group(self, self.shadow_group)

        self.bind(
            pos=self._update_shadow,
            size=self._update_shadow,
            radius=self._update_shadow,
        )
        super().__init__(**kwargs)

    def on_shadow_group(self, instance, value):
        """
        This function controls the shadow group of the widget.
        Do not use Directly to change the group. instead, use the shadow_group
        :attr:`property`.
        """

        groups = CommonElevationBehavior.__shadow_groups
        if self.prev_shadow_group:
            group = groups[self.prev_shadow_group]
            for widget in group[:]:
                if widget() is self:
                    group.remove(widget)
        group = self.prev_shadow_group = self.shadow_group
        if group not in groups:
            groups[group] = []
        r = ref(self, CommonElevationBehavior._clear_shadow_groups)
        groups[group].append(r)

    @staticmethod
    def _clear_shadow_groups(wk):
        # auto flush the element when the weak reference have been deleted
        groups = CommonElevationBehavior.__shadow_groups
        for group in list(groups.values()):
            if not group:
                break
            if wk in group:
                group.remove(wk)
                break

    def force_shadow_pos(self, shadow_pos):
        """
        This property forces the shadow position in every widget inside the
        widget. The argument :attr:`shadow_pos` is expected as a <class 'list'>
        or <class 'tuple'>.
        """

        if self.shadow_group is None:
            return
        group = CommonElevationBehavior.__shadow_groups[self.shadow_group]
        for wk in group[:]:
            widget = wk()
            if widget is None:
                group.remove(wk)
            widget.shadow_pos = shadow_pos
        del group

    def update_group_property(self, property_name, value):
        """
        This functions allows to change properties of every widget inside the
        shadow group.
        """

        if self.shadow_group is None:
            return
        group = CommonElevationBehavior.__shadow_groups[self.shadow_group]
        for wk in group[:]:
            widget = wk()
            if widget is None:
                group.remove(wk)
            setattr(widget, property_name, value)
        del group

    def shadow_preset(self, *args):
        """
        This function is meant to set the default configuration of the
        elevation.

        After a new instance is created, the elevation property will be launched
        and thus this function will update the elevation if the KV lang have not
        done it already.

        Works similar to an `__after_init__` call inside a widget.
        """

        from kivymd.uix.card import MDCard

        if self.elevation is None and not issubclass(self.__class__, MDCard):
            self.elevation = 10
        if self._fake_elevation is False:
            self._update_shadow(self, self.elevation)
        self.bind(
            pos=self._update_shadow,
            size=self._update_shadow,
            _elevation=self._update_shadow,
        )

    def on_elevation(self, instance, value):
        """
        Elevation event that sets the current elevation value to `_elevation`.
        """

        if value is not None:
            self._elevation = value

    def _set_soft_shadow_a(self, value):
        value = 0 if value < 0 else (1 if value > 1 else value)
        self.soft_shadow_cl[-1] = value
        return True

    def _set_hard_shadow_a(self, value):
        value = 0 if value < 0 else (1 if value > 1 else value)
        self.hard_shadow_cl[-1] = value
        return True

    def _get_soft_shadow_a(self):
        return self.soft_shadow_cl[-1]

    def _get_hard_shadow_a(self):
        return self.hard_shadow_cl[-1]

    _soft_shadow_a = AliasProperty(_get_soft_shadow_a,
                                   _set_soft_shadow_a,
                                   bind=["soft_shadow_cl"])
    _hard_shadow_a = AliasProperty(_get_hard_shadow_a,
                                   _set_hard_shadow_a,
                                   bind=["hard_shadow_cl"])

    def on_disabled(self, instance, value):
        """
        This function hides the shadow when the widget is disabled.
        It sets the shadow to `0`.
        """

        if self.disabled is True:
            self._elevation = 0
        else:
            self._elevation = 0 if self.elevation is None else self.elevation
        self._update_shadow(self, self._elevation)
        try:
            super().on_disabled(instance, value)
        except Exception:
            pass

    def _update_elevation(self, instance, value):
        self._elevation = value
        self._update_shadow(instance, value)

    def _update_shadow_pos(self, instance, value):
        if self._elevation > 0:
            self.hard_shadow_pos = [
                self.x - dp(self.hard_shadow_offset),  # + self.shadow_pos[0],
                self.y - dp(self.hard_shadow_offset),  # + self.shadow_pos[1],
            ]
            if self.shadow_pos == [0, 0]:
                self.soft_shadow_pos = [
                    self.x + self._shadow_pos[0] - self._elevation -
                    dp(self.soft_shadow_offset),
                    self.y + self._shadow_pos[1] - self._elevation -
                    dp(self.soft_shadow_offset),
                ]
            else:
                self.soft_shadow_pos = [
                    self.x + self.shadow_pos[0] - self._elevation -
                    dp(self.soft_shadow_offset),
                    self.y + self.shadow_pos[1] - self._elevation -
                    dp(self.soft_shadow_offset),
                ]
            self._shadow_origin = [
                self.soft_shadow_pos[0] + self.soft_shadow_size[0] / 2,
                self.soft_shadow_pos[1] + self.soft_shadow_size[1] / 2,
            ]

    def on__shadow_pos(self, ins, val):
        """
        Updates the shadow with the computed value.

        Call this function every time you need to force a shadow update.
        """

        self._update_shadow_pos(ins, val)

    def on_shadow_pos(self, ins, val):
        """
        Updates the shadow with the fixed value.

        Call this function every time you need to force a shadow update.
        """

        self._update_shadow_pos(ins, val)

    def _update_shadow(self, instance, value):
        self._update_shadow_pos(instance, value)
        if self._elevation > 0 and self._fake_elevation is False:
            # dynamic elevation position for the shadow
            if self.shadow_pos == [0, 0]:
                self._shadow_pos = [0, -self._elevation * 0.4]

            # HARD Shadow
            offset = int(dp(self.hard_shadow_offset))
            size = [
                int(self.size[0] + (offset * 2)),
                int(self.size[1] + (offset * 2)),
            ]
            im = BytesIO()
            # context
            img = Image.new("RGBA", tuple(size), color=(0, 0, 0, 0))
            # draw context
            shadow = ImageDraw.Draw(img)
            self.draw_shadow()([offset, offset], [
                int(size[0] - 1 - offset),
                int(size[1] - 1 - offset),
            ],
                               context=shadow
                               # context=ref(shadow)
                               )
            img = img.filter(
                ImageFilter.GaussianBlur(
                    radius=int(dp(1 + self.hard_shadow_offset / 3))))
            img.save(im, format="png")
            im.seek(0)
            self.hard_shadow_size = size
            self.hard_shadow_texture = CoreImage(im, ext="png").texture

            # soft shadow
            if self.soft_shadow_cl[-1] > 0:
                offset = dp(self.soft_shadow_offset)
                size = [
                    int(self.size[0] + dp(self._elevation * 2) + (offset * 2)),
                    int(self.size[1] + dp(self._elevation * 2) + (offset * 2)),
                    # ((self._elevation)*2) + x + (offset*2)) for x in self.size
                ]
                im = BytesIO()
                img = Image.new("RGBA", tuple(size), color=((0, ) * 4))
                shadow = ImageDraw.Draw(img)
                _offset = int(dp(self._elevation + offset))
                self.draw_shadow()([
                    _offset,
                    _offset,
                ], [int(size[0] - _offset - 1),
                    int(size[1] - _offset - 1)],
                                   context=shadow
                                   # context=ref(shadow)
                                   )
                img = img.filter(
                    ImageFilter.GaussianBlur(radius=self._elevation // 2))
                shadow = ImageDraw.Draw(img)
                img.save(im, format="png")
                im.seek(0)
                self.soft_shadow_size = size
                self._soft_shadow_texture = CoreImage(im, ext="png").texture
        else:
            im = BytesIO()
            Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im,
                                                               format="png")
            im.seek(0)
            self._soft_shadow_texture = self.hard_shadow_texture = CoreImage(
                im, ext="png").texture
            return

    def _get_center(self):
        center = [self.pos[0] + self.width / 2, self.pos[1] + self.height / 2]
        return center

    def __draw_shadow__(self, origin, end, context=None):
        Logger.warning(
            f"KivyMD: "
            f"If you see this error, this means that either youre using "
            f"`CommonElevationBehavio`r directly or your 'shader' dont have a "
            f"`_draw_shadow` instruction, remember to overwrite this function"
            f"to draw over the image context. Тhe figure you would like. "
            f"Or your class {self.__class__.__name__} is not inherited from "
            f"any of the classes {__all__}")
    def test_reference(self):
        from kivy.properties import NumericProperty, ReferenceListProperty

        x = NumericProperty(0)
        x.link(wid, 'x')
        x.link_deps(wid, 'x')
        y = NumericProperty(0)
        y.link(wid, 'y')
        y.link_deps(wid, 'y')
        pos = ReferenceListProperty(x, y)
        pos.link(wid, 'pos')
        pos.link_deps(wid, 'pos')

        self.assertEqual(x.get(wid), 0)
        self.assertEqual(y.get(wid), 0)
        self.assertEqual(pos.get(wid), [0, 0])

        x.set(wid, 50)
        self.assertEqual(pos.get(wid), [50, 0])

        y.set(wid, 50)
        self.assertEqual(pos.get(wid), [50, 50])

        pos.set(wid, [0, 0])
        self.assertEqual(pos.get(wid), [0, 0])
        self.assertEqual(x.get(wid), 0)
        self.assertEqual(y.get(wid), 0)

        # test observer
        global observe_called
        observe_called = 0

        def observe(obj, value):
            global observe_called
            observe_called = 1

        pos.bind(wid, observe)

        self.assertEqual(observe_called, 0)
        x.set(wid, 99)
        self.assertEqual(observe_called, 1)
Esempio n. 21
0
class Slider(Widget):
    """Class for creating a Slider widget.

    Check module documentation for more details.
    """

    value = NumericProperty(0.)
    '''Current value used for the slider.

    :attr:`value` is a :class:`~kivy.properties.NumericProperty` and defaults
    to 0.'''

    min = NumericProperty(0.)
    '''Minimum value allowed for :attr:`value`.

    :attr:`min` is a :class:`~kivy.properties.NumericProperty` and defaults to
    0.'''

    max = NumericProperty(100.)
    '''Maximum value allowed for :attr:`value`.

    :attr:`max` is a :class:`~kivy.properties.NumericProperty` and defaults to
    100.'''

    padding = NumericProperty('16sp')
    '''Padding of the slider. The padding is used for graphical representation
    and interaction. It prevents the cursor from going out of the bounds of the
    slider bounding box.

    By default, padding is 16sp. The range of the slider is reduced from
    padding \\*2 on the screen. It allows drawing the default cursor of 32sp
    width without having the cursor go out of the widget.

    :attr:`padding` is a :class:`~kivy.properties.NumericProperty` and defaults
    to 16sp.'''

    orientation = OptionProperty('horizontal',
                                 options=('vertical', 'horizontal'))
    '''Orientation of the slider.

    :attr:`orientation` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'horizontal'. Can take a value of 'vertical' or 'horizontal'.
    '''

    range = ReferenceListProperty(min, max)
    '''Range of the slider in the format (minimum value, maximum value)::

        >>> slider = Slider(min=10, max=80)
        >>> slider.range
        [10, 80]
        >>> slider.range = (20, 100)
        >>> slider.min
        20
        >>> slider.max
        100

    :attr:`range` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`min`, :attr:`max`) properties.
    '''

    step = BoundedNumericProperty(0, min=0)
    '''Step size of the slider.

    .. versionadded:: 1.4.0

    Determines the size of each interval or step the slider takes between
    min and max. If the value range can't be evenly divisible by step the
    last step will be capped by slider.max

    :attr:`step` is a :class:`~kivy.properties.NumericProperty` and defaults
    to 1.'''

    background_horizontal = StringProperty(
        'atlas://data/images/defaulttheme/sliderh_background')
    """Background of the slider used in the horizontal orientation.

    .. versionadded:: 1.10.0

    :attr:`background_horizontal` is a :class:`~kivy.properties.StringProperty`
    and defaults to `atlas://data/images/defaulttheme/sliderh_background`.
    """

    background_disabled_horizontal = StringProperty(
        'atlas://data/images/defaulttheme/sliderh_background_disabled')
    """Background of the disabled slider used in the horizontal orientation.

    .. versionadded:: 1.10.0

    :attr:`background_disabled_horizontal` is a
    :class:`~kivy.properties.StringProperty` and defaults to
    `atlas://data/images/defaulttheme/sliderh_background_disabled`.
    """

    background_vertical = StringProperty(
        'atlas://data/images/defaulttheme/sliderv_background')
    """Background of the slider used in the vertical orientation.

    .. versionadded:: 1.10.0

    :attr:`background_vertical` is a :class:`~kivy.properties.StringProperty`
    and defaults to `atlas://data/images/defaulttheme/sliderv_background`.
    """

    background_disabled_vertical = StringProperty(
        'atlas://data/images/defaulttheme/sliderv_background_disabled')
    """Background of the disabled slider used in the vertical orientation.

    .. versionadded:: 1.10.0

    :attr:`background_disabled_vertical` is a
    :class:`~kivy.properties.StringProperty` and defaults to
    `atlas://data/images/defaulttheme/sliderv_background_disabled`.
    """

    background_width = NumericProperty('36sp')
    """Slider's background's width (thickness), used in both horizontal
    and vertical orientations.

    .. versionadded 1.10.0

    :attr:`background_width` is a
    :class:`~kivy.properties.NumericProperty` and defaults to 36sp.
    """

    cursor_image = StringProperty(
        'atlas://data/images/defaulttheme/slider_cursor')
    """Path of the image used to draw the slider cursor.

    .. versionadded 1.10.0

    :attr:`cursor_image` is a :class:`~kivy.properties.StringProperty`
    and defaults to `atlas://data/images/defaulttheme/slider_cursor`.
    """

    cursor_disabled_image = StringProperty(
        'atlas://data/images/defaulttheme/slider_cursor_disabled')
    """Path of the image used to draw the disabled slider cursor.

    .. versionadded 1.10.0

    :attr:`cursor_image` is a :class:`~kivy.properties.StringProperty`
    and defaults to `atlas://data/images/defaulttheme/slider_cursor_disabled`.
    """

    cursor_width = NumericProperty('32sp')
    """Width of the cursor image.

    .. versionadded 1.10.0

    :attr:`cursor_width` is a :class:`~kivy.properties.NumericProperty`
    and defaults to 32sp.
    """

    cursor_height = NumericProperty('32sp')
    """Height of the cursor image.

    .. versionadded 1.10.0

    :attr:`cursor_height` is a :class:`~kivy.properties.NumericProperty`
    and defaults to 32sp.
    """

    cursor_size = ReferenceListProperty(cursor_width, cursor_height)
    """Size of the cursor image.

    .. versionadded 1.10.0

    :attr:`cursor_size` is a :class:`~kivy.properties.ReferenceListProperty`
    of (:attr:`cursor_width`, :attr:`cursor_height`) properties.
    """

    border_horizontal = ListProperty([0, 18, 0, 18])
    """Border used to draw the slider background in horizontal orientation.

    .. versionadded 1.10.0

    :attr:`border_horizontal` is a :class:`~kivy.properties.ListProperty`
    and defaults to [0, 18, 0, 18].
    """

    border_vertical = ListProperty([18, 0, 18, 0])
    """Border used to draw the slider background in vertical orientation.

    .. versionadded 1.10.0

    :attr:`border_horizontal` is a :class:`~kivy.properties.ListProperty`
    and defaults to [18, 0, 18, 0].
    """

    value_track = BooleanProperty(False)
    """Decides if slider should draw the line indicating the
    space between :attr:`min` and :attr:`value` properties values.

    .. versionadded 1.10.0

    :attr:`value_track` is a :class:`~kivy.properties.BooleanProperty`
    and defaults to False.
    """

    value_track_color = ListProperty([1, 1, 1, 1])
    """Color of the :attr:`value_line` in rgba format.

    .. versionadded 1.10.0

    :attr:`value_track_color` is a :class:`~kivy.properties.ListProperty`
    and defaults to [1, 1, 1, 1].
    """

    value_track_width = NumericProperty('3dp')
    """Width of the track line.

    .. versionadded 1.10.0

    :attr:`value_track_width` is a :class:`~kivy.properties.NumericProperty`
    and defaults to 3dp.
    """

    sensitivity = OptionProperty('all', options=('all', 'handle'))
    """Whether the touch collides with the whole body of the widget
    or with the slider handle part only.

    .. versionadded:: 1.10.1

    :attr:`sensitivity` is a :class:`~kivy.properties.OptionProperty`
    and defaults to 'all'. Can take a value of 'all' or 'handle'.
    """

    # The following two methods constrain the slider's value
    # to range(min,max). Otherwise it may happen that self.value < self.min
    # at init.

    def on_min(self, *largs):
        self.value = min(self.max, max(self.min, self.value))

    def on_max(self, *largs):
        self.value = min(self.max, max(self.min, self.value))

    def get_norm_value(self):
        vmin = self.min
        d = self.max - vmin
        if d == 0:
            return 0
        return (self.value - vmin) / float(d)

    def set_norm_value(self, value):
        vmin = self.min
        vmax = self.max
        step = self.step
        val = min(value * (vmax - vmin) + vmin, vmax)
        if step == 0:
            self.value = val
        else:
            self.value = min(round((val - vmin) / step) * step + vmin, vmax)

    value_normalized = AliasProperty(get_norm_value,
                                     set_norm_value,
                                     bind=('value', 'min', 'max'),
                                     cache=True)
    '''Normalized value inside the :attr:`range` (min/max) to 0-1 range::

        >>> slider = Slider(value=50, min=0, max=100)
        >>> slider.value
        50
        >>> slider.value_normalized
        0.5
        >>> slider.value = 0
        >>> slider.value_normalized
        0
        >>> slider.value = 100
        >>> slider.value_normalized
        1

    You can also use it for setting the real value without knowing the minimum
    and maximum::

        >>> slider = Slider(min=0, max=200)
        >>> slider.value_normalized = .5
        >>> slider.value
        100
        >>> slider.value_normalized = 1.
        >>> slider.value
        200

    :attr:`value_normalized` is an :class:`~kivy.properties.AliasProperty`.
    '''

    def get_value_pos(self):
        padding = self.padding
        x = self.x
        y = self.y
        nval = self.value_normalized
        if self.orientation == 'horizontal':
            return (x + padding + nval * (self.width - 2 * padding), y)
        else:
            return (x, y + padding + nval * (self.height - 2 * padding))

    def set_value_pos(self, pos):
        padding = self.padding
        x = min(self.right - padding, max(pos[0], self.x + padding))
        y = min(self.top - padding, max(pos[1], self.y + padding))
        if self.orientation == 'horizontal':
            if self.width == 0:
                self.value_normalized = 0
            else:
                self.value_normalized = (
                    x - self.x - padding) / float(self.width - 2 * padding)
        else:
            if self.height == 0:
                self.value_normalized = 0
            else:
                self.value_normalized = (
                    y - self.y - padding) / float(self.height - 2 * padding)

    value_pos = AliasProperty(get_value_pos,
                              set_value_pos,
                              bind=('pos', 'size', 'min', 'max', 'padding',
                                    'value_normalized', 'orientation'),
                              cache=True)
    '''Position of the internal cursor, based on the normalized value.

    :attr:`value_pos` is an :class:`~kivy.properties.AliasProperty`.
    '''

    def on_touch_down(self, touch):
        if self.disabled or not self.collide_point(*touch.pos):
            return
        if touch.is_mouse_scrolling:
            if 'down' in touch.button or 'left' in touch.button:
                if self.step:
                    self.value = min(self.max, self.value + self.step)
                else:
                    self.value = min(self.max,
                                     self.value + (self.max - self.min) / 20)
            if 'up' in touch.button or 'right' in touch.button:
                if self.step:
                    self.value = max(self.min, self.value - self.step)
                else:
                    self.value = max(self.min,
                                     self.value - (self.max - self.min) / 20)
        elif self.sensitivity == 'handle':
            if self.children[0].collide_point(*touch.pos):
                touch.grab(self)
        else:
            touch.grab(self)
            self.value_pos = touch.pos
        return True

    def on_touch_move(self, touch):
        if touch.grab_current == self:
            self.value_pos = touch.pos
            return True

    def on_touch_up(self, touch):
        if touch.grab_current == self:
            self.value_pos = touch.pos
            return True
    def test_reference_child_update(self):
        from kivy.properties import NumericProperty, ReferenceListProperty

        x = NumericProperty(0)
        x.link(wid, 'x')
        x.link_deps(wid, 'x')
        y = NumericProperty(0)
        y.link(wid, 'y')
        y.link_deps(wid, 'y')
        pos = ReferenceListProperty(x, y)
        pos.link(wid, 'pos')
        pos.link_deps(wid, 'pos')

        pos.get(wid)[0] = 10
        self.assertEqual(pos.get(wid), [10, 0])

        pos.get(wid)[:] = (20, 30)
        self.assertEqual(pos.get(wid), [20, 30])
Esempio n. 23
0
class StackLayout(Layout):
    '''Stack layout class. See module documentation for more information.
    '''

    spacing = VariableListProperty([0, 0], length=2)
    '''Spacing between children: [spacing_horizontal, spacing_vertical].

    spacing also accepts a single argument form [spacing].

    :attr:`spacing` is a
    :class:`~kivy.properties.VariableListProperty` and defaults to [0, 0].

    '''

    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 single argument form [padding].

    .. versionchanged:: 1.7.0
        Replaced the NumericProperty with a VariableListProperty.

    :attr:`padding` is a
    :class:`~kivy.properties.VariableListProperty` and defaults to
    [0, 0, 0, 0].

    '''

    orientation = OptionProperty('lr-tb',
                                 options=('lr-tb', 'tb-lr', 'rl-tb', 'tb-rl',
                                          'lr-bt', 'bt-lr', 'rl-bt', 'bt-rl'))
    '''Orientation of the layout.

    :attr:`orientation` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'lr-tb'.

    Valid orientations are 'lr-tb', 'tb-lr', 'rl-tb', 'tb-rl', 'lr-bt',
    'bt-lr', 'rl-bt' and 'bt-rl'.

    .. versionchanged:: 1.5.0
        :attr:`orientation` now correctly handles all valid combinations of
        'lr','rl','tb','bt'. Before this version only 'lr-tb' and
        'tb-lr' were supported, and 'tb-lr' was misnamed and placed
        widgets from bottom to top and from right to left (reversed compared
        to what was expected).

    .. note::

        'lr' means Left to Right.
        'rl' means Right to Left.
        'tb' means Top to Bottom.
        'bt' means Bottom to Top.
    '''

    minimum_width = NumericProperty(0)
    '''Minimum width needed to contain all children. It is automatically set
    by the layout.

    .. versionadded:: 1.0.8

    :attr:`minimum_width` is a :class:`kivy.properties.NumericProperty` and
    defaults to 0.
    '''

    minimum_height = NumericProperty(0)
    '''Minimum height needed to contain all children. It is automatically set
    by the layout.

    .. versionadded:: 1.0.8

    :attr:`minimum_height` is a :class:`kivy.properties.NumericProperty` and
    defaults to 0.
    '''

    minimum_size = ReferenceListProperty(minimum_width, minimum_height)
    '''Minimum size needed to contain all children. It is automatically set
    by the layout.

    .. versionadded:: 1.0.8

    :attr:`minimum_size` is a
    :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`minimum_width`, :attr:`minimum_height`) properties.
    '''
    def __init__(self, **kwargs):
        super(StackLayout, self).__init__(**kwargs)
        trigger = self._trigger_layout
        fbind = self.fbind
        fbind('padding', trigger)
        fbind('spacing', trigger)
        fbind('children', trigger)
        fbind('orientation', trigger)
        fbind('size', trigger)
        fbind('pos', trigger)

    def do_layout(self, *largs):
        if not self.children:
            self.minimum_size = (0., 0.)
            return

        # optimize layout by preventing looking at the same attribute in a loop
        selfpos = self.pos
        selfsize = self.size
        orientation = self.orientation.split('-')
        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
        spacing_x, spacing_y = self.spacing

        # Determine which direction and in what order to place the widgets
        posattr = [0] * 2
        posdelta = [0] * 2
        posstart = [0] * 2
        for i in (0, 1):
            posattr[i] = 1 * (orientation[i] in ('tb', 'bt'))
            k = posattr[i]
            if orientation[i] == 'lr':
                # left to right
                posdelta[i] = 1
                posstart[i] = selfpos[k] + padding_left
            elif orientation[i] == 'bt':
                # bottom to top
                posdelta[i] = 1
                posstart[i] = selfpos[k] + padding_bottom
            elif orientation[i] == 'rl':
                # right to left
                posdelta[i] = -1
                posstart[i] = selfpos[k] + selfsize[k] - padding_right
            else:
                # top to bottom
                posdelta[i] = -1
                posstart[i] = selfpos[k] + selfsize[k] - padding_top

        innerattr, outerattr = posattr
        ustart, vstart = posstart
        deltau, deltav = posdelta
        del posattr, posdelta, posstart

        u = ustart  # inner loop position variable
        v = vstart  # outer loop position variable

        # space calculation, used for determining when a row or column is full

        if orientation[0] in ('lr', 'rl'):
            sv = padding_y  # size in v-direction, for minimum_size property
            su = padding_x  # size in h-direction
            spacing_u = spacing_x
            spacing_v = spacing_y
            padding_u = padding_x
            padding_v = padding_y
        else:
            sv = padding_x  # size in v-direction, for minimum_size property
            su = padding_y  # size in h-direction
            spacing_u = spacing_y
            spacing_v = spacing_x
            padding_u = padding_y
            padding_v = padding_x

        # space calculation, row height or column width, for arranging widgets
        lv = 0

        urev = (deltau < 0)
        vrev = (deltav < 0)
        firstchild = self.children[0]
        sizes = []
        lc = []
        for c in reversed(self.children):
            if c.size_hint[outerattr] is not None:
                c.size[outerattr] = max(
                    1,
                    _compute_size(c, selfsize[outerattr] - padding_v,
                                  outerattr))

            # does the widget fit in the row/column?
            ccount = len(lc)
            totalsize = availsize = max(
                0, selfsize[innerattr] - padding_u - spacing_u * ccount)
            if not lc:
                if c.size_hint[innerattr] is not None:
                    childsize = max(1, _compute_size(c, totalsize, innerattr))
                else:
                    childsize = max(0, c.size[innerattr])
                availsize = selfsize[innerattr] - padding_u - childsize
                testsizes = [childsize]
            else:
                testsizes = [0] * (ccount + 1)
                for i, child in enumerate(lc):
                    if availsize <= 0:
                        # no space left but we're trying to add another widget.
                        availsize = -1
                        break
                    if child.size_hint[innerattr] is not None:
                        testsizes[i] = childsize = max(
                            1, _compute_size(child, totalsize, innerattr))
                    else:
                        childsize = max(0, child.size[innerattr])
                        testsizes[i] = childsize
                    availsize -= childsize
                if c.size_hint[innerattr] is not None:
                    testsizes[-1] = max(1,
                                        _compute_size(c, totalsize, innerattr))
                else:
                    testsizes[-1] = max(0, c.size[innerattr])
                availsize -= testsizes[-1]

            # Tiny value added in order to avoid issues with float precision
            # causing unexpected children reordering when parent resizes.
            # e.g. if size is 101 and children size_hint_x is 1./5
            # 5 children would not fit in one line because 101*(1./5) > 101/5
            if (availsize + 1e-10) >= 0 or not lc:
                # even if there's no space, we always add one widget to a row
                lc.append(c)
                sizes = testsizes
                lv = max(lv, c.size[outerattr])
                continue

            # apply the sizes
            for i, child in enumerate(lc):
                if child.size_hint[innerattr] is not None:
                    child.size[innerattr] = sizes[i]

            # push the line
            sv += lv + spacing_v
            for c2 in lc:
                if urev:
                    u -= c2.size[innerattr]
                c2.pos[innerattr] = u
                pos_outer = v
                if vrev:
                    # v position is actually the top/right side of the widget
                    # when going from high to low coordinate values,
                    # we need to subtract the height/width from the position.
                    pos_outer -= c2.size[outerattr]
                c2.pos[outerattr] = pos_outer
                if urev:
                    u -= spacing_u
                else:
                    u += c2.size[innerattr] + spacing_u

            v += deltav * lv
            v += deltav * spacing_v
            lc = [c]
            lv = c.size[outerattr]
            if c.size_hint[innerattr] is not None:
                sizes = [
                    max(
                        1,
                        _compute_size(c, selfsize[innerattr] - padding_u,
                                      innerattr))
                ]
            else:
                sizes = [max(0, c.size[innerattr])]
            u = ustart

        if lc:
            # apply the sizes
            for i, child in enumerate(lc):
                if child.size_hint[innerattr] is not None:
                    child.size[innerattr] = sizes[i]

            # push the last (incomplete) line
            sv += lv + spacing_v
            for c2 in lc:
                if urev:
                    u -= c2.size[innerattr]
                c2.pos[innerattr] = u
                pos_outer = v
                if vrev:
                    pos_outer -= c2.size[outerattr]
                c2.pos[outerattr] = pos_outer
                if urev:
                    u -= spacing_u
                else:
                    u += c2.size[innerattr] + spacing_u

        self.minimum_size[outerattr] = sv
Esempio n. 24
0
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
Esempio n. 25
0
class CircularTimePicker(BoxLayout, ThemableBehavior):
    """Widget that makes use of :class:`CircularHourPicker` and
    :class:`CircularMinutePicker` to create a user-friendly, animated
    time picker like the one seen on Android.

    See module documentation for more details.
    """

    primary_dark = ListProperty([1, 1, 1])

    hours = NumericProperty(0)
    """The hours, in military format (0-23).

    :attr:`hours` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0 (12am).
    """

    minutes = NumericProperty(0)
    """The minutes.

    :attr:`minutes` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.
    """

    time_list = ReferenceListProperty(hours, minutes)
    """Packs :attr:`hours` and :attr:`minutes` in a list for convenience.

    :attr:`time_list` is a :class:`~kivy.properties.ReferenceListProperty`.
    """

    # military = BooleanProperty(False)
    time_format = StringProperty(
        "[color={hours_color}][ref=hours]{hours}[/ref][/color][color={primary_dark}][ref=colon]:[/ref][/color]\
[color={minutes_color}][ref=minutes]{minutes:02d}[/ref][/color]")
    """String that will be formatted with the time and shown in the time label.
    Can be anything supported by :meth:`str.format`. Make sure you don't
    remove the refs. See the default for the arguments passed to format.
    :attr:`time_format` is a :class:`~kivy.properties.StringProperty` and
    defaults to "[color={hours_color}][ref=hours]{hours}[/ref][/color]:[color={minutes_color}][ref=minutes]\
        {minutes:02d}[/ref][/color]".
    """

    ampm_format = StringProperty("[color={am_color}][ref=am]AM[/ref][/color]\n"
                                 "[color={pm_color}][ref=pm]PM[/ref][/color]")
    """String that will be formatted and shown in the AM/PM label.
    Can be anything supported by :meth:`str.format`. Make sure you don't
    remove the refs. See the default for the arguments passed to format.

    :attr:`ampm_format` is a :class:`~kivy.properties.StringProperty` and
    defaults to "[color={am_color}][ref=am]AM[/ref][/color]\n
                 [color={pm_color}][ref=pm]PM[/ref][/color]".
    """

    picker = OptionProperty("hours", options=("minutes", "hours"))
    """Currently shown time picker. Can be one of "minutes", "hours".

    :attr:`picker` is a :class:`~kivy.properties.OptionProperty` and
    defaults to "hours".
    """

    # selector_color = ListProperty([.337, .439, .490])
    selector_color = ListProperty([0, 0, 0])
    """Color of the number selector and of the highlighted text. RGB.

    :attr:`selector_color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [.337, .439, .490] (material green).
    """

    color = ListProperty([1, 1, 1])
    """Color of the number labels and of the center dot. RGB.

    :attr:`color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [1, 1, 1] (white).
    """

    selector_alpha = BoundedNumericProperty(0.3, min=0, max=1)
    """Alpha value for the transparent parts of the selector.

    :attr:`selector_alpha`
    is a :class:`~kivy.properties.BoundedNumericProperty` and
    defaults to 0.3 (min=0, max=1).
    """

    _am = BooleanProperty(True)
    _h_picker = ObjectProperty(None)
    _m_picker = ObjectProperty(None)
    _bound = DictProperty({})

    def _get_time(self):
        try:
            return datetime.time(*self.time_list)
        except ValueError:
            self.time_list = [self.hours, 0]
            return datetime.time(*self.time_list)

    def set_time(self, dt):
        if dt.hour >= 12:
            dt.strftime("%I:%M")
            self._am = False
        self.time_list = [dt.hour, dt.minute]

    time = AliasProperty(_get_time, set_time, bind=("time_list", ))
    """Selected time as a datetime.time object.

    :attr:`time` is an :class:`~kivy.properties.AliasProperty`.
    """

    def _get_picker(self):
        if self.picker == "hours":
            return self._h_picker
        return self._m_picker

    _picker = AliasProperty(_get_picker, None)

    def _get_time_text(self):
        hc = (rgb_to_hex(0, 0, 0) if self.picker == "hours" else rgb_to_hex(
            *self.primary_dark))
        mc = (rgb_to_hex(0, 0, 0) if self.picker == "minutes" else rgb_to_hex(
            *self.primary_dark))
        h = (self.hours == 0 and 12 or self.hours <= 12 and self.hours
             or self.hours - 12)
        m = self.minutes
        primary_dark = rgb_to_hex(*self.primary_dark)
        return self.time_format.format(
            hours_color=hc,
            minutes_color=mc,
            hours=h,
            minutes=m,
            primary_dark=primary_dark,
        )

    time_text = AliasProperty(_get_time_text,
                              None,
                              bind=("hours", "minutes", "time_format",
                                    "picker"))

    def _get_ampm_text(self, *args):
        amc = (rgb_to_hex(0, 0, 0) if self._am else rgb_to_hex(
            *self.primary_dark))
        pmc = (rgb_to_hex(0, 0, 0) if not self._am else rgb_to_hex(
            *self.primary_dark))
        return self.ampm_format.format(am_color=amc, pm_color=pmc)

    ampm_text = AliasProperty(_get_ampm_text,
                              None,
                              bind=("hours", "ampm_format", "_am"))

    def __init__(self, **kw):
        super().__init__(**kw)
        self.selector_color = (
            self.theme_cls.primary_color[0],
            self.theme_cls.primary_color[1],
            self.theme_cls.primary_color[2],
        )
        self.color = self.theme_cls.text_color
        self.primary_dark = (
            self.theme_cls.primary_dark[0] / 2,
            self.theme_cls.primary_dark[1] / 2,
            self.theme_cls.primary_dark[2] / 2,
        )
        self.on_ampm()
        if self.hours >= 12:
            self._am = False
        self.bind(
            time_list=self.on_time_list,
            picker=self._switch_picker,
            _am=self.on_ampm,
            primary_dark=self._get_ampm_text,
        )
        self._h_picker = CircularHourPicker()
        self.h_picker_touch = False
        self._m_picker = CircularMinutePicker()
        self.animating = False
        Clock.schedule_once(self.on_selected)
        Clock.schedule_once(self.on_time_list)
        Clock.schedule_once(self._init_later)
        Clock.schedule_once(lambda *a: self._switch_picker(noanim=True))

    def _init_later(self, *args):
        self.ids.timelabel.bind(on_ref_press=self.on_ref_press)
        self.ids.ampmlabel.bind(on_ref_press=self.on_ref_press)

    def on_ref_press(self, ign, ref):
        if not self.animating:
            if ref == "hours":
                self.picker = "hours"
            elif ref == "minutes":
                self.picker = "minutes"
        if ref == "am":
            self._am = True
        elif ref == "pm":
            self._am = False

    def on_selected(self, *a):
        if not self._picker:
            return
        if self.picker == "hours":
            hours = (self._picker.selected
                     if self._am else self._picker.selected + 12)
            if hours == 24 and not self._am:
                hours = 12
            elif hours == 12 and self._am:
                hours = 0
            self.hours = hours
        elif self.picker == "minutes":
            self.minutes = self._picker.selected

    def on_time_list(self, *a):
        if not self._picker:
            return
        self._h_picker.selected = (self.hours == 0 and 12
                                   or self._am and self.hours
                                   or self.hours - 12)
        self._m_picker.selected = self.minutes
        self.on_selected()

    def on_ampm(self, *a):
        if self._am:
            self.hours = self.hours if self.hours < 12 else self.hours - 12
        else:
            self.hours = self.hours if self.hours >= 12 else self.hours + 12

    def is_animating(self, *args):
        self.animating = True

    def is_not_animating(self, *args):
        self.animating = False

    def on_touch_down(self, touch):
        if not self._h_picker.collide_point(*touch.pos):
            self.h_picker_touch = False
        else:
            self.h_picker_touch = True
        super().on_touch_down(touch)

    def on_touch_up(self, touch):
        try:
            if not self.h_picker_touch:
                return
            if not self.animating:
                if touch.grab_current is not self:
                    if self.picker == "hours":
                        self.picker = "minutes"
        except AttributeError:
            pass
        super().on_touch_up(touch)

    def _switch_picker(self, *a, **kw):
        noanim = "noanim" in kw
        if noanim:
            noanim = kw["noanim"]

        try:
            container = self.ids.picker_container
        except (AttributeError, NameError):
            Clock.schedule_once(lambda *a: self._switch_picker(noanim=noanim))

        if self.picker == "hours":
            picker = self._h_picker
            prevpicker = self._m_picker
        elif self.picker == "minutes":
            picker = self._m_picker
            prevpicker = self._h_picker

        if len(self._bound) > 0:
            prevpicker.unbind(selected=self.on_selected)
            self.unbind(**self._bound)
        picker.bind(selected=self.on_selected)
        self._bound = {
            "selector_color": picker.setter("selector_color"),
            "color": picker.setter("color"),
            "selector_alpha": picker.setter("selector_alpha"),
        }
        self.bind(**self._bound)

        if len(container._bound) > 0:
            container.unbind(**container._bound)
        container._bound = {
            "size": picker.setter("size"),
            "pos": picker.setter("pos"),
        }
        container.bind(**container._bound)

        picker.pos = container.pos
        picker.size = container.size
        picker.selector_color = self.selector_color
        picker.color = self.color
        picker.selector_alpha = self.selector_alpha
        if noanim:
            if prevpicker in container.children:
                container.remove_widget(prevpicker)
            if picker.parent:
                picker.parent.remove_widget(picker)
            container.add_widget(picker)
        else:
            self.is_animating()
            if prevpicker in container.children:
                anim = Animation(scale=1.5, d=0.5, t="in_back") & Animation(
                    opacity=0, d=0.5, t="in_cubic")
                anim.start(prevpicker)
                Clock.schedule_once(
                    lambda *y: container.remove_widget(prevpicker),
                    0.5)  # .31)
            picker.scale = 1.5
            picker.opacity = 0
            if picker.parent:
                picker.parent.remove_widget(picker)
            container.add_widget(picker)
            anim = Animation(scale=1, d=0.5, t="out_back") & Animation(
                opacity=1, d=0.5, t="out_cubic")
            anim.bind(on_complete=self.is_not_animating)
            Clock.schedule_once(lambda *y: anim.start(picker), 0.3)
Esempio n. 26
0
class Widget(WidgetBase):
    '''Widget class. See module documentation for more information.

    :Events:
        `on_touch_down`:
            Fired when a new touch event occurs
        `on_touch_move`:
            Fired when an existing touch moves
        `on_touch_up`:
            Fired when an existing touch disappears

    ..warning::
        Adding a `__del__` method to a class derived from Widget with python
        prior to 3.4, will disable automatic garbage collection for instances
        of that class. That is because Widget creates ref cycles, thereby
        removing it from garbage collection as described
        `here <https://docs.python.org/2/library/gc.html#gc.garbage>`_.

    .. versionchanged:: 1.0.9
        Everything related to event properties has been moved to the
        :class:`~kivy.event.EventDispatcher`. Event properties can now be used
        when contructing a simple class without subclassing :class:`Widget`.

    .. versionchanged:: 1.5.0
        The constructor now accepts on_* arguments to automatically bind
        callbacks to properties or events, as in the Kv language.
    '''

    __metaclass__ = WidgetMetaclass
    __events__ = ('on_touch_down', 'on_touch_move', 'on_touch_up')

    def __init__(self, **kwargs):
        # Before doing anything, ensure the windows exist.
        EventLoop.ensure_window()

        # assign the default context of the widget creation
        if not hasattr(self, '_context'):
            self._context = get_current_context()

        super(Widget, self).__init__(**kwargs)

        # Create the default canvas if not exist
        if self.canvas is None:
            self.canvas = Canvas(opacity=self.opacity)

        # Apply all the styles
        if '__no_builder' not in kwargs:
            #current_root = Builder.idmap.get('root')
            #Builder.idmap['root'] = self
            Builder.apply(self)
            #if current_root is not None:
            #    Builder.idmap['root'] = current_root
            #else:
            #    Builder.idmap.pop('root')

        # Bind all the events
        for argument in kwargs:
            if argument[:3] == 'on_':
                self.bind(**{argument: kwargs[argument]})

    @property
    def proxy_ref(self):
        '''Return a proxy reference to the widget, i.e. without creating a
        reference to the widget. See `weakref.proxy
        <http://docs.python.org/2/library/weakref.html?highlight\
        =proxy#weakref.proxy>`_ for more information.

        .. versionadded:: 1.7.2
        '''
        if hasattr(self, '_proxy_ref'):
            return self._proxy_ref

        f = partial(_widget_destructor, self.uid)
        self._proxy_ref = _proxy_ref = proxy(self, f)
        # only f should be enough here, but it appears that is a very
        # specific case, the proxy destructor is not called if both f and
        # _proxy_ref are not together in a tuple
        _widget_destructors[self.uid] = (f, _proxy_ref)
        return _proxy_ref

    def __eq__(self, other):
        if not isinstance(other, Widget):
            return False
        return self.proxy_ref is other.proxy_ref

    def __hash__(self):
        return id(self)

    @property
    def __self__(self):
        return self

    #
    # Collision
    #
    def collide_point(self, x, y):
        '''Check if a point (x, y) is inside the widget's axis aligned bounding
        box.

        :Parameters:
            `x`: numeric
                X position of the point (in window coordinates)
            `y`: numeric
                Y position of the point (in window coordinates)

        :Returns:
            bool, True if the point is inside the bounding box.

        >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40)
        True
        '''
        return self.x <= x <= self.right and self.y <= y <= self.top

    def collide_widget(self, wid):
        '''Check if the other widget collides with this widget.
        Performs an axis-aligned bounding box intersection test by default.

        :Parameters:
            `wid`: :class:`Widget` class
                Widget to collide with.

        :Returns:
            bool, True if the other widget collides with this widget.

        >>> wid = Widget(size=(50, 50))
        >>> wid2 = Widget(size=(50, 50), pos=(25, 25))
        >>> wid.collide_widget(wid2)
        True
        >>> wid2.pos = (55, 55)
        >>> wid.collide_widget(wid2)
        False
        '''
        if self.right < wid.x:
            return False
        if self.x > wid.right:
            return False
        if self.top < wid.y:
            return False
        if self.y > wid.top:
            return False
        return True

    #
    # Default event handlers
    #
    def on_touch_down(self, touch):
        '''Receive a touch down event.

        :Parameters:
            `touch`: :class:`~kivy.input.motionevent.MotionEvent` class
                Touch received. The touch is in parent coordinates. See
                :mod:`~kivy.uix.relativelayout` for a discussion on
                coordinate systems.

        :Returns:
            bool. If True, the dispatching of the touch event will stop.
        '''
        if self.disabled and self.collide_point(*touch.pos):
            return True
        for child in self.children[:]:
            if child.dispatch('on_touch_down', touch):
                return True

    def on_touch_move(self, touch):
        '''Receive a touch move event. The touch is in parent coordinates.

        See :meth:`on_touch_down` for more information.
        '''
        if self.disabled:
            return
        for child in self.children[:]:
            if child.dispatch('on_touch_move', touch):
                return True

    def on_touch_up(self, touch):
        '''Receive a touch up event. The touch is in parent coordinates.

        See :meth:`on_touch_down` for more information.
        '''
        if self.disabled:
            return
        for child in self.children[:]:
            if child.dispatch('on_touch_up', touch):
                return True

    def on_disabled(self, instance, value):
        for child in self.children:
            child.disabled = value

    #
    # Tree management
    #
    def add_widget(self, widget, index=0):
        '''Add a new widget as a child of this widget.

        :Parameters:
            `widget`: :class:`Widget`
                Widget to add to our list of children.
            `index`: int, defaults to 0
                Index to insert the widget in the list

                .. versionadded:: 1.0.5

        >>> from kivy.uix.button import Button
        >>> from kivy.uix.slider import Slider
        >>> root = Widget()
        >>> root.add_widget(Button())
        >>> slider = Slider()
        >>> root.add_widget(slider)

        '''
        if not isinstance(widget, Widget):
            raise WidgetException(
                'add_widget() can be used only with Widget classes.')

        widget = widget.__self__
        if widget is self:
            raise WidgetException('You cannot add yourself in a Widget')
        parent = widget.parent
        # check if widget is already a child of another widget
        if parent:
            raise WidgetException('Cannot add %r, it already has a parent %r' %
                                  (widget, parent))
        widget.parent = parent = self
        # child will be disabled if added to a disabled parent
        if parent.disabled:
            widget.disabled = True

        if index == 0 or len(self.children) == 0:
            self.children.insert(0, widget)
            self.canvas.add(widget.canvas)
        else:
            canvas = self.canvas
            children = self.children
            if index >= len(children):
                index = len(children)
                next_index = 0
            else:
                next_child = children[index]
                next_index = canvas.indexof(next_child.canvas)
                if next_index == -1:
                    next_index = canvas.length()
                else:
                    next_index += 1

            children.insert(index, widget)
            # we never want to insert widget _before_ canvas.before.
            if next_index == 0 and canvas.has_before:
                next_index = 1
            canvas.insert(next_index, widget.canvas)

    def remove_widget(self, widget):
        '''Remove a widget from the children of this widget.

        :Parameters:
            `widget`: :class:`Widget`
                Widget to remove from our children list.

        >>> from kivy.uix.button import Button
        >>> root = Widget()
        >>> button = Button()
        >>> root.add_widget(button)
        >>> root.remove_widget(button)
        '''
        if widget not in self.children:
            return
        self.children.remove(widget)
        self.canvas.remove(widget.canvas)
        widget.parent = None

    def clear_widgets(self, children=None):
        '''Remove all widgets added to this widget.

        .. versionchanged:: 1.8.0
            `children` argument can be used to select the children we want to
            remove. It should be a list of children (or filtered list) of the
            current widget.
        '''

        if not children:
            children = self.children
        remove_widget = self.remove_widget
        for child in children[:]:
            remove_widget(child)

    def export_to_png(self, filename, *args):
        '''Saves an image of the widget and its children in png format at the
        specified filename. Works by removing the widget canvas from its
        parent, rendering to an :class:`~kivy.graphics.fbo.Fbo`, and calling
        :meth:`~kivy.graphics.texture.Texture.save`.

        .. note::

            The image includes only this widget and its children. If you want to
            include widgets elsewhere in the tree, you must call
            :meth:`~Widget.export_to_png` from their common parent, or use
            :meth:`~kivy.core.window.Window.screenshot` to capture the whole
            window.

        .. note::

            The image will be saved in png format, you should include the
            extension in your filename.

        .. versionadded:: 1.8.1
        '''

        if self.parent is not None:
            canvas_parent_index = self.parent.canvas.indexof(self.canvas)
            self.parent.canvas.remove(self.canvas)

        fbo = Fbo(size=self.size)

        with fbo:
            ClearColor(0, 0, 0, 1)
            ClearBuffers()
            Translate(-self.x, -self.y, 0)

        fbo.add(self.canvas)
        fbo.draw()
        fbo.texture.save(filename)
        fbo.remove(self.canvas)

        if self.parent is not None:
            self.parent.canvas.insert(canvas_parent_index, self.canvas)

        return True

    def get_root_window(self):
        '''Return the root window.

        :Returns:
            Instance of the root window. Can be a
            :class:`~kivy.core.window.WindowBase` or
            :class:`Widget`.
        '''
        if self.parent:
            return self.parent.get_root_window()

    def get_parent_window(self):
        '''Return the parent window.

        :Returns:
            Instance of the parent window. Can be a
            :class:`~kivy.core.window.WindowBase` or
            :class:`Widget`.
        '''
        if self.parent:
            return self.parent.get_parent_window()

    def _walk(self, restrict=False, loopback=False, index=None):
        # we pass index only when we are going on the parent.
        # so don't yield the parent as well.
        if index is None:
            index = len(self.children)
            yield self

        for child in reversed(self.children[:index]):
            for walk_child in child._walk(restrict=True):
                yield walk_child

        # if we want to continue with our parent, just do it
        if not restrict:
            parent = self.parent
            try:
                if parent is None or not isinstance(parent, Widget):
                    raise ValueError
                index = parent.children.index(self)
            except ValueError:
                # self is root, if wanted to loopback from first element then ->
                if not loopback:
                    return
                # if we started with root (i.e. index==None), then we have to
                # start from root again, so we return self again. Otherwise, we
                # never returned it, so return it now starting with it
                parent = self
                index = None
            for walk_child in parent._walk(loopback=loopback, index=index):
                yield walk_child

    def walk(self, restrict=False, loopback=False):
        ''' Iterator that walks the widget tree starting with this widget and
        goes forward returning widgets in the order in which layouts display
        them.

        :Parameters:
            `restrict`:
                If True, it will only iterate through the widget and its
                children (or children of its children etc.). Defaults to False.
            `loopback`:
                If True, when the last widget in the tree is reached,
                it'll loop back to the uppermost root and start walking until
                we hit this widget again. Naturally, it can only loop back when
                `restrict` is False. Defaults to False.

        :return:
            A generator that walks the tree, returning widgets in the
            forward layout order.

        For example, given a tree with the following structure::

            GridLayout:
                Button
                BoxLayout:
                    id: box
                    Widget
                    Button
                Widget

        walking this tree::

            >>> # Call walk on box with loopback True, and restrict False
            >>> [type(widget) for widget in box.walk(loopback=True)]
            [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>,
                <class 'Widget'>, <class 'GridLayout'>, <class 'Button'>]
            >>> # Now with loopback False, and restrict False
            >>> [type(widget) for widget in box.walk()]
            [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>,
                <class 'Widget'>]
            >>> # Now with restrict True
            >>> [type(widget) for widget in box.walk(restrict=True)]
            [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>]

        .. versionadded:: 1.8.1
        '''
        gen = self._walk(restrict, loopback)
        yield next(gen)
        for node in gen:
            if node is self:
                return
            yield node

    def _walk_reverse(self, loopback=False, go_up=False):
        # process is walk up level, walk down its children tree, then walk up
        # next level etc.
        # default just walk down the children tree
        root = self
        index = 0
        # we need to go up a level before walking tree
        if go_up:
            root = self.parent
            try:
                if root is None or not isinstance(root, Widget):
                    raise ValueError
                index = root.children.index(self) + 1
            except ValueError:
                if not loopback:
                    return
                index = 0
                go_up = False
                root = self

        # now walk children tree starting with last-most child
        for child in islice(root.children, index, None):
            for walk_child in child._walk_reverse(loopback=loopback):
                yield walk_child
        # we need to return ourself last, in all cases
        yield root

        # if going up, continue walking up the parent tree
        if go_up:
            for walk_child in root._walk_reverse(loopback=loopback,
                                                 go_up=go_up):
                yield walk_child

    def walk_reverse(self, loopback=False):
        ''' Iterator that walks the widget tree backwards starting with the
        widget before this, and going backwards returning widgets in the
        reverse order in which layouts display them.

        This walks in the opposite direction of :meth:`walk`, so a list of the
        tree generated with :meth:`walk` will be in reverse order compared
        to the list generated with this, provided `loopback` is True.

        :Parameters:
            `loopback`:
                If True, when the uppermost root in the tree is
                reached, it'll loop back to the last widget and start walking
                back until after we hit widget again. Defaults to False

        :return:
            A generator that walks the tree, returning widgets in the
            reverse layout order.

        For example, given a tree with the following structure::

            GridLayout:
                Button
                BoxLayout:
                    id: box
                    Widget
                    Button
                Widget

        walking this tree::

            >>> # Call walk on box with loopback True
            >>> [type(widget) for widget in box.walk_reverse(loopback=True)]
            [<class 'Button'>, <class 'GridLayout'>, <class 'Widget'>,
                <class 'Button'>, <class 'Widget'>, <class 'BoxLayout'>]
            >>> # Now with loopback False
            >>> [type(widget) for widget in box.walk_reverse()]
            [<class 'Button'>, <class 'GridLayout'>]
            >>> forward = [w for w in box.walk(loopback=True)]
            >>> backward = [w for w in box.walk_reverse(loopback=True)]
            >>> forward == backward[::-1]
            True

        .. versionadded:: 1.8.1

        '''
        for node in self._walk_reverse(loopback=loopback, go_up=True):
            yield node
            if node is self:
                return

    def to_widget(self, x, y, relative=False):
        '''Convert the given coordinate from window to local widget
        coordinates. See :mod:`~kivy.uix.relativelayout` for details on the
        coordinate systems.
        '''
        if self.parent:
            x, y = self.parent.to_widget(x, y)
        return self.to_local(x, y, relative=relative)

    def to_window(self, x, y, initial=True, relative=False):
        '''Transform local coordinates to window coordinates. See
        :mod:`~kivy.uix.relativelayout` for details on the coordinate systems.
        '''
        if not initial:
            x, y = self.to_parent(x, y, relative=relative)
        if self.parent:
            return self.parent.to_window(x,
                                         y,
                                         initial=False,
                                         relative=relative)
        return (x, y)

    def to_parent(self, x, y, relative=False):
        '''Transform local coordinates to parent coordinates. See
        :mod:`~kivy.uix.relativelayout` for details on the coordinate systems.

        :Parameters:
            `relative`: bool, defaults to False
                Change to True if you want to translate relative positions from
                a widget to its parent coordinates.
        '''
        if relative:
            return (x + self.x, y + self.y)
        return (x, y)

    def to_local(self, x, y, relative=False):
        '''Transform parent coordinates to local coordinates. See
        :mod:`~kivy.uix.relativelayout` for details on the coordinate systems.

        :Parameters:
            `relative`: bool, defaults to False
                Change to True if you want to translate coordinates to
                relative widget coordinates.
        '''
        if relative:
            return (x - self.x, y - self.y)
        return (x, y)

    x = NumericProperty(0)
    '''X position of the widget.

    :attr:`x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0.
    '''

    y = NumericProperty(0)
    '''Y position of the widget.

    :attr:`y` is a :class:`~kivy.properties.NumericProperty` and defaults to 0.
    '''

    width = NumericProperty(100)
    '''Width of the widget.

    :attr:`width` is a :class:`~kivy.properties.NumericProperty` ans defaults
    to 100.

    .. warning::
        Keep in mind that the `width` property is subject to layout logic and
        that this has not yet happened at the time of the widget's `__init__`
        method.
    '''

    height = NumericProperty(100)
    '''Height of the widget.

    :attr:`height` is a :class:`~kivy.properties.NumericProperty` and defaults
    to 100.

    .. warning::
        Keep in mind that the `height` property is subject to layout logic and
        that this has not yet happened at the time of the widget's `__init__`
        method.
    '''

    pos = ReferenceListProperty(x, y)
    '''Position of the widget.

    :attr:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`x`, :attr:`y`) properties.
    '''

    size = ReferenceListProperty(width, height)
    '''Size of the widget.

    :attr:`size` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`width`, :attr:`height`) properties.
    '''

    def get_right(self):
        return self.x + self.width

    def set_right(self, value):
        self.x = value - self.width

    right = AliasProperty(get_right, set_right, bind=('x', 'width'))
    '''Right position of the widget.

    :attr:`right` is an :class:`~kivy.properties.AliasProperty` of
    (:attr:`x` + :attr:`width`),
    '''

    def get_top(self):
        return self.y + self.height

    def set_top(self, value):
        self.y = value - self.height

    top = AliasProperty(get_top, set_top, bind=('y', 'height'))
    '''Top position of the widget.

    :attr:`top` is an :class:`~kivy.properties.AliasProperty` of
    (:attr:`y` + :attr:`height`),
    '''

    def get_center_x(self):
        return self.x + self.width / 2.

    def set_center_x(self, value):
        self.x = value - self.width / 2.

    center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width'))
    '''X center position of the widget.

    :attr:`center_x` is an :class:`~kivy.properties.AliasProperty` of
    (:attr:`x` + :attr:`width` / 2.),
    '''

    def get_center_y(self):
        return self.y + self.height / 2.

    def set_center_y(self, value):
        self.y = value - self.height / 2.

    center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height'))
    '''Y center position of the widget.

    :attr:`center_y` is an :class:`~kivy.properties.AliasProperty` of
    (:attr:`y` + :attr:`height` / 2.)
    '''

    center = ReferenceListProperty(center_x, center_y)
    '''Center position of the widget.

    :attr:`center` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`center_x`, :attr:`center_y`)
    '''

    cls = ListProperty([])
    '''Class of the widget, used for styling.
    '''

    id = StringProperty(None, allownone=True)
    '''Unique identifier of the widget in the tree.

    :attr:`id` is a :class:`~kivy.properties.StringProperty` and defaults to
    None.

    .. warning::

        If the :attr:`id` is already used in the tree, an exception will
        be raised.
    '''

    children = ListProperty([])
    '''List of children of this widget.

    :attr:`children` is a :class:`~kivy.properties.ListProperty` and
    defaults to an empty list.

    Use :meth:`add_widget` and :meth:`remove_widget` for manipulating the
    children list. Don't manipulate the children list directly unless you know
    what you are doing.
    '''

    parent = ObjectProperty(None, allownone=True)
    '''Parent of this widget.

    :attr:`parent` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to None.

    The parent of a widget is set when the widget is added to another widget
    and unset when the widget is removed from its parent.
    '''

    size_hint_x = NumericProperty(1, allownone=True)
    '''X size hint. Represents how much space the widget should use in the
    direction of the X axis relative to its parent's width.
    Only the :class:`~kivy.uix.layout.Layout` and
    :class:`~kivy.core.window.Window` classes make use of the hint.

    The value is in percent as a float from 0. to 1., where 1. means the full
    size of his parent. 0.5 represents 50%.

    :attr:`size_hint_x` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1.
    '''

    size_hint_y = NumericProperty(1, allownone=True)
    '''Y size hint.

    :attr:`size_hint_y` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1.

    See :attr:`size_hint_x` for more information
    '''

    size_hint = ReferenceListProperty(size_hint_x, size_hint_y)
    '''Size hint.

    :attr:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`size_hint_x`, :attr:`size_hint_y`).

    See :attr:`size_hint_x` for more information
    '''

    pos_hint = ObjectProperty({})
    '''Position hint. This property allows you to set the position of
    the widget inside its parent layout, in percent (similar to
    size_hint).

    For example, if you want to set the top of the widget to be at 90%
    height of its parent layout, you can write::

        widget = Widget(pos_hint={'top': 0.9})

    The keys 'x', 'right' and 'center_x' will use the parent width.
    The keys 'y', 'top' and 'center_y' will use the parent height.

    See :doc:`api-kivy.uix.floatlayout` for further reference.

    Position hint is only used by the
    :class:`~kivy.uix.floatlayout.FloatLayout` and
    :class:`~kivy.core.window.Window`.

    :attr:`pos_hint` is an :class:`~kivy.properties.ObjectProperty`
    containing a dict.
    '''

    ids = DictProperty({})
    '''This is a Dictionary of id's defined in your kv language. This will only
    be populated if you use id's in your kv language code.

    .. versionadded:: 1.7.0

    :attr:`ids` is a :class:`~kivy.properties.DictProperty` and defaults to a
    empty dict {}.
    '''

    opacity = NumericProperty(1.0)
    '''Opacity of the widget and all the children.

    .. versionadded:: 1.4.1

    The opacity attribute controls the opacity of the widget and its children.
    Be careful, it's a cumulative attribute: the value is multiplied by the
    current global opacity and the result is applied to the current context
    color.

    For example, if the parent has an opacity of 0.5 and a child has an
    opacity of 0.2, the real opacity of the child will be 0.5 * 0.2 = 0.1.

    Then, the opacity is applied by the shader as::

        frag_color = color * vec4(1.0, 1.0, 1.0, opacity);

    :attr:`opacity` is a :class:`~kivy.properties.NumericProperty` and defaults
    to 1.0.
    '''

    def on_opacity(self, instance, value):
        canvas = self.canvas
        if canvas is not None:
            canvas.opacity = value

    canvas = None
    '''Canvas of the widget.

    The canvas is a graphics object that contains all the drawing instructions
    for the graphical representation of the widget.

    There are no general properties for the Widget class, such as background
    color, to keep the design simple and lean. Some derived classes, such as
    Button, do add such convenience properties but generally the developer is
    responsible for implementing the graphics representation for a custom
    widget from the ground up. See the derived widget classes for patterns to
    follow and extend.

    See :class:`~kivy.graphics.Canvas` for more information about the usage.
    '''

    disabled = BooleanProperty(False)
    '''Indicates whether this widget can interact with input or not.
Esempio n. 27
0
class MainApp(App):

    initpos_x = NumericProperty(start_pointX[maze_number])
    initpos_y = NumericProperty(start_pointY[maze_number])
    position = ReferenceListProperty(initpos_x, initpos_y)
    print("Start : ", initpos_x, " - ", initpos_y)

    def build(self):

        self.parent = MainWidget()
        #self.parent.serve_agent()
        self.parent.agent.pos = self.position
        self.parent.agent.velocity = Vector(6, 0)
        self.My_Clock = Clock
        if (maze_file[maze_number].find("maze_train") != -1):
            pass
        else:
            dateTimeObj = datetime.now()
            # log file
            log_file = open(maze_file[maze_number] + "_logfile.txt", "a")
            log_file.writelines("\nStart : " + str(dateTimeObj) + "\n")
            log_file.close()
            self.My_Clock.schedule_interval(self.parent.update, 1.0 / 45.0)
            mind.load()
            if ddqn:
                mind.target_load()
                print("Target Loaded")

        self.painter = MyPaintWidget()
        startbtn = Button(text='Start')
        pausebtn = Button(text='Pause', pos=(self.parent.width, 0))
        savebtn = Button(text='Save Memory', pos=(2 * self.parent.width, 0))
        loadbtn = Button(text='Load Memory', pos=(3 * self.parent.width, 0))
        clearbtn = Button(text='Clear frame', pos=(4 * self.parent.width, 0))

        startbtn.bind(on_release=self.start)
        savebtn.bind(on_release=self.save)
        loadbtn.bind(on_release=self.load)
        pausebtn.bind(on_release=self.on_pause)
        clearbtn.bind(on_release=self.clear_canvas)
        self.parent.add_widget(self.painter)
        self.parent.add_widget(startbtn)
        self.parent.add_widget(savebtn)
        self.parent.add_widget(loadbtn)
        self.parent.add_widget(pausebtn)
        self.parent.add_widget(clearbtn)
        return self.parent

    def start(self, obj):
        self.My_Clock.schedule_interval(self.parent.update, 1.0 / 60.0)

    def save(self, obj):
        print("saving mind...")
        dateTimeObj = datetime.now()
        file_name = str(maze_file[maze_number]) + '_' + str(
            dateTimeObj.year) + '_' + str(dateTimeObj.month) + '_' + str(
                dateTimeObj.day) + str(dateTimeObj.hour) + '_' + str(
                    dateTimeObj.minute) + '_' + str(
                        dateTimeObj.second) + '_' + str(
                            dateTimeObj.microsecond)
        file_name = file_name + "_DDQN_" + str(ddqn)
        mind.save(scores, file_name)
        plt.plot(scores)
        plt.savefig(file_name + ".png")
        plt.show()

    def load(self, obj):
        print("loading last saved mind...")
        mind.load()

    def on_pause(self, obj):
        self.My_Clock.unschedule(self.parent.update)
        print("Paused")

    def clear_canvas(self, obj):
        global white_space
        self.painter.canvas.clear()
        white_space = maze_magnified.copy()
Esempio n. 28
0
class TreeView(Widget):
    '''TreeView class. See module documentation for more information.

    :Events:
        `on_node_expand`: (node, )
            Fired when a node is being expanded
        `on_node_collapse`: (node, )
            Fired when a node is being collapsed
    '''

    __events__ = ('on_node_expand', 'on_node_collapse')

    def __init__(self, **kwargs):
        self._trigger_layout = Clock.create_trigger(self._do_layout, -1)
        super(TreeView, self).__init__(**kwargs)
        tvlabel = TreeViewLabel(text='Root', is_open=True, level=0)
        for key, value in self.root_options.items():
            setattr(tvlabel, key, value)
        self._root = self.add_node(tvlabel, None)
        self.bind(pos=self._trigger_layout,
                  size=self._trigger_layout,
                  indent_level=self._trigger_layout,
                  indent_start=self._trigger_layout)
        self._trigger_layout()

    def add_node(self, node, parent=None):
        '''Add a new node in the tree.

        :Parameters:
            `node`: instance of a :class:`TreeViewNode`
                Node to add into the tree
            `parent`: instance of a :class:`TreeViewNode`, defaults to None
                Parent node to attach the new node
        '''
        # check if the widget is "ok" for a node
        if not isinstance(node, TreeViewNode):
            raise TreeViewException(
                'The node must be a subclass of TreeViewNode')
        # create node
        if parent is None and self._root:
            parent = self._root
        if parent:
            parent.is_leaf = False
            parent.nodes.append(node)
            node.parent_node = parent
            node.level = parent.level + 1
        node.bind(size=self._trigger_layout)
        self._trigger_layout()
        return node

    def remove_node(self, node):
        '''Remove a node in a tree.

        .. versionadded:: 1.0.7

        :Parameters:
            `node`: instance of a :class:`TreeViewNode`
                Node to remove from the tree
        '''
        # check if the widget is "ok" for a node
        if not isinstance(node, TreeViewNode):
            raise TreeViewException(
                'The node must be a subclass of TreeViewNode')
        parent = node.parent_node
        if parent is not None:
            nodes = parent.nodes
            if node in nodes:
                nodes.remove(node)
            parent.is_leaf = not bool(len(nodes))
            node.parent_node = None
            node.unbind(size=self._trigger_layout)
            self._trigger_layout()

    def on_node_expand(self, node):
        pass

    def on_node_collapse(self, node):
        pass

    def select_node(self, node):
        '''Select a node in the tree.
        '''
        if node.no_selection:
            return
        if self._selected_node:
            self._selected_node.is_selected = False
        node.is_selected = True
        self._selected_node = node

    def toggle_node(self, node):
        '''Toggle the state of the node (open/collapsed).
        '''
        node.is_open = not node.is_open
        if node.is_open:
            if self.load_func and not node.is_loaded:
                self._do_node_load(node)
            self.dispatch('on_node_expand', node)
        else:
            self.dispatch('on_node_collapse', node)
        self._trigger_layout()

    def get_node_at_pos(self, pos):
        '''Get the node at the position (x, y).
        '''
        x, y = pos
        for node in self.iterate_open_nodes(self.root):
            if self.x <= x <= self.right and \
               node.y <= y <= node.top:
                return node

    def iterate_open_nodes(self, node=None):
        '''Generator to iterate over expanded nodes.

        To get all the open nodes::

            treeview = TreeView()
            # ... add nodes ...
            for node in treeview.iterate_open_nodes():
                print(node)

        '''
        if not node:
            node = self.root
        if self.hide_root and node is self.root:
            pass
        else:
            yield node
        if not node.is_open:
            return
        f = self.iterate_open_nodes
        for cnode in node.nodes:
            for ynode in f(cnode):
                yield ynode

    def iterate_all_nodes(self, node=None):
        '''Generator to iterate over all nodes, expanded or not.
        '''
        if not node:
            node = self.root
        yield node
        f = self.iterate_all_nodes
        for cnode in node.nodes:
            for ynode in f(cnode):
                yield ynode

    #
    # Private
    #
    def on_load_func(self, instance, value):
        if value:
            Clock.schedule_once(self._do_initial_load)

    def _do_initial_load(self, *largs):
        if not self.load_func:
            return
        self._do_node_load(None)

    def _do_node_load(self, node):
        gen = self.load_func(self, node)
        if node:
            node.is_loaded = True
        if not gen:
            return
        for cnode in gen:
            self.add_node(cnode, node)

    def on_root_options(self, instance, value):
        if not self.root:
            return
        for key, value in value.items():
            setattr(self.root, key, value)

    def _do_layout(self, *largs):
        self.clear_widgets()
        # display only the one who are is_open
        self._do_open_node(self.root)
        # now do layout
        self._do_layout_node(self.root, 0, self.top)
        # now iterate for calculating minimum size
        min_width = min_height = 0
        count = 0
        for node in self.iterate_open_nodes(self.root):
            node.odd = False if count % 2 else True
            count += 1
            min_width = max(
                min_width, node.width + self.indent_level +
                node.level * self.indent_level)
            min_height += node.height
        self.minimum_size = (min_width, min_height)

    def _do_open_node(self, node):
        if self.hide_root and node is self.root:
            height = 0
        else:
            self.add_widget(node)
            height = node.height
            if not node.is_open:
                return height
        for cnode in node.nodes:
            height += self._do_open_node(cnode)
        return height

    def _do_layout_node(self, node, level, y):
        if self.hide_root and node is self.root:
            level -= 1
        else:
            node.x = self.x + self.indent_start + level * self.indent_level
            node.top = y
            if node.size_hint_x:
                node.width = (self.width -
                              (node.x - self.x)) * node.size_hint_x
            y -= node.height
            if not node.is_open:
                return y
        for cnode in node.nodes:
            y = self._do_layout_node(cnode, level + 1, y)
        return y

    def on_touch_down(self, touch):
        node = self.get_node_at_pos(touch.pos)
        if not node:
            return
        if node.disabled:
            return
        # toggle node or selection ?
        if node.x - self.indent_start <= touch.x < node.x:
            self.toggle_node(node)
        elif node.x <= touch.x:
            self.select_node(node)
            node.dispatch('on_touch_down', touch)
        return True

    #
    # Private properties
    #
    _root = ObjectProperty(None)

    _selected_node = ObjectProperty(None)

    #
    # Properties
    #

    minimum_width = NumericProperty(0)
    '''Minimum width needed to contain all children.

    .. versionadded:: 1.0.9

    :data:`minimum_width` is a :class:`kivy.properties.NumericProperty` and
    defaults to 0.
    '''

    minimum_height = NumericProperty(0)
    '''Minimum height needed to contain all children.

    .. versionadded:: 1.0.9

    :data:`minimum_height` is a :class:`kivy.properties.NumericProperty` and
    defaults to 0.
    '''

    minimum_size = ReferenceListProperty(minimum_width, minimum_height)
    '''Minimum size needed to contain all children.

    .. versionadded:: 1.0.9

    :data:`minimum_size` is a :class:`~kivy.properties.ReferenceListProperty`
    of (:data:`minimum_width`, :data:`minimum_height`) properties.
    '''

    indent_level = NumericProperty('16dp')
    '''Width used for the indentation of each level except the first level.

    Computation of spacing for each level of tree::

        :data:`indent_start` + level * :data:`indent_level`

    :data:`indent_level` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 16.
    '''

    indent_start = NumericProperty('24dp')
    '''Indentation width of the level 0 / root node. This is mostly the initial
    size to accommodate a tree icon (collapsed / expanded). See
    :data:`indent_level` for more information about the computation of level
    indentation.

    :data:`indent_start` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 24.
    '''

    hide_root = BooleanProperty(False)
    '''Use this property to show/hide the initial root node. If True, the root
    node will be appear as a closed node.

    :data:`hide_root` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    def get_selected_node(self):
        return self._selected_node

    selected_node = AliasProperty(get_selected_node,
                                  None,
                                  bind=('_selected_node', ))
    '''Node selected by :meth:`TreeView.select_node` or by touch.

    :data:`selected_node` is a :class:`~kivy.properties.AliasProperty` and
    defaults to None. It is read-only.
    '''

    def get_root(self):
        return self._root

    root = AliasProperty(get_root, None, bind=('_root', ))
    '''Root node.

    By default, the root node widget is a :class:`TreeViewLabel` with text
    'Root'. If you want to change the default options passed to the widget
    creation, use the :data:`root_options` property::

        treeview = TreeView(root_options={
            'text': 'Root directory',
            'font_size': 15})

    :data:`root_options` will change the properties of the
    :class:`TreeViewLabel` instance. However, you cannot change the class used
    for root node yet.

    :data:`root` is an :class:`~kivy.properties.AliasProperty` and defaults to
    None. It is read-only. However, the content of the widget can be changed.
    '''

    root_options = ObjectProperty({})
    '''Default root options to pass for root widget. See :data:`root` property
    for more information about the usage of root_options.

    :data:`root_options` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to {}.
    '''

    load_func = ObjectProperty(None)
    '''Callback to use for asynchronous loading. If set, asynchronous loading
Esempio n. 29
0
class Robot(Widget):

    # Initialise the robot's angle (this is the angle between the x-axis and the robot's axis).
    angle = NumericProperty(0)

    # Initialise the robot's rotations
    # This allows the robot to:
    #    rotate 0 degrees (move forward),
    #    rotate -21 degrees (turn left), or
    #    rotate +21 degrees (turn right).
    rotation = NumericProperty(0)

    # Initialise x co-ordinate of the velocity vector for the robot.
    velocity_x = NumericProperty(0)

    # Initialise y co-ordinate of the velocity vector for the robot.
    velocity_y = NumericProperty(0)

    # Initialise the robot's velocity vector.
    # This uses velocity_x and velocity_y as the x and y co-ordinates.
    velocity = ReferenceListProperty(velocity_x, velocity_y)

    # Initialising the x and y co-ordinates of the centre sensor.
    sensor_centre_x = NumericProperty(0)
    sensor_centre_y = NumericProperty(0)
    # The vector of the centre sensor
    sensor_centre = ReferenceListProperty(sensor_centre_x, sensor_centre_y)

    # Initialising the x and y co-ordinates of the left sensor.
    sensor_left_x = NumericProperty(0)
    sensor_left_y = NumericProperty(0)
    # The vector of the left sensor
    sensor_left = ReferenceListProperty(sensor_left_x, sensor_left_y)

    # Initialising the x and y co-ordinates of the right sensor.
    sensor_right_x = NumericProperty(0)
    sensor_right_y = NumericProperty(0)
    # The vector of the right sensor
    sensor_right = ReferenceListProperty(sensor_right_x, sensor_right_y)

    # Initialising the input signals from the sensors.
    signal_centre = NumericProperty(0)
    signal_left = NumericProperty(0)
    signal_right = NumericProperty(0)

    def move(self, rotation):

        # Update the position of the robot.
        # This uses the robot's previous velocity and position.
        self.pos = Vector(*self.velocity) + self.pos

        # This is the rotatio =n of the robot.
        self.rotation = rotation

        # This updates the angle of turn (of the robot).
        self.angle = self.angle + self.rotation

        # Updating the positions of the sensors.
        self.sensor_centre = Vector(30, 0).rotate(self.angle) + self.pos
        self.sensor_right = Vector(30, 0).rotate(
            (self.angle - 30) % 360) + self.pos
        self.sensor_left = Vector(30, 0).rotate(
            (self.angle + 30) % 360) + self.pos

        # Calculating the density of the obstacles around the sensors.
        self.signal_centre = int(
            np.sum(obstacle[int(self.sensor_centre_x) -
                            10:int(self.sensor_centre_x) + 10,
                            int(self.sensor_centre_y) -
                            10:int(self.sensor_centre_y) + 10])) / 400.
        self.signal_left = int(
            np.sum(obstacle[int(self.sensor_left_x) -
                            10:int(self.sensor_left_x) + 10,
                            int(self.sensor_left_y) -
                            10:int(self.sensor_left_y) + 10])) / 400.
        self.signal_right = int(
            np.sum(obstacle[int(self.sensor_right_x) -
                            10:int(self.sensor_right_x) + 10,
                            int(self.sensor_right_y) -
                            10:int(self.sensor_right_y) + 10])) / 400.

        # Check if the sensor is out of the map.
        if self.sensor_centre_x > map_width - 10 or self.sensor_centre_x < 10 or self.sensor_centre_y > map_height - 10 or self.sensor_centre_y < 10:
            # This makes the density of the obstacles very high, making sure that the robot doesn't wander off the map.
            self.signal_centre = 1.

        if self.sensor_left_x > map_width - 10 or self.sensor_left_x < 10 or self.sensor_left_y > map_height - 10 or self.sensor_left_y < 10:
            self.signal_left = 1.

        if self.sensor_right_x > map_width - 10 or self.sensor_right_x < 10 or self.sensor_right_y > map_height - 10 or self.sensor_right_y < 10:
            self.signal_right = 1.
Esempio n. 30
0
class ProfileService(EventDispatcher):
    '''
    Simple service for managing profiles.
    '''

    active_profile = StringProperty('')
    profiles = ListProperty([])

    left_488 = NumericProperty(0)
    right_488 = NumericProperty(0)
    left_405 = NumericProperty(0)
    right_405 = NumericProperty(0)
    left_561 = NumericProperty(0)
    right_561 = NumericProperty(0)
    left_638 = NumericProperty(0)
    right_638 = NumericProperty(0)

    saved_offsets = ListProperty([])
    applied_offsets = ListProperty([])
    current_offsets = ReferenceListProperty(left_488, right_488, left_405,
                                            right_405, left_561, right_561,
                                            left_638, right_638)

    def __init__(self, path, initial_profile='default', **kwargs):
        super(ProfileService, self).__init__(**kwargs)
        self._path = self._ensure_file(path)
        self._config = ConfigParser()
        self._config.read(self._path)
        self.set_profile(initial_profile)
        self.profiles = filter(lambda x: 'file_path' not in x,
                               self._config.sections())

    @property
    def parameters(self):
        keys = list(
            filter(
                lambda attr: attr.startswith('left_') or attr.startswith(
                    'right_'), dir(self)))
        return {key: getattr(self, key) for key in keys}

    def _ensure_file(self, path):
        if not os.path.isfile(path):
            open(path, 'w').close()
        return path

    def set_profile(self, section):
        if hasattr(section, 'text'):
            section = section.text
        self.active_profile = section
        for item in self._config.items(section):
            param, val = item
            setattr(self, param, round(float(val), 6))
        self.saved_offsets = self.current_offsets

    def save_profile(self, widget=None):
        print('Profile saved.')
        #self.saved_offsets = self.current_offsets
        for param, val in self.parameters.items():
            self._config.set(self.active_profile, param, str(val))
        with open(self._path, 'r+') as f:
            self._config.write(f)

    def add_profile(self, widget):
        section = widget.text
        if section in self._config.sections():
            return
        self.profiles.append(section)
        self._config.add_section(section)
        for param, val in self.parameters.items():
            self._config.set(section, param, str(val))
        self.save_profile()
        self.set_profile(section)

    def delete_profile(self, section, _):
        self._config.remove_section(section)
        self.profiles.pop(self.profiles.index(section))
        self.set_profile(self.profiles[0])
        self.save_profile()
        return True
Esempio n. 31
0
    def test_reference(self):
        from kivy.properties import NumericProperty, ReferenceListProperty

        x = NumericProperty(0)
        x.link(wid, 'x')
        x.link_deps(wid, 'x')
        y = NumericProperty(0)
        y.link(wid, 'y')
        y.link_deps(wid, 'y')
        pos = ReferenceListProperty(x, y)
        pos.link(wid, 'pos')
        pos.link_deps(wid, 'pos')

        self.assertEqual(x.get(wid), 0)
        self.assertEqual(y.get(wid), 0)
        self.assertEqual(pos.get(wid), [0, 0])

        x.set(wid, 50)
        self.assertEqual(pos.get(wid), [50, 0])

        y.set(wid, 50)
        self.assertEqual(pos.get(wid), [50, 50])

        pos.set(wid, [0, 0])
        self.assertEqual(pos.get(wid), [0, 0])
        self.assertEqual(x.get(wid), 0)
        self.assertEqual(y.get(wid), 0)

        # test observer
        global observe_called
        observe_called = 0

        def observe(obj, value):
            global observe_called
            observe_called = 1
        pos.bind(wid, observe)

        self.assertEqual(observe_called, 0)
        x.set(wid, 99)
        self.assertEqual(observe_called, 1)
Esempio n. 32
0
class Brick(DragBehavior, Label):

    # NOTE: values of properties without defaults
    # shall be set in the .kv file
    background_color = ListProperty()
    border_color = ListProperty()

    # (declaring the following class-wide constants as properties makes it
    # easier to specify/inherit/overridde their values just in the .kv file)
    detached_border_color = ListProperty()
    move_border_color = ListProperty()
    attached_border_color = ListProperty()
    equal_border_color = ListProperty()
    final_border_color = ListProperty()

    max_snap_x_distance = NumericProperty()
    max_snap_y_distance = NumericProperty()
    max_double_attach_x_distance = NumericProperty()
    max_double_attach_y_distance = NumericProperty()

    target_x = NumericProperty(0)
    target_y = NumericProperty(0)
    target_pos = ReferenceListProperty(target_x, target_y)

    def get_target_right(self):
        return self.target_x + self.width

    def set_target_right(self, value):
        self.target_x = value - self.width

    target_right = AliasProperty(get_target_right,
                                 set_target_right,
                                 bind=('target_x', 'width'))

    target_right_pos = ReferenceListProperty(target_right, target_y)

    state = OptionProperty('detached',
                           options=[
                               'detached',
                               'move',
                               'attached',
                               'equal',
                               'final',
                           ])

    left_attached_brick = None
    right_attached_brick = None

    @property
    def symbol(self):
        return self.text

    # event dispatch

    def on_touch_down(self, touch):
        if self.state != 'final' and super(Brick, self).on_touch_down(touch):
            self.update_states_before_detach()
            self.detach()
            self.state = 'move'
            return True
        return False

    def on_touch_up(self, touch):
        if self.state != 'final' and super(Brick, self).on_touch_up(touch):
            self.target_pos = self.pos
            assert self.state == 'move'
            if self.attach():
                self.update_states_after_attach()
            else:
                self.state = 'detached'
            return True
        return False

    # detaching

    def update_states_before_detach(self):
        for brick_seq in (self.collect_all_left(), self.collect_all_right()):
            if brick_seq:
                if len(brick_seq) == 1:
                    brick_seq[0].state = 'detached'
                elif self.is_brick_seq_equal(brick_seq):
                    for brick in brick_seq:
                        brick.state = 'equal'
                else:
                    for brick in brick_seq:
                        brick.state = 'attached'

    def detach(self):
        left_brick = self.left_attached_brick
        if left_brick is not None:
            left_brick.right_attached_brick = None
            self.left_attached_brick = None
        right_brick = self.right_attached_brick
        if right_brick is not None:
            right_brick.left_attached_brick = None
            self.right_attached_brick = None

    # attaching

    def attach(self):
        (left_brick, right_brick,
         target_pos) = self.get_bricks_and_target_pos()
        if target_pos is not None:
            if left_brick is not None:
                left_brick.right_attached_brick = self.proxy_ref
                self.left_attached_brick = left_brick.proxy_ref
            if right_brick is not None:
                right_brick.left_attached_brick = self.proxy_ref
                self.right_attached_brick = right_brick.proxy_ref
            self.target_pos = target_pos
            return True
        return False

    def get_bricks_and_target_pos(self):
        left_brick = self.choose_left_brick()
        right_brick = self.choose_right_brick()
        if right_brick is not None:
            target_pos_by_right = (right_brick.target_x - self.width,
                                   right_brick.target_y)
            if left_brick is not None:
                target_pos_by_left = tuple(left_brick.target_right_pos)
                distance_from_left = (Vector(self.target_pos).distance(
                    left_brick.target_right_pos))
                distance_from_right = (Vector(self.target_right_pos).distance(
                    right_brick.target_pos))
                if self.can_attach_to_both(left_brick, right_brick,
                                           target_pos_by_left,
                                           target_pos_by_right,
                                           distance_from_left,
                                           distance_from_right):
                    target_pos = interpolate(target_pos_by_left,
                                             target_pos_by_right,
                                             step=2)
                elif self.should_attach_to_left(left_brick, right_brick,
                                                distance_from_left,
                                                distance_from_right):
                    target_pos = target_pos_by_left
                    right_brick = None
                else:
                    target_pos = target_pos_by_right
                    left_brick = None
            else:
                target_pos = target_pos_by_right
        elif left_brick is not None:
            target_pos = left_brick.target_right_pos
        else:
            target_pos = None
        return left_brick, right_brick, target_pos

    def choose_left_brick(self):
        _distance = Vector(self.target_pos).distance
        bricks_and_distances = [(brick, _distance(brick.target_right_pos),
                                 abs(self.target_x - brick.target_right))
                                for brick in self.iter_all_bricks()
                                if brick.right_attached_brick is None]
        return self.get_attachable_brick(bricks_and_distances)

    def choose_right_brick(self):
        _distance = Vector(self.target_right_pos).distance
        bricks_and_distances = [(brick, _distance(brick.target_pos),
                                 abs(self.target_right - brick.target_x))
                                for brick in self.iter_all_bricks()
                                if brick.left_attached_brick is None]
        return self.get_attachable_brick(bricks_and_distances)

    def get_attachable_brick(self, bricks_and_distances):
        bricks_and_distances.sort(key=operator.itemgetter(1))
        for brick, _, x_distance in bricks_and_distances:
            if brick == self:
                continue
            # (for checking snap limits, using x and y separately
            # plays better than using the real x*y distance)
            y_distance = abs(self.target_y - brick.target_y)
            if (x_distance > self.max_snap_x_distance
                    or y_distance > self.max_snap_y_distance):
                return None
            if self.can_be_attached_to(brick):
                return brick

    def can_be_attached_to(self, brick):
        return brick.state != 'move'

    def can_attach_to_both(self, left_brick, right_brick, target_pos_by_left,
                           target_pos_by_right, distance_from_left,
                           distance_from_right):
        return (Vector(target_pos_by_left).distance(target_pos_by_right) <
                self.width / 3) or (
                    (distance_from_right / 3.5 <= distance_from_left <=
                     3.5 * distance_from_right) and
                    (abs(self.target_x - left_brick.target_right) <=
                     self.max_double_attach_x_distance) and
                    (abs(self.target_y - left_brick.target_y) <=
                     self.max_double_attach_y_distance) and
                    (abs(self.target_right - right_brick.target_x) <=
                     self.max_double_attach_x_distance) and
                    (abs(self.target_y - right_brick.target_y) <=
                     self.max_double_attach_y_distance))

    def should_attach_to_left(self, left_brick, right_brick,
                              distance_from_left, distance_from_right):
        from_left = abs(self.target_y - left_brick.target_y)
        from_right = abs(self.target_y - right_brick.target_y)
        if (from_left < self.height / 4 and from_right < self.height / 4) or (
                from_right / 1.4 <= from_left <= 1.4 * from_right):
            # y distances are too small or too similar to
            # be conclusive => let's use real x*y distances
            from_left = distance_from_left
            from_right = distance_from_right
        if from_left <= from_right:
            return True
        else:
            assert from_left > from_right
            return False

    def update_states_after_attach(self):
        brick_seq = self.collect_all_left()
        brick_seq.append(self)
        self.collect_all_right(brick_seq)
        if self.is_brick_seq_equal(brick_seq):
            for brick in brick_seq:
                brick.state = 'equal'
            all_bricks = list(self.iter_all_bricks())
            if all(brick.state == 'equal' for brick in all_bricks):
                for brick in all_bricks:
                    brick.state = 'final'
                self.parent.finish_game()
        else:
            for brick in brick_seq:
                brick.state = 'attached'

    # commons

    def collect_all_left(self, brick_seq=None):
        if brick_seq is None:
            brick_seq = []
        left_attached_brick = self.left_attached_brick
        if left_attached_brick is not None:
            left_attached_brick.collect_all_left(brick_seq)
            brick_seq.append(left_attached_brick)
        return brick_seq

    def collect_all_right(self, brick_seq=None):
        if brick_seq is None:
            brick_seq = []
        right_attached_brick = self.right_attached_brick
        if right_attached_brick is not None:
            brick_seq.append(right_attached_brick)
            right_attached_brick.collect_all_right(brick_seq)
        return brick_seq

    def is_brick_seq_equal(self, brick_seq):
        expr_str = ''.join(brick.symbol for brick in brick_seq)
        if '==' not in expr_str:
            return False
        try:
            return eval(expr_str)
        except (SyntaxError, ArithmeticError):
            return False

    def iter_all_bricks(self):
        return self.parent.iter_all_bricks()