Esempio n. 1
0
class ScrollView(StencilView):
    '''ScrollView class. See module documentation for more information.

    .. versionchanged:: 1.7.0

        `auto_scroll`, `scroll_friction`, `scroll_moves`, `scroll_stoptime' has
        been deprecated, use :data:`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.

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

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

    :data:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty`,
    default 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
    :data:`do_scroll_x` is True.

    :data:`scroll_x` is a :class:`~kivy.properties.NumericProperty`,
    default 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
    :data:`do_scroll_y` is True.

    :data:`scroll_y` is a :class:`~kivy.properties.NumericProperty`,
    default to 1.
    '''

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

    :data:`do_scroll_x` is a :class:`~kivy.properties.BooleanProperty`,
    default to True.
    '''

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

    :data:`do_scroll_y` is a :class:`~kivy.properties.BooleanProperty`,
    default 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.

    :data:`do_scroll` is a :class:`~kivy.properties.AliasProperty` of
    (:data:`do_scroll_x` + :data:`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.

    :data:`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.

    :data:`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

    :data:`bar_color` is a :class:`~kivy.properties.ListProperty`, default to
    [.7, .7, .7, .9].
    '''

    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

    :data:`bar_width` is a :class:`~kivy.properties.NumericProperty`, default
    to 2
    '''

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

    .. versionadded:: 1.2.0

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

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

    .. versionadded:: 1.7.0

    :data:`effect_cls` is a :class:`~kivy.properties.ObjectProperty`, default to
    :class:`DampedScrollEffect`.
    '''

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

    .. versionadded:: 1.7.0

    :data:`effect_x` is a :class:`~kivy.properties.ObjectProperty`, default to
    None
    '''

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

    .. versionadded:: 1.7.0

    :data:`effect_y` is a :class:`~kivy.properties.ObjectProperty`, default 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', 'both'))
    '''Sets the type of scrolling to use for the content of the scrollview.
    Available options are: content, bars, both.

    .. versionadded:: 1.8.0

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

    def on_scroll_type(self, instance, value):
        self.bar_width = max('9dp', self.bar_width)\
            if value[0] == 'b' else self.bar_width

    # private, for internal use only

    _viewport = ObjectProperty(None, allownone=True)
    bar_alpha = NumericProperty(1.)

    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

    def __init__(self, **kwargs):
        self._touch = None
        self._trigger_update_from_scroll = Clock.create_trigger(
            self.update_from_scroll, -1)
        super(ScrollView, self).__init__(**kwargs)
        if self.effect_x is None and self.effect_cls is not None:
            self.effect_x = self.effect_cls(target_widget=self._viewport)
        if self.effect_y is None and self.effect_cls is not None:
            self.effect_y = self.effect_cls(target_widget=self._viewport)
        self.bind(width=self._update_effect_x_bounds,
                  height=self._update_effect_y_bounds,
                  viewport_size=self._update_effect_bounds,
                  _viewport=self._update_effect_widget,
                  scroll_x=self._trigger_update_from_scroll,
                  scroll_y=self._trigger_update_from_scroll,
                  pos=self._trigger_update_from_scroll,
                  size=self._trigger_update_from_scroll)

        self._update_effect_widget()
        self._update_effect_x_bounds()
        self._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):
        self.effect_x = self.effect_cls(target_widget=self._viewport)
        self.effect_x.bind(scroll=self._update_effect_x)
        self.effect_y = self.effect_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
        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
        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 on_touch_down(self, touch):
        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 super(ScrollView, self).on_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 vp and 'button' in touch.profile and \
            touch.button.startswith('scroll'):
            btn = touch.button
            m = self.scroll_distance
            e = None

            if (self.effect_x and self.do_scroll_y and vp.height > self.height
                    and btn in ('scrolldown', 'scrollup')):
                e = self.effect_y

            elif (self.effect_y and self.do_scroll_x and vp.width > self.width
                  and btn in ('scrollleft', 'scrollright')):
                e = 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

        scroll_type = self.scroll_type
        # no mouse scrolling, so the user is going to drag the scrollview with
        # this touch.
        self._touch = touch
        uid = self._get_uid()
        touch.grab(self)
        ud = touch.ud
        ud[uid] = {
            'mode': 'unknown',
            'dx': 0,
            'dy': 0,
            'user_stopped': False,
            'time': touch.time_start
        }
        if self.do_scroll_x and self.effect_x:
            if scroll_type[0] == 'b' and touch.y < self.bar_width:
                ud['in_bar_x'] = True
            else:
                if scroll_type != 'bars':
                    self.effect_x.start(touch.x)
        if self.do_scroll_y and self.effect_y:
            if scroll_type[0] == 'b' and touch.x > self.right - self.bar_width:
                ud['in_bar_y'] = True
            else:
                if scroll_type != 'bars':
                    self.effect_y.start(touch.y)

        if (ud.get('in_bar_x', False) or ud.get('in_bar_y', False)):
            return
        if scroll_type == 'bars':
            self._change_touch_mode()
        else:
            Clock.schedule_once(self._change_touch_mode,
                                self.scroll_timeout / 1000.)
        return True

    def on_touch_move(self, touch):
        if self._get_uid('svavoid') in touch.ud:
            return
        if self._touch is not touch:
            super(ScrollView, self).on_touch_move(touch)
            return self._get_uid() in touch.ud
        if touch.grab_current is not self:
            return True

        uid = self._get_uid()
        ud = touch.ud[uid]
        mode = ud['mode']

        # check if the minimum distance has been travelled
        if mode == 'unknown' or mode == 'scroll':
            if 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.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 mode == 'unknown':
            ud['dx'] += abs(touch.dx)
            ud['dy'] += abs(touch.dy)
            if ud['dx'] > self.scroll_distance:
                if not self.do_scroll_x:
                    self._change_touch_mode()
                    return
                mode = 'scroll'

            if ud['dy'] > self.scroll_distance:
                if not self.do_scroll_y:
                    self._change_touch_mode()
                    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 True

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

        if self in [x() for x in touch.grab_list]:
            touch.ungrab(self)
            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']:
                    super(ScrollView, self).on_touch_down(touch)
                Clock.schedule_once(partial(self._do_touch_up, touch), .2)
        else:
            if self._touch is not touch and self.uid not in touch.ud:
                super(ScrollView, self).on_touch_up(touch)

        # 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 update_from_scroll(self, *largs):
        '''Force the reposition of the content, according to current value of
        :data:`scroll_x` and :data:`scroll_y`.

        This method is automatically called when one of the :data:`scroll_x`,
        :data:`scroll_y`, :data:`pos` or :data:`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
        vp.pos = x, y

        # new in 1.2.0, show bar when scrolling happen
        # and slowly remove them when no scroll is happening.
        self.bar_alpha = 1.
        Animation.stop_all(self, 'bar_alpha')
        Clock.unschedule(self._start_decrease_alpha)
        Clock.schedule_once(self._start_decrease_alpha, .5)

    def _start_decrease_alpha(self, *l):
        self.bar_alpha = 1.
        # show bars if scroll_type != content
        bar_alpha = .2 if self.scroll_type[0] != 'c' else 0
        Animation(bar_alpha=bar_alpha, d=.5, t='out_quart').start(self)

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

    def remove_widget(self, widget):
        super(ScrollView, self).remove_widget(widget)
        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
        ud = touch.ud[uid]
        if ud['mode'] != 'unknown' or ud['user_stopped']:
            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 possibily 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
        # correctly calculate the position of the touch inside the
        # scrollview
        touch.push()
        touch.apply_transform_2d(self.to_widget)
        touch.apply_transform_2d(self.to_parent)
        super(ScrollView, self).on_touch_down(touch)
        touch.pop()
        return

    def _do_touch_up(self, touch, *largs):
        super(ScrollView, self).on_touch_up(touch)
        # 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
            super(ScrollView, self).on_touch_up(touch)
        touch.grab_current = None
Esempio n. 2
0
class MDDropdownMenu(ThemableBehavior, FloatLayout):
    items = ListProperty()
    """
    See :attr:`~kivy.uix.recycleview.RecycleView.data`.

    :attr:`items` is a :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    width_mult = NumericProperty(1)
    """
    This number multiplied by the standard increment (56dp on mobile,
    64dp on desktop, determines the width of the menu items.

    If the resulting number were to be too big for the application Window,
    the multiplier will be adjusted for the biggest possible one.

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

    max_height = NumericProperty()
    """
    The menu will grow no bigger than this number. Set to 0 for no limit.

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

    border_margin = NumericProperty("4dp")
    """
    Margin between Window border and menu.

    :attr:`border_margin` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `4dp`.
    """

    ver_growth = OptionProperty(None, allownone=True, options=["up", "down"])
    """
    Where the menu will grow vertically to when opening. Set to None to let
    the widget pick for you. Available options are: `'up'`, `'down'`.

    :attr:`ver_growth` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `None`.
    """

    hor_growth = OptionProperty(None,
                                allownone=True,
                                options=["left", "right"])
    """
    Where the menu will grow horizontally to when opening. Set to None to let
    the widget pick for you. Available options are: `'left'`, `'right'`.

    :attr:`hor_growth` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `None`.
    """

    background_color = ListProperty()
    """
    Color of the background of the menu.

    :attr:`background_color` is a :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    opening_transition = StringProperty("out_cubic")
    """
    Type of animation for opening a menu window.

    :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'out_cubic'`.
    """

    opening_time = NumericProperty(0.2)
    """
    Menu window opening animation time.

    :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `0.2`.
    """

    caller = ObjectProperty()
    """
    The widget object that caller the menu window.

    :attr:`caller` is a :class:`~kivy.properties.ObjectProperty`
    and defaults to `None`.
    """

    callback = ObjectProperty()
    """
    The method that will be called when you click menu items.

    :attr:`callback` is a :class:`~kivy.properties.ObjectProperty`
    and defaults to `None`.
    """

    position = OptionProperty("auto", options=["auto", "center", "bottom"])
    """
    Menu window position relative to parent element.
    Available options are: `'auto'`, `'center'`, `'bottom'`.

    :attr:`position` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `'auto'`.
    """

    use_icon_item = BooleanProperty(True)
    """Whether to use menu items with an icon on the left.
    
    :attr:`use_icon_item` is a :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    _start_coords = []
    _calculate_complete = False
    _calculate_process = False

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Window.bind(on_resize=self.check_position_caller)
        self.register_event_type("on_dismiss")
        self.menu = self.ids.md_menu
        Clock.schedule_once(self.set_menu_properties, 2)

    def check_position_caller(self, instance, width, height):
        self.set_menu_properties(0)

    def create_menu_items(self):
        """Creates menu items."""

        for data in self.items:
            item = MDMenuItemIcon(text=data.get("text", ""),
                                  divider=data.get("divider", "Full"))
            if not self.use_icon_item:
                item.remove_widget(item.ids._left_container)
                item._txt_left_pad = dp(16)
            else:
                item.icon = data.get("icon", "")
            if self.callback:
                item.bind(on_release=self.callback)
            right_content_cls = data.get("right_content_cls", None)
            # Set right content.
            if isinstance(right_content_cls, RightContent):
                item.ids._right_container.width = right_content_cls.width + dp(
                    20)
                item.ids._right_container.padding = ("10dp", 0, 0, 0)
                item.add_widget(right_content_cls)
            else:
                if "_right_container" in item.ids:
                    item.ids._right_container.width = 0
            self.menu.ids.box.add_widget(item)

    def set_menu_properties(self, interval):
        """Sets the size and position for the menu window."""

        if not self.menu.ids.box.children:
            self.create_menu_items()
        # We need to pick a starting point, see how big we need to be,
        # and where to grow to.
        self._start_coords = self.caller.to_window(self.caller.center_x,
                                                   self.caller.center_y)
        self.target_width = self.width_mult * m_res.STANDARD_INCREMENT

        # If we're wider than the Window...
        if self.target_width > Window.width:
            # ...reduce our multiplier to max allowed.
            self.target_width = (int(Window.width / m_res.STANDARD_INCREMENT) *
                                 m_res.STANDARD_INCREMENT)

        # The height of each MDMenuItem or MDMenuItemIcon
        menu_item_height = MDMenuItemIcon().height
        # Set the target_height of the menu depending on the size of
        # each MDMenuItem or MDMenuItemIcon
        self.target_height = menu_item_height * len(self.items)

        # If we're over max_height...
        if 0 < self.max_height < self.target_height:
            self.target_height = self.max_height

        # Establish vertical growth direction.
        if self.ver_growth is not None:
            ver_growth = self.ver_growth
        else:
            # If there's enough space below us:
            if self.target_height <= self._start_coords[1] - self.border_margin:
                ver_growth = "down"
            # if there's enough space above us:
            elif (self.target_height <
                  Window.height - self._start_coords[1] - self.border_margin):
                ver_growth = "up"
            # Otherwise, let's pick the one with more space and adjust ourselves.
            else:
                # If there"s more space below us:
                if (self._start_coords[1] >=
                        Window.height - self._start_coords[1]):
                    ver_growth = "down"
                    self.target_height = (self._start_coords[1] -
                                          self.border_margin)
                # If there's more space above us:
                else:
                    ver_growth = "up"
                    self.target_height = (Window.height -
                                          self._start_coords[1] -
                                          self.border_margin)

        if self.hor_growth is not None:
            hor_growth = self.hor_growth
        else:
            # If there's enough space to the right:
            if (self.target_width <=
                    Window.width - self._start_coords[0] - self.border_margin):
                hor_growth = "right"
            # if there's enough space to the left:
            elif self.target_width < self._start_coords[0] - self.border_margin:
                hor_growth = "left"
            # Otherwise, let's pick the one with more space and adjust ourselves.
            else:
                # if there"s more space to the right:
                if (Window.width - self._start_coords[0] >=
                        self._start_coords[0]):
                    hor_growth = "right"
                    self.target_width = (Window.width - self._start_coords[0] -
                                         self.border_margin)
                # if there"s more space to the left:
                else:
                    hor_growth = "left"
                    self.target_width = (self._start_coords[0] -
                                         self.border_margin)

        if ver_growth == "down":
            self.tar_y = self._start_coords[1] - self.target_height
        else:  # should always be "up"
            self.tar_y = self._start_coords[1]

        if hor_growth == "right":
            self.tar_x = self._start_coords[0]
        else:  # should always be "left"
            self.tar_x = self._start_coords[0] - self.target_width
        self._calculate_complete = True

    def open(self):
        """Animate the opening of a menu window."""
        def open(interval):
            if not self._calculate_complete:
                return
            if self.position == "auto":
                self.menu.pos = self._start_coords
                anim = Animation(
                    x=self.tar_x,
                    y=self.tar_y,
                    width=self.target_width,
                    height=self.target_height,
                    duration=self.opening_time,
                    opacity=1,
                    transition=self.opening_transition,
                )
                anim.start(self.menu)
            else:
                if self.position == "center":
                    self.menu.pos = (
                        self._start_coords[0] - self.target_width / 2,
                        self._start_coords[1] - self.target_height / 2,
                    )
                elif self.position == "bottom":
                    self.menu.pos = (
                        self._start_coords[0] - self.target_width / 2,
                        self.caller.pos[1] - self.target_height,
                    )
                anim = Animation(
                    width=self.target_width,
                    height=self.target_height,
                    duration=self.opening_time,
                    opacity=1,
                    transition=self.opening_transition,
                )
            anim.start(self.menu)
            Window.add_widget(self)
            Clock.unschedule(open)
            self._calculate_process = False

        if not self._calculate_process:
            self._calculate_process = True
            Clock.schedule_interval(open, 0)

    def on_touch_down(self, touch):
        if not self.menu.collide_point(*touch.pos):
            self.dispatch("on_dismiss")
            return True
        super().on_touch_down(touch)
        return True

    def on_touch_move(self, touch):
        super().on_touch_move(touch)
        return True

    def on_touch_up(self, touch):
        super().on_touch_up(touch)
        return True

    def on_dismiss(self):
        Window.remove_widget(self)
        self.menu.width = 0
        self.menu.height = 0
        self.menu.opacity = 0

    def dismiss(self):
        self.on_dismiss()
Esempio n. 3
0
class MDSwitch(ThemableBehavior, ButtonBehavior, FloatLayout):
    active = BooleanProperty(False)
    """
    Indicates if the switch is active or inactive.

    :attr:`active` is a :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    _thumb_color = ColorProperty(get_color_from_hex(colors["Gray"]["50"]))

    def _get_thumb_color(self):
        return self._thumb_color

    def _set_thumb_color(self, color, alpha=None):
        if len(color) == 2:
            self._thumb_color = get_color_from_hex(colors[color[0]][color[1]])
            if alpha:
                self._thumb_color[3] = alpha
        elif len(color) == 4:
            self._thumb_color = color

    thumb_color = AliasProperty(_get_thumb_color,
                                _set_thumb_color,
                                bind=["_thumb_color"])
    """
    Get thumb color ``rgba`` format.

    :attr:`thumb_color` is an :class:`~kivy.properties.AliasProperty`
    and property is readonly.
    """

    _thumb_color_down = ColorProperty([1, 1, 1, 1])

    def _get_thumb_color_down(self):
        return self._thumb_color_down

    def _set_thumb_color_down(self, color, alpha=None):
        if len(color) == 2:
            self._thumb_color_down = get_color_from_hex(
                colors[color[0]][color[1]])
            if alpha:
                self._thumb_color_down[3] = alpha
            else:
                self._thumb_color_down[3] = 1
        elif len(color) == 4:
            self._thumb_color_down = color

    _thumb_color_disabled = ColorProperty(
        get_color_from_hex(colors["Gray"]["400"]))

    thumb_color_disabled = get_color_from_hex(colors["Gray"]["800"])
    """
    Get thumb color disabled ``rgba`` format.

    :attr:`thumb_color_disabled` is an :class:`~kivy.properties.AliasProperty`
    and property is readonly.
    """

    def _get_thumb_color_disabled(self):
        return self._thumb_color_disabled

    def _set_thumb_color_disabled(self, color, alpha=None):
        if len(color) == 2:
            self._thumb_color_disabled = get_color_from_hex(
                colors[color[0]][color[1]])
            if alpha:
                self._thumb_color_disabled[3] = alpha
        elif len(color) == 4:
            self._thumb_color_disabled = color

    thumb_color_down = AliasProperty(
        _get_thumb_color_disabled,
        _set_thumb_color_disabled,
        bind=["_thumb_color_disabled"],
    )
    """
    Get thumb color down ``rgba`` format.

    :attr:`thumb_color_down` is an :class:`~kivy.properties.AliasProperty`
    and property is readonly.
    """

    theme_thumb_color = OptionProperty("Primary",
                                       options=["Primary", "Custom"])
    """
    Thumb color scheme name

    :attr:`theme_thumb_color` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `Primary`.
    """

    theme_thumb_down_color = OptionProperty("Primary",
                                            options=["Primary", "Custom"])
    """
    Thumb Down color scheme name

    :attr:`theme_thumb_down_color` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `Primary`.
    """

    _track_color_active = ColorProperty([0, 0, 0, 0])
    _track_color_normal = ColorProperty([0, 0, 0, 0])
    _track_color_disabled = ColorProperty([0, 0, 0, 0])
    _thumb_pos = ListProperty([0, 0])

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.theme_cls.bind(
            theme_style=self._set_colors,
            primary_color=self._set_colors,
            primary_palette=self._set_colors,
        )
        self.bind(active=self._update_thumb_pos)
        Clock.schedule_once(self._set_colors)
        self.size_hint = (None, None)
        self.size = (dp(36), dp(48))

    def _set_colors(self, *args):
        self._track_color_normal = self.theme_cls.disabled_hint_text_color
        if self.theme_cls.theme_style == "Dark":

            if self.theme_thumb_down_color == "Primary":
                self._track_color_active = self.theme_cls.primary_color
            else:
                self._track_color_active = self.thumb_color_down

            self._track_color_active[3] = 0.5
            self._track_color_disabled = get_color_from_hex("FFFFFF")
            self._track_color_disabled[3] = 0.1

            if self.theme_thumb_color == "Primary":
                self.thumb_color = get_color_from_hex(colors["Gray"]["400"])

            if self.theme_thumb_down_color == "Primary":
                self.thumb_color_down = get_color_from_hex(
                    colors[self.theme_cls.primary_palette]["200"])
        else:
            if self.theme_thumb_down_color == "Primary":
                self._track_color_active = get_color_from_hex(
                    colors[self.theme_cls.primary_palette]["200"])
            else:
                self._track_color_active = self.thumb_color_down

            self._track_color_active[3] = 0.5
            self._track_color_disabled = self.theme_cls.disabled_hint_text_color

            if self.theme_thumb_down_color == "Primary":
                self.thumb_color_down = self.theme_cls.primary_color

            if self.theme_thumb_color == "Primary":
                self.thumb_color = get_color_from_hex(colors["Gray"]["50"])

    def _update_thumb_pos(self, *args, animation=True):
        if self.active:
            _thumb_pos = (self.width - dp(14), self.height / 2 - dp(12))
        else:
            _thumb_pos = (0, self.height / 2 - dp(12))
        Animation.cancel_all(self, "_thumb_pos")
        if animation:
            Animation(_thumb_pos=_thumb_pos, duration=0.2,
                      t="out_quad").start(self)
        else:
            self._thumb_pos = _thumb_pos

    def on_size(self, *args):
        self._update_thumb_pos(animation=False)
Esempio n. 4
0
class ButtonBehavior(object):
    '''
    This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
    :class:`~kivy.uix.button.Button` behavior. Please see the
    :mod:`button behaviors module <kivy.uix.behaviors.button>` documentation
    for more information.

    :Events:
        `on_press`
            Fired when the button is pressed.
        `on_release`
            Fired when the button is released (i.e. the touch/click that
            pressed the button goes away).

    '''

    state = OptionProperty('normal', options=('normal', 'down'))
    '''The state of the button, must be one of 'normal' or 'down'.
    The state is 'down' only when the button is currently touched/clicked,
    otherwise its 'normal'.

    :attr:`state` is an :class:`~kivy.properties.OptionProperty` and defaults
    to 'normal'.
    '''

    last_touch = ObjectProperty(None)
    '''Contains the last relevant touch received by the Button. This can
    be used in `on_press` or `on_release` in order to know which touch
    dispatched the event.

    .. versionadded:: 1.8.0

    :attr:`last_touch` is a :class:`~kivy.properties.ObjectProperty` and
    defaults to `None`.
    '''

    min_state_time = NumericProperty(0)
    '''The minimum period of time which the widget must remain in the
    `'down'` state.

    .. versionadded:: 1.9.1

    :attr:`min_state_time` is a float and defaults to 0.035. This value is
    taken from :class:`~kivy.config.Config`.
    '''

    always_release = BooleanProperty(False)
    '''This determines whether or not the widget fires an `on_release` event if
    the touch_up is outside the widget.

    .. versionadded:: 1.9.0

    .. versionchanged:: 1.10.0
        The default value is now False.

    :attr:`always_release` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to `False`.
    '''
    def __init__(self, **kwargs):
        self.register_event_type('on_press')
        self.register_event_type('on_release')
        if 'min_state_time' not in kwargs:
            self.min_state_time = float(
                Config.get('graphics', 'min_state_time'))
        super(ButtonBehavior, self).__init__(**kwargs)
        self.__state_event = None
        self.__touch_time = None
        self.fbind('state', self.cancel_event)

    def _do_press(self):
        self.state = 'down'

    def _do_release(self, *args):
        self.state = 'normal'

    def cancel_event(self, *args):
        if self.__state_event:
            self.__state_event.cancel()
            self.__state_event = None

    def on_touch_down(self, touch):
        if super(ButtonBehavior, self).on_touch_down(touch):
            return True
        if touch.is_mouse_scrolling:
            return False
        if not self.collide_point(touch.x, touch.y):
            return False
        if self in touch.ud:
            return False
        touch.grab(self)
        touch.ud[self] = True
        self.last_touch = touch
        self.__touch_time = time()
        self._do_press()
        self.dispatch('on_press')
        return True

    def on_touch_move(self, touch):
        if touch.grab_current is self:
            return True
        if super(ButtonBehavior, self).on_touch_move(touch):
            return True
        return self in touch.ud

    def on_touch_up(self, touch):
        if touch.grab_current is not self:
            return super(ButtonBehavior, self).on_touch_up(touch)
        assert (self in touch.ud)
        touch.ungrab(self)
        self.last_touch = touch

        if (not self.always_release and not self.collide_point(*touch.pos)):
            self._do_release()
            return

        touchtime = time() - self.__touch_time
        if touchtime < self.min_state_time:
            self.__state_event = Clock.schedule_once(
                self._do_release, self.min_state_time - touchtime)
        else:
            self._do_release()
        self.dispatch('on_release')
        return True

    def on_press(self):
        pass

    def on_release(self):
        pass

    def trigger_action(self, duration=0.1):
        '''Trigger whatever action(s) have been bound to the button by calling
        both the on_press and on_release callbacks.

        This simulates a quick button press without using any touch events.

        Duration is the length of the press in seconds. Pass 0 if you want
        the action to happen instantly.

        .. versionadded:: 1.8.0
        '''
        self._do_press()
        self.dispatch('on_press')

        def trigger_release(dt):
            self._do_release()
            self.dispatch('on_release')

        if not duration:
            trigger_release(0)
        else:
            Clock.schedule_once(trigger_release, duration)
Esempio n. 5
0
class ColorSelector(MDIconButton):
    color_name = OptionProperty("Indigo", options=palette)

    def rgb_hex(self, col):
        return get_color_from_hex(colors[col][self.theme_cls.accent_hue])
Esempio n. 6
0
class DeckBuilderLayout(Layout):
    """Sizes and positions :class:`Card` objects based on their order
    within ``decks``, a list of lists where each sublist is a deck of
    cards.

    """
    direction = OptionProperty('ascending',
                               options=['ascending', 'descending'])
    """Should the beginning card of each deck appear on the bottom
    ('ascending'), or the top ('descending')?

    """
    card_size_hint_x = BoundedNumericProperty(1, min=0, max=1)
    """Each card's width, expressed as a proportion of my width."""
    card_size_hint_y = BoundedNumericProperty(1, min=0, max=1)
    """Each card's height, expressed as a proportion of my height."""
    card_size_hint = ReferenceListProperty(card_size_hint_x, card_size_hint_y)
    """Size hint of cards, relative to my size."""
    starting_pos_hint = DictProperty({'x': 0, 'y': 0})
    """Pos hint at which to place the initial card of the initial deck."""
    card_x_hint_step = NumericProperty(0)
    """Each time I put another card on a deck, I'll move it this much of
    my width to the right of the previous card.

    """
    card_y_hint_step = NumericProperty(-1)
    """Each time I put another card on a deck, I'll move it this much of
    my height above the previous card.

    """
    card_hint_step = ReferenceListProperty(card_x_hint_step, card_y_hint_step)
    """An offset, expressed in proportion to my size, applied to each
    successive card in a given deck.

    """
    deck_x_hint_step = NumericProperty(1)
    """When I start a new deck, it will be this far to the right of the
    previous deck, expressed as a proportion of my width.

    """
    deck_y_hint_step = NumericProperty(0)
    """When I start a new deck, it will be this far above the previous
    deck, expressed as a proportion of my height.

    """
    deck_hint_step = ReferenceListProperty(deck_x_hint_step, deck_y_hint_step)
    """Offset of each deck with respect to the previous, as a proportion
    of my size.

    """
    decks = ListProperty([[]])  # list of lists of cards
    """Put a list of lists of :class:`Card` objects here and I'll position
    them appropriately. Please don't use ``add_widget``.

    """
    deck_x_hint_offsets = ListProperty([])
    """An additional proportional x-offset for each deck, defaulting to 0."""
    deck_y_hint_offsets = ListProperty([])
    """An additional proportional y-offset for each deck, defaulting to 0."""
    foundation_color = ListProperty([1, 1, 1, 1])
    """Color to use for the outline showing where a deck is when it's
    empty.

    """
    insertion_deck = BoundedNumericProperty(None, min=0, allownone=True)
    """Index of the deck that a card is being dragged into."""
    insertion_card = BoundedNumericProperty(None, min=0, allownone=True)
    """Index within the current deck that a card is being dragged into."""
    _foundations = ListProperty([])
    """Private. A list of :class:`Foundation` widgets, one per deck."""
    def __init__(self, **kwargs):
        """Bind most of my custom properties to ``_trigger_layout``."""
        super().__init__(**kwargs)
        self.bind(card_size_hint=self._trigger_layout,
                  starting_pos_hint=self._trigger_layout,
                  card_hint_step=self._trigger_layout,
                  deck_hint_step=self._trigger_layout,
                  decks=self._trigger_layout,
                  deck_x_hint_offsets=self._trigger_layout,
                  deck_y_hint_offsets=self._trigger_layout,
                  insertion_deck=self._trigger_layout,
                  insertion_card=self._trigger_layout)

    def scroll_deck_x(self, decknum, scroll_x):
        """Move a deck left or right."""
        if decknum >= len(self.decks):
            raise IndexError("I have no deck at {}".format(decknum))
        if decknum >= len(self.deck_x_hint_offsets):
            self.deck_x_hint_offsets = list(self.deck_x_hint_offsets) + [0] * (
                decknum - len(self.deck_x_hint_offsets) + 1)
        self.deck_x_hint_offsets[decknum] += scroll_x
        self._trigger_layout()

    def scroll_deck_y(self, decknum, scroll_y):
        """Move a deck up or down."""
        if decknum >= len(self.decks):
            raise IndexError("I have no deck at {}".format(decknum))
        if decknum >= len(self.deck_y_hint_offsets):
            self.deck_y_hint_offsets = list(self.deck_y_hint_offsets) + [0] * (
                decknum - len(self.deck_y_hint_offsets) + 1)
        self.deck_y_hint_offsets[decknum] += scroll_y
        self._trigger_layout()

    def scroll_deck(self, decknum, scroll_x, scroll_y):
        """Move a deck."""
        self.scroll_deck_x(decknum, scroll_x)
        self.scroll_deck_y(decknum, scroll_y)

    def _get_foundation_pos(self, i):
        """Private. Get the absolute coordinates to use for a deck's
        foundation, based on the ``starting_pos_hint``, the
        ``deck_hint_step``, ``deck_x_hint_offsets``, and
        ``deck_y_hint_offsets``.

        """
        (phx, phy) = get_pos_hint(self.starting_pos_hint, *self.card_size_hint)
        phx += self.deck_x_hint_step * i + self.deck_x_hint_offsets[i]
        phy += self.deck_y_hint_step * i + self.deck_y_hint_offsets[i]
        x = phx * self.width + self.x
        y = phy * self.height + self.y
        return (x, y)

    def _get_foundation(self, i):
        """Return a :class:`Foundation` for some deck, creating it if
        needed.

        """
        if i >= len(self._foundations) or self._foundations[i] is None:
            oldfound = list(self._foundations)
            extend = i - len(oldfound) + 1
            if extend > 0:
                oldfound += [None] * extend
            width = self.card_size_hint_x * self.width
            height = self.card_size_hint_y * self.height
            found = Foundation(pos=self._get_foundation_pos(i),
                               size=(width, height),
                               deck=i)
            self.bind(pos=found.upd_pos,
                      card_size_hint=found.upd_pos,
                      deck_hint_step=found.upd_pos,
                      size=found.upd_pos,
                      deck_x_hint_offsets=found.upd_pos,
                      deck_y_hint_offsets=found.upd_pos)
            self.bind(size=found.upd_size, card_size_hint=found.upd_size)
            oldfound[i] = found
            self._foundations = oldfound
        return self._foundations[i]

    def on_decks(self, *args):
        """Inform the cards of their deck and their index within the deck;
        extend the ``_hint_offsets`` properties as needed; and trigger
        a layout.

        """
        if None in (self.canvas, self.decks, self.deck_x_hint_offsets,
                    self.deck_y_hint_offsets):
            Clock.schedule_once(self.on_decks, 0)
            return
        decknum = 0
        for deck in self.decks:
            cardnum = 0
            for card in deck:
                if not isinstance(card, Card):
                    raise TypeError("You must only put Card in decks")
                if card not in self.children:
                    self.add_widget(card)
                if card.deck != decknum:
                    card.deck = decknum
                if card.idx != cardnum:
                    card.idx = cardnum
                cardnum += 1
            decknum += 1
        if len(self.deck_x_hint_offsets) < len(self.decks):
            self.deck_x_hint_offsets = list(self.deck_x_hint_offsets) + [0] * (
                len(self.decks) - len(self.deck_x_hint_offsets))
        if len(self.deck_y_hint_offsets) < len(self.decks):
            self.deck_y_hint_offsets = list(self.deck_y_hint_offsets) + [0] * (
                len(self.decks) - len(self.deck_y_hint_offsets))
        self._trigger_layout()

    def point_before_card(self, card, x, y):
        """Return whether ``(x, y)`` is somewhere before ``card``, given how I
        know cards to be arranged.

        If the cards are being stacked down and to the right, that
        means I'm testing whether ``(x, y)`` is above or to the left
        of the card.

        """
        def ycmp():
            if self.card_y_hint_step == 0:
                return False
            elif self.card_y_hint_step > 0:
                # stacking upward
                return y < card.y
            else:
                # stacking downward
                return y > card.top

        if self.card_x_hint_step > 0:
            # stacking to the right
            if x < card.x:
                return True
            return ycmp()
        elif self.card_x_hint_step == 0:
            return ycmp()
        else:
            # stacking to the left
            if x > card.right:
                return True
            return ycmp()

    def point_after_card(self, card, x, y):
        """Return whether ``(x, y)`` is somewhere after ``card``, given how I
        know cards to be arranged.

        If the cards are being stacked down and to the right, that
        means I'm testing whether ``(x, y)`` is below or to the left
        of ``card``.

        """
        def ycmp():
            if self.card_y_hint_step == 0:
                return False
            elif self.card_y_hint_step > 0:
                # stacking upward
                return y > card.top
            else:
                # stacking downward
                return y < card.y

        if self.card_x_hint_step > 0:
            # stacking to the right
            if x > card.right:
                return True
            return ycmp()
        elif self.card_x_hint_step == 0:
            return ycmp()
        else:
            # stacking to the left
            if x < card.x:
                return True
            return ycmp()

    def on_touch_move(self, touch):
        """If a card is being dragged, move other cards out of the way to show
        where the dragged card will go if you drop it.

        """
        if ('card' not in touch.ud or 'layout' not in touch.ud
                or touch.ud['layout'] != self):
            return
        if (touch.ud['layout'] == self
                and not hasattr(touch.ud['card'], '_topdecked')):
            touch.ud['card']._topdecked = InstructionGroup()
            touch.ud['card']._topdecked.add(touch.ud['card'].canvas)
            self.canvas.after.add(touch.ud['card']._topdecked)
        i = 0
        for deck in self.decks:
            cards = [card for card in deck if not card.dragging]
            maxidx = max(card.idx for card in cards) if cards else 0
            if self.direction == 'descending':
                cards.reverse()
            cards_collided = [
                card for card in cards if card.collide_point(*touch.pos)
            ]
            if cards_collided:
                collided = cards_collided.pop()
                for card in cards_collided:
                    if card.idx > collided.idx:
                        collided = card
                if collided.deck == touch.ud['deck']:
                    self.insertion_card = (
                        1 if collided.idx == 0 else maxidx +
                        1 if collided.idx == maxidx else collided.idx +
                        1 if collided.idx > touch.ud['idx'] else collided.idx)
                else:
                    dropdeck = self.decks[collided.deck]
                    maxidx = max(card.idx for card in dropdeck)
                    self.insertion_card = (
                        1 if collided.idx == 0 else maxidx +
                        1 if collided.idx == maxidx else collided.idx + 1)
                if self.insertion_deck != collided.deck:
                    self.insertion_deck = collided.deck
                return
            else:
                if self.insertion_deck == i:
                    if self.insertion_card in (0, len(deck)):
                        pass
                    elif self.point_before_card(cards[0], *touch.pos):
                        self.insertion_card = 0
                    elif self.point_after_card(cards[-1], *touch.pos):
                        self.insertion_card = cards[-1].idx
                else:
                    j = 0
                    for found in self._foundations:
                        if (found is not None
                                and found.collide_point(*touch.pos)):
                            self.insertion_deck = j
                            self.insertion_card = 0
                        return
                        j += 1
            i += 1

    def on_touch_up(self, touch):
        """If a card is being dragged, put it in the place it was just dropped
        and trigger a layout.

        """
        if ('card' not in touch.ud or 'layout' not in touch.ud
                or touch.ud['layout'] != self):
            return
        if hasattr(touch.ud['card'], '_topdecked'):
            self.canvas.after.remove(touch.ud['card']._topdecked)
            del touch.ud['card']._topdecked
        if None not in (self.insertion_deck, self.insertion_card):
            # need to sync to adapter.data??
            card = touch.ud['card']
            del card.parent.decks[card.deck][card.idx]
            for i in range(0, len(card.parent.decks[card.deck])):
                card.parent.decks[card.deck][i].idx = i
            deck = self.decks[self.insertion_deck]
            if self.insertion_card >= len(deck):
                deck.append(card)
            else:
                deck.insert(self.insertion_card, card)
            card.deck = self.insertion_deck
            card.idx = self.insertion_card
            self.decks[self.insertion_deck] = deck
            self.insertion_deck = self.insertion_card = None
        self._trigger_layout()

    def on_insertion_card(self, *args):
        """Trigger a layout"""
        if self.insertion_card is not None:
            self._trigger_layout()

    def do_layout(self, *args):
        """Layout each of my decks"""
        if self.size == [1, 1]:
            return
        for i in range(0, len(self.decks)):
            self.layout_deck(i)

    def layout_deck(self, i):
        """Stack the cards, starting at my deck's foundation, and proceeding
        by ``card_pos_hint``

        """
        def get_dragidx(cards):
            j = 0
            for card in cards:
                if card.dragging:
                    return j
                j += 1

        # Put a None in the card list in place of the card you're
        # hovering over, if you're dragging another card. This will
        # result in an empty space where the card will go if you drop
        # it now.
        cards = list(self.decks[i])
        dragidx = get_dragidx(cards)
        if dragidx is not None:
            del cards[dragidx]
        if self.insertion_deck == i and self.insertion_card is not None:
            insdx = self.insertion_card
            if dragidx is not None and insdx > dragidx:
                insdx -= 1
            cards.insert(insdx, None)
        if self.direction == 'descending':
            cards.reverse()
        # Work out the initial pos_hint for this deck
        (phx, phy) = get_pos_hint(self.starting_pos_hint, *self.card_size_hint)
        phx += self.deck_x_hint_step * i + self.deck_x_hint_offsets[i]
        phy += self.deck_y_hint_step * i + self.deck_y_hint_offsets[i]
        (w, h) = self.size
        (x, y) = self.pos
        # start assigning pos and size to cards
        found = self._get_foundation(i)
        if found in self.children:
            self.remove_widget(found)
        self.add_widget(found)
        for card in cards:
            if card is not None:
                if card in self.children:
                    self.remove_widget(card)
                (shw, shh) = self.card_size_hint
                card.pos = (x + phx * w, y + phy * h)
                card.size = (w * shw, h * shh)
                self.add_widget(card)
            phx += self.card_x_hint_step
            phy += self.card_y_hint_step
Esempio n. 7
0
class MaterialList(ThemeBehaviour, BoxLayout):

    title = StringProperty('')

    tile_rows = BoundedNumericProperty(1, min=1, max=3)

    list_type = OptionProperty('text',
                               options=('text', 'icon_text', 'avatar_text',
                                        'avatar_text_icon'))

    text_color = ListProperty()

    text_color_selected = ListProperty()

    background_color_selected = ListProperty()

    background_color_disabled = ListProperty()

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

    selection_mode = OptionProperty('single',
                                    options=('none', 'single', 'multiple'))

    allow_empty_selection = BooleanProperty(False)

    selection_limit = NumericProperty(-1)

    list_data = ListProperty()

    selection = ListProperty([])

    _tile_height = NumericProperty(dp(48))
    _list_view = ObjectProperty()

    def __init__(self, **kwargs):
        super(MaterialList, self).__init__(**kwargs)
        self.register_event_type('on_selection')
        self.text_color = self._theme_cls.primary_text_color()
        self.text_color_selected = self._theme_cls.accent_color
        self.background_color_selected = get_rgba_color(
            [self._theme_cls.theme_style, 'FlatButtonDown'])
        self.background_color_disabled = self._theme_cls.disabled_bg_color()

        self.bind(tile_rows=self._set_tile_height, list_data=self._get_adapter)

    def on_selection(self, *args):
        pass

    def _get_adapter(self, *args):
        if self.list_type == 'text':
            converter = lambda row_index, rec: {
                'text':
                rec['text'],
                'secondary_text':
                rec['secondary_text'] if 'secondary_text' in rec else '',
                'tile_rows':
                self.tile_rows,
                'text_color_selected':
                self.text_color_selected,
                'background_color_selected':
                self.background_color_selected,
                'background_color_disabled':
                self.background_color_disabled,
                'divider_color':
                self.divider_color,
                'size_hint_y':
                None,
                'height':
                self._tile_height,
                'callback':
                rec['callback'] if 'callback' in rec else None
            }
            adapter = ListAdapter(data=self.list_data,
                                  args_converter=converter,
                                  cls=TextTile)
            adapter.bind(selection=self.setter('selection'))
            self._list_view.adapter = adapter
            self._list_view.adapter.selection_mode = self.selection_mode
            self._list_view.adapter.selection_limit = self.selection_limit
            self._list_view.adapter.allow_empty_selection = self.allow_empty_selection

        if self.list_type == 'icon_text':
            converter = lambda row_index, rec: {
                'icon':
                rec['icon'],
                'text':
                rec['text'],
                'secondary_text':
                rec['secondary_text'] if 'secondary_text' in rec else '',
                'tile_rows':
                self.tile_rows,
                'text_color_selected':
                self.text_color_selected,
                'background_color_selected':
                self.background_color_selected,
                'background_color_disabled':
                self.background_color_disabled,
                'divider_color':
                self.divider_color,
                'size_hint_y':
                None,
                'height':
                self._tile_height,
                'callback':
                rec['callback'] if 'callback' in rec else None
            }

            adapter = ListAdapter(data=self.list_data,
                                  args_converter=converter,
                                  cls=IconTextTile)
            adapter.bind(selection=self.setter('selection'))
            self._list_view.adapter = adapter
            self._list_view.adapter.selection_mode = self.selection_mode
            self._list_view.adapter.selection_limit = self.selection_limit
            self._list_view.adapter.allow_empty_selection = self.allow_empty_selection
        if self.list_type == 'avatar_text':
            converter = lambda row_index, rec: {
                'avatar':
                rec['avatar'],
                'text':
                rec['text'],
                'secondary_text':
                rec['secondary_text'] if 'secondary_text' in rec else '',
                'tile_rows':
                self.tile_rows,
                'text_color_selected':
                self.text_color_selected,
                'background_color_selected':
                self.background_color_selected,
                'background_color_disabled':
                self.background_color_disabled,
                'divider_color':
                self.divider_color,
                'size_hint_y':
                None,
                'height':
                self._tile_height,
                'callback':
                rec['callback'] if 'callback' in rec else None
            }

            adapter = ListAdapter(data=self.list_data,
                                  args_converter=converter,
                                  cls=AvatarTextTile)
            adapter.bind(selection=self.setter('selection'))
            self._list_view.adapter = adapter
            self._list_view.adapter.selection_mode = self.selection_mode
            self._list_view.adapter.selection_limit = self.selection_limit
            self._list_view.adapter.allow_empty_selection = self.allow_empty_selection

    def _set_tile_height(self, *args):
        if self.tile_rows == 1:
            if self.tile_type == 'text' or self.tile_type == 'icon_text':
                self._tile_height = dp(48)
            else:
                self._tile_height = dp(56)
        elif self.tile_rows == 2:
            self._tile_height = dp(72)
        else:
            self._tile_height = dp(88)
Esempio n. 8
0
class TabbedPanel(GridLayout):
    '''The TabbedPanel class. See module documentation for more information.
    '''

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

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

    border = ListProperty([16, 16, 16, 16])
    '''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
    graphics instruction, used itself for :attr:`background_image`.
    Can be changed for a custom background.

    It must be a list of four values: (bottom, right, top, left). Read the
    BorderImage instructions for more information.

    :attr:`border` is a :class:`~kivy.properties.ListProperty` and
    defaults to (16, 16, 16, 16)
    '''

    background_image = StringProperty('atlas://data/images/defaulttheme/tab')
    '''Background image of the main shared content object.

    :attr:`background_image` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/tab'.
    '''

    background_disabled_image = StringProperty(
        'atlas://data/images/defaulttheme/tab_disabled')
    '''Background image of the main shared content object when disabled.

    .. versionadded:: 1.8.0

    :attr:`background_disabled_image` is a
    :class:`~kivy.properties.StringProperty` and defaults to
    'atlas://data/images/defaulttheme/tab'.
    '''

    strip_image = StringProperty(
        'atlas://data/images/defaulttheme/action_view')
    '''Background image of the tabbed strip.

    .. versionadded:: 1.8.0

    :attr:`strip_image` is a :class:`~kivy.properties.StringProperty`
    and defaults to a empty image.
    '''

    strip_border = ListProperty([4, 4, 4, 4])
    '''Border to be used on :attr:`strip_image`.

    .. versionadded:: 1.8.0

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

    _current_tab = ObjectProperty(None)

    def get_current_tab(self):
        return self._current_tab

    current_tab = AliasProperty(get_current_tab, None, bind=('_current_tab', ))
    '''Links to the currently selected or active tab.

    .. versionadded:: 1.4.0

    :attr:`current_tab` is an :class:`~kivy.AliasProperty`, read-only.
    '''

    tab_pos = OptionProperty(
        'top_left',
        options=('left_top', 'left_mid', 'left_bottom', 'top_left', 'top_mid',
                 'top_right', 'right_top', 'right_mid', 'right_bottom',
                 'bottom_left', 'bottom_mid', 'bottom_right'))
    '''Specifies the position of the tabs relative to the content.
    Can be one of: `left_top`, `left_mid`, `left_bottom`, `top_left`,
    `top_mid`, `top_right`, `right_top`, `right_mid`, `right_bottom`,
    `bottom_left`, `bottom_mid`, `bottom_right`.

    :attr:`tab_pos` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'top_left'.
    '''

    tab_height = NumericProperty('40dp')
    '''Specifies the height of the tab header.

    :attr:`tab_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 40.
    '''

    tab_width = NumericProperty('100dp', allownone=True)
    '''Specifies the width of the tab header.

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

    do_default_tab = BooleanProperty(True)
    '''Specifies whether a default_tab head is provided.

    .. versionadded:: 1.5.0

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

    default_tab_text = StringProperty('Default tab')
    '''Specifies the text displayed on the default tab header.

    :attr:`default_tab_text` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'default tab'.
    '''

    default_tab_cls = ObjectProperty(TabbedPanelHeader)
    '''Specifies the class to use for the styling of the default tab.

    .. versionadded:: 1.4.0

    .. warning::
        `default_tab_cls` should be subclassed from `TabbedPanelHeader`

    :attr:`default_tab_cls` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to `TabbedPanelHeader`. If you set a string, the
    :class:`~kivy.factory.Factory` will be used to resolve the class.

    .. versionchanged:: 1.8.0
        The :class:`~kivy.factory.Factory` will resolve the class if a string
        is set.
    '''

    def get_tab_list(self):
        if self._tab_strip:
            return self._tab_strip.children
        return 1.

    tab_list = AliasProperty(get_tab_list, None)
    '''List of all the tab headers.

    :attr:`tab_list` is an :class:`~kivy.properties.AliasProperty` and is
    read-only.
    '''

    content = ObjectProperty(None)
    '''This is the object holding (current_tab's content is added to this)
    the content of the current tab. To Listen to the changes in the content
    of the current tab, you should bind to current_tabs `content` property.

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

    _default_tab = ObjectProperty(None, allow_none=True)

    def get_def_tab(self):
        return self._default_tab

    def set_def_tab(self, new_tab):
        if not issubclass(new_tab.__class__, TabbedPanelHeader):
            raise TabbedPanelException('`default_tab_class` should be\
                subclassed from `TabbedPanelHeader`')
        if self._default_tab == new_tab:
            return
        oltab = self._default_tab
        self._default_tab = new_tab
        self.remove_widget(oltab)
        self._original_tab = None
        self.switch_to(new_tab)
        new_tab.state = 'down'

    default_tab = AliasProperty(get_def_tab,
                                set_def_tab,
                                bind=('_default_tab', ))
    '''Holds the default tab.

    .. Note:: For convenience, the automatically provided default tab is
              deleted when you change default_tab to something else.
              As of 1.5.0, this behaviour has been extended to every
              `default_tab` for consistency and not just the automatically
              provided one.

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

    def get_def_tab_content(self):
        return self.default_tab.content

    def set_def_tab_content(self, *l):
        self.default_tab.content = l[0]

    default_tab_content = AliasProperty(get_def_tab_content,
                                        set_def_tab_content)
    '''Holds the default tab content.

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

    _update_top_ev = _update_tab_ev = _update_tabs_ev = None

    def __init__(self, **kwargs):
        # these variables need to be initialized before the kv lang is
        # processed setup the base layout for the tabbed panel
        self._childrens = []
        self._tab_layout = StripLayout(rows=1)
        self.rows = 1
        self._tab_strip = TabbedPanelStrip(tabbed_panel=self,
                                           rows=1,
                                           size_hint=(None, None),
                                           height=self.tab_height,
                                           width=self.tab_width)

        self._partial_update_scrollview = None
        self.content = TabbedPanelContent()
        self._current_tab = self._original_tab \
            = self._default_tab = TabbedPanelHeader()

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

        self.fbind('size', self._reposition_tabs)
        if not self.do_default_tab:
            Clock.schedule_once(self._switch_to_first_tab)
            return
        self._setup_default_tab()
        self.switch_to(self.default_tab)

    def switch_to(self, header, do_scroll=False):
        '''Switch to a specific panel header.

        .. versionchanged:: 1.10.0

        If used with `do_scroll=True`, it scrolls
        to the header's tab too.
        '''
        header_content = header.content
        self._current_tab.state = 'normal'
        header.state = 'down'
        self._current_tab = header
        self.clear_widgets()
        if header_content is None:
            return
        # if content has a previous parent remove it from that parent
        parent = header_content.parent
        if parent:
            parent.remove_widget(header_content)
        self.add_widget(header_content)

        if do_scroll:
            tabs = self._tab_strip
            tabs.parent.scroll_to(header)

    def clear_tabs(self, *l):
        self_tabs = self._tab_strip
        self_tabs.clear_widgets()
        if self.do_default_tab:
            self_default_tab = self._default_tab
            self_tabs.add_widget(self_default_tab)
            self_tabs.width = self_default_tab.width
        self._reposition_tabs()

    def add_widget(self, widget, index=0):
        content = self.content
        if content is None:
            return
        parent = widget.parent
        if parent:
            parent.remove_widget(widget)
        if widget in (content, self._tab_layout):
            super(TabbedPanel, self).add_widget(widget, index)
        elif isinstance(widget, TabbedPanelHeader):
            self_tabs = self._tab_strip
            self_tabs.add_widget(widget, index)
            widget.group = '__tab%r__' % self_tabs.uid
            self.on_tab_width()
        else:
            widget.pos_hint = {'x': 0, 'top': 1}
            self._childrens.append(widget)
            content.disabled = self.current_tab.disabled
            content.add_widget(widget, index)

    def remove_widget(self, widget):
        content = self.content
        if content is None:
            return
        if widget in (content, self._tab_layout):
            super(TabbedPanel, self).remove_widget(widget)
        elif isinstance(widget, TabbedPanelHeader):
            if not (self.do_default_tab and widget is self._default_tab):
                self_tabs = self._tab_strip
                self_tabs.width -= widget.width
                self_tabs.remove_widget(widget)
                if widget.state == 'down' and self.do_default_tab:
                    self._default_tab.on_release()
                self._reposition_tabs()
            else:
                Logger.info('TabbedPanel: default tab! can\'t be removed.\n' +
                            'Change `default_tab` to a different tab.')
        else:
            self._childrens.pop(widget, None)
            if widget in content.children:
                content.remove_widget(widget)

    def clear_widgets(self, **kwargs):
        content = self.content
        if content is None:
            return
        if kwargs.get('do_super', False):
            super(TabbedPanel, self).clear_widgets()
        else:
            content.clear_widgets()

    def on_strip_image(self, instance, value):
        if not self._tab_layout:
            return
        self._tab_layout.background_image = value

    def on_strip_border(self, instance, value):
        if not self._tab_layout:
            return
        self._tab_layout.border = value

    def on_do_default_tab(self, instance, value):
        if not value:
            dft = self.default_tab
            if dft in self.tab_list:
                self.remove_widget(dft)
                self._switch_to_first_tab()
                self._default_tab = self._current_tab
        else:
            self._current_tab.state = 'normal'
            self._setup_default_tab()

    def on_default_tab_text(self, *args):
        self._default_tab.text = self.default_tab_text

    def on_tab_width(self, *l):
        ev = self._update_tab_ev
        if ev is None:
            ev = self._update_tab_ev = Clock.create_trigger(
                self._update_tab_width, 0)
        ev()

    def on_tab_height(self, *l):
        self._tab_layout.height = self._tab_strip.height = self.tab_height
        self._reposition_tabs()

    def on_tab_pos(self, *l):
        # ensure canvas
        self._reposition_tabs()

    def _setup_default_tab(self):
        if self._default_tab in self.tab_list:
            return
        content = self._default_tab.content
        _tabs = self._tab_strip
        cls = self.default_tab_cls

        if isinstance(cls, string_types):
            cls = Factory.get(cls)

        if not issubclass(cls, TabbedPanelHeader):
            raise TabbedPanelException('`default_tab_class` should be\
                subclassed from `TabbedPanelHeader`')

        # no need to instantiate if class is TabbedPanelHeader
        if cls != TabbedPanelHeader:
            self._current_tab = self._original_tab = self._default_tab = cls()

        default_tab = self.default_tab
        if self._original_tab == self.default_tab:
            default_tab.text = self.default_tab_text

        default_tab.height = self.tab_height
        default_tab.group = '__tab%r__' % _tabs.uid
        default_tab.state = 'down'
        default_tab.width = self.tab_width if self.tab_width else 100
        default_tab.content = content

        tl = self.tab_list
        if default_tab not in tl:
            _tabs.add_widget(default_tab, len(tl))

        if default_tab.content:
            self.clear_widgets()
            self.add_widget(self.default_tab.content)
        else:
            Clock.schedule_once(self._load_default_tab_content)
        self._current_tab = default_tab

    def _switch_to_first_tab(self, *l):
        ltl = len(self.tab_list) - 1
        if ltl > -1:
            self._current_tab = dt = self._original_tab \
                = self.tab_list[ltl]
            self.switch_to(dt)

    def _load_default_tab_content(self, dt):
        if self.default_tab:
            self.switch_to(self.default_tab)

    def _reposition_tabs(self, *l):
        ev = self._update_tabs_ev
        if ev is None:
            ev = self._update_tabs_ev = Clock.create_trigger(
                self._update_tabs, 0)
        ev()

    def _update_tabs(self, *l):
        self_content = self.content
        if not self_content:
            return
        # cache variables for faster access
        tab_pos = self.tab_pos
        tab_layout = self._tab_layout
        tab_layout.clear_widgets()
        scrl_v = ScrollView(size_hint=(None, 1))
        tabs = self._tab_strip
        parent = tabs.parent
        if parent:
            parent.remove_widget(tabs)
        scrl_v.add_widget(tabs)
        scrl_v.pos = (0, 0)
        self_update_scrollview = self._update_scrollview

        # update scrlv width when tab width changes depends on tab_pos
        if self._partial_update_scrollview is not None:
            tabs.unbind(width=self._partial_update_scrollview)
        self._partial_update_scrollview = partial(self_update_scrollview,
                                                  scrl_v)
        tabs.bind(width=self._partial_update_scrollview)

        # remove all widgets from the tab_strip
        self.clear_widgets(do_super=True)
        tab_height = self.tab_height

        widget_list = []
        tab_list = []
        pos_letter = tab_pos[0]
        if pos_letter == 'b' or pos_letter == 't':
            # bottom or top positions
            # one col containing the tab_strip and the content
            self.cols = 1
            self.rows = 2
            # tab_layout contains the scrollview containing tabs and two blank
            # dummy widgets for spacing
            tab_layout.rows = 1
            tab_layout.cols = 3
            tab_layout.size_hint = (1, None)
            tab_layout.height = (tab_height + tab_layout.padding[1] +
                                 tab_layout.padding[3] + dp(2))
            self_update_scrollview(scrl_v)

            if pos_letter == 'b':
                # bottom
                if tab_pos == 'bottom_mid':
                    tab_list = (Widget(), scrl_v, Widget())
                    widget_list = (self_content, tab_layout)
                else:
                    if tab_pos == 'bottom_left':
                        tab_list = (scrl_v, Widget(), Widget())
                    elif tab_pos == 'bottom_right':
                        # add two dummy widgets
                        tab_list = (Widget(), Widget(), scrl_v)
                    widget_list = (self_content, tab_layout)
            else:
                # top
                if tab_pos == 'top_mid':
                    tab_list = (Widget(), scrl_v, Widget())
                elif tab_pos == 'top_left':
                    tab_list = (scrl_v, Widget(), Widget())
                elif tab_pos == 'top_right':
                    tab_list = (Widget(), Widget(), scrl_v)
                widget_list = (tab_layout, self_content)
        elif pos_letter == 'l' or pos_letter == 'r':
            # left ot right positions
            # one row containing the tab_strip and the content
            self.cols = 2
            self.rows = 1
            # tab_layout contains two blank dummy widgets for spacing
            # "vertically" and the scatter containing scrollview
            # containing tabs
            tab_layout.rows = 3
            tab_layout.cols = 1
            tab_layout.size_hint = (None, 1)
            tab_layout.width = tab_height
            scrl_v.height = tab_height
            self_update_scrollview(scrl_v)

            # rotate the scatter for vertical positions
            rotation = 90 if tab_pos[0] == 'l' else -90
            sctr = Scatter(do_translation=False,
                           rotation=rotation,
                           do_rotation=False,
                           do_scale=False,
                           size_hint=(None, None),
                           auto_bring_to_front=False,
                           size=scrl_v.size)
            sctr.add_widget(scrl_v)

            lentab_pos = len(tab_pos)

            # Update scatter's top when it's pos changes.
            # Needed for repositioning scatter to the correct place after its
            # added to the parent. Use clock_schedule_once to ensure top is
            # calculated after the parent's pos on canvas has been calculated.
            # This is needed for when tab_pos changes to correctly position
            # scatter. Without clock.schedule_once the positions would look
            # fine but touch won't translate to the correct position

            if tab_pos[lentab_pos - 4:] == '_top':
                # on positions 'left_top' and 'right_top'
                sctr.bind(pos=partial(self._update_top, sctr, 'top', None))
                tab_list = (sctr, )
            elif tab_pos[lentab_pos - 4:] == '_mid':
                # calculate top of scatter
                sctr.bind(
                    pos=partial(self._update_top, sctr, 'mid', scrl_v.width))
                tab_list = (Widget(), sctr, Widget())
            elif tab_pos[lentab_pos - 7:] == '_bottom':
                tab_list = (Widget(), Widget(), sctr)

            if pos_letter == 'l':
                widget_list = (tab_layout, self_content)
            else:
                widget_list = (self_content, tab_layout)

        # add widgets to tab_layout
        add = tab_layout.add_widget
        for widg in tab_list:
            add(widg)

        # add widgets to self
        add = self.add_widget
        for widg in widget_list:
            add(widg)

    def _update_tab_width(self, *l):
        if self.tab_width:
            for tab in self.tab_list:
                tab.size_hint_x = 1
            tsw = self.tab_width * len(self._tab_strip.children)
        else:
            # tab_width = None
            tsw = 0
            for tab in self.tab_list:
                if tab.size_hint_x:
                    # size_hint_x: x/.xyz
                    tab.size_hint_x = 1
                    # drop to default tab_width
                    tsw += 100
                else:
                    # size_hint_x: None
                    tsw += tab.width
        self._tab_strip.width = tsw
        self._reposition_tabs()

    def _update_top(self, *args):
        sctr, top, scrl_v_width, x, y = args
        ev = self._update_top_ev
        if ev is not None:
            ev.cancel()

        ev = self._update_top_ev = Clock.schedule_once(
            partial(self._updt_top, sctr, top, scrl_v_width), 0)

    def _updt_top(self, sctr, top, scrl_v_width, *args):
        if top[0] == 't':
            sctr.top = self.top
        else:
            sctr.top = self.top - (self.height - scrl_v_width) / 2

    def _update_scrollview(self, scrl_v, *l):
        self_tab_pos = self.tab_pos
        self_tabs = self._tab_strip
        if self_tab_pos[0] == 'b' or self_tab_pos[0] == 't':
            # bottom or top
            scrl_v.width = min(self.width, self_tabs.width)
            # required for situations when scrl_v's pos is calculated
            # when it has no parent
            scrl_v.top += 1
            scrl_v.top -= 1
        else:
            # left or right
            scrl_v.width = min(self.height, self_tabs.width)
            self_tabs.pos = (0, 0)
class MDTapTargetView(ThemableBehavior, EventDispatcher):
    """Rough try to mimic the working of Android's TapTargetView.

    :Events:
        :attr:`on_open`
            Called at the time of the start of the widget opening animation.
        :attr:`on_close`
            Called at the time of the start of the widget closed animation.
    """

    widget = ObjectProperty()
    """
    Widget to add ``TapTargetView`` upon.

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

    outer_radius = NumericProperty(dp(200))
    """
    Radius for outer circle.

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-outer-radius.png
        :align: center

    :attr:`outer_radius` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `dp(200)`.
    """

    outer_circle_color = ListProperty()
    """
    Color for the outer circle in ``rgb`` format.

    .. code-block:: python

        self.tap_target_view = MDTapTargetView(
            ...
            outer_circle_color=(1, 0, 0)
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-outer-circle-color.png
        :align: center

    :attr:`outer_circle_color` is an :class:`~kivy.properties.ListProperty`
    and defaults to ``theme_cls.primary_color``.
    """

    outer_circle_alpha = NumericProperty(0.96)
    """
    Alpha value for outer circle.

    :attr:`outer_circle_alpha` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0.96`.
    """

    target_radius = NumericProperty(dp(45))
    """
    Radius for target circle.

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-target-radius.png
        :align: center

    :attr:`target_radius` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `dp(45)`.
    """

    target_circle_color = ListProperty([1, 1, 1])
    """
    Color for target circle in ``rgb`` format.

    .. code-block:: python

        self.tap_target_view = MDTapTargetView(
            ...
            target_circle_color=(1, 0, 0)
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-target-circle-color.png
        :align: center

    :attr:`target_circle_color` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[1, 1, 1]`.
    """

    title_text = StringProperty()
    """
    Title to be shown on the view.

    :attr:`title_text` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    title_text_size = NumericProperty(dp(25))
    """
    Text size for title.

    :attr:`title_text_size` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `dp(25)`.
    """

    title_text_color = ListProperty([1, 1, 1, 1])
    """
    Text color for title.

    :attr:`title_text_color` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[1, 1, 1, 1]`.
    """

    title_text_bold = BooleanProperty(True)
    """
    Whether title should be bold.

    :attr:`title_text_bold` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    description_text = StringProperty()
    """
    Description to be shown below the title (keep it short).

    :attr:`description_text` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    description_text_size = NumericProperty(dp(20))
    """
    Text size for description text.

    :attr:`description_text_size` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `dp(20)`.
    """

    description_text_color = ListProperty([0.9, 0.9, 0.9, 1])
    """
    Text size for description text.

    :attr:`description_text_color` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0.9, 0.9, 0.9, 1]`.
    """

    description_text_bold = BooleanProperty(False)
    """
    Whether description should be bold.

    :attr:`description_text_bold` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    draw_shadow = BooleanProperty(False)
    """
    Whether to show shadow.

    :attr:`draw_shadow` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    cancelable = BooleanProperty(False)
    """
    Whether clicking outside the outer circle dismisses the view.

    :attr:`cancelable` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    widget_position = OptionProperty(
        "left",
        options=[
            "left",
            "right",
            "top",
            "bottom",
            "left_top",
            "right_top",
            "left_bottom",
            "right_bottom",
            "center",
        ],
    )
    """
    Sets the position of the widget on the :attr:`~outer_circle`. Available options are
    `'left`', `'right`', `'top`', `'bottom`', `'left_top`', `'right_top`',
    `'left_bottom`', `'right_bottom`', `'center`'.

    :attr:`widget_position` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'left'`.
    """

    title_position = OptionProperty(
        "auto",
        options=[
            "auto",
            "left",
            "right",
            "top",
            "bottom",
            "left_top",
            "right_top",
            "left_bottom",
            "right_bottom",
        ],
    )
    """
    Sets the position of :attr`~title_text` on the outer circle. Only works if
    :attr`~widget_position` is set to `'center'`. In all other cases, it
    calculates the :attr`~title_position` itself.
    Must be set to other than `'auto`' when :attr`~widget_position` is set
    to `'center`'.

    Available options are `'auto'`, `'left`', `'right`', `'top`', `'bottom`',
    `'left_top`', `'right_top`', `'left_bottom`', `'right_bottom`', `'center`'.

    :attr:`title_position` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'auto'`.
    """

    stop_on_outer_touch = BooleanProperty(False)
    """
    Whether clicking on outer circle stops the animation.

    :attr:`stop_on_outer_touch` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    stop_on_target_touch = BooleanProperty(True)
    """
    Whether clicking on target circle should stop the animation.

    :attr:`stop_on_target_touch` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    state = OptionProperty("close", options=["close", "open"])
    """
    State of :class:`~MDTapTargetView`.

    :attr:`state` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'close'`.
    """

    _outer_radius = NumericProperty(0)
    _target_radius = NumericProperty(0)

    def __init__(self, **kwargs):
        self.ripple_max_dist = dp(90)
        self.on_outer_radius(self, self.outer_radius)
        self.on_target_radius(self, self.target_radius)

        self.core_title_text = Label(
            markup=True, size_hint=(None, None), bold=self.title_text_bold
        )
        self.core_title_text.bind(texture_size=self.core_title_text.setter("size"))
        self.core_description_text = Label(markup=True, size_hint=(None, None))
        self.core_description_text.bind(
            texture_size=self.core_description_text.setter("size")
        )

        super().__init__(**kwargs)
        self.register_event_type("on_outer_touch")
        self.register_event_type("on_target_touch")
        self.register_event_type("on_outside_click")
        self.register_event_type("on_open")
        self.register_event_type("on_close")

        if not self.outer_circle_color:
            self.outer_circle_color = self.theme_cls.primary_color[:-1]

    def _initialize(self):
        setattr(self.widget, "_outer_radius", 0)
        setattr(self.widget, "_target_radius", 0)
        setattr(self.widget, "target_ripple_radius", 0)
        setattr(self.widget, "target_ripple_alpha", 0)

        # Bind some function on widget event when this function is called
        # instead of when the class itself is initialized to prevent all
        # widgets of all instances to get bind at once and start messing up.
        self.widget.bind(on_touch_down=self._some_func)

    def _draw_canvas(self):
        _pos = self._ttv_pos()
        self.widget.canvas.before.clear()

        with self.widget.canvas.before:
            # Outer circle.
            Color(
                *self.outer_circle_color,
                self.outer_circle_alpha,
                group="ttv_group",
            )
            _rad1 = self.widget._outer_radius
            Ellipse(size=(_rad1, _rad1), pos=_pos[0], group="ttv_group")

            # Title text.
            Color(*self.title_text_color, group="ttv_group")
            Rectangle(
                size=self.core_title_text.texture.size,
                texture=self.core_title_text.texture,
                pos=_pos[1],
                group="ttv_group",
            )

            # Description text.
            Color(*self.description_text_color, group="ttv_group")
            Rectangle(
                size=self.core_description_text.texture.size,
                texture=self.core_description_text.texture,
                pos=(
                    _pos[1][0],
                    _pos[1][1] - self.core_description_text.size[1] - 5,
                ),
                group="ttv_group",
            )

            # Target circle.
            Color(*self.target_circle_color, group="ttv_group")
            _rad2 = self.widget._target_radius
            Ellipse(
                size=(_rad2, _rad2),
                pos=(
                    self.widget.x - (_rad2 / 2 - self.widget.size[0] / 2),
                    self.widget.y - (_rad2 / 2 - self.widget.size[0] / 2),
                ),
                group="ttv_group",
            )

            # Target ripple.
            Color(
                *self.target_circle_color,
                self.widget.target_ripple_alpha,
                group="ttv_group",
            )
            _rad3 = self.widget.target_ripple_radius
            Ellipse(
                size=(_rad3, _rad3),
                pos=(
                    self.widget.x - (_rad3 / 2 - self.widget.size[0] / 2),
                    self.widget.y - (_rad3 / 2 - self.widget.size[0] / 2),
                ),
                group="ttv_group",
            )

    def stop(self, *args):
        """Starts widget close animation."""

        # It needs a better implementation.
        self.anim_ripple.unbind(on_complete=self._repeat_ripple)
        self.core_title_text.opacity = 0
        self.core_description_text.opacity = 0
        anim = Animation(
            d=0.15,
            t="in_cubic",
            **dict(
                zip(
                    ["_outer_radius", "_target_radius", "target_ripple_radius"],
                    [0, 0, 0],
                )
            ),
        )
        anim.bind(on_complete=self._after_stop)
        anim.start(self.widget)

    def _after_stop(self, *args):
        self.widget.canvas.before.remove_group("ttv_group")
        args[0].stop_all(self.widget)
        elev = getattr(self.widget, "elevation", None)

        if elev:
            self._fix_elev()
        self.dispatch("on_close")

        # Don't forget to unbind the function or it'll mess
        # up with other next bindings.
        self.widget.unbind(on_touch_down=self._some_func)
        self.state = "close"

    def _fix_elev(self):
        with self.widget.canvas.before:
            Color(a=self.widget._soft_shadow_a)
            Rectangle(
                texture=self.widget._soft_shadow_texture,
                size=self.widget._soft_shadow_size,
                pos=self.widget._soft_shadow_pos,
            )
            Color(a=self.widget._hard_shadow_a)
            Rectangle(
                texture=self.widget._hard_shadow_texture,
                size=self.widget._hard_shadow_size,
                pos=self.widget._hard_shadow_pos,
            )
            Color(a=1)

    def start(self, *args):
        """Starts widget opening animation."""

        self._initialize()
        self._animate_outer()
        self.state = "open"
        self.core_title_text.opacity = 1
        self.core_description_text.opacity = 1
        self.dispatch("on_open")

    def _animate_outer(self):
        anim = Animation(
            d=0.2,
            t="out_cubic",
            **dict(
                zip(
                    ["_outer_radius", "_target_radius"],
                    [self._outer_radius, self._target_radius],
                )
            ),
        )
        anim.cancel_all(self.widget)
        anim.bind(on_progress=lambda x, y, z: self._draw_canvas())
        anim.bind(on_complete=self._animate_ripple)
        anim.start(self.widget)
        setattr(self.widget, "target_ripple_radius", self._target_radius)
        setattr(self.widget, "target_ripple_alpha", 1)

    def _animate_ripple(self, *args):
        self.anim_ripple = Animation(
            d=1,
            t="in_cubic",
            target_ripple_radius=self._target_radius + self.ripple_max_dist,
            target_ripple_alpha=0,
        )
        self.anim_ripple.stop_all(self.widget)
        self.anim_ripple.bind(on_progress=lambda x, y, z: self._draw_canvas())
        self.anim_ripple.bind(on_complete=self._repeat_ripple)
        self.anim_ripple.start(self.widget)

    def _repeat_ripple(self, *args):
        setattr(self.widget, "target_ripple_radius", self._target_radius)
        setattr(self.widget, "target_ripple_alpha", 1)
        self._animate_ripple()

    def on_open(self, *args):
        """Called at the time of the start of the widget opening animation."""

    def on_close(self, *args):
        """Called at the time of the start of the widget closed animation."""

    def on_draw_shadow(self, instance, value):
        Logger.warning(
            "The shadow adding method will be implemented in future versions"
        )

    def on_description_text(self, instance, value):
        self.core_description_text.text = value

    def on_description_text_size(self, instance, value):
        self.core_description_text.font_size = value

    def on_description_text_bold(self, instance, value):
        self.core_description_text.bold = value

    def on_title_text(self, instance, value):
        self.core_title_text.text = value

    def on_title_text_size(self, instance, value):
        self.core_title_text.font_size = value

    def on_title_text_bold(self, instance, value):
        self.core_title_text.bold = value

    def on_outer_radius(self, instance, value):
        self._outer_radius = self.outer_radius * 2

    def on_target_radius(self, instance, value):
        self._target_radius = self.target_radius * 2

    def on_target_touch(self):
        if self.stop_on_target_touch:
            self.stop()

    def on_outer_touch(self):
        if self.stop_on_outer_touch:
            self.stop()

    def on_outside_click(self):
        if self.cancelable:
            self.stop()

    def _some_func(self, wid, touch):
        """
        This function decides which one to dispatch based on the touch
        position.
        """

        if self._check_pos_target(touch.pos):
            self.dispatch("on_target_touch")
        elif self._check_pos_outer(touch.pos):
            self.dispatch("on_outer_touch")
        else:
            self.dispatch("on_outside_click")

    def _check_pos_outer(self, pos):
        """
        Checks if a given `pos` coordinate is within the :attr:`~outer_radius`.
        """

        cx = self.circ_pos[0] + self._outer_radius / 2
        cy = self.circ_pos[1] + self._outer_radius / 2
        r = self._outer_radius / 2
        h, k = pos

        lhs = (cx - h) ** 2 + (cy - k) ** 2
        rhs = r ** 2
        if lhs <= rhs:
            return True
        return False

    def _check_pos_target(self, pos):
        """
        Checks if a given `pos` coordinate is within the
        :attr:`~target_radius`.
        """

        cx = self.widget.pos[0] + self.widget.width / 2
        cy = self.widget.pos[1] + self.widget.height / 2
        r = self._target_radius / 2
        h, k = pos

        lhs = (cx - h) ** 2 + (cy - k) ** 2
        rhs = r ** 2
        if lhs <= rhs:
            return True
        return False

    def _ttv_pos(self):
        """
        Calculates the `pos` value for outer circle and text
        based on the position provided.

        :returns: A tuple containing pos for the circle and text.
        """

        _rad1 = self.widget._outer_radius
        _center_x = self.widget.x - (_rad1 / 2 - self.widget.size[0] / 2)
        _center_y = self.widget.y - (_rad1 / 2 - self.widget.size[0] / 2)

        if self.widget_position == "left":
            circ_pos = (_center_x + _rad1 / 3, _center_y)
            title_pos = (_center_x + _rad1 / 1.4, _center_y + _rad1 / 1.4)
        elif self.widget_position == "right":
            circ_pos = (_center_x - _rad1 / 3, _center_y)
            title_pos = (_center_x - _rad1 / 10, _center_y + _rad1 / 1.4)
        elif self.widget_position == "top":
            circ_pos = (_center_x, _center_y - _rad1 / 3)
            title_pos = (_center_x + _rad1 / 4, _center_y + _rad1 / 4)
        elif self.widget_position == "bottom":
            circ_pos = (_center_x, _center_y + _rad1 / 3)
            title_pos = (_center_x + _rad1 / 4, _center_y + _rad1 / 1.2)
        # Corner ones need to be at a little smaller distance
        # than edge ones that's why _rad1/4.
        elif self.widget_position == "left_top":
            circ_pos = (_center_x + _rad1 / 4, _center_y - _rad1 / 4)
            title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 4)
        elif self.widget_position == "right_top":
            circ_pos = (_center_x - _rad1 / 4, _center_y - _rad1 / 4)
            title_pos = (_center_x - _rad1 / 10, _center_y + _rad1 / 4)
        elif self.widget_position == "left_bottom":
            circ_pos = (_center_x + _rad1 / 4, _center_y + _rad1 / 4)
            title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 1.2)
        elif self.widget_position == "right_bottom":
            circ_pos = (_center_x - _rad1 / 4, _center_y + _rad1 / 4)
            title_pos = (_center_x, _center_y + _rad1 / 1.2)
        else:
            # Center.
            circ_pos = (_center_x, _center_y)

            if self.title_position == "auto":
                raise ValueError(
                    "widget_position='center' requires title_position to be set."
                )
            elif self.title_position == "left":
                title_pos = (_center_x + _rad1 / 10, _center_y + _rad1 / 2)
            elif self.title_position == "right":
                title_pos = (_center_x + _rad1 / 1.6, _center_y + _rad1 / 2)
            elif self.title_position == "top":
                title_pos = (_center_x + _rad1 / 2.5, _center_y + _rad1 / 1.3)
            elif self.title_position == "bottom":
                title_pos = (_center_x + _rad1 / 2.5, _center_y + _rad1 / 4)
            elif self.title_position == "left_top":
                title_pos = (_center_x + _rad1 / 8, _center_y + _rad1 / 1.4)
            elif self.title_position == "right_top":
                title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 1.3)
            elif self.title_position == "left_bottom":
                title_pos = (_center_x + _rad1 / 8, _center_y + _rad1 / 4)
            elif self.title_position == "right_bottom":
                title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 3.5)
            else:
                raise ValueError(
                    f"'{self.title_position}'"
                    f"is not a valid value for title_position"
                )

        self.circ_pos = circ_pos
        return circ_pos, title_pos
Esempio n. 10
0
class MDProgressBar(ThemableBehavior, ProgressBar):
    reversed = BooleanProperty(False)
    """
    Reverse the direction the progressbar moves.

    :attr:`reversed` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    orientation = OptionProperty(
        "horizontal", options=["horizontal", "vertical"]
    )
    """
    Orientation of progressbar. Available options are: `'horizontal '`,
    `'vertical'`.

    :attr:`orientation` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'horizontal'`.
    """

    color = ColorProperty(None)
    """
    Progress bar color in ``rgba`` format.

    :attr:`color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    back_color = ColorProperty(None)
    """
    Progress bar back color in ``rgba`` format.

    .. versionadded:: 1.0.0

    :attr:`back_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    running_transition = StringProperty("in_cubic")
    """
    Running transition.

    :attr:`running_transition` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'in_cubic'`.
    """

    catching_transition = StringProperty("out_quart")
    """
    Catching transition.

    :attr:`catching_transition` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'out_quart'`.
    """

    running_duration = NumericProperty(0.5)
    """
    Running duration.

    :attr:`running_duration` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0.5`.
    """

    catching_duration = NumericProperty(0.8)
    """
    Catching duration.

    :attr:`running_duration` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0.8`.
    """

    type = OptionProperty(
        None, options=["indeterminate", "determinate"], allownone=True
    )
    """
    Type of progressbar. Available options are: `'indeterminate '`,
    `'determinate'`.

    :attr:`type` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `None`.
    """

    _x = NumericProperty(0)

    def __init__(self, **kwargs):
        self.catching_anim = None
        self.running_anim = None
        super().__init__(**kwargs)
        Clock.schedule_once(self.check_size)

    def check_size(self, interval: Union[int, float]) -> None:
        if self.size == [100, 100]:
            if self.orientation == "horizontal":
                self.size_hint_y = None
                self.height = dp(4)
            elif self.orientation == "vertical":
                self.size_hint_x = None
                self.width = dp(4)

    def start(self) -> None:
        """Start animation."""

        if self.type in ("indeterminate", "determinate"):
            Clock.schedule_once(self._set_default_value)
            if not self.catching_anim and not self.running_anim:
                if self.type == "indeterminate":
                    self._create_indeterminate_animations()
                else:
                    self._create_determinate_animations()
            self.running_away()

    def stop(self) -> None:
        """Stop animation."""

        Animation.cancel_all(self)
        self._set_default_value(0)

    def running_away(self, *args) -> None:
        self._set_default_value(0)
        self.running_anim.start(self)

    def catching_up(self, *args) -> None:
        if self.type == "indeterminate":
            self.reversed = True
        self.catching_anim.start(self)

    def _create_determinate_animations(self):
        self.running_anim = Animation(
            value=100,
            opacity=1,
            t=self.running_transition,
            d=self.running_duration,
        )
        self.running_anim.bind(on_complete=self.catching_up)
        self.catching_anim = Animation(
            opacity=0,
            t=self.catching_transition,
            d=self.catching_duration,
        )
        self.catching_anim.bind(on_complete=self.running_away)

    def _create_indeterminate_animations(self):
        self.running_anim = Animation(
            _x=self.width / 2,
            value=50,
            t=self.running_transition,
            d=self.running_duration,
        )
        self.running_anim.bind(on_complete=self.catching_up)
        self.catching_anim = Animation(
            value=0, t=self.catching_transition, d=self.catching_duration
        )
        self.catching_anim.bind(on_complete=self.running_away)

    def _set_default_value(self, interval):
        self._x = 0
        self.value = 0
        self.reversed = False
Esempio n. 11
0
class MDToolbar(
        ThemableBehavior,
        RectangularElevationBehavior,
        SpecificBackgroundColorBehavior,
        BoxLayout,
):
    """
    :Events:
        `on_action_button`
            Method for the button used for the :class:`~MDBottomAppBar` class.
    """

    elevation = NumericProperty(6)
    """
    Elevation value.

    :attr:`elevation` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `6`.
    """

    left_action_items = ListProperty()
    """The icons on the left of the toolbar.
    To add one, append a list like the following:

    .. code-block:: kv

        left_action_items: [`'icon_name'`, callback]

    where `'icon_name'` is a string that corresponds to an icon definition and
    ``callback`` is the function called on a touch release event.

    :attr:`left_action_items` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    right_action_items = ListProperty()
    """The icons on the left of the toolbar.
    Works the same way as :attr:`left_action_items`.

    :attr:`right_action_items` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    title = StringProperty()
    """Text toolbar.

    :attr:`title` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    md_bg_color = ListProperty([0, 0, 0, 0])
    """Color toolbar.

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

    anchor_title = StringProperty("left")

    mode = OptionProperty("center",
                          options=["free-end", "free-center", "end", "center"])
    """Floating button position. Onle for :class:`~MDBottomAppBar` class.
    Available options are: `'free-end'`, `'free-center'`, `'end'`, `'center'`.

    :attr:`mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'center'`.
    """

    round = NumericProperty("10dp")
    """
    Rounding the corners at the notch for a button.
    Onle for :class:`~MDBottomAppBar` class.

    :attr:`round` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `'10dp'`.
    """

    icon = StringProperty("android")
    """
    Floating button. Onle for :class:`~MDBottomAppBar` class.

    :attr:`icon` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'android'`.
    """

    icon_color = ListProperty()
    """
    Color action button. Onle for :class:`~MDBottomAppBar` class.

    :attr:`icon_color` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    type = OptionProperty("top", options=["top", "bottom"])
    """
    When using the :class:`~MDBottomAppBar` class, the parameter ``type``
    must be set to `'bottom'`:

    .. code-block:: kv

        MDBottomAppBar:

            MDToolbar:
                type: "bottom"

    Available options are: `'top'`, `'bottom'`.

    :attr:`type` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'top'`.
    """

    _shift = NumericProperty("3.5dp")
    _angle_start = NumericProperty(90)
    _angle_end = NumericProperty(270)

    def __init__(self, **kwargs):
        self.action_button = MDActionBottomAppBarButton()
        super().__init__(**kwargs)
        self.register_event_type("on_action_button")
        self.action_button.bind(
            on_release=lambda x: self.dispatch("on_action_button"))
        self.action_button.x = Window.width / 2 - self.action_button.width / 2
        self.action_button.y = ((self.center[1] - self.height / 2) +
                                self.theme_cls.standard_increment / 2 +
                                self._shift)
        if not self.icon_color:
            self.icon_color = self.theme_cls.primary_color
        Window.bind(on_resize=self._on_resize)
        self.bind(specific_text_color=self.update_action_bar_text_colors)
        Clock.schedule_once(
            lambda x: self.on_left_action_items(0, self.left_action_items))
        Clock.schedule_once(
            lambda x: self.on_right_action_items(0, self.right_action_items))

    def on_action_button(self, *args):
        pass

    def on_md_bg_color(self, instance, value):
        if self.type == "bottom":
            self.md_bg_color = [0, 0, 0, 0]

    def on_left_action_items(self, instance, value):
        self.update_action_bar(self.ids["left_actions"], value)

    def on_right_action_items(self, instance, value):
        self.update_action_bar(self.ids["right_actions"], value)

    def update_action_bar(self, action_bar, action_bar_items):
        action_bar.clear_widgets()
        new_width = 0
        for item in action_bar_items:
            new_width += dp(48)
            action_bar.add_widget(
                MDIconButton(
                    icon=item[0],
                    on_release=item[1],
                    opposite_colors=True,
                    text_color=self.specific_text_color,
                    theme_text_color="Custom",
                ))
        action_bar.width = new_width

    def update_action_bar_text_colors(self, instance, value):
        for child in self.ids["left_actions"].children:
            child.text_color = self.specific_text_color
        for child in self.ids["right_actions"].children:
            child.text_color = self.specific_text_color

    def _on_resize(self, instance, width, height):
        if self.mode == "center":
            self.action_button.x = width / 2 - self.action_button.width / 2
        else:
            self.action_button.x = width - self.action_button.width * 2

    def on_icon(self, instance, value):
        self.action_button.icon = value

    def on_icon_color(self, instance, value):
        self.action_button.md_bg_color = value

    def on_mode(self, instance, value):
        def set_button_pos(*args):
            self.action_button.x = x
            self.action_button.y = y
            self.action_button._hard_shadow_size = (0, 0)
            self.action_button._soft_shadow_size = (0, 0)
            anim = Animation(_scale_x=1, _scale_y=1, d=0.05)
            anim.bind(on_complete=self.set_shadow)
            anim.start(self.action_button)

        if value == "center":
            self.set_notch()
            x = Window.width / 2 - self.action_button.width / 2
            y = ((self.center[1] - self.height / 2) +
                 self.theme_cls.standard_increment / 2 + self._shift)
        elif value == "end":

            self.set_notch()
            x = Window.width - self.action_button.width * 2
            y = ((self.center[1] - self.height / 2) +
                 self.theme_cls.standard_increment / 2 + self._shift)
            self.right_action_items = []
        elif value == "free-end":
            self.remove_notch()
            x = Window.width - self.action_button.width - dp(10)
            y = self.action_button.height + self.action_button.height / 2
        elif value == "free-center":
            self.remove_notch()
            x = Window.width / 2 - self.action_button.width / 2
            y = self.action_button.height + self.action_button.height / 2
        self.remove_shadow()
        anim = Animation(_scale_x=0, _scale_y=0, d=0.05)
        anim.bind(on_complete=set_button_pos)
        anim.start(self.action_button)

    def remove_notch(self):
        self._angle_start = 0
        self._angle_end = 0
        self.round = 0
        self._shift = 0

    def set_notch(self):
        self._angle_start = 90
        self._angle_end = 270
        self.round = dp(10)
        self._shift = dp(3.5)

    def remove_shadow(self):
        self.action_button._hard_shadow_size = (0, 0)
        self.action_button._soft_shadow_size = (0, 0)

    def set_shadow(self, *args):
        self.action_button._hard_shadow_size = (dp(112), dp(112))
        self.action_button._soft_shadow_size = (dp(112), dp(112))
Esempio n. 12
0
class Screen(RelativeLayout):
    '''Screen is an element intended to be used with a :class:`ScreenManager`.
    Check module documentation for more information.

    :Events:
        `on_pre_enter`: ()
            Event fired when the screen is about to be used: the entering
            animation is started.
        `on_enter`: ()
            Event fired when the screen is displayed: the entering animation is
            complete.
        `on_pre_leave`: ()
            Event fired when the screen is about to be removed: the leaving
            animation is started.
        `on_leave`: ()
            Event fired when the screen is removed: the leaving animation is
            finished.

    .. versionchanged:: 1.6.0
        Events `on_pre_enter`, `on_enter`, `on_pre_leave` and `on_leave` were
        added.
    '''

    name = StringProperty('')
    '''
    Name of the screen which must be unique within a :class:`ScreenManager`.
    This is the name used for :attr:`ScreenManager.current`.

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

    manager = ObjectProperty(None, allownone=True)
    ''':class:`ScreenManager` object, set when the screen is added to a
    manager.

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

    '''

    transition_progress = NumericProperty(0.)
    '''Value that represents the completion of the current transition, if any
    is occuring.

    If a transition is in progress, whatever the mode, the value will change
    from 0 to 1. If you want to know if it's an entering or leaving animation,
    check the :attr:`transition_state`.

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

    transition_state = OptionProperty('out', options=('in', 'out'))
    '''Value that represents the state of the transition:

    - 'in' if the transition is going to show your screen
    - 'out' if the transition is going to hide your screen

    After the transition is complete, the state will retain it's last value (in
    or out).

    :attr:`transition_state` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'out'.
    '''

    __events__ = ('on_pre_enter', 'on_enter', 'on_pre_leave', 'on_leave')

    def on_pre_enter(self, *args):
        pass

    def on_enter(self, *args):
        pass

    def on_pre_leave(self, *args):
        pass

    def on_leave(self, *args):
        pass

    def __repr__(self):
        return '<Screen name=%r>' % self.name
Esempio n. 13
0
class VideoPlayer(GridLayout):
    '''VideoPlayer class. See module documentation for more information.
    '''

    source = StringProperty('')
    '''Source of the video to read.

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

    .. versionchanged:: 1.4.0
    '''

    thumbnail = StringProperty('')
    '''Thumbnail of the video to show. If None, VideoPlayer will try to find
    the thumbnail from the :attr:`source` + '.png'.

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

    .. versionchanged:: 1.4.0
    '''

    duration = NumericProperty(-1)
    '''Duration of the video. The duration defaults to -1 and is set to the
    real duration when the video is loaded.

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

    position = NumericProperty(0)
    '''Position of the video between 0 and :attr:`duration`. The position
    defaults to -1 and is set to the real position when the video is loaded.

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

    volume = NumericProperty(1.0)
    '''Volume of the video in the range 0-1. 1 means full volume and 0 means
    mute.

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

    state = OptionProperty('stop', options=('play', 'pause', 'stop'))
    '''String, indicates whether to play, pause, or stop the video::

        # start playing the video at creation
        video = VideoPlayer(source='movie.mkv', state='play')

        # create the video, and start later
        video = VideoPlayer(source='movie.mkv')
        # and later
        video.state = 'play'

    :attr:`state` is an :class:`~kivy.properties.OptionProperty` and defaults
    to 'play'.
    '''

    play = BooleanProperty(False)
    '''
    .. deprecated:: 1.4.0
        Use :attr:`state` instead.

    Boolean, indicates whether the video is playing or not. You can start/stop
    the video by setting this property::

        # start playing the video at creation
        video = VideoPlayer(source='movie.mkv', play=True)

        # create the video, and start later
        video = VideoPlayer(source='movie.mkv')
        # and later
        video.play = True

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

    image_overlay_play = StringProperty(
        'atlas://data/images/defaulttheme/player-play-overlay')
    '''Image filename used to show a "play" overlay when the video has not yet
    started.

    :attr:`image_overlay_play` is a
    :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/player-play-overlay'.

    '''

    image_loading = StringProperty('data/images/image-loading.gif')
    '''Image filename used when the video is loading.

    :attr:`image_loading` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'data/images/image-loading.gif'.
    '''

    image_play = StringProperty(
        'atlas://data/images/defaulttheme/media-playback-start')
    '''Image filename used for the "Play" button.

    :attr:`image_play` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/media-playback-start'.
    '''

    image_stop = StringProperty(
        'atlas://data/images/defaulttheme/media-playback-stop')
    '''Image filename used for the "Stop" button.

    :attr:`image_stop` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/media-playback-stop'.
    '''

    image_pause = StringProperty(
        'atlas://data/images/defaulttheme/media-playback-pause')
    '''Image filename used for the "Pause" button.

    :attr:`image_pause` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/media-playback-pause'.
    '''

    image_volumehigh = StringProperty(
        'atlas://data/images/defaulttheme/audio-volume-high')
    '''Image filename used for the volume icon when the volume is high.

    :attr:`image_volumehigh` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/audio-volume-high'.
    '''

    image_volumemedium = StringProperty(
        'atlas://data/images/defaulttheme/audio-volume-medium')
    '''Image filename used for the volume icon when the volume is medium.

    :attr:`image_volumemedium` is a :class:`~kivy.properties.StringProperty`
    and defaults to 'atlas://data/images/defaulttheme/audio-volume-medium'.
    '''

    image_volumelow = StringProperty(
        'atlas://data/images/defaulttheme/audio-volume-low')
    '''Image filename used for the volume icon when the volume is low.

    :attr:`image_volumelow` is a :class:`~kivy.properties.StringProperty`
    and defaults to 'atlas://data/images/defaulttheme/audio-volume-low'.
    '''

    image_volumemuted = StringProperty(
        'atlas://data/images/defaulttheme/audio-volume-muted')
    '''Image filename used for the volume icon when the volume is muted.

    :attr:`image_volumemuted` is a :class:`~kivy.properties.StringProperty`
    and defaults to 'atlas://data/images/defaulttheme/audio-volume-muted'.
    '''

    annotations = StringProperty('')
    '''If set, it will be used for reading annotations box.

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

    fullscreen = BooleanProperty(False)
    '''Switch to fullscreen view. This should be used with care. When
    activated, the widget will remove itself from its parent, remove all
    children from the window and will add itself to it. When fullscreen is
    unset, all the previous children are restored and the widget is restored to
    its previous parent.

    .. warning::

        The re-add operation doesn't care about the index position of it's
        children within the parent.

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

    allow_fullscreen = BooleanProperty(True)
    '''By default, you can double-tap on the video to make it fullscreen. Set
    this property to False to prevent this behavior.

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

    options = DictProperty({})
    '''Optional parameters can be passed to a :class:`~kivy.uix.video.Video`
    instance with this property.

    :attr:`options` a :class:`~kivy.properties.DictProperty` and
    defaults to {}.
    '''

    # internals
    container = ObjectProperty(None)

    def __init__(self, **kwargs):
        self._video = None
        self._image = None
        self._annotations = ''
        self._annotations_labels = []
        super(VideoPlayer, self).__init__(**kwargs)
        self._load_thumbnail()
        self._load_annotations()

        if self.source:
            self._trigger_video_load()

    def _trigger_video_load(self, *largs):
        Clock.unschedule(self._do_video_load)
        Clock.schedule_once(self._do_video_load, -1)

    def on_source(self, instance, value):
        # we got a value, try to see if we have an image for it
        self._load_thumbnail()
        self._load_annotations()
        if self._video is not None:
            self._video.unload()
            self._video = None
        if value:
            self._trigger_video_load()

    def _load_thumbnail(self):
        if not self.container:
            return
        self.container.clear_widgets()
        # get the source, remove extension, and use png
        thumbnail = self.thumbnail
        if not thumbnail:
            filename = self.source.rsplit('.', 1)
            thumbnail = filename[0] + '.png'
        self._image = VideoPlayerPreview(source=thumbnail, video=self)
        self.container.add_widget(self._image)

    def _load_annotations(self):
        if not self.container:
            return
        self._annotations_labels = []
        annotations = self.annotations
        if not annotations:
            filename = self.source.rsplit('.', 1)
            annotations = filename[0] + '.jsa'
        if exists(annotations):
            with open(annotations, 'r') as fd:
                self._annotations = load(fd)
        if self._annotations:
            for ann in self._annotations:
                self._annotations_labels.append(
                    VideoPlayerAnnotation(annotation=ann))

    def on_state(self, instance, value):
        if self._video is not None:
            self._video.state = value

    def _set_state(self, instance, value):
        self.state = value

    def _do_video_load(self, *largs):
        self._video = Video(source=self.source,
                            state=self.state,
                            volume=self.volume,
                            pos_hint={
                                'x': 0,
                                'y': 0
                            },
                            **self.options)
        self._video.bind(texture=self._play_started,
                         duration=self.setter('duration'),
                         position=self.setter('position'),
                         volume=self.setter('volume'),
                         state=self._set_state)

    def on_play(self, instance, value):
        value = 'play' if value else 'stop'
        return self.on_state(instance, value)

    def on_volume(self, instance, value):
        if not self._video:
            return
        self._video.volume = value

    def on_position(self, instance, value):
        labels = self._annotations_labels
        if not labels:
            return
        for label in labels:
            start = label.start
            duration = label.duration
            if start > value or (start + duration) < value:
                if label.parent:
                    label.parent.remove_widget(label)
            elif label.parent is None:
                self.container.add_widget(label)

    def seek(self, percent):
        '''Change the position to a percentage of the duration. Percentage must
        be a value between 0-1.

        .. warning::

            Calling seek() before video is loaded has no effect.
        '''
        if not self._video:
            return
        self._video.seek(percent)

    def _play_started(self, instance, value):
        self.container.clear_widgets()
        self.container.add_widget(self._video)

    def on_touch_down(self, touch):
        if not self.collide_point(*touch.pos):
            return False
        if touch.is_double_tap and self.allow_fullscreen:
            self.fullscreen = not self.fullscreen
            return True
        return super(VideoPlayer, self).on_touch_down(touch)

    def on_fullscreen(self, instance, value):
        window = self.get_parent_window()
        if not window:
            Logger.warning('VideoPlayer: Cannot switch to fullscreen, '
                           'window not found.')
            if value:
                self.fullscreen = False
            return
        if not self.parent:
            Logger.warning('VideoPlayer: Cannot switch to fullscreen, '
                           'no parent.')
            if value:
                self.fullscreen = False
            return

        if value:
            self._fullscreen_state = state = {
                'parent': self.parent,
                'pos': self.pos,
                'size': self.size,
                'pos_hint': self.pos_hint,
                'size_hint': self.size_hint,
                'window_children': window.children[:]
            }

            # remove all window children
            for child in window.children[:]:
                window.remove_widget(child)

            # put the video in fullscreen
            if state['parent'] is not window:
                state['parent'].remove_widget(self)
            window.add_widget(self)

            # ensure the video widget is in 0, 0, and the size will be
            # reajusted
            self.pos = (0, 0)
            self.size = (100, 100)
            self.pos_hint = {}
            self.size_hint = (1, 1)
        else:
            state = self._fullscreen_state
            window.remove_widget(self)
            for child in state['window_children']:
                window.add_widget(child)
            self.pos_hint = state['pos_hint']
            self.size_hint = state['size_hint']
            self.pos = state['pos']
            self.size = state['size']
            if state['parent'] is not window:
                state['parent'].add_widget(self)
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('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 = ColorProperty([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].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`.
    """

    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
class ThemeManager(Widget):
    primary_palette = OptionProperty("Blue", options=palette)
    primary_hue = OptionProperty("500", options=hue)
    primary_light_hue = OptionProperty("200", options=hue)
    primary_dark_hue = OptionProperty("700", options=hue)

    def _get_primary_color(self):
        return get_color_from_hex(
            colors[self.primary_palette][self.primary_hue])

    primary_color = AliasProperty(_get_primary_color,
                                  bind=("primary_palette", "primary_hue"))

    def _get_primary_light(self):
        return get_color_from_hex(
            colors[self.primary_palette][self.primary_light_hue])

    primary_light = AliasProperty(_get_primary_light,
                                  bind=("primary_palette",
                                        "primary_light_hue"))

    def _get_primary_dark(self):
        return get_color_from_hex(
            colors[self.primary_palette][self.primary_dark_hue])

    primary_dark = AliasProperty(_get_primary_dark,
                                 bind=("primary_palette", "primary_dark_hue"))

    accent_palette = OptionProperty("Amber", options=palette)
    accent_hue = OptionProperty("500", options=hue)
    accent_light_hue = OptionProperty("200", options=hue)
    accent_dark_hue = OptionProperty("700", options=hue)

    def _get_accent_color(self):
        return get_color_from_hex(colors[self.accent_palette][self.accent_hue])

    accent_color = AliasProperty(_get_accent_color,
                                 bind=["accent_palette", "accent_hue"])

    def _get_accent_light(self):
        return get_color_from_hex(
            colors[self.accent_palette][self.accent_light_hue])

    accent_light = AliasProperty(_get_accent_light,
                                 bind=["accent_palette", "accent_light_hue"])

    def _get_accent_dark(self):
        return get_color_from_hex(
            colors[self.accent_palette][self.accent_dark_hue])

    accent_dark = AliasProperty(_get_accent_dark,
                                bind=["accent_palette", "accent_dark_hue"])

    theme_style = OptionProperty("Light", options=["Light", "Dark"])

    def _get_theme_style(self, opposite):
        if opposite:
            return "Light" if self.theme_style == "Dark" else "Dark"
        else:
            return self.theme_style

    def _get_bg_darkest(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            return get_color_from_hex(colors["Light"]["StatusBar"])
        elif theme_style == "Dark":
            return get_color_from_hex(colors["Dark"]["StatusBar"])

    bg_darkest = AliasProperty(_get_bg_darkest, bind=["theme_style"])

    def _get_op_bg_darkest(self):
        return self._get_bg_darkest(True)

    opposite_bg_darkest = AliasProperty(_get_op_bg_darkest,
                                        bind=["theme_style"])

    def _get_bg_dark(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            return get_color_from_hex(colors["Light"]["AppBar"])
        elif theme_style == "Dark":
            return get_color_from_hex(colors["Dark"]["AppBar"])

    bg_dark = AliasProperty(_get_bg_dark, bind=["theme_style"])

    def _get_op_bg_dark(self):
        return self._get_bg_dark(True)

    opposite_bg_dark = AliasProperty(_get_op_bg_dark, bind=["theme_style"])

    def _get_bg_normal(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            return get_color_from_hex(colors["Light"]["Background"])
        elif theme_style == "Dark":
            return get_color_from_hex(colors["Dark"]["Background"])

    bg_normal = AliasProperty(_get_bg_normal, bind=["theme_style"])

    def _get_op_bg_normal(self):
        return self._get_bg_normal(True)

    opposite_bg_normal = AliasProperty(_get_op_bg_normal, bind=["theme_style"])

    def _get_bg_light(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            return get_color_from_hex(colors["Light"]["CardsDialogs"])
        elif theme_style == "Dark":
            return get_color_from_hex(colors["Dark"]["CardsDialogs"])

    bg_light = AliasProperty(_get_bg_light, bind=["theme_style"])

    def _get_op_bg_light(self):
        return self._get_bg_light(True)

    opposite_bg_light = AliasProperty(_get_op_bg_light, bind=["theme_style"])

    def _get_divider_color(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            color = get_color_from_hex("000000")
        elif theme_style == "Dark":
            color = get_color_from_hex("FFFFFF")
        color[3] = 0.12
        return color

    divider_color = AliasProperty(_get_divider_color, bind=["theme_style"])

    def _get_op_divider_color(self):
        return self._get_divider_color(True)

    opposite_divider_color = AliasProperty(_get_op_divider_color,
                                           bind=["theme_style"])

    def _get_text_color(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            color = get_color_from_hex("000000")
            color[3] = 0.87
        elif theme_style == "Dark":
            color = get_color_from_hex("FFFFFF")
        return color

    text_color = AliasProperty(_get_text_color, bind=["theme_style"])

    def _get_op_text_color(self):
        return self._get_text_color(True)

    opposite_text_color = AliasProperty(_get_op_text_color,
                                        bind=["theme_style"])

    def _get_secondary_text_color(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            color = get_color_from_hex("000000")
            color[3] = 0.54
        elif theme_style == "Dark":
            color = get_color_from_hex("FFFFFF")
            color[3] = 0.70
        return color

    secondary_text_color = AliasProperty(_get_secondary_text_color,
                                         bind=["theme_style"])

    def _get_op_secondary_text_color(self):
        return self._get_secondary_text_color(True)

    opposite_secondary_text_color = AliasProperty(_get_op_secondary_text_color,
                                                  bind=["theme_style"])

    def _get_icon_color(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            color = get_color_from_hex("000000")
            color[3] = 0.54
        elif theme_style == "Dark":
            color = get_color_from_hex("FFFFFF")
        return color

    icon_color = AliasProperty(_get_icon_color, bind=["theme_style"])

    def _get_op_icon_color(self):
        return self._get_icon_color(True)

    opposite_icon_color = AliasProperty(_get_op_icon_color,
                                        bind=["theme_style"])

    def _get_disabled_hint_text_color(self, opposite=False):
        theme_style = self._get_theme_style(opposite)
        if theme_style == "Light":
            color = get_color_from_hex("000000")
            color[3] = 0.38
        elif theme_style == "Dark":
            color = get_color_from_hex("FFFFFF")
            color[3] = 0.50
        return color

    disabled_hint_text_color = AliasProperty(_get_disabled_hint_text_color,
                                             bind=["theme_style"])

    def _get_op_disabled_hint_text_color(self):
        return self._get_disabled_hint_text_color(True)

    opposite_disabled_hint_text_color = AliasProperty(
        _get_op_disabled_hint_text_color, bind=["theme_style"])

    # Hardcoded because muh standard
    def _get_error_color(self):
        return get_color_from_hex(colors["Red"]["A700"])

    error_color = AliasProperty(_get_error_color)

    def _get_ripple_color(self):
        return self._ripple_color

    def _set_ripple_color(self, value):
        self._ripple_color = value

    _ripple_color = ListProperty(get_color_from_hex(colors["Gray"]["400"]))
    ripple_color = AliasProperty(_get_ripple_color,
                                 _set_ripple_color,
                                 bind=["_ripple_color"])

    def _determine_device_orientation(self, _, window_size):
        if window_size[0] > window_size[1]:
            self.device_orientation = "landscape"
        elif window_size[1] >= window_size[0]:
            self.device_orientation = "portrait"

    device_orientation = StringProperty("")

    def _get_standard_increment(self):
        if DEVICE_TYPE == "mobile":
            if self.device_orientation == "landscape":
                return dp(48)
            else:
                return dp(56)
        else:
            return dp(64)

    standard_increment = AliasProperty(_get_standard_increment,
                                       bind=["device_orientation"])

    def _get_horizontal_margins(self):
        if DEVICE_TYPE == "mobile":
            return dp(16)
        else:
            return dp(24)

    horizontal_margins = AliasProperty(_get_horizontal_margins)

    def on_theme_style(self, instance, value):
        if (hasattr(App.get_running_app(), "theme_cls")
                and App.get_running_app().theme_cls == self):
            self.set_clearcolor_by_theme_style(value)

    def set_clearcolor_by_theme_style(self, theme_style):
        if theme_style == "Light":
            Window.clearcolor = get_color_from_hex(
                colors["Light"]["Background"])
        elif theme_style == "Dark":
            Window.clearcolor = get_color_from_hex(
                colors["Dark"]["Background"])

    # font name, size (sp), always caps, letter spacing (sp)
    font_styles = DictProperty({
        "H1": ["RobotoLight", 96, False, -1.5],
        "H2": ["RobotoLight", 60, False, -0.5],
        "H3": ["Roboto", 48, False, 0],
        "H4": ["Roboto", 34, False, 0.25],
        "H5": ["Roboto", 24, False, 0],
        "H6": ["RobotoMedium", 20, False, 0.15],
        "Subtitle1": ["Roboto", 16, False, 0.15],
        "Subtitle2": ["RobotoMedium", 14, False, 0.1],
        "Body1": ["Roboto", 16, False, 0.5],
        "Body2": ["Roboto", 14, False, 0.25],
        "Button": ["RobotoMedium", 14, True, 1.25],
        "Caption": ["Roboto", 12, False, 0.4],
        "Overline": ["Roboto", 10, True, 1.5],
        "Icon": ["Icons", 24, False, 0],
    })

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.rec_shadow = Atlas(f"{images_path}rec_shadow.atlas")
        self.rec_st_shadow = Atlas(f"{images_path}rec_st_shadow.atlas")
        self.quad_shadow = Atlas(f"{images_path}quad_shadow.atlas")
        self.round_shadow = Atlas(f"{images_path}round_shadow.atlas")
        Clock.schedule_once(lambda x: self.on_theme_style(0, self.theme_style))
        self._determine_device_orientation(None, Window.size)
        Window.bind(size=self._determine_device_orientation)
Esempio n. 16
0
class MDDropdownMenu(ThemableBehavior, FloatLayout):
    """
    :Events:
        `on_release`
            The method that will be called when you click menu items.
    """

    header_cls = ObjectProperty()
    """
    An instance of the class (`Kivy` or `KivyMD` widget) that will be added
    to the menu header.

    .. versionadded:: 0.104.2

    :attr:`header_cls` is a :class:`~kivy.properties.ObjectProperty`
    and defaults to `None`.
    """

    items = ListProperty()
    """
    See :attr:`~kivy.uix.recycleview.RecycleView.data`.

    :attr:`items` is a :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    width_mult = NumericProperty(1)
    """
    This number multiplied by the standard increment (56dp on mobile,
    64dp on desktop, determines the width of the menu items.

    If the resulting number were to be too big for the application Window,
    the multiplier will be adjusted for the biggest possible one.

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

    max_height = NumericProperty()
    """
    The menu will grow no bigger than this number. Set to 0 for no limit.

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

    border_margin = NumericProperty("4dp")
    """
    Margin between Window border and menu.

    :attr:`border_margin` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `4dp`.
    """

    ver_growth = OptionProperty(None, allownone=True, options=["up", "down"])
    """
    Where the menu will grow vertically to when opening. Set to None to let
    the widget pick for you. Available options are: `'up'`, `'down'`.

    :attr:`ver_growth` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `None`.
    """

    hor_growth = OptionProperty(None,
                                allownone=True,
                                options=["left", "right"])
    """
    Where the menu will grow horizontally to when opening. Set to None to let
    the widget pick for you. Available options are: `'left'`, `'right'`.

    :attr:`hor_growth` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `None`.
    """

    background_color = ColorProperty(None)
    """
    Color of the background of the menu.

    :attr:`background_color` is a :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    opening_transition = StringProperty("out_cubic")
    """
    Type of animation for opening a menu window.

    :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'out_cubic'`.
    """

    opening_time = NumericProperty(0.2)
    """
    Menu window opening animation time and you can set it to 0
    if you don't want animation of menu opening.

    :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `0.2`.
    """

    caller = ObjectProperty()
    """
    The widget object that calls the menu window.

    :attr:`caller` is a :class:`~kivy.properties.ObjectProperty`
    and defaults to `None`.
    """

    position = OptionProperty("auto",
                              options=["top", "auto", "center", "bottom"])
    """
    Menu window position relative to parent element.
    Available options are: `'auto'`, `'center'`, `'bottom'`.

    :attr:`position` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `'auto'`.
    """

    radius = VariableListProperty([dp(7)])
    """
    Menu radius.

    :attr:`radius` is a :class:`~kivy.properties.VariableListProperty`
    and defaults to `'[dp(7)]'`.
    """

    elevation = NumericProperty(10)
    """
    Elevation value of menu dialog.

    .. versionadded:: 1.0.0

    :attr:`elevation` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `10`.
    """

    _start_coords = []
    _calculate_complete = False
    _calculate_process = False

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Window.bind(on_resize=self.check_position_caller)
        Window.bind(on_maximize=self.set_menu_properties)
        Window.bind(on_restore=self.set_menu_properties)
        self.register_event_type("on_dismiss")
        self.menu = self.ids.md_menu
        self.target_height = 0

    def check_position_caller(self, instance_window: WindowSDL, width: int,
                              height: int) -> NoReturn:
        """Called when the application root window is resized."""

        # FIXME: Menu position is not recalculated when changing the size of
        #  the root application window.
        self.set_menu_properties(0)

    def set_menu_properties(self, interval: Union[int, float] = 0) -> NoReturn:
        """Sets the size and position for the menu window."""

        if self.caller:
            self.ids.md_menu.data = self.items
            # We need to pick a starting point, see how big we need to be,
            # and where to grow to.
            self._start_coords = self.caller.to_window(self.caller.center_x,
                                                       self.caller.center_y)
            self.target_width = self.width_mult * m_res.STANDARD_INCREMENT

            # If we're wider than the Window...
            if self.target_width > Window.width:
                # ...reduce our multiplier to max allowed.
                self.target_width = (
                    int(Window.width / m_res.STANDARD_INCREMENT) *
                    m_res.STANDARD_INCREMENT)

            # Set the target_height of the menu depending on the size of
            # each MDMenuItem or MDMenuItemIcon.
            self.target_height = 0
            for item in self.ids.md_menu.data:
                self.target_height += item.get("height", dp(72))

            # If we're over max_height...
            if 0 < self.max_height < self.target_height:
                self.target_height = self.max_height

            # Establish vertical growth direction.
            if self.ver_growth is not None:
                ver_growth = self.ver_growth
            else:
                # If there's enough space below us:
                if (self.target_height <=
                        self._start_coords[1] - self.border_margin):
                    ver_growth = "down"
                # if there's enough space above us:
                elif (self.target_height < Window.height -
                      self._start_coords[1] - self.border_margin):
                    ver_growth = "up"
                # Otherwise, let's pick the one with more space and adjust ourselves.
                else:
                    # If there"s more space below us:
                    if (self._start_coords[1] >=
                            Window.height - self._start_coords[1]):
                        ver_growth = "down"
                        self.target_height = (self._start_coords[1] -
                                              self.border_margin)
                    # If there's more space above us:
                    else:
                        ver_growth = "up"
                        self.target_height = (Window.height -
                                              self._start_coords[1] -
                                              self.border_margin)

            if self.hor_growth is not None:
                hor_growth = self.hor_growth
            else:
                # If there's enough space to the right:
                if (self.target_width <= Window.width - self._start_coords[0] -
                        self.border_margin):
                    hor_growth = "right"
                # if there's enough space to the left:
                elif (self.target_width <
                      self._start_coords[0] - self.border_margin):
                    hor_growth = "left"
                # Otherwise, let's pick the one with more space and adjust ourselves.
                else:
                    # if there"s more space to the right:
                    if (Window.width - self._start_coords[0] >=
                            self._start_coords[0]):
                        hor_growth = "right"
                        self.target_width = (Window.width -
                                             self._start_coords[0] -
                                             self.border_margin)
                    # if there"s more space to the left:
                    else:
                        hor_growth = "left"
                        self.target_width = (self._start_coords[0] -
                                             self.border_margin)

            if ver_growth == "down":
                self.tar_y = self._start_coords[1] - self.target_height
            else:  # should always be "up"
                self.tar_y = self._start_coords[1]

            if hor_growth == "right":
                self.tar_x = self._start_coords[0]
            else:  # should always be "left"
                self.tar_x = self._start_coords[0] - self.target_width
            self._calculate_complete = True

    def open(self) -> NoReturn:
        """Animate the opening of a menu window."""
        def open(interval):
            if not self._calculate_complete:
                return
            if self.position == "auto":
                self.menu.pos = self._start_coords
                anim = Animation(
                    x=self.tar_x,
                    y=self.tar_y,
                    width=self.target_width,
                    height=self.target_height,
                    duration=self.opening_time,
                    opacity=1,
                    transition=self.opening_transition,
                )
                anim.start(self.menu)
            else:
                if self.position == "center":
                    self.menu.pos = (
                        self._start_coords[0] - self.target_width / 2,
                        self._start_coords[1] - self.target_height / 2,
                    )
                elif self.position == "bottom":
                    self.menu.pos = (
                        self._start_coords[0] - self.target_width / 2,
                        self.caller.pos[1] - self.target_height,
                    )
                elif self.position == "top":
                    self.menu.pos = (
                        self._start_coords[0] - self.target_width / 2,
                        self.caller.pos[1] + self.caller.height,
                    )
                anim = Animation(
                    width=self.target_width,
                    height=self.target_height,
                    duration=self.opening_time,
                    opacity=1,
                    transition=self.opening_transition,
                )
            anim.start(self.menu)
            Window.add_widget(self)
            Clock.unschedule(open)
            self._calculate_process = False

        self.set_menu_properties()
        if not self._calculate_process:
            self._calculate_process = True
            Clock.schedule_interval(open, 0)

    def on_header_cls(self, instance_dropdown_menu,
                      instance_user_menu_header) -> NoReturn:
        def add_content_header_cls(interval):
            self.ids.content_header.clear_widgets()
            self.ids.content_header.add_widget(instance_user_menu_header)

        Clock.schedule_once(add_content_header_cls, 1)

    def on_touch_down(self, touch):
        if not self.menu.collide_point(*touch.pos):
            self.dispatch("on_dismiss")
            return True
        super().on_touch_down(touch)
        return True

    def on_touch_move(self, touch):
        super().on_touch_move(touch)
        return True

    def on_touch_up(self, touch):
        super().on_touch_up(touch)
        return True

    def on_dismiss(self) -> NoReturn:
        """Called when the menu is closed."""

        Window.remove_widget(self)
        self.menu.width = 0
        self.menu.height = 0
        self.menu.opacity = 0

    def dismiss(self) -> NoReturn:
        """Closes the menu."""

        self.on_dismiss()
Esempio n. 17
0
class Carousel(StencilView):
    '''Carousel class. See module documentation for more information.
    '''

    slides = ListProperty([])
    '''List of slides inside the Carousel. The slides are the
    widgets added to the Carousel using the :attr:`add_widget` method.

    :attr:`slides` is a :class:`~kivy.properties.ListProperty` and is
    read-only.
    '''

    def _get_slides_container(self):
        return [x.parent for x in self.slides]

    slides_container = AliasProperty(_get_slides_container, None,
                                     bind=('slides', ))

    direction = OptionProperty('right',
                               options=('right', 'left', 'top', 'bottom'))
    '''Specifies the direction in which the slides are ordered. This
    corresponds to the direction from which the user swipes to go from one
    slide to the next. It
    can be `right`, `left`, `top`, or `bottom`. For example, with
    the default value of `right`, the second slide is to the right
    of the first and the user would swipe from the right towards the
    left to get to the second slide.

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

    min_move = NumericProperty(0.2)
    '''Defines the minimum distance to be covered before the touch is
    considered a swipe gesture and the Carousel content changed.
    This is a expressed as a fraction of the Carousel's width.
    If the movement doesn't reach this minimum value, the movement is
    cancelled and the content is restored to its original position.

    :attr:`min_move` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.2.
    '''

    anim_move_duration = NumericProperty(0.5)
    '''Defines the duration of the Carousel animation between pages.

    :attr:`anim_move_duration` is a :class:`~kivy.properties.NumericProperty`
    and defaults to 0.5.
    '''

    anim_cancel_duration = NumericProperty(0.3)
    '''Defines the duration of the animation when a swipe movement is not
    accepted. This is generally when the user does not make a large enough
    swipe. See :attr:`min_move`.

    :attr:`anim_cancel_duration` is a :class:`~kivy.properties.NumericProperty`
    and defaults to 0.3.
    '''

    loop = BooleanProperty(False)
    '''Allow the Carousel to loop infinitely. If True, when the user tries to
    swipe beyond last page, it will return to the first. If False, it will
    remain on the last page.

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

    def _get_index(self):
        if self.slides:
            return self._index % len(self.slides)
        return None

    def _set_index(self, value):
        if self.slides:
            self._index = value % len(self.slides)
        else:
            self._index = None
    index = AliasProperty(_get_index, _set_index, bind=('_index', 'slides'))
    '''Get/Set the current slide based on the index.

    :attr:`index` is an :class:`~kivy.properties.AliasProperty` and defaults
    to 0 (the first item).
    '''

    def _prev_slide(self):
        slides = self.slides
        len_slides = len(slides)
        index = self.index
        if len_slides < 2:  # None, or 1 slide
            return None
        if len_slides == 2:
            if index == 0:
                return None
            if index == 1:
                return slides[0]
        if self.loop and index == 0:
            return slides[-1]
        if index > 0:
            return slides[index - 1]

    previous_slide = AliasProperty(_prev_slide, None, bind=('slides', 'index'))
    '''The previous slide in the Carousel. It is None if the current slide is
    the first slide in the Carousel. This ordering reflects the order in which
    the slides are added: their presentation varies according to the
    :attr:`direction` property.

    :attr:`previous_slide` is an :class:`~kivy.properties.AliasProperty`.

    .. versionchanged:: 1.5.0
        This property no longer exposes the slides container. It returns
        the widget you have added.
    '''

    def _curr_slide(self):
        if len(self.slides):
            return self.slides[self.index]
    current_slide = AliasProperty(_curr_slide, None, bind=('slides', 'index'))
    '''The currently shown slide.

    :attr:`current_slide` is an :class:`~kivy.properties.AliasProperty`.

    .. versionchanged:: 1.5.0
        The property no longer exposes the slides container. It returns
        the widget you have added.
    '''

    def _next_slide(self):
        if len(self.slides) < 2:  # None, or 1 slide
            return None
        if len(self.slides) == 2:
            if self.index == 0:
                return self.slides[1]
            if self.index == 1:
                return None
        if self.loop and self.index == len(self.slides) - 1:
            return self.slides[0]
        if self.index < len(self.slides) - 1:
            return self.slides[self.index + 1]
    next_slide = AliasProperty(_next_slide, None, bind=('slides', 'index'))
    '''The next slide in the Carousel. It is None if the current slide is
    the last slide in the Carousel. This ordering reflects the order in which
    the slides are added: their presentation varies according to the
    :attr:`direction` property.

    :attr:`next_slide` is an :class:`~kivy.properties.AliasProperty`.

    .. versionchanged:: 1.5.0
        The property no longer exposes the slides container.
        It returns the widget you have added.
    '''

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

    :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 200 (milliseconds)

    .. versionadded:: 1.5.0
    '''

    scroll_distance = NumericProperty('20dp')
    '''Distance to move before scrolling the :class:`Carousel` in pixels. As
    soon as the distance has been traveled, the :class:`Carousel` 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 20dp.

    .. versionadded:: 1.5.0
    '''

    anim_type = StringProperty('out_quad')
    '''Type of animation to use while animating to the next/previous slide.
    This should be the name of an
    :class:`~kivy.animation.AnimationTransition` function.

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

    .. versionadded:: 1.8.0
    '''

    ignore_perpendicular_swipes = BooleanProperty(False)
    '''Ignore swipes on axis perpendicular to direction.

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

    .. versionadded:: 1.9.2
    '''

    #### private properties, for internal use only ###
    _index = NumericProperty(0, allownone=True)
    _prev = ObjectProperty(None, allownone=True)
    _current = ObjectProperty(None, allownone=True)
    _next = ObjectProperty(None, allownone=True)
    _offset = NumericProperty(0)
    _touch = ObjectProperty(None, allownone=True)

    _change_touch_mode_ev = None

    def __init__(self, **kwargs):
        self._trigger_position_visible_slides = Clock.create_trigger(
            self._position_visible_slides, -1)
        super(Carousel, self).__init__(**kwargs)
        self._skip_slide = None
        self.touch_mode_change = False

    def load_slide(self, slide):
        '''Animate to the slide that is passed as the argument.

        .. versionchanged:: 1.8.0
        '''
        slides = self.slides
        start, stop = slides.index(self.current_slide), slides.index(slide)
        if start == stop:
            return

        self._skip_slide = stop
        if stop > start:
            self._insert_visible_slides(_next_slide=slide)
            self.load_next()
        else:
            self._insert_visible_slides(_prev_slide=slide)
            self.load_previous()

    def load_previous(self):
        '''Animate to the previous slide.

        .. versionadded:: 1.7.0
        '''
        self.load_next(mode='prev')

    def load_next(self, mode='next'):
        '''Animate to the next slide.

        .. versionadded:: 1.7.0
        '''
        if not self.index is None:
            w, h = self.size
            _direction = {
                'top': -h / 2,
                'bottom': h / 2,
                'left': w / 2,
                'right': -w / 2}
            _offset = _direction[self.direction]
            if mode == 'prev':
                _offset = -_offset

            self._start_animation(min_move=0, offset=_offset)

    def get_slide_container(self, slide):
        return slide.parent

    def _insert_visible_slides(self, _next_slide=None, _prev_slide=None):
        get_slide_container = self.get_slide_container

        previous_slide = _prev_slide if _prev_slide else self.previous_slide
        if previous_slide:
            self._prev = get_slide_container(previous_slide)
        else:
            self._prev = None

        current_slide = self.current_slide
        if current_slide:
            self._current = get_slide_container(current_slide)
        else:
            self._current = None

        next_slide = _next_slide if _next_slide else self.next_slide
        if next_slide:
            self._next = get_slide_container(next_slide)
        else:
            self._next = None

        super_remove = super(Carousel, self).remove_widget
        for container in self.slides_container:
            super_remove(container)

        if self._prev and self._prev.parent is not self:
            super(Carousel, self).add_widget(self._prev)
        if self._next and self._next.parent is not self:
            super(Carousel, self).add_widget(self._next)
        if self._current:
            super(Carousel, self).add_widget(self._current)

    def _position_visible_slides(self, *args):
        slides, index = self.slides, self.index
        no_of_slides = len(slides) - 1
        if not slides:
            return
        x, y, width, height = self.x, self.y, self.width, self.height
        _offset, direction = self._offset, self.direction
        _prev, _next, _current = self._prev, self._next, self._current
        get_slide_container = self.get_slide_container
        last_slide = get_slide_container(slides[-1])
        first_slide = get_slide_container(slides[0])
        skip_next = False
        _loop = self.loop

        if direction[0] in ['r', 'l']:
            xoff = x + _offset
            x_prev = {'l': xoff + width, 'r': xoff - width}
            x_next = {'l': xoff - width, 'r': xoff + width}
            if _prev:
                _prev.pos = (x_prev[direction[0]], y)
            elif _loop and _next and index == 0:
                # if first slide is moving to right with direction set to right
                # or toward left with direction set to left
                if ((_offset > 0 and direction[0] == 'r') or
                        (_offset < 0 and direction[0] == 'l')):
                    # put last_slide before first slide
                    last_slide.pos = (x_prev[direction[0]], y)
                    skip_next = True
            if _current:
                _current.pos = (xoff, y)
            if skip_next:
                return
            if _next:
                _next.pos = (x_next[direction[0]], y)
            elif _loop and _prev and index == no_of_slides:
                if ((_offset < 0 and direction[0] == 'r') or
                        (_offset > 0 and direction[0] == 'l')):
                    first_slide.pos = (x_next[direction[0]], y)
        if direction[0] in ['t', 'b']:
            yoff = y + _offset
            y_prev = {'t': yoff - height, 'b': yoff + height}
            y_next = {'t': yoff + height, 'b': yoff - height}
            if _prev:
                _prev.pos = (x, y_prev[direction[0]])
            elif _loop and _next and index == 0:
                if ((_offset > 0 and direction[0] == 't') or
                        (_offset < 0 and direction[0] == 'b')):
                    last_slide.pos = (x, y_prev[direction[0]])
                    skip_next = True
            if _current:
                _current.pos = (x, yoff)
            if skip_next:
                return
            if _next:
                _next.pos = (x, y_next[direction[0]])
            elif _loop and _prev and index == no_of_slides:
                if ((_offset < 0 and direction[0] == 't') or
                        (_offset > 0 and direction[0] == 'b')):
                    first_slide.pos = (x, y_next[direction[0]])

    def on_size(self, *args):
        size = self.size
        for slide in self.slides_container:
            slide.size = size
        self._trigger_position_visible_slides()

    def on_pos(self, *args):
        self._trigger_position_visible_slides()

    def on_index(self, *args):
        self._insert_visible_slides()
        self._trigger_position_visible_slides()
        self._offset = 0

    def on_slides(self, *args):
        if self.slides:
            self.index = self.index % len(self.slides)
        self._insert_visible_slides()
        self._trigger_position_visible_slides()

    def on__offset(self, *args):
        self._trigger_position_visible_slides()
        # if reached full offset, switch index to next or prev
        direction = self.direction
        _offset = self._offset
        width = self.width
        height = self.height
        index = self.index
        if self._skip_slide is not None or index is None:
            return

        if direction[0] == 'r':
            if _offset <= -width:
                index += 1
            if _offset >= width:
                index -= 1
        if direction[0] == 'l':
            if _offset <= -width:
                index -= 1
            if _offset >= width:
                index += 1
        if direction[0] == 't':
            if _offset <= - height:
                index += 1
            if _offset >= height:
                index -= 1
        if direction[0] == 'b':
            if _offset <= -height:
                index -= 1
            if _offset >= height:
                index += 1
        self.index = index

    def _start_animation(self, *args, **kwargs):
        # compute target offset for ease back, next or prev
        new_offset = 0
        direction = kwargs.get('direction', self.direction)
        is_horizontal = direction[0] in ['r', 'l']
        extent = self.width if is_horizontal else self.height
        min_move = kwargs.get('min_move', self.min_move)
        _offset = kwargs.get('offset', self._offset)

        if _offset < min_move * -extent:
            new_offset = -extent
        elif _offset > min_move * extent:
            new_offset = extent

        # if new_offset is 0, it wasnt enough to go next/prev
        dur = self.anim_move_duration
        if new_offset == 0:
            dur = self.anim_cancel_duration

        # detect edge cases if not looping
        len_slides = len(self.slides)
        index = self.index
        if not self.loop or len_slides == 1:
            is_first = (index == 0)
            is_last = (index == len_slides - 1)
            if direction[0] in ['r', 't']:
                towards_prev = (new_offset > 0)
                towards_next = (new_offset < 0)
            else:
                towards_prev = (new_offset < 0)
                towards_next = (new_offset > 0)
            if (is_first and towards_prev) or (is_last and towards_next):
                new_offset = 0

        anim = Animation(_offset=new_offset, d=dur, t=self.anim_type)
        anim.cancel_all(self)

        def _cmp(*l):
            if self._skip_slide is not None:
                self.index = self._skip_slide
                self._skip_slide = None

        anim.bind(on_complete=_cmp)
        anim.start(self)

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

    def on_touch_down(self, touch):
        if not self.collide_point(*touch.pos):
            touch.ud[self._get_uid('cavoid')] = True
            return
        if self.disabled:
            return True
        if self._touch:
            return super(Carousel, self).on_touch_down(touch)
        Animation.cancel_all(self)
        self._touch = touch
        uid = self._get_uid()
        touch.grab(self)
        touch.ud[uid] = {
            'mode': 'unknown',
            'time': touch.time_start}
        self._change_touch_mode_ev = Clock.schedule_once(
            self._change_touch_mode, self.scroll_timeout / 1000.)
        self.touch_mode_change = False
        return True

    def on_touch_move(self, touch):
        if self.touch_mode_change == False:
            if self.ignore_perpendicular_swipes and self.direction in ('top','bottom'):
                if abs(touch.oy - touch.y) < self.scroll_distance:
                    if abs(touch.ox - touch.x) > self.scroll_distance:
                        self._change_touch_mode()
                        self.touchModeChange = True
            elif self.ignore_perpendicular_swipes and self.direction in ('right','left'):
                if abs(touch.ox - touch.x) < self.scroll_distance:
                    if abs(touch.oy - touch.y) > self.scroll_distance:
                        self._change_touch_mode()
                        self.touchModeChange = True

        if self._get_uid('cavoid') in touch.ud:
            return
        if self._touch is not touch:
            super(Carousel, self).on_touch_move(touch)
            return self._get_uid() in touch.ud
        if touch.grab_current is not self:
            return True
        ud = touch.ud[self._get_uid()]
        direction = self.direction
        if ud['mode'] == 'unknown':
            if direction[0] in ('r', 'l'):
                distance = abs(touch.ox - touch.x)
            else:
                distance = abs(touch.oy - touch.y)
            if distance > self.scroll_distance:
                ev = self._change_touch_mode_ev
                if ev is not None:
                    ev.cancel()
                ud['mode'] = 'scroll'
        else:
            if direction[0] in ('r', 'l'):
                self._offset += touch.dx
            if direction[0] in ('t', 'b'):
                self._offset += touch.dy
        return True

    def on_touch_up(self, touch):
        if self._get_uid('cavoid') in touch.ud:
            return
        if self in [x() for x in touch.grab_list]:
            touch.ungrab(self)
            self._touch = None
            ud = touch.ud[self._get_uid()]
            if ud['mode'] == 'unknown':
                ev = self._change_touch_mode_ev
                if ev is not None:
                    ev.cancel()
                super(Carousel, self).on_touch_down(touch)
                Clock.schedule_once(partial(self._do_touch_up, touch), .1)
            else:
                self._start_animation()

        else:
            if self._touch is not touch and self.uid not in touch.ud:
                super(Carousel, self).on_touch_up(touch)
        return self._get_uid() in touch.ud

    def _do_touch_up(self, touch, *largs):
        super(Carousel, self).on_touch_up(touch)
        # 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
            super(Carousel, self).on_touch_up(touch)
        touch.grab_current = None

    def _change_touch_mode(self, *largs):
        if not self._touch:
            return
        self._start_animation()
        uid = self._get_uid()
        touch = self._touch
        ud = touch.ud[uid]
        if ud['mode'] == 'unknown':
            touch.ungrab(self)
            self._touch = None
            super(Carousel, self).on_touch_down(touch)
            return

    def add_widget(self, widget, index=0):
        slide = RelativeLayout(size=self.size, x=self.x - self.width, y=self.y)
        slide.add_widget(widget)
        super(Carousel, self).add_widget(slide, index)
        if index != 0:
            self.slides.insert(index - len(self.slides), widget)
        else:
            self.slides.append(widget)

    def remove_widget(self, widget, *args, **kwargs):
        # XXX be careful, the widget.parent refer to the RelativeLayout
        # added in add_widget(). But it will break if RelativeLayout
        # implementation change.
        # if we passed the real widget
        if widget in self.slides:
            slide = widget.parent
            self.slides.remove(widget)
            return slide.remove_widget(widget, *args, **kwargs)
        return super(Carousel, self).remove_widget(widget, *args, **kwargs)

    def clear_widgets(self):
        for slide in self.slides[:]:
            self.remove_widget(slide)
        super(Carousel, self).clear_widgets()
Esempio n. 18
0
class ElectrumWindow(App):

    electrum_config = ObjectProperty(None)
    language = StringProperty('en')

    # properties might be updated by the network
    num_blocks = NumericProperty(0)
    num_nodes = NumericProperty(0)
    server_host = StringProperty('')
    server_port = StringProperty('')
    num_chains = NumericProperty(0)
    blockchain_name = StringProperty('')
    fee_status = StringProperty('Fee')
    balance = StringProperty('')
    fiat_balance = StringProperty('')
    is_fiat = BooleanProperty(False)
    blockchain_forkpoint = NumericProperty(0)

    auto_connect = BooleanProperty(False)

    def on_auto_connect(self, instance, x):
        net_params = self.network.get_parameters()
        net_params = net_params._replace(auto_connect=self.auto_connect)
        self.network.run_from_another_thread(
            self.network.set_parameters(net_params))

    def toggle_auto_connect(self, x):
        self.auto_connect = not self.auto_connect

    oneserver = BooleanProperty(False)

    def on_oneserver(self, instance, x):
        net_params = self.network.get_parameters()
        net_params = net_params._replace(oneserver=self.oneserver)
        self.network.run_from_another_thread(
            self.network.set_parameters(net_params))

    def toggle_oneserver(self, x):
        self.oneserver = not self.oneserver

    proxy_str = StringProperty('')

    def update_proxy_str(self, proxy: dict):
        mode = proxy.get('mode')
        host = proxy.get('host')
        port = proxy.get('port')
        self.proxy_str = (host + ':' + port) if mode else _('None')

    def choose_server_dialog(self, popup):
        from .uix.dialogs.choice_dialog import ChoiceDialog
        protocol = 's'

        def cb2(host):
            from electrum import constants
            pp = servers.get(host, constants.net.DEFAULT_PORTS)
            port = pp.get(protocol, '')
            popup.ids.host.text = host
            popup.ids.port.text = port

        servers = self.network.get_servers()
        ChoiceDialog(_('Choose a server'), sorted(servers),
                     popup.ids.host.text, cb2).open()

    def choose_blockchain_dialog(self, dt):
        from .uix.dialogs.choice_dialog import ChoiceDialog
        chains = self.network.get_blockchains()

        def cb(name):
            with blockchain.blockchains_lock:
                blockchain_items = list(blockchain.blockchains.items())
            for chain_id, b in blockchain_items:
                if name == b.get_name():
                    self.network.run_from_another_thread(
                        self.network.follow_chain_given_id(chain_id))

        chain_objects = [
            blockchain.blockchains.get(chain_id) for chain_id in chains
        ]
        chain_objects = filter(lambda b: b is not None, chain_objects)
        names = [b.get_name() for b in chain_objects]
        if len(names) > 1:
            cur_chain = self.network.blockchain().get_name()
            ChoiceDialog(_('Choose your chain'), names, cur_chain, cb).open()

    use_rbf = BooleanProperty(False)

    def on_use_rbf(self, instance, x):
        self.electrum_config.set_key('use_rbf', self.use_rbf, True)

    use_change = BooleanProperty(False)

    def on_use_change(self, instance, x):
        if self.wallet:
            self.wallet.use_change = self.use_change
            self.wallet.storage.put('use_change', self.use_change)
            self.wallet.storage.write()

    use_unconfirmed = BooleanProperty(False)

    def on_use_unconfirmed(self, instance, x):
        self.electrum_config.set_key('confirmed_only',
                                     not self.use_unconfirmed, True)

    def set_URI(self, uri):
        self.switch_to('send')
        self.send_screen.set_URI(uri)

    def set_ln_invoice(self, invoice):
        self.switch_to('send')
        self.send_screen.set_ln_invoice(invoice)

    def on_new_intent(self, intent):
        data = intent.getDataString()
        if intent.getScheme() == 'bitcoin':
            self.set_URI(data)
        elif intent.getScheme() == 'lightning':
            self.set_ln_invoice(data)

    def on_language(self, instance, language):
        Logger.info('language: {}'.format(language))
        _.switch_lang(language)

    def update_history(self, *dt):
        if self.history_screen:
            self.history_screen.update()

    def on_quotes(self, d):
        Logger.info("on_quotes")
        self._trigger_update_status()
        self._trigger_update_history()

    def on_history(self, d):
        Logger.info("on_history")
        if self.wallet:
            self.wallet.clear_coin_price_cache()
        self._trigger_update_history()

    def on_fee_histogram(self, *args):
        self._trigger_update_history()

    def on_payment_received(self, event, wallet, key, status):
        if self.request_popup and self.request_popup.key == key:
            self.request_popup.set_status(status)
        if status == PR_PAID:
            self.show_info(_('Payment Received') + '\n' + key)

    def on_payment_status(self, event, key, status, *args):
        self.update_tab('send')
        if status == 'success':
            self.show_info(_('Payment was sent'))
            self._trigger_update_history()
        elif status == 'progress':
            pass
        elif status == 'failure':
            self.show_info(_('Payment failed'))
        elif status == 'error':
            e = args[0]
            self.show_error(_('Error') + '\n' + str(e))

    def _get_bu(self):
        decimal_point = self.electrum_config.get('decimal_point',
                                                 DECIMAL_POINT_DEFAULT)
        try:
            return decimal_point_to_base_unit_name(decimal_point)
        except UnknownBaseUnit:
            return decimal_point_to_base_unit_name(DECIMAL_POINT_DEFAULT)

    def _set_bu(self, value):
        assert value in base_units.keys()
        decimal_point = base_unit_name_to_decimal_point(value)
        self.electrum_config.set_key('decimal_point', decimal_point, True)
        self._trigger_update_status()
        self._trigger_update_history()

    wallet_name = StringProperty(_('No Wallet'))
    base_unit = AliasProperty(_get_bu, _set_bu)
    fiat_unit = StringProperty('')

    def on_fiat_unit(self, a, b):
        self._trigger_update_history()

    def decimal_point(self):
        return base_units[self.base_unit]

    def btc_to_fiat(self, amount_str):
        if not amount_str:
            return ''
        if not self.fx.is_enabled():
            return ''
        rate = self.fx.exchange_rate()
        if rate.is_nan():
            return ''
        fiat_amount = self.get_amount(amount_str + ' ' +
                                      self.base_unit) * rate / pow(10, 8)
        return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.')

    def fiat_to_btc(self, fiat_amount):
        if not fiat_amount:
            return ''
        rate = self.fx.exchange_rate()
        if rate.is_nan():
            return ''
        satoshis = int(pow(10, 8) * Decimal(fiat_amount) / Decimal(rate))
        return format_satoshis_plain(satoshis, self.decimal_point())

    def get_amount(self, amount_str):
        a, u = amount_str.split()
        assert u == self.base_unit
        try:
            x = Decimal(a)
        except:
            return None
        p = pow(10, self.decimal_point())
        return int(p * x)

    _orientation = OptionProperty('landscape',
                                  options=('landscape', 'portrait'))

    def _get_orientation(self):
        return self._orientation

    orientation = AliasProperty(_get_orientation,
                                None,
                                bind=('_orientation', ))
    '''Tries to ascertain the kind of device the app is running on.
    Cane be one of `tablet` or `phone`.

    :data:`orientation` is a read only `AliasProperty` Defaults to 'landscape'
    '''

    _ui_mode = OptionProperty('phone', options=('tablet', 'phone'))

    def _get_ui_mode(self):
        return self._ui_mode

    ui_mode = AliasProperty(_get_ui_mode, None, bind=('_ui_mode', ))
    '''Defines tries to ascertain the kind of device the app is running on.
    Cane be one of `tablet` or `phone`.

    :data:`ui_mode` is a read only `AliasProperty` Defaults to 'phone'
    '''

    def __init__(self, **kwargs):
        # initialize variables
        self._clipboard = Clipboard
        self.info_bubble = None
        self.nfcscanner = None
        self.tabs = None
        self.is_exit = False
        self.wallet = None  # type: Optional[Abstract_Wallet]
        self.pause_time = 0
        self.asyncio_loop = asyncio.get_event_loop()

        App.__init__(self)  #, **kwargs)

        self.electrum_config = config = kwargs.get('config',
                                                   None)  # type: SimpleConfig
        self.language = config.get('language', 'en')
        self.network = network = kwargs.get('network', None)  # type: Network
        if self.network:
            self.num_blocks = self.network.get_local_height()
            self.num_nodes = len(self.network.get_interfaces())
            net_params = self.network.get_parameters()
            self.server_host = net_params.host
            self.server_port = net_params.port
            self.auto_connect = net_params.auto_connect
            self.oneserver = net_params.oneserver
            self.proxy_config = net_params.proxy if net_params.proxy else {}
            self.update_proxy_str(self.proxy_config)

        self.plugins = kwargs.get('plugins', None)  # type: Plugins
        self.gui_object = kwargs.get('gui_object', None)  # type: ElectrumGui
        self.daemon = self.gui_object.daemon
        self.fx = self.daemon.fx

        self.is_lightning_enabled = bool(config.get('lightning'))

        self.use_rbf = config.get('use_rbf', True)
        self.use_unconfirmed = not config.get('confirmed_only', False)

        # create triggers so as to minimize updating a max of 2 times a sec
        self._trigger_update_wallet = Clock.create_trigger(
            self.update_wallet, .5)
        self._trigger_update_status = Clock.create_trigger(
            self.update_status, .5)
        self._trigger_update_history = Clock.create_trigger(
            self.update_history, .5)
        self._trigger_update_interfaces = Clock.create_trigger(
            self.update_interfaces, .5)

        self._periodic_update_status_during_sync = Clock.schedule_interval(
            self.update_wallet_synchronizing_progress, .5)

        # cached dialogs
        self._settings_dialog = None
        self._password_dialog = None
        self._channels_dialog = None
        self._addresses_dialog = None
        self.fee_status = self.electrum_config.get_fee_status()
        self.request_popup = None

    def on_pr(self, pr):
        if not self.wallet:
            self.show_error(_('No wallet loaded.'))
            return
        if pr.verify(self.wallet.contacts):
            key = pr.get_id()
            invoice = self.wallet.get_invoice(key)
            if invoice and invoice['status'] == PR_PAID:
                self.show_error("invoice already paid")
                self.send_screen.do_clear()
            elif pr.has_expired():
                self.show_error(_('Payment request has expired'))
            else:
                self.switch_to('send')
                self.send_screen.set_request(pr)
        else:
            self.show_error("invoice error:" + pr.error)
            self.send_screen.do_clear()

    def on_qr(self, data):
        from electrum.bitcoin import base_decode, is_address
        data = data.strip()
        if is_address(data):
            self.set_URI(data)
            return
        if data.startswith('bitcoin:'):
            self.set_URI(data)
            return
        if data.startswith('ln'):
            self.set_ln_invoice(data)
            return
        # try to decode transaction
        from electrum.transaction import Transaction
        from electrum.util import bh2u
        try:
            text = bh2u(base_decode(data, None, base=43))
            tx = Transaction(text)
            tx.deserialize()
        except:
            tx = None
        if tx:
            self.tx_dialog(tx)
            return
        # show error
        self.show_error("Unable to decode QR data")

    def update_tab(self, name):
        s = getattr(self, name + '_screen', None)
        if s:
            s.update()

    @profiler
    def update_tabs(self):
        for tab in ['invoices', 'send', 'history', 'receive', 'address']:
            self.update_tab(tab)

    def switch_to(self, name):
        s = getattr(self, name + '_screen', None)
        if s is None:
            s = self.tabs.ids[name + '_screen']
            s.load_screen()
        panel = self.tabs.ids.panel
        tab = self.tabs.ids[name + '_tab']
        panel.switch_to(tab)

    def show_request(self, is_lightning, key):
        from .uix.dialogs.request_dialog import RequestDialog
        request = self.wallet.get_request(key)
        status = request['status']
        data = request['invoice'] if is_lightning else request['URI']
        self.request_popup = RequestDialog('Request', data, key)
        self.request_popup.set_status(request['status'])
        self.request_popup.open()

    def show_invoice(self, is_lightning, key):
        from .uix.dialogs.invoice_dialog import InvoiceDialog
        invoice = self.wallet.get_invoice(key)
        if not invoice:
            return
        status = invoice['status']
        data = invoice['invoice'] if is_lightning else key
        self.invoice_popup = InvoiceDialog('Invoice', data, key)
        self.invoice_popup.open()

    def qr_dialog(self, title, data, show_text=False, text_for_clipboard=None):
        from .uix.dialogs.qr_dialog import QRDialog

        def on_qr_failure():
            popup.dismiss()
            msg = _('Failed to display QR code.')
            if text_for_clipboard:
                msg += '\n' + _('Text copied to clipboard.')
                self._clipboard.copy(text_for_clipboard)
            Clock.schedule_once(lambda dt: self.show_info(msg))

        popup = QRDialog(title,
                         data,
                         show_text,
                         failure_cb=on_qr_failure,
                         text_for_clipboard=text_for_clipboard)
        popup.open()

    def scan_qr(self, on_complete):
        if platform != 'android':
            return
        from jnius import autoclass, cast
        from android import activity
        PythonActivity = autoclass('org.kivy.android.PythonActivity')
        SimpleScannerActivity = autoclass(
            "org.electrum.qr.SimpleScannerActivity")
        Intent = autoclass('android.content.Intent')
        intent = Intent(PythonActivity.mActivity, SimpleScannerActivity)

        def on_qr_result(requestCode, resultCode, intent):
            try:
                if resultCode == -1:  # RESULT_OK:
                    #  this doesn't work due to some bug in jnius:
                    # contents = intent.getStringExtra("text")
                    String = autoclass("java.lang.String")
                    contents = intent.getStringExtra(String("text"))
                    on_complete(contents)
            except Exception as e:  # exc would otherwise get lost
                send_exception_to_crash_reporter(e)
            finally:
                activity.unbind(on_activity_result=on_qr_result)

        activity.bind(on_activity_result=on_qr_result)
        PythonActivity.mActivity.startActivityForResult(intent, 0)

    def do_share(self, data, title):
        if platform != 'android':
            return
        from jnius import autoclass, cast
        JS = autoclass('java.lang.String')
        Intent = autoclass('android.content.Intent')
        sendIntent = Intent()
        sendIntent.setAction(Intent.ACTION_SEND)
        sendIntent.setType("text/plain")
        sendIntent.putExtra(Intent.EXTRA_TEXT, JS(data))
        PythonActivity = autoclass('org.kivy.android.PythonActivity')
        currentActivity = cast('android.app.Activity',
                               PythonActivity.mActivity)
        it = Intent.createChooser(sendIntent,
                                  cast('java.lang.CharSequence', JS(title)))
        currentActivity.startActivity(it)

    def build(self):
        return Builder.load_file('electrum/gui/kivy/main.kv')

    def _pause(self):
        if platform == 'android':
            # move activity to back
            from jnius import autoclass
            python_act = autoclass('org.kivy.android.PythonActivity')
            mActivity = python_act.mActivity
            mActivity.moveTaskToBack(True)

    def handle_crash_on_startup(func):
        def wrapper(self, *args, **kwargs):
            try:
                return func(self, *args, **kwargs)
            except Exception as e:
                from .uix.dialogs.crash_reporter import CrashReporter
                # show the crash reporter, and when it's closed, shutdown the app
                cr = CrashReporter(self,
                                   exctype=type(e),
                                   value=e,
                                   tb=e.__traceback__)
                cr.on_dismiss = lambda: self.stop()
                Clock.schedule_once(lambda _, cr=cr: cr.open(), 0)

        return wrapper

    @handle_crash_on_startup
    def on_start(self):
        ''' This is the start point of the kivy ui
        '''
        import time
        Logger.info('Time to on_start: {} <<<<<<<<'.format(
            time.process_time()))
        Window.bind(size=self.on_size, on_keyboard=self.on_keyboard)
        Window.bind(on_key_down=self.on_key_down)
        #Window.softinput_mode = 'below_target'
        self.on_size(Window, Window.size)
        self.init_ui()
        crash_reporter.ExceptionHook(self)
        # init plugins
        run_hook('init_kivy', self)
        # fiat currency
        self.fiat_unit = self.fx.ccy if self.fx.is_enabled() else ''
        # default tab
        self.switch_to('history')
        # bind intent for bitcoin: URI scheme
        if platform == 'android':
            from android import activity
            from jnius import autoclass
            PythonActivity = autoclass('org.kivy.android.PythonActivity')
            mactivity = PythonActivity.mActivity
            self.on_new_intent(mactivity.getIntent())
            activity.bind(on_new_intent=self.on_new_intent)
        # connect callbacks
        if self.network:
            interests = [
                'wallet_updated', 'network_updated', 'blockchain_updated',
                'status', 'new_transaction', 'verified'
            ]
            self.network.register_callback(self.on_network_event, interests)
            self.network.register_callback(self.on_fee, ['fee'])
            self.network.register_callback(self.on_fee_histogram,
                                           ['fee_histogram'])
            self.network.register_callback(self.on_quotes, ['on_quotes'])
            self.network.register_callback(self.on_history, ['on_history'])
            self.network.register_callback(self.on_payment_received,
                                           ['payment_received'])
            self.network.register_callback(self.on_channels, ['channels'])
            self.network.register_callback(self.on_channel, ['channel'])
            self.network.register_callback(self.on_payment_status,
                                           ['payment_status'])
        # load wallet
        self.load_wallet_by_name(
            self.electrum_config.get_wallet_path(use_gui_last_wallet=True))
        # URI passed in config
        uri = self.electrum_config.get('url')
        if uri:
            self.set_URI(uri)

    def get_wallet_path(self):
        if self.wallet:
            return self.wallet.storage.path
        else:
            return ''

    def on_wizard_complete(self, wizard, storage):
        if storage:
            wallet = Wallet(storage, config=self.electrum_config)
            wallet.start_network(self.daemon.network)
            self.daemon.add_wallet(wallet)
            self.load_wallet(wallet)
        elif not self.wallet:
            # wizard did not return a wallet; and there is no wallet open atm
            # try to open last saved wallet (potentially start wizard again)
            self.load_wallet_by_name(
                self.electrum_config.get_wallet_path(use_gui_last_wallet=True),
                ask_if_wizard=True)

    def load_wallet_by_name(self, path, ask_if_wizard=False):
        if not path:
            return
        if self.wallet and self.wallet.storage.path == path:
            return
        wallet = self.daemon.load_wallet(path, None)
        if wallet:
            if platform == 'android' and wallet.has_password():
                self.password_dialog(wallet, _('Enter PIN code'),
                                     lambda x: self.load_wallet(wallet),
                                     self.stop)
            else:
                self.load_wallet(wallet)
        else:

            def launch_wizard():
                wizard = Factory.InstallWizard(self.electrum_config,
                                               self.plugins)
                wizard.path = path
                wizard.bind(on_wizard_complete=self.on_wizard_complete)
                storage = WalletStorage(path, manual_upgrades=True)
                if not storage.file_exists():
                    wizard.run('new')
                elif storage.is_encrypted():
                    raise Exception(
                        "Kivy GUI does not support encrypted wallet files.")
                elif storage.requires_upgrade():
                    wizard.upgrade_storage(storage)
                else:
                    raise Exception("unexpected storage file situation")

            if not ask_if_wizard:
                launch_wizard()
            else:
                from .uix.dialogs.question import Question

                def handle_answer(b: bool):
                    if b:
                        launch_wizard()
                    else:
                        try:
                            os.unlink(path)
                        except FileNotFoundError:
                            pass
                        self.stop()

                d = Question(_('Do you want to launch the wizard again?'),
                             handle_answer)
                d.open()

    def on_stop(self):
        Logger.info('on_stop')
        if self.wallet:
            self.electrum_config.save_last_wallet(self.wallet)
        self.stop_wallet()

    def stop_wallet(self):
        if self.wallet:
            self.daemon.stop_wallet(self.wallet.storage.path)
            self.wallet = None

    def on_key_down(self, instance, key, keycode, codepoint, modifiers):
        if 'ctrl' in modifiers:
            # q=24 w=25
            if keycode in (24, 25):
                self.stop()
            elif keycode == 27:
                # r=27
                # force update wallet
                self.update_wallet()
            elif keycode == 112:
                # pageup
                #TODO move to next tab
                pass
            elif keycode == 117:
                # pagedown
                #TODO move to prev tab
                pass
        #TODO: alt+tab_number to activate the particular tab

    def on_keyboard(self, instance, key, keycode, codepoint, modifiers):
        if key == 27 and self.is_exit is False:
            self.is_exit = True
            self.show_info(_('Press again to exit'))
            return True
        # override settings button
        if key in (319, 282):  #f1/settings button on android
            #self.gui.main_gui.toggle_settings(self)
            return True

    def settings_dialog(self):
        from .uix.dialogs.settings import SettingsDialog
        if self._settings_dialog is None:
            self._settings_dialog = SettingsDialog(self)
        self._settings_dialog.update()
        self._settings_dialog.open()

    def lightning_open_channel_dialog(self):
        d = LightningOpenChannelDialog(self)
        d.open()

    def lightning_channels_dialog(self):
        if self._channels_dialog is None:
            self._channels_dialog = LightningChannelsDialog(self)
        self._channels_dialog.open()

    def on_channel(self, evt, chan):
        if self._channels_dialog:
            Clock.schedule_once(lambda dt: self._channels_dialog.update())

    def on_channels(self, evt):
        if self._channels_dialog:
            Clock.schedule_once(lambda dt: self._channels_dialog.update())

    def popup_dialog(self, name):
        if name == 'settings':
            self.settings_dialog()
        elif name == 'wallets':
            from .uix.dialogs.wallets import WalletDialog
            d = WalletDialog()
            d.open()
        elif name == 'status':
            popup = Builder.load_file('electrum/gui/kivy/uix/ui_screens/' +
                                      name + '.kv')
            master_public_keys_layout = popup.ids.master_public_keys
            for xpub in self.wallet.get_master_public_keys()[1:]:
                master_public_keys_layout.add_widget(
                    TopLabel(text=_('Master Public Key')))
                ref = RefLabel()
                ref.name = _('Master Public Key')
                ref.data = xpub
                master_public_keys_layout.add_widget(ref)
            popup.open()
        elif name.endswith("_dialog"):
            getattr(self, name)()
        else:
            popup = Builder.load_file('electrum/gui/kivy/uix/ui_screens/' +
                                      name + '.kv')
            popup.open()

    @profiler
    def init_ui(self):
        ''' Initialize The Ux part of electrum. This function performs the basic
        tasks of setting up the ui.
        '''
        #from weakref import ref

        self.funds_error = False
        # setup UX
        self.screens = {}

        #setup lazy imports for mainscreen
        Factory.register('AnimatedPopup',
                         module='electrum.gui.kivy.uix.dialogs')
        Factory.register('QRCodeWidget',
                         module='electrum.gui.kivy.uix.qrcodewidget')

        # preload widgets. Remove this if you want to load the widgets on demand
        #Cache.append('electrum_widgets', 'AnimatedPopup', Factory.AnimatedPopup())
        #Cache.append('electrum_widgets', 'QRCodeWidget', Factory.QRCodeWidget())

        # load and focus the ui
        self.root.manager = self.root.ids['manager']

        self.history_screen = None
        self.contacts_screen = None
        self.send_screen = None
        self.invoices_screen = None
        self.receive_screen = None
        self.requests_screen = None
        self.address_screen = None
        self.icon = "electrum/gui/icons/electrum.png"
        self.tabs = self.root.ids['tabs']

    def update_interfaces(self, dt):
        net_params = self.network.get_parameters()
        self.num_nodes = len(self.network.get_interfaces())
        self.num_chains = len(self.network.get_blockchains())
        chain = self.network.blockchain()
        self.blockchain_forkpoint = chain.get_max_forkpoint()
        self.blockchain_name = chain.get_name()
        interface = self.network.interface
        if interface:
            self.server_host = interface.host
        else:
            self.server_host = str(net_params.host) + ' (connecting...)'
        self.proxy_config = net_params.proxy or {}
        self.update_proxy_str(self.proxy_config)

    def on_network_event(self, event, *args):
        Logger.info('network event: ' + event)
        if event == 'network_updated':
            self._trigger_update_interfaces()
            self._trigger_update_status()
        elif event == 'wallet_updated':
            self._trigger_update_wallet()
            self._trigger_update_status()
        elif event == 'blockchain_updated':
            # to update number of confirmations in history
            self._trigger_update_wallet()
        elif event == 'status':
            self._trigger_update_status()
        elif event == 'new_transaction':
            self._trigger_update_wallet()
        elif event == 'verified':
            self._trigger_update_wallet()

    @profiler
    def load_wallet(self, wallet: 'Abstract_Wallet'):
        if self.wallet:
            self.stop_wallet()
        self.wallet = wallet
        self.wallet_name = wallet.basename()
        self.update_wallet()
        # Once GUI has been initialized check if we want to announce something
        # since the callback has been called before the GUI was initialized
        if self.receive_screen:
            self.receive_screen.clear()
        self.update_tabs()
        run_hook('load_wallet', wallet, self)
        try:
            wallet.try_detecting_internal_addresses_corruption()
        except InternalAddressCorruption as e:
            self.show_error(str(e))
            send_exception_to_crash_reporter(e)
            return
        self.use_change = self.wallet.use_change

    def update_status(self, *dt):
        if not self.wallet:
            return
        if self.network is None or not self.network.is_connected():
            status = _("Offline")
        elif self.network.is_connected():
            self.num_blocks = self.network.get_local_height()
            server_height = self.network.get_server_height()
            server_lag = self.num_blocks - server_height
            if not self.wallet.up_to_date or server_height == 0:
                num_sent, num_answered = self.wallet.get_history_sync_state_details(
                )
                status = ("{} [size=18dp]({}/{})[/size]".format(
                    _("Synchronizing..."), num_answered, num_sent))
            elif server_lag > 1:
                status = _("Server is lagging ({} blocks)").format(server_lag)
            else:
                status = ''
        else:
            status = _("Disconnected")
        if status:
            self.balance = status
            self.fiat_balance = status
        else:
            c, u, x = self.wallet.get_balance()
            text = self.format_amount(c + x + u)
            self.balance = str(
                text.strip()) + ' [size=22dp]%s[/size]' % self.base_unit
            self.fiat_balance = self.fx.format_amount(
                c + u + x) + ' [size=22dp]%s[/size]' % self.fx.ccy

    def update_wallet_synchronizing_progress(self, *dt):
        if not self.wallet:
            return
        if not self.wallet.up_to_date:
            self._trigger_update_status()

    def get_max_amount(self):
        from electrum.transaction import TxOutput
        if run_hook('abort_send', self):
            return ''
        inputs = self.wallet.get_spendable_coins(None)
        if not inputs:
            return ''
        addr = None
        if self.send_screen:
            addr = str(self.send_screen.screen.address)
        if not addr:
            addr = self.wallet.dummy_address()
        outputs = [TxOutput(TYPE_ADDRESS, addr, '!')]
        try:
            tx = self.wallet.make_unsigned_transaction(inputs, outputs)
        except NoDynamicFeeEstimates as e:
            Clock.schedule_once(
                lambda dt, bound_e=e: self.show_error(str(bound_e)))
            return ''
        except NotEnoughFunds:
            return ''
        except InternalAddressCorruption as e:
            self.show_error(str(e))
            send_exception_to_crash_reporter(e)
            return ''
        amount = tx.output_value()
        __, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet,
                                    tx) or (None, 0)
        amount_after_all_fees = amount - x_fee_amount
        return format_satoshis_plain(amount_after_all_fees,
                                     self.decimal_point())

    def format_amount(self, x, is_diff=False, whitespaces=False):
        return format_satoshis(x,
                               0,
                               self.decimal_point(),
                               is_diff=is_diff,
                               whitespaces=whitespaces)

    def format_amount_and_units(self, x):
        return format_satoshis_plain(
            x, self.decimal_point()) + ' ' + self.base_unit

    def format_fee_rate(self, fee_rate):
        # fee_rate is in sat/kB
        return format_fee_satoshis(fee_rate / 1000) + ' sat/byte'

    #@profiler
    def update_wallet(self, *dt):
        self._trigger_update_status()
        if self.wallet and (self.wallet.up_to_date or not self.network
                            or not self.network.is_connected()):
            self.update_tabs()

    def notify(self, message):
        try:
            global notification, os
            if not notification:
                from plyer import notification
            icon = (os.path.dirname(os.path.realpath(__file__)) + '/../../' +
                    self.icon)
            notification.notify('Electrum',
                                message,
                                app_icon=icon,
                                app_name='Electrum')
        except ImportError:
            Logger.Error(
                'Notification: needs plyer; `sudo python3 -m pip install plyer`'
            )

    def on_pause(self):
        self.pause_time = time.time()
        # pause nfc
        if self.nfcscanner:
            self.nfcscanner.nfc_disable()
        return True

    def on_resume(self):
        now = time.time()
        if self.wallet and self.wallet.has_password(
        ) and now - self.pause_time > 60:
            self.password_dialog(self.wallet, _('Enter PIN'), None, self.stop)
        if self.nfcscanner:
            self.nfcscanner.nfc_enable()

    def on_size(self, instance, value):
        width, height = value
        self._orientation = 'landscape' if width > height else 'portrait'
        self._ui_mode = 'tablet' if min(width,
                                        height) > inch(3.51) else 'phone'

    def on_ref_label(self, label):
        if not label.data:
            return
        self.qr_dialog(label.name, label.data, True)

    def show_error(self,
                   error,
                   width='200dp',
                   pos=None,
                   arrow_pos=None,
                   exit=False,
                   icon='atlas://electrum/gui/kivy/theming/light/error',
                   duration=0,
                   modal=False):
        ''' Show an error Message Bubble.
        '''
        self.show_info_bubble(text=error,
                              icon=icon,
                              width=width,
                              pos=pos or Window.center,
                              arrow_pos=arrow_pos,
                              exit=exit,
                              duration=duration,
                              modal=modal)

    def show_info(self,
                  error,
                  width='200dp',
                  pos=None,
                  arrow_pos=None,
                  exit=False,
                  duration=0,
                  modal=False):
        ''' Show an Info Message Bubble.
        '''
        self.show_error(
            error,
            icon='atlas://electrum/gui/kivy/theming/light/important',
            duration=duration,
            modal=modal,
            exit=exit,
            pos=pos,
            arrow_pos=arrow_pos)

    def show_info_bubble(self,
                         text=_('Hello World'),
                         pos=None,
                         duration=0,
                         arrow_pos='bottom_mid',
                         width=None,
                         icon='',
                         modal=False,
                         exit=False):
        '''Method to show an Information Bubble

        .. parameters::
            text: Message to be displayed
            pos: position for the bubble
            duration: duration the bubble remains on screen. 0 = click to hide
            width: width of the Bubble
            arrow_pos: arrow position for the bubble
        '''
        info_bubble = self.info_bubble
        if not info_bubble:
            info_bubble = self.info_bubble = Factory.InfoBubble()

        win = Window
        if info_bubble.parent:
            win.remove_widget(info_bubble if not info_bubble.modal else
                              info_bubble._modal_view)

        if not arrow_pos:
            info_bubble.show_arrow = False
        else:
            info_bubble.show_arrow = True
            info_bubble.arrow_pos = arrow_pos
        img = info_bubble.ids.img
        if text == 'texture':
            # icon holds a texture not a source image
            # display the texture in full screen
            text = ''
            img.texture = icon
            info_bubble.fs = True
            info_bubble.show_arrow = False
            img.allow_stretch = True
            info_bubble.dim_background = True
            info_bubble.background_image = 'atlas://electrum/gui/kivy/theming/light/card'
        else:
            info_bubble.fs = False
            info_bubble.icon = icon
            #if img.texture and img._coreimage:
            #    img.reload()
            img.allow_stretch = False
            info_bubble.dim_background = False
            info_bubble.background_image = 'atlas://data/images/defaulttheme/bubble'
        info_bubble.message = text
        if not pos:
            pos = (win.center[0], win.center[1] - (info_bubble.height / 2))
        info_bubble.show(pos, duration, width, modal=modal, exit=exit)

    def tx_dialog(self, tx):
        from .uix.dialogs.tx_dialog import TxDialog
        d = TxDialog(self, tx)
        d.open()

    def sign_tx(self, *args):
        threading.Thread(target=self._sign_tx, args=args).start()

    def _sign_tx(self, tx, password, on_success, on_failure):
        try:
            self.wallet.sign_transaction(tx, password)
        except InvalidPassword:
            Clock.schedule_once(lambda dt: on_failure(_("Invalid PIN")))
            return
        on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success,
                              on_failure) or on_success
        Clock.schedule_once(lambda dt: on_success(tx))

    def _broadcast_thread(self, tx, on_complete):
        status = False
        try:
            self.network.run_from_another_thread(
                self.network.broadcast_transaction(tx))
        except TxBroadcastError as e:
            msg = e.get_message_for_gui()
        except BestEffortRequestFailed as e:
            msg = repr(e)
        else:
            status, msg = True, tx.txid()
        Clock.schedule_once(lambda dt: on_complete(status, msg))

    def broadcast(self, tx, pr=None):
        def on_complete(ok, msg):
            if ok:
                self.show_info(_('Payment sent.'))
                if self.send_screen:
                    self.send_screen.do_clear()
                if pr:
                    self.wallet.invoices.set_paid(pr, tx.txid())
                    self.wallet.invoices.save()
                    self.update_tab('invoices')
            else:
                msg = msg or ''
                self.show_error(msg)

        if self.network and self.network.is_connected():
            self.show_info(_('Sending'))
            threading.Thread(target=self._broadcast_thread,
                             args=(tx, on_complete)).start()
        else:
            self.show_info(
                _('Cannot broadcast transaction') + ':\n' + _('Not connected'))

    def description_dialog(self, screen):
        from .uix.dialogs.label_dialog import LabelDialog
        text = screen.message

        def callback(text):
            screen.message = text

        d = LabelDialog(_('Enter description'), text, callback)
        d.open()

    def amount_dialog(self, screen, show_max):
        from .uix.dialogs.amount_dialog import AmountDialog
        amount = screen.amount
        if amount:
            amount, u = str(amount).split()
            assert u == self.base_unit

        def cb(amount):
            screen.amount = amount

        popup = AmountDialog(show_max, amount, cb)
        popup.open()

    def addresses_dialog(self):
        from .uix.dialogs.addresses import AddressesDialog
        if self._addresses_dialog is None:
            self._addresses_dialog = AddressesDialog(self)
        self._addresses_dialog.update()
        self._addresses_dialog.open()

    def fee_dialog(self, label, dt):
        from .uix.dialogs.fee_dialog import FeeDialog

        def cb():
            self.fee_status = self.electrum_config.get_fee_status()

        fee_dialog = FeeDialog(self, self.electrum_config, cb)
        fee_dialog.open()

    def on_fee(self, event, *arg):
        self.fee_status = self.electrum_config.get_fee_status()

    def protected(self, msg, f, args):
        if self.wallet.has_password():
            on_success = lambda pw: f(*(args + (pw, )))
            self.password_dialog(self.wallet, msg, on_success, lambda: None)
        else:
            f(*(args + (None, )))

    def delete_wallet(self):
        from .uix.dialogs.question import Question
        basename = os.path.basename(self.wallet.storage.path)
        d = Question(
            _('Delete wallet?') + '\n' + basename, self._delete_wallet)
        d.open()

    def _delete_wallet(self, b):
        if b:
            basename = self.wallet.basename()
            self.protected(
                _("Enter your PIN code to confirm deletion of {}").format(
                    basename), self.__delete_wallet, ())

    def __delete_wallet(self, pw):
        wallet_path = self.get_wallet_path()
        basename = os.path.basename(wallet_path)
        if self.wallet.has_password():
            try:
                self.wallet.check_password(pw)
            except:
                self.show_error("Invalid PIN")
                return
        self.stop_wallet()
        os.unlink(wallet_path)
        self.show_error(_("Wallet removed: {}").format(basename))
        new_path = self.electrum_config.get_wallet_path(
            use_gui_last_wallet=True)
        self.load_wallet_by_name(new_path)

    def show_seed(self, label):
        self.protected(_("Enter your PIN code in order to decrypt your seed"),
                       self._show_seed, (label, ))

    def _show_seed(self, label, password):
        if self.wallet.has_password() and password is None:
            return
        keystore = self.wallet.keystore
        try:
            seed = keystore.get_seed(password)
            passphrase = keystore.get_passphrase(password)
        except:
            self.show_error("Invalid PIN")
            return
        label.data = seed
        if passphrase:
            label.data += '\n\n' + _('Passphrase') + ': ' + passphrase

    def password_dialog(self, wallet, msg, on_success, on_failure):
        from .uix.dialogs.password_dialog import PasswordDialog
        if self._password_dialog is None:
            self._password_dialog = PasswordDialog()
        self._password_dialog.init(self, wallet, msg, on_success, on_failure)
        self._password_dialog.open()

    def change_password(self, cb):
        from .uix.dialogs.password_dialog import PasswordDialog
        if self._password_dialog is None:
            self._password_dialog = PasswordDialog()
        message = _("Changing PIN code.") + '\n' + _("Enter your current PIN:")

        def on_success(old_password, new_password):
            self.wallet.update_password(old_password, new_password)
            self.show_info(_("Your PIN code was updated"))

        on_failure = lambda: self.show_error(_("PIN codes do not match"))
        self._password_dialog.init(self,
                                   self.wallet,
                                   message,
                                   on_success,
                                   on_failure,
                                   is_change=1)
        self._password_dialog.open()

    def export_private_keys(self, pk_label, addr):
        if self.wallet.is_watching_only():
            self.show_info(
                _('This is a watching-only wallet. It does not contain private keys.'
                  ))
            return

        def show_private_key(addr, pk_label, password):
            if self.wallet.has_password() and password is None:
                return
            if not self.wallet.can_export():
                return
            try:
                key = str(self.wallet.export_private_key(addr, password)[0])
                pk_label.data = key
            except InvalidPassword:
                self.show_error("Invalid PIN")
                return

        self.protected(
            _("Enter your PIN code in order to decrypt your private key"),
            show_private_key, (addr, pk_label))
Esempio n. 19
0
class DeckBuilderScrollBar(FloatLayout):
    """A widget that looks a lot like one of the scrollbars on the sides
    of eg. :class:`kivy.uix.ScrollView`, which moves a single deck
    within a :class:`DeckBuilderLayout`.

    """
    orientation = OptionProperty('vertical',
                                 options=['horizontal', 'vertical'])
    """Which way to scroll? Options are 'horizontal' and 'vertical'."""
    deckbuilder = ObjectProperty()
    """The :class:`DeckBuilderLayout` of the deck to scroll."""
    deckidx = NumericProperty(0)
    """The index of the deck to scroll, within its
    :class:`DeckBuilderLayout`'s ``decks`` property.

    """
    scrolling = BooleanProperty(False)
    """Has the user grabbed me?"""
    scroll_min = NumericProperty(-1)
    """How far left (if horizontal) or down (if vertical) I can move my
    deck, expressed as a proportion of the
    :class:`DeckBuilderLayout`'s width or height, respectively.

    """
    scroll_max = NumericProperty(1)
    """How far right (if horizontal) or up (if vertical) I can move my
    deck, expressed as a proportion of the
    :class:`DeckBuilderLayout`'s width or height, respectively.

    """

    scroll_hint = AliasProperty(
        lambda self: abs(self.scroll_max - self.scroll_min),
        lambda self, v: None,
        bind=('scroll_min', 'scroll_max'))
    """The distance between ``scroll_max`` and ``scroll_min``."""
    _scroll = NumericProperty(0)
    """Private. The current adjustment to the deck's ``pos_hint_x`` or
    ``pos_hint_y``.

    """
    def _get_scroll(self):
        zero = self._scroll - self.scroll_min
        return zero / self.scroll_hint

    def _set_scroll(self, v):
        if v < 0:
            v = 0
        if v > 1:
            v = 1
        normal = v * self.scroll_hint
        self._scroll = self.scroll_min + normal

    scroll = AliasProperty(_get_scroll,
                           _set_scroll,
                           bind=('_scroll', 'scroll_min', 'scroll_max'))
    """A number between 0 and 1 representing how far beyond ``scroll_min``
    toward ``scroll_max`` I am presently scrolled.

    """

    def _get_vbar(self):
        if self.deckbuilder is None:
            return (0, 1)
        vh = self.deckbuilder.height * (self.scroll_hint + 1)
        h = self.height
        if vh < h or vh == 0:
            return (0, 1)
        ph = max(0.01, h / vh)
        sy = min(1.0, max(0.0, self.scroll))
        py = (1 - ph) * sy
        return (py, ph)

    vbar = AliasProperty(_get_vbar,
                         None,
                         bind=('_scroll', 'scroll_min', 'scroll_max'))
    """A tuple of ``(y, height)`` for my scroll bar, if it's vertical."""

    def _get_hbar(self):
        if self.deckbuilder is None:
            return (0, 1)
        vw = self.deckbuilder.width * self.scroll_hint
        w = self.width
        if vw < w or vw == 0:
            return (0, 1)
        pw = max(0.01, w / vw)
        sx = min(1.0, max(0.0, self.scroll))
        px = (1 - pw) * sx
        return (px, pw)

    hbar = AliasProperty(_get_hbar,
                         None,
                         bind=('_scroll', 'scroll_min', 'scroll_max'))
    """A tuple of ``(x, width)`` for my scroll bar, if it's horizontal."""
    bar_color = ListProperty([.7, .7, .7, .9])
    """Color to use for the scroll bar when scrolling. RGBA format."""
    bar_inactive_color = ListProperty([.7, .7, .7, .2])
    """Color to use for the scroll bar when not scrolling. RGBA format."""
    bar_texture = ObjectProperty(None, allownone=True)
    """Texture for the scroll bar, normally ``None``."""

    def __init__(self, **kwargs):
        """Arrange to be laid out whenever I'm scrolled or the range of my
        scrolling changes.

        """
        super().__init__(**kwargs)
        self.bind(_scroll=self._trigger_layout,
                  scroll_min=self._trigger_layout,
                  scroll_max=self._trigger_layout)

    def do_layout(self, *args):
        """Put the bar where it's supposed to be, and size it in proportion to
        the size of the scrollable area.

        """
        if 'bar' not in self.ids:
            Clock.schedule_once(self.do_layout)
            return
        if self.orientation == 'horizontal':
            self.ids.bar.size_hint_x = self.hbar[1]
            self.ids.bar.pos_hint = {'x': self.hbar[0], 'y': 0}
        else:
            self.ids.bar.size_hint_y = self.vbar[1]
            self.ids.bar.pos_hint = {'x': 0, 'y': self.vbar[0]}
        super().do_layout(*args)

    def upd_scroll(self, *args):
        """Update my own ``scroll`` property to where my deck is actually
        scrolled.

        """
        att = 'deck_{}_hint_offsets'.format('x' if self.orientation ==
                                            'horizontal' else 'y')
        self._scroll = getattr(self.deckbuilder, att)[self.deckidx]

    def on_deckbuilder(self, *args):
        """Bind my deckbuilder to update my ``scroll``, and my ``scroll`` to
        update my deckbuilder.

        """
        if self.deckbuilder is None:
            return
        att = 'deck_{}_hint_offsets'.format('x' if self.orientation ==
                                            'horizontal' else 'y')
        offs = getattr(self.deckbuilder, att)
        if len(offs) <= self.deckidx:
            Clock.schedule_once(self.on_deckbuilder, 0)
            return
        self.bind(scroll=self.handle_scroll)
        self.deckbuilder.bind(**{att: self.upd_scroll})
        self.upd_scroll()
        self.deckbuilder._trigger_layout()

    def handle_scroll(self, *args):
        """When my ``scroll`` changes, tell my deckbuilder how it's scrolled
        now.

        """
        if 'bar' not in self.ids:
            Clock.schedule_once(self.handle_scroll, 0)
            return
        att = 'deck_{}_hint_offsets'.format('x' if self.orientation ==
                                            'horizontal' else 'y')
        offs = list(getattr(self.deckbuilder, att))
        if len(offs) <= self.deckidx:
            Clock.schedule_once(self.on_scroll, 0)
            return
        offs[self.deckidx] = self._scroll
        setattr(self.deckbuilder, att, offs)
        self.deckbuilder._trigger_layout()

    def bar_touched(self, bar, touch):
        """Start scrolling, and record where I started scrolling."""
        self.scrolling = True
        self._start_bar_pos_hint = get_pos_hint(bar.pos_hint, *bar.size_hint)
        self._start_touch_pos_hint = (touch.x / self.width,
                                      touch.y / self.height)
        self._start_bar_touch_hint = (self._start_touch_pos_hint[0] -
                                      self._start_bar_pos_hint[0],
                                      self._start_touch_pos_hint[1] -
                                      self._start_bar_pos_hint[1])
        touch.grab(self)

    def on_touch_move(self, touch):
        """Move the scrollbar to the touch, and update my ``scroll``
        accordingly.

        """
        if not self.scrolling or 'bar' not in self.ids:
            touch.ungrab(self)
            return
        touch.push()
        touch.apply_transform_2d(self.parent.to_local)
        touch.apply_transform_2d(self.to_local)
        if self.orientation == 'horizontal':
            hint_right_of_bar = (touch.x - self.ids.bar.x) / self.width
            hint_correction = hint_right_of_bar - self._start_bar_touch_hint[0]
            self.scroll += hint_correction
        else:  # self.orientation == 'vertical'
            hint_above_bar = (touch.y - self.ids.bar.y) / self.height
            hint_correction = hint_above_bar - self._start_bar_touch_hint[1]
            self.scroll += hint_correction
        touch.pop()

    def on_touch_up(self, touch):
        """Stop scrolling."""
        self.scrolling = False
Esempio n. 20
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'),
                              cache=True)
    '''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,
                         bind=('scroll_y', '_viewport', 'viewport_size',
                               'height'),
                         cache=True)
    '''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,
                         bind=('scroll_x', '_viewport', 'viewport_size',
                               'width'),
                         cache=True)
    '''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'].

    +---------------------+------------------------------------------------+
    | ['content']         | Content is scrolled by dragging or swiping the |
    |                     | content directly.                              |
    +---------------------+------------------------------------------------+
    | ['bars']            | Content is scrolled by dragging or swiping the |
    |                     | scoll bars.                                    |
    +---------------------+------------------------------------------------+
    | ['bars', 'content'] | Content is scrolled by either of the above     |
    |                     | methods.                                       |
    +---------------------+------------------------------------------------+

    .. versionadded:: 1.8.0

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

    smooth_scroll_end = NumericProperty(None, allownone=True)
    '''Whether smooth scroll end should be used when scrolling with the
    mouse-wheel and the factor of transforming the scroll distance to
    velocity. This option also enables velocity addition meaning if you
    scroll more, you will scroll faster and further. The recommended value
    is `10`. The velocity is calculated as :attr:`scroll_wheel_distance` *
    :attr:`smooth_scroll_end`.

    .. versionadded:: 1.11.0

    :attr:`smooth_scroll_end` is a :class:`~kivy.properties.NumericProperty`
    and defaults to None.
    '''

    # 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)
        fbind('scroll_y', self._update_effect_bounds)
        fbind('scroll_x', self._update_effect_bounds)

        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 bar_y(vertical)
        width_scrollable = vp.width > self.width
        height_scrollable = vp.height > self.height

        d = {
            'bottom': touch.y - self.y - self.bar_margin,
            'top': self.top - touch.y - self.bar_margin,
            'left': touch.x - self.x - self.bar_margin,
            'right': self.right - touch.x - self.bar_margin
        }

        ud['in_bar_x'] = (scroll_bar and width_scrollable
                          and (0 <= d[self.bar_pos_x] <= self.bar_width))
        ud['in_bar_y'] = (scroll_bar and height_scrollable
                          and (0 <= d[self.bar_pos_y] <= self.bar_width))

        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'):
                    if self.smooth_scroll_end:
                        e.velocity -= m * self.smooth_scroll_end
                    else:
                        e.value = max(e.value - m, e.min)
                        e.velocity = 0
                elif btn in ('scrollup', 'scrollright'):
                    if self.smooth_scroll_end:
                        e.velocity += m * self.smooth_scroll_end
                    else:
                        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:
            # don't pass on touch to children if outside the sv
            if self.collide_point(*touch.pos):
                # 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 not any(
                isinstance(key, str) and key.startswith('sv.')
                for key in touch.ud):
            # don't pass on touch to children if outside the sv
            if self.collide_point(*touch.pos):
                # touch is in window coordinates
                touch.push()
                touch.apply_transform_2d(self.to_local)
                res = super(ScrollView, self).on_touch_move(touch)
                touch.pop()
                return res
            return False

        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 uid not in touch.ud:
            self._touch = False
            return self.on_scroll_start(touch, False)
        ud = touch.ud[uid]

        # check if the minimum distance has been travelled
        if ud['mode'] == 'unknown':
            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
            ud['dx'] += abs(touch.dx)
            ud['dy'] += abs(touch.dy)
            if ((ud['dx'] > self.scroll_distance and self.do_scroll_x)
                    or (ud['dy'] > self.scroll_distance and self.do_scroll_y)):
                ud['mode'] = 'scroll'

        if ud['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
            ud['dt'] = touch.time_update - ud['time']
            ud['time'] = touch.time_update
            ud['user_stopped'] = True
        return rv

    def on_touch_up(self, touch):
        uid = self._get_uid('svavoid')
        if self._touch is not touch and uid not in touch.ud:
            # don't pass on touch to children if outside the sv
            if self.collide_point(*touch.pos):
                # 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 _viewport is layout and has pending operation, reschedule
        if hasattr(self._viewport, 'do_layout'):
            if self._viewport._trigger_layout.is_triggered:
                Clock.schedule_once(
                    lambda *dt: self.scroll_to(widget, padding, animate))
                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:
            w = vp.size_hint_x * self.width
            if vp.size_hint_min_x is not None:
                w = max(w, vp.size_hint_min_x)
            if vp.size_hint_max_x is not None:
                w = min(w, vp.size_hint_max_x)
            vp.width = w
        if vp.size_hint_y is not None:
            h = vp.size_hint_y * self.height
            if vp.size_hint_min_y is not None:
                h = max(h, vp.size_hint_min_y)
            if vp.size_hint_max_y is not None:
                h = min(h, vp.size_hint_max_y)
            vp.height = h

        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,
                    size_hint_min=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. 21
0
class ActionGroup(ActionItem, Button):
    '''
    ActionGroup class, see module documentation for more information.
    '''

    use_separator = BooleanProperty(False)
    '''
    Specifies whether to use a separator after/before this group or not.

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

    separator_image = StringProperty(
        'atlas://data/images/defaulttheme/separator')
    '''
    Background Image for an ActionSeparator in an ActionView.

    :attr:`separator_image` is a :class:`~kivy.properties.StringProperty`
    and defaults to 'atlas://data/images/defaulttheme/separator'.
    '''

    separator_width = NumericProperty(0)
    '''
    Width of the ActionSeparator in an ActionView.

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

    mode = OptionProperty('normal', options=('normal', 'spinner'))
    '''
    Sets the current mode of an ActionGroup. If mode is 'normal', the
    ActionGroups children will be displayed normally if there is enough
    space, otherwise they will be displayed in a spinner. If mode is
    'spinner', then the children will always be displayed in a spinner.

    :attr:`mode` is an :class:`~kivy.properties.OptionProperty` and defaults
    to 'normal'.
    '''

    dropdown_width = NumericProperty(0)
    '''
    If non zero, provides the width for the associated DropDown. This is
    useful when some items in the ActionGroup's DropDown are wider than usual
    and you don't want to make the ActionGroup widget itself wider.

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

    .. versionadded:: 1.10.0
    '''

    is_open = BooleanProperty(False)
    '''By default, the DropDown is not open. Set to True to open it.

    :attr:`is_open` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''
    def __init__(self, **kwargs):
        self.list_action_item = []
        self._list_overflow_items = []
        super(ActionGroup, self).__init__(**kwargs)

        # real is_open independent on public event
        self._is_open = False

        # create DropDown for the group and save its state to _is_open
        self._dropdown = ActionDropDown()
        self._dropdown.bind(attach_to=lambda ins, value: setattr(
            self, '_is_open', True if value else False))

        # put open/close responsibility to the event
        # - trigger dropdown opening when clicked
        self.bind(on_release=lambda *args: setattr(self, 'is_open', True))

        # - trigger dropdown closing when an item
        #   in the dropdown is clicked
        self._dropdown.bind(
            on_dismiss=lambda *args: setattr(self, 'is_open', False))

    def on_is_open(self, instance, value):
        # opening only if the DropDown is closed
        if value and not self._is_open:
            self._toggle_dropdown()
            self._dropdown.open(self)
            return

        # closing is_open manually, dismiss manually
        if not value and self._is_open:
            self._dropdown.dismiss()

    def _toggle_dropdown(self, *largs):
        ddn = self._dropdown
        ddn.size_hint_x = None

        # if container was set incorrectly and/or is missing
        if not ddn.container:
            return
        children = ddn.container.children

        # set DropDown width manually or if not set, then widen
        # the ActionGroup + DropDown until the widest child fits
        if children:
            ddn.width = self.dropdown_width or max(
                self.width, max(c.pack_width for c in children))
        else:
            ddn.width = self.width

        # set the DropDown children's height
        for item in children:
            item.size_hint_y = None
            item.height = max([self.height, sp(48)])

            # dismiss DropDown manually
            # auto_dismiss applies to touching outside of the DropDown
            item.bind(on_release=ddn.dismiss)

    def add_widget(self, item):
        # if adding ActionSeparator ('normal' mode,
        # everything visible), add it to the parent
        if isinstance(item, ActionSeparator):
            super(ActionGroup, self).add_widget(item)
            return

        if not isinstance(item, ActionItem):
            raise ActionBarException('ActionGroup only accepts ActionItem')

        self.list_action_item.append(item)

    def show_group(self):
        # 'normal' mode, items can fit to the view
        self.clear_widgets()
        for item in self._list_overflow_items + self.list_action_item:
            item.inside_group = True
            self._dropdown.add_widget(item)

    def clear_widgets(self):
        self._dropdown.clear_widgets()
Esempio n. 22
0
class BoxLayout(Layout):
    '''Box layout class. See module documentation for more information.
    '''

    spacing = NumericProperty(0)
    '''Spacing between children, in pixels.

    :data:`spacing` is a :class:`~kivy.properties.NumericProperty`, default to
    0.
    '''

    padding = NumericProperty(0)
    '''Padding between layout box and children, in pixels.

    :data:`padding` is a :class:`~kivy.properties.NumericProperty`, default to
    0.
    '''

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

    :data:`orientation` is an :class:`~kivy.properties.OptionProperty`, default
    to 'horizontal'. Can be 'vertical' or 'horizontal'.
    '''
    def __init__(self, **kwargs):
        super(BoxLayout, self).__init__(**kwargs)
        self.bind(spacing=self._trigger_layout,
                  padding=self._trigger_layout,
                  children=self._trigger_layout,
                  orientation=self._trigger_layout,
                  parent=self._trigger_layout,
                  size=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
        selfx = self.x
        selfy = self.y
        selfw = self.width
        selfh = self.height
        padding = self.padding
        spacing = self.spacing
        orientation = self.orientation
        padding2 = padding * 2

        # calculate maximum space used by size_hint
        stretch_weight_x = 0.
        stretch_weight_y = 0.
        minimum_size_x = padding2 + spacing * (len_children - 1)
        minimum_size_y = minimum_size_x
        for w in self.children:
            shw = w.size_hint_x
            shh = w.size_hint_y
            if shw is None:
                minimum_size_x += w.width
            else:
                stretch_weight_x += shw
            if shh is None:
                minimum_size_y += w.height
            else:
                stretch_weight_y += shh

        if orientation == 'horizontal':
            x = y = padding
            stretch_space = max(0.0, selfw - minimum_size_x)
            for c in reversed(self.children):
                shw = c.size_hint_x
                shh = c.size_hint_y
                w = c.width
                h = c.height
                cx = selfx + x
                cy = selfy + y

                if shw:
                    w = stretch_space * shw / stretch_weight_x
                if shh:
                    h = shh * (selfh - padding2)

                for key, value in c.pos_hint.iteritems():
                    posy = value * (selfh - padding2)
                    if key == 'y':
                        cy = y + posy
                    elif key == 'top':
                        cy = y + posy - h
                    elif key == 'center_y':
                        cy = y - h / 2. + posy

                c.x = cx
                c.y = cy
                c.width = w
                c.height = h
                x += w + spacing

        if orientation == 'vertical':
            x = y = padding
            stretch_space = max(0.0, selfh - minimum_size_y)
            for c in self.children:
                shw = c.size_hint_x
                shh = c.size_hint_y
                w = c.width
                h = c.height
                cx = selfx + x
                cy = selfy + y

                if shh:
                    h = stretch_space * shh / stretch_weight_y
                if shw:
                    w = shw * (selfw - padding2)

                for key, value in c.pos_hint.iteritems():
                    posx = value * (selfw - padding2)
                    if key == 'x':
                        cx = x + posx
                    elif key == 'right':
                        cx = x + posx - w
                    elif key == 'center_x':
                        cx = x - w / 2. + posx

                c.x = cx
                c.y = cy
                c.width = w
                c.height = h
                y += h + spacing

    def add_widget(self, widget, index=0):
        widget.bind(size=self._trigger_layout,
                    size_hint=self._trigger_layout,
                    pos_hint=self._trigger_layout)
        return super(Layout, self).add_widget(widget, index)

    def remove_widget(self, widget):
        widget.unbind(size=self._trigger_layout,
                      size_hint=self._trigger_layout,
                      pos_hint=self._trigger_layout)
        return super(Layout, self).remove_widget(widget)
Esempio n. 23
0
class ElectrumWindow(App):

    electrum_config = ObjectProperty(None)
    language = StringProperty('en')

    # properties might be updated by the network
    num_blocks = NumericProperty(0)
    num_nodes = NumericProperty(0)
    server_host = StringProperty('')
    server_port = StringProperty('')
    num_chains = NumericProperty(0)
    blockchain_name = StringProperty('')
    blockchain_checkpoint = NumericProperty(0)

    auto_connect = BooleanProperty(False)

    def on_auto_connect(self, instance, x):
        host, port, protocol, proxy, auto_connect = self.network.get_parameters(
        )
        self.network.set_parameters(host, port, protocol, proxy,
                                    self.auto_connect)

    def toggle_auto_connect(self, x):
        self.auto_connect = not self.auto_connect

    def choose_server_dialog(self, popup):
        from uix.dialogs.choice_dialog import ChoiceDialog
        protocol = 's'

        def cb2(host):
            from electrum_ltc.network import DEFAULT_PORTS
            pp = servers.get(host, DEFAULT_PORTS)
            port = pp.get(protocol, '')
            popup.ids.host.text = host
            popup.ids.port.text = port

        servers = self.network.get_servers()
        ChoiceDialog(_('Choose a server'), sorted(servers),
                     popup.ids.host.text, cb2).open()

    def choose_blockchain_dialog(self, dt):
        from uix.dialogs.choice_dialog import ChoiceDialog
        chains = self.network.get_blockchains()

        def cb(name):
            for index, b in self.network.blockchains.items():
                if name == self.network.get_blockchain_name(b):
                    self.network.follow_chain(index)
                    #self.block

        names = [self.network.blockchains[b].get_name() for b in chains]
        if len(names) > 1:
            ChoiceDialog(_('Choose your chain'), names, '', cb).open()

    use_rbf = BooleanProperty(False)

    def on_use_rbf(self, instance, x):
        self.electrum_config.set_key('use_rbf', self.use_rbf, True)

    use_change = BooleanProperty(False)

    def on_use_change(self, instance, x):
        self.electrum_config.set_key('use_change', self.use_change, True)

    use_unconfirmed = BooleanProperty(False)

    def on_use_unconfirmed(self, instance, x):
        self.electrum_config.set_key('confirmed_only',
                                     not self.use_unconfirmed, True)

    def set_URI(self, uri):
        self.switch_to('send')
        self.send_screen.set_URI(uri)

    def on_new_intent(self, intent):
        if intent.getScheme() != 'litecoin':
            return
        uri = intent.getDataString()
        self.set_URI(uri)

    def on_language(self, instance, language):
        Logger.info('language: {}'.format(language))
        _.switch_lang(language)

    def update_history(self, *dt):
        if self.history_screen:
            self.history_screen.update()

    def on_quotes(self, d):
        Logger.info("on_quotes")
        self._trigger_update_history()

    def on_history(self, d):
        Logger.info("on_history")
        self._trigger_update_history()

    def _get_bu(self):
        return self.electrum_config.get('base_unit', 'ZCL')

    def _set_bu(self, value):
        assert value in base_units.keys()
        self.electrum_config.set_key('base_unit', value, True)
        self._trigger_update_status()
        self._trigger_update_history()

    base_unit = AliasProperty(_get_bu, _set_bu)
    status = StringProperty('')
    fiat_unit = StringProperty('')

    def on_fiat_unit(self, a, b):
        self._trigger_update_history()

    def decimal_point(self):
        return base_units[self.base_unit]

    def btc_to_fiat(self, amount_str):
        if not amount_str:
            return ''
        rate = self.fx.exchange_rate()
        if not rate:
            return ''
        fiat_amount = self.get_amount(amount_str + ' ' +
                                      self.base_unit) * rate / pow(10, 8)
        return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.')

    def fiat_to_btc(self, fiat_amount):
        if not fiat_amount:
            return ''
        rate = self.fx.exchange_rate()
        if not rate:
            return ''
        satoshis = int(pow(10, 8) * Decimal(fiat_amount) / Decimal(rate))
        return format_satoshis_plain(satoshis, self.decimal_point())

    def get_amount(self, amount_str):
        a, u = amount_str.split()
        assert u == self.base_unit
        try:
            x = Decimal(a)
        except:
            return None
        p = pow(10, self.decimal_point())
        return int(p * x)

    _orientation = OptionProperty('landscape',
                                  options=('landscape', 'portrait'))

    def _get_orientation(self):
        return self._orientation

    orientation = AliasProperty(_get_orientation,
                                None,
                                bind=('_orientation', ))
    '''Tries to ascertain the kind of device the app is running on.
    Cane be one of `tablet` or `phone`.

    :data:`orientation` is a read only `AliasProperty` Defaults to 'landscape'
    '''

    _ui_mode = OptionProperty('phone', options=('tablet', 'phone'))

    def _get_ui_mode(self):
        return self._ui_mode

    ui_mode = AliasProperty(_get_ui_mode, None, bind=('_ui_mode', ))
    '''Defines tries to ascertain the kind of device the app is running on.
    Cane be one of `tablet` or `phone`.

    :data:`ui_mode` is a read only `AliasProperty` Defaults to 'phone'
    '''

    def __init__(self, **kwargs):
        # initialize variables
        self._clipboard = Clipboard
        self.info_bubble = None
        self.nfcscanner = None
        self.tabs = None
        self.is_exit = False
        self.wallet = None

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

        title = _('Electrum-ZCL App')
        self.electrum_config = config = kwargs.get('config', None)
        self.language = config.get('language', 'en')

        self.network = network = kwargs.get('network', None)
        if self.network:
            self.num_blocks = self.network.get_local_height()
            self.num_nodes = len(self.network.get_interfaces())
            host, port, protocol, proxy_config, auto_connect = self.network.get_parameters(
            )
            self.server_host = host
            self.server_port = port
            self.auto_connect = auto_connect
            self.proxy_config = proxy_config if proxy_config else {}

        self.plugins = kwargs.get('plugins', [])
        self.gui_object = kwargs.get('gui_object', None)
        self.daemon = self.gui_object.daemon
        self.fx = self.daemon.fx

        self.use_rbf = config.get('use_rbf', False)
        self.use_change = config.get('use_change', True)
        self.use_unconfirmed = not config.get('confirmed_only', False)

        # create triggers so as to minimize updation a max of 2 times a sec
        self._trigger_update_wallet = Clock.create_trigger(
            self.update_wallet, .5)
        self._trigger_update_status = Clock.create_trigger(
            self.update_status, .5)
        self._trigger_update_history = Clock.create_trigger(
            self.update_history, .5)
        self._trigger_update_interfaces = Clock.create_trigger(
            self.update_interfaces, .5)
        # cached dialogs
        self._settings_dialog = None
        self._password_dialog = None

    def wallet_name(self):
        return os.path.basename(
            self.wallet.storage.path) if self.wallet else ' '

    def on_pr(self, pr):
        if pr.verify(self.wallet.contacts):
            key = self.wallet.invoices.add(pr)
            if self.invoices_screen:
                self.invoices_screen.update()
            status = self.wallet.invoices.get_status(key)
            if status == PR_PAID:
                self.show_error("invoice already paid")
                self.send_screen.do_clear()
            else:
                if pr.has_expired():
                    self.show_error(_('Payment request has expired'))
                else:
                    self.switch_to('send')
                    self.send_screen.set_request(pr)
        else:
            self.show_error("invoice error:" + pr.error)
            self.send_screen.do_clear()

    def on_qr(self, data):
        from electrum_zcl.bitcoin import base_decode, is_address
        data = data.strip()
        if is_address(data):
            self.set_URI(data)
            return
        if data.startswith('zclassic:'):
            self.set_URI(data)
            return
        # try to decode transaction
        from electrum_zcl.transaction import Transaction
        try:
            text = base_decode(data, None, base=43).encode('hex')
            tx = Transaction(text)
            tx.deserialize()
        except:
            tx = None
        if tx:
            self.tx_dialog(tx)
            return
        # show error
        self.show_error("Unable to decode QR data")

    def update_tab(self, name):
        s = getattr(self, name + '_screen', None)
        if s:
            s.update()

    @profiler
    def update_tabs(self):
        for tab in ['invoices', 'send', 'history', 'receive', 'requests']:
            self.update_tab(tab)

    def switch_to(self, name):
        s = getattr(self, name + '_screen', None)
        if s is None:
            s = self.tabs.ids[name + '_screen']
            s.load_screen()
        panel = self.tabs.ids.panel
        tab = self.tabs.ids[name + '_tab']
        panel.switch_to(tab)

    def show_request(self, addr):
        self.switch_to('receive')
        self.receive_screen.screen.address = addr

    def show_pr_details(self, req, status, is_invoice):
        from electrum_zcl.util import format_time
        requestor = req.get('requestor')
        exp = req.get('exp')
        memo = req.get('memo')
        amount = req.get('amount')
        popup = Builder.load_file('gui/kivy/uix/ui_screens/invoice.kv')
        popup.is_invoice = is_invoice
        popup.amount = amount
        popup.requestor = requestor if is_invoice else req.get('address')
        popup.exp = format_time(exp) if exp else ''
        popup.description = memo if memo else ''
        popup.signature = req.get('signature', '')
        popup.status = status
        txid = req.get('txid')
        popup.tx_hash = txid or ''
        popup.on_open = lambda: popup.ids.output_list.update(
            req.get('outputs', []))
        popup.open()

    def qr_dialog(self, title, data, show_text=False):
        from uix.dialogs.qr_dialog import QRDialog
        popup = QRDialog(title, data, show_text)
        popup.open()

    def scan_qr(self, on_complete):
        if platform != 'android':
            return
        from jnius import autoclass
        from android import activity
        PythonActivity = autoclass('org.kivy.android.PythonActivity')
        Intent = autoclass('android.content.Intent')
        intent = Intent("com.google.zxing.client.android.SCAN")
        intent.putExtra("SCAN_MODE", "QR_CODE_MODE")

        def on_qr_result(requestCode, resultCode, intent):
            if requestCode == 0:
                if resultCode == -1:  # RESULT_OK:
                    contents = intent.getStringExtra("SCAN_RESULT")
                    if intent.getStringExtra(
                            "SCAN_RESULT_FORMAT") == 'QR_CODE':
                        on_complete(contents)
                    else:
                        self.show_error(
                            "wrong format " +
                            intent.getStringExtra("SCAN_RESULT_FORMAT"))

        activity.bind(on_activity_result=on_qr_result)
        try:
            PythonActivity.mActivity.startActivityForResult(intent, 0)
        except:
            self.show_error(
                _('Could not start Barcode Scanner.') + ' ' +
                _('Please install the Barcode Scanner app from ZXing'))

    def scan_qr_zxing(self, on_complete):
        # uses zxing embedded lib
        if platform != 'android':
            return
        from jnius import autoclass
        from android import activity
        PythonActivity = autoclass('org.kivy.android.PythonActivity')
        IntentIntegrator = autoclass(
            'com.google.zxing.integration.android.IntentIntegrator')
        integrator = IntentIntegrator(PythonActivity.mActivity)

        def on_qr_result(requestCode, resultCode, intent):
            if requestCode == 0:
                if resultCode == -1:  # RESULT_OK:
                    contents = intent.getStringExtra("SCAN_RESULT")
                    if intent.getStringExtra(
                            "SCAN_RESULT_FORMAT") == 'QR_CODE':
                        on_complete(contents)
                    else:
                        self.show_error(
                            "wrong format " +
                            intent.getStringExtra("SCAN_RESULT_FORMAT"))

        activity.bind(on_activity_result=on_qr_result)
        integrator.initiateScan()

    def do_share(self, data, title):
        if platform != 'android':
            return
        from jnius import autoclass, cast
        JS = autoclass('java.lang.String')
        Intent = autoclass('android.content.Intent')
        sendIntent = Intent()
        sendIntent.setAction(Intent.ACTION_SEND)
        sendIntent.setType("text/plain")
        sendIntent.putExtra(Intent.EXTRA_TEXT, JS(data))
        PythonActivity = autoclass('org.kivy.android.PythonActivity')
        currentActivity = cast('android.app.Activity',
                               PythonActivity.mActivity)
        it = Intent.createChooser(sendIntent,
                                  cast('java.lang.CharSequence', JS(title)))
        currentActivity.startActivity(it)

    def build(self):
        return Builder.load_file('gui/kivy/main.kv')

    def _pause(self):
        if platform == 'android':
            # move activity to back
            from jnius import autoclass
            python_act = autoclass('org.kivy.android.PythonActivity')
            mActivity = python_act.mActivity
            mActivity.moveTaskToBack(True)

    def on_start(self):
        ''' This is the start point of the kivy ui
        '''
        import time
        Logger.info('Time to on_start: {} <<<<<<<<'.format(time.clock()))
        win = Window
        win.bind(size=self.on_size, on_keyboard=self.on_keyboard)
        win.bind(on_key_down=self.on_key_down)
        #win.softinput_mode = 'below_target'
        self.on_size(win, win.size)
        self.init_ui()
        self.load_wallet_by_name(self.electrum_config.get_wallet_path())
        # init plugins
        run_hook('init_kivy', self)
        # fiat currency
        self.fiat_unit = self.fx.ccy if self.fx.is_enabled() else ''
        # default tab
        self.switch_to('history')
        # bind intent for bitcoin: URI scheme
        if platform == 'android':
            from android import activity
            from jnius import autoclass
            PythonActivity = autoclass('org.kivy.android.PythonActivity')
            mactivity = PythonActivity.mActivity
            self.on_new_intent(mactivity.getIntent())
            activity.bind(on_new_intent=self.on_new_intent)
        # connect callbacks
        if self.network:
            interests = [
                'updated', 'status', 'new_transaction', 'verified',
                'interfaces'
            ]
            self.network.register_callback(self.on_network_event, interests)
            self.network.register_callback(self.on_quotes, ['on_quotes'])
            self.network.register_callback(self.on_history, ['on_history'])
        # URI passed in config
        uri = self.electrum_config.get('url')
        if uri:
            self.set_URI(uri)

    def get_wallet_path(self):
        if self.wallet:
            return self.wallet.storage.path
        else:
            return ''

    def on_wizard_complete(self, instance, wallet):
        if wallet:
            wallet.start_threads(self.daemon.network)
            self.daemon.add_wallet(wallet)
            self.load_wallet(wallet)
        self.on_resume()

    def load_wallet_by_name(self, path):
        if not path:
            return
        wallet = self.daemon.load_wallet(path, None)
        if wallet:
            if wallet != self.wallet:
                self.stop_wallet()
                self.load_wallet(wallet)
                self.on_resume()
        else:
            Logger.debug(
                'Electrum: Wallet not found. Launching install wizard')
            storage = WalletStorage(path)
            wizard = Factory.InstallWizard(self.electrum_config, storage)
            wizard.bind(on_wizard_complete=self.on_wizard_complete)
            action = wizard.storage.get_action()
            wizard.run(action)

    def on_stop(self):
        self.stop_wallet()

    def stop_wallet(self):
        if self.wallet:
            self.daemon.stop_wallet(self.wallet.storage.path)
            self.wallet = None

    def on_key_down(self, instance, key, keycode, codepoint, modifiers):
        if 'ctrl' in modifiers:
            # q=24 w=25
            if keycode in (24, 25):
                self.stop()
            elif keycode == 27:
                # r=27
                # force update wallet
                self.update_wallet()
            elif keycode == 112:
                # pageup
                #TODO move to next tab
                pass
            elif keycode == 117:
                # pagedown
                #TODO move to prev tab
                pass
        #TODO: alt+tab_number to activate the particular tab

    def on_keyboard(self, instance, key, keycode, codepoint, modifiers):
        if key == 27 and self.is_exit is False:
            self.is_exit = True
            self.show_info(_('Press again to exit'))
            return True
        # override settings button
        if key in (319, 282):  #f1/settings button on android
            #self.gui.main_gui.toggle_settings(self)
            return True

    def settings_dialog(self):
        if self._settings_dialog is None:
            from uix.dialogs.settings import SettingsDialog
            self._settings_dialog = SettingsDialog(self)
        self._settings_dialog.update()
        self._settings_dialog.open()

    def popup_dialog(self, name):
        if name == 'settings':
            self.settings_dialog()
        elif name == 'wallets':
            from uix.dialogs.wallets import WalletDialog
            d = WalletDialog()
            d.open()
        else:
            popup = Builder.load_file('gui/kivy/uix/ui_screens/' + name +
                                      '.kv')
            popup.open()

    @profiler
    def init_ui(self):
        ''' Initialize The Ux part of electrum. This function performs the basic
        tasks of setting up the ui.
        '''
        from weakref import ref

        self.funds_error = False
        # setup UX
        self.screens = {}

        #setup lazy imports for mainscreen
        Factory.register('AnimatedPopup',
                         module='electrum_zcl_gui.kivy.uix.dialogs')
        Factory.register('QRCodeWidget',
                         module='electrum_zcl_gui.kivy.uix.qrcodewidget')

        # preload widgets. Remove this if you want to load the widgets on demand
        #Cache.append('electrum_zcl_widgets', 'AnimatedPopup', Factory.AnimatedPopup())
        #Cache.append('electrum_zcl_widgets', 'QRCodeWidget', Factory.QRCodeWidget())

        # load and focus the ui
        self.root.manager = self.root.ids['manager']

        self.history_screen = None
        self.contacts_screen = None
        self.send_screen = None
        self.invoices_screen = None
        self.receive_screen = None
        self.requests_screen = None
        self.icon = "icons/electrum-ltc.png"
        self.tabs = self.root.ids['tabs']

    def update_interfaces(self, dt):
        self.num_nodes = len(self.network.get_interfaces())
        self.num_chains = len(self.network.get_blockchains())
        chain = self.network.blockchain()
        self.blockchain_checkpoint = chain.get_checkpoint()
        self.blockchain_name = chain.get_name()
        if self.network.interface:
            self.server_host = self.network.interface.host

    def on_network_event(self, event, *args):
        Logger.info('network event: ' + event)
        if event == 'interfaces':
            self._trigger_update_interfaces()
        elif event == 'updated':
            self._trigger_update_wallet()
            self._trigger_update_status()
        elif event == 'status':
            self._trigger_update_status()
        elif event == 'new_transaction':
            self._trigger_update_wallet()
        elif event == 'verified':
            self._trigger_update_wallet()

    @profiler
    def load_wallet(self, wallet):
        self.wallet = wallet
        self.update_wallet()
        # Once GUI has been initialized check if we want to announce something
        # since the callback has been called before the GUI was initialized
        if self.receive_screen:
            self.receive_screen.clear()
        self.update_tabs()
        run_hook('load_wallet', wallet, self)

    def update_status(self, *dt):
        self.num_blocks = self.network.get_local_height()
        if not self.wallet:
            self.status = _("No Wallet")
            return
        if self.network is None or not self.network.is_running():
            status = _("Offline")
        elif self.network.is_connected():
            server_height = self.network.get_server_height()
            server_lag = self.network.get_local_height() - server_height
            if not self.wallet.up_to_date or server_height == 0:
                status = _("Synchronizing...")
            elif server_lag > 1:
                status = _("Server lagging (%d blocks)" % server_lag)
            else:
                c, u, x = self.wallet.get_balance()
                text = self.format_amount(c + x + u)
                status = str(text.strip() + ' ' + self.base_unit)
        else:
            status = _("Disconnected")

        n = self.wallet.basename()
        self.status = '[size=15dp]%s[/size]\n%s' % (n, status)
        #fiat_balance = self.fx.format_amount_and_units(c+u+x) or ''

    def get_max_amount(self):
        inputs = self.wallet.get_spendable_coins(None, self.electrum_config)
        addr = str(
            self.send_screen.screen.address) or self.wallet.dummy_address()
        outputs = [(TYPE_ADDRESS, addr, '!')]
        tx = self.wallet.make_unsigned_transaction(inputs, outputs,
                                                   self.electrum_config)
        amount = tx.output_value()
        return format_satoshis_plain(amount, self.decimal_point())

    def format_amount(self, x, is_diff=False, whitespaces=False):
        return format_satoshis(x, is_diff, 0, self.decimal_point(),
                               whitespaces)

    def format_amount_and_units(self, x):
        return format_satoshis_plain(
            x, self.decimal_point()) + ' ' + self.base_unit

    @profiler
    def update_wallet(self, *dt):
        self._trigger_update_status()
        if self.wallet and (self.wallet.up_to_date or not self.network
                            or not self.network.is_connected()):
            self.update_tabs()

    def notify(self, message):
        try:
            global notification, os
            if not notification:
                from plyer import notification
            icon = (os.path.dirname(os.path.realpath(__file__)) + '/../../' +
                    self.icon)
            notification.notify('Electrum-ZCL',
                                message,
                                app_icon=icon,
                                app_name='Electrum-ZCL')
        except ImportError:
            Logger.Error('Notification: needs plyer; `sudo pip install plyer`')

    def on_pause(self):
        # pause nfc
        if self.nfcscanner:
            self.nfcscanner.nfc_disable()
        return True

    def on_resume(self):
        if self.nfcscanner:
            self.nfcscanner.nfc_enable()
        # workaround p4a bug:
        # show an empty info bubble, to refresh the display
        self.show_info_bubble('',
                              duration=0.1,
                              pos=(0, 0),
                              width=1,
                              arrow_pos=None)

    def on_size(self, instance, value):
        width, height = value
        self._orientation = 'landscape' if width > height else 'portrait'
        self._ui_mode = 'tablet' if min(width,
                                        height) > inch(3.51) else 'phone'

    def on_ref_label(self, label, touch):
        if label.touched:
            label.touched = False
            self.qr_dialog(label.name, label.data, True)
        else:
            label.touched = True
            self._clipboard.copy(label.data)
            Clock.schedule_once(lambda dt: self.show_info(
                _('Text copied to clipboard.\nTap again to display it as QR code.'
                  )))

    def set_send(self, address, amount, label, message):
        self.send_payment(address, amount=amount, label=label, message=message)

    def show_error(self,
                   error,
                   width='200dp',
                   pos=None,
                   arrow_pos=None,
                   exit=False,
                   icon='atlas://gui/kivy/theming/light/error',
                   duration=0,
                   modal=False):
        ''' Show a error Message Bubble.
        '''
        self.show_info_bubble(text=error,
                              icon=icon,
                              width=width,
                              pos=pos or Window.center,
                              arrow_pos=arrow_pos,
                              exit=exit,
                              duration=duration,
                              modal=modal)

    def show_info(self,
                  error,
                  width='200dp',
                  pos=None,
                  arrow_pos=None,
                  exit=False,
                  duration=0,
                  modal=False):
        ''' Show a Info Message Bubble.
        '''
        self.show_error(error,
                        icon='atlas://gui/kivy/theming/light/important',
                        duration=duration,
                        modal=modal,
                        exit=exit,
                        pos=pos,
                        arrow_pos=arrow_pos)

    def show_info_bubble(self,
                         text=_('Hello World'),
                         pos=None,
                         duration=0,
                         arrow_pos='bottom_mid',
                         width=None,
                         icon='',
                         modal=False,
                         exit=False):
        '''Method to show a Information Bubble

        .. parameters::
            text: Message to be displayed
            pos: position for the bubble
            duration: duration the bubble remains on screen. 0 = click to hide
            width: width of the Bubble
            arrow_pos: arrow position for the bubble
        '''
        info_bubble = self.info_bubble
        if not info_bubble:
            info_bubble = self.info_bubble = Factory.InfoBubble()

        win = Window
        if info_bubble.parent:
            win.remove_widget(info_bubble if not info_bubble.modal else
                              info_bubble._modal_view)

        if not arrow_pos:
            info_bubble.show_arrow = False
        else:
            info_bubble.show_arrow = True
            info_bubble.arrow_pos = arrow_pos
        img = info_bubble.ids.img
        if text == 'texture':
            # icon holds a texture not a source image
            # display the texture in full screen
            text = ''
            img.texture = icon
            info_bubble.fs = True
            info_bubble.show_arrow = False
            img.allow_stretch = True
            info_bubble.dim_background = True
            info_bubble.background_image = 'atlas://gui/kivy/theming/light/card'
        else:
            info_bubble.fs = False
            info_bubble.icon = icon
            #if img.texture and img._coreimage:
            #    img.reload()
            img.allow_stretch = False
            info_bubble.dim_background = False
            info_bubble.background_image = 'atlas://data/images/defaulttheme/bubble'
        info_bubble.message = text
        if not pos:
            pos = (win.center[0], win.center[1] - (info_bubble.height / 2))
        info_bubble.show(pos, duration, width, modal=modal, exit=exit)

    def tx_dialog(self, tx):
        from uix.dialogs.tx_dialog import TxDialog
        d = TxDialog(self, tx)
        d.open()

    def sign_tx(self, *args):
        threading.Thread(target=self._sign_tx, args=args).start()

    def _sign_tx(self, tx, password, on_success, on_failure):
        try:
            self.wallet.sign_transaction(tx, password)
        except InvalidPassword:
            Clock.schedule_once(lambda dt: on_failure(_("Invalid PIN")))
            return
        Clock.schedule_once(lambda dt: on_success(tx))

    def _broadcast_thread(self, tx, on_complete):
        ok, txid = self.network.broadcast(tx)
        Clock.schedule_once(lambda dt: on_complete(ok, txid))

    def broadcast(self, tx, pr=None):
        def on_complete(ok, msg):
            if ok:
                self.show_info(_('Payment sent.'))
                if self.send_screen:
                    self.send_screen.do_clear()
                if pr:
                    self.wallet.invoices.set_paid(pr, tx.txid())
                    self.wallet.invoices.save()
                    self.update_tab('invoices')
            else:
                self.show_error(msg)

        if self.network and self.network.is_connected():
            self.show_info(_('Sending'))
            threading.Thread(target=self._broadcast_thread,
                             args=(tx, on_complete)).start()
        else:
            self.show_info(
                _('Cannot broadcast transaction') + ':\n' + _('Not connected'))

    def description_dialog(self, screen):
        from uix.dialogs.label_dialog import LabelDialog
        text = screen.message

        def callback(text):
            screen.message = text

        d = LabelDialog(_('Enter description'), text, callback)
        d.open()

    @profiler
    def amount_dialog(self, screen, show_max):
        from uix.dialogs.amount_dialog import AmountDialog
        amount = screen.amount
        if amount:
            amount, u = str(amount).split()
            assert u == self.base_unit

        def cb(amount):
            screen.amount = amount

        popup = AmountDialog(show_max, amount, cb)
        popup.open()

    def protected(self, msg, f, args):
        if self.wallet.has_password():
            self.password_dialog(msg, f, args)
        else:
            f(*(args + (None, )))

    def delete_wallet(self):
        from uix.dialogs.question import Question
        basename = os.path.basename(self.wallet.storage.path)
        d = Question(
            _('Delete wallet?') + '\n' + basename, self._delete_wallet)
        d.open()

    def _delete_wallet(self, b):
        if b:
            basename = os.path.basename(self.wallet.storage.path)
            self.protected(
                _("Enter your PIN code to confirm deletion of %s") % basename,
                self.__delete_wallet, ())

    def __delete_wallet(self, pw):
        wallet_path = self.get_wallet_path()
        dirname = os.path.dirname(wallet_path)
        basename = os.path.basename(wallet_path)
        if self.wallet.has_password():
            try:
                self.wallet.check_password(pw)
            except:
                self.show_error("Invalid PIN")
                return
        self.stop_wallet()
        os.unlink(wallet_path)
        self.show_error("Wallet removed:" + basename)
        d = os.listdir(dirname)
        name = 'default_wallet'
        new_path = os.path.join(dirname, name)
        self.load_wallet_by_name(new_path)

    def show_seed(self, label):
        self.protected(_("Enter your PIN code in order to decrypt your seed"),
                       self._show_seed, (label, ))

    def _show_seed(self, label, password):
        if self.wallet.has_password() and password is None:
            return
        keystore = self.wallet.keystore
        try:
            seed = keystore.get_seed(password)
            passphrase = keystore.get_passphrase(password)
        except:
            self.show_error("Invalid PIN")
            return
        label.text = _('Seed') + ':\n' + seed
        if passphrase:
            label.text += '\n\n' + _('Passphrase') + ': ' + passphrase

    def change_password(self, cb):
        if self.wallet.has_password():
            self.protected(
                _("Changing PIN code.") + '\n' + _("Enter your current PIN:"),
                self._change_password, (cb, ))
        else:
            self._change_password(cb, None)

    def _change_password(self, cb, old_password):
        if self.wallet.has_password():
            if old_password is None:
                return
            try:
                self.wallet.check_password(old_password)
            except InvalidPassword:
                self.show_error("Invalid PIN")
                return
        self.password_dialog(_('Enter new PIN'), self._change_password2, (
            cb,
            old_password,
        ))

    def _change_password2(self, cb, old_password, new_password):
        self.password_dialog(_('Confirm new PIN'), self._change_password3,
                             (cb, old_password, new_password))

    def _change_password3(self, cb, old_password, new_password,
                          confirmed_password):
        if new_password == confirmed_password:
            self.wallet.update_password(old_password, new_password)
            cb()
        else:
            self.show_error("PIN numbers do not match")

    def password_dialog(self, msg, f, args):
        def callback(pw):
            Clock.schedule_once(lambda _: f(*(args + (pw, ))), 0.1)

        if self._password_dialog is None:
            from uix.dialogs.password_dialog import PasswordDialog
            self._password_dialog = PasswordDialog()
        self._password_dialog.init(msg, callback)
        self._password_dialog.open()
Esempio n. 24
0
class MaterialLabel(ThemableBehavior, Label):

    font_style = OptionProperty('Body1',
                                options=[
                                    'Body1', 'Body2', 'Caption', 'Subhead',
                                    'Title', 'Headline', 'Display1',
                                    'Display2', 'Display3', 'Display4',
                                    'Button', 'Icon'
                                ])

    # Font, Bold, Mobile size, Desktop size (None if same as Mobile)
    _font_styles = DictProperty({
        'Body1': ['Roboto', False, 14, 13],
        'Body2': ['Roboto', True, 14, 13],
        'Caption': ['Roboto', False, 12, None],
        'Subhead': ['Roboto', False, 16, 15],
        'Title': ['Roboto', True, 20, None],
        'Headline': ['Roboto', False, 24, None],
        'Display1': ['Roboto', False, 34, None],
        'Display2': ['Roboto', False, 45, None],
        'Display3': ['Roboto', False, 56, None],
        'Display4': ['RobotoLight', False, 112, None],
        'Button': ['Roboto', True, 14, None],
        'Icon': ['Icons', False, 24, None]
    })

    theme_text_color = OptionProperty(
        None,
        allownone=True,
        options=['Primary', 'Secondary', 'Hint', 'Error', 'Custom'])

    text_color = ListProperty(None, allownone=True)

    def __init__(self, **kwargs):
        super(MaterialLabel, self).__init__(**kwargs)
        self.on_theme_text_color(None, self.theme_text_color)
        self.on_font_style(None, self.font_style)
        self.on_opposite_colors(None, self.opposite_colors)

    def on_font_style(self, instance, style):
        info = self._font_styles[style]
        self.font_name = info[0]
        self.bold = info[1]
        if DEVICE_TYPE == 'desktop' and info[3] is not None:
            self.font_size = sp(info[3])
        else:
            self.font_size = sp(info[2])

    def on_theme_text_color(self, instance, value):
        t = self.theme_cls
        op = self.opposite_colors
        if value == 'Primary':
            self.color = t.text_color if not op else t.opposite_text_color
        elif value == 'Secondary':
            self.color = t.secondary_text_color if not op else \
             t.opposite_secondary_text_color
        elif value == 'Hint':
            self.color = t.disabled_hint_text_color if not op else \
             t.opposite_disabled_hint_text_color
        elif value == 'Error':
            self.color = self.theme_cls.error_color
        elif value == 'Custom':
            self.color = self.text_color if self.text_color else (0, 0, 0, 1)

    def on_text_color(self, *args):
        if self.theme_text_color == 'Custom':
            self.color = self.text_color

    def on_opposite_colors(self, instance, value):
        self.on_theme_text_color(self, self.theme_text_color)
Esempio n. 25
0
class MDTextField(ThemableBehavior, TextInput):
    helper_text = StringProperty("This field is required")
    """
    Text for ``helper_text`` mode.

    :attr:`helper_text` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'This field is required'`.
    """

    helper_text_mode = OptionProperty(
        "none", options=["none", "on_error", "persistent", "on_focus"])
    """
    Helper text mode. Available options are: `'on_error'`, `'persistent'`,
    `'on_focus'`.

    :attr:`helper_text_mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'none'`.
    """

    max_text_length = NumericProperty(None)
    """
    Maximum allowed value of characters in a text field.

    :attr:`max_text_length` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `None`.
    """

    required = BooleanProperty(False)
    """
    Required text. If True then the text field requires text.

    :attr:`required` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    color_mode = OptionProperty("primary",
                                options=["primary", "accent", "custom"])
    """
    Color text mode. Available options are: `'primary'`, `'accent'`,
    `'custom'`.

    :attr:`color_mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'primary'`.
    """

    mode = OptionProperty("line", options=["rectangle", "fill"])
    """
    Text field mode. Available options are: `'line'`, `'rectangle'`, `'fill'`.

    :attr:`mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'line'`.
    """

    line_color_normal = ColorProperty(None)
    """
    Line color normal in ``rgba`` format.

    :attr:`line_color_normal` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    line_color_focus = ColorProperty(None)
    """
    Line color focus in ``rgba`` format.

    :attr:`line_color_focus` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    line_anim = BooleanProperty(True)
    """
    If True, then text field shows animated line when on focus.

    :attr:`line_anim` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    error_color = ColorProperty(None)
    """
    Error color in ``rgba`` format for ``required = True``.

    :attr:`error_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    fill_color = ColorProperty([0, 0, 0, 0])
    """
    The background color of the fill in rgba format when the ``mode`` parameter
    is "fill".

    :attr:`fill_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `(0, 0, 0, 0)`.
    """

    active_line = BooleanProperty(True)
    """
    Show active line or not.

    :attr:`active_line` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    error = BooleanProperty(False)
    """
    If True, then the text field goes into ``error`` mode.

    :attr:`error` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    current_hint_text_color = ColorProperty(None)
    """
    ``hint_text`` text color.

    :attr:`current_hint_text_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    icon_right = StringProperty()
    """
    Right icon.

    :attr:`icon_right` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    icon_right_color = ColorProperty((0, 0, 0, 1))
    """
    Color of right icon in ``rgba`` format.

    :attr:`icon_right_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `(0, 0, 0, 1)`.
    """

    text_color = ColorProperty(None)
    """
    Text color in ``rgba`` format.

    :attr:`text_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    font_size = NumericProperty("16sp")
    """
    Font size of the text in pixels.

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

    # TODO: Add minimum allowed height. Otherwise, if the value is,
    #  for example, 20, the text field will simply be lessened.
    max_height = NumericProperty(0)
    """
    Maximum height of the text box when `multiline = True`.

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

    radius = ListProperty([10, 10, 0, 0])
    """
    The corner radius for a text field in `fill` mode.

    :attr:`radius` is a :class:`~kivy.properties.ListProperty` and
    defaults to `[10, 10, 0, 0]`.
    """

    _text_len_error = BooleanProperty(False)
    _hint_lbl_font_size = NumericProperty("16sp")
    _line_blank_space_right_point = NumericProperty(0)
    _line_blank_space_left_point = NumericProperty(0)
    _hint_y = NumericProperty("38dp")
    _line_width = NumericProperty(0)
    _current_line_color = ColorProperty((0, 0, 0, 0))
    _current_error_color = ColorProperty((0, 0, 0, 0))
    _current_hint_text_color = ColorProperty((0, 0, 0, 0))
    _current_right_lbl_color = ColorProperty((0, 0, 0, 0))
    _msg_lbl = None
    _right_msg_lbl = None
    _hint_lbl = None
    _lbl_icon_right = None

    def __init__(self, **kwargs):
        self.set_objects_labels()
        super().__init__(**kwargs)
        # Sets default colors.
        self.line_color_normal = self.theme_cls.divider_color
        self.line_color_focus = self.theme_cls.primary_color
        self.error_color = self.theme_cls.error_color
        self._current_hint_text_color = self.theme_cls.disabled_hint_text_color
        self._current_line_color = self.theme_cls.primary_color

        self.bind(
            helper_text=self._set_msg,
            hint_text=self.on_hint_text,
            _hint_lbl_font_size=self._hint_lbl.setter("font_size"),
            helper_text_mode=self._set_message_mode,
            max_text_length=self._set_max_text_length,
        )
        self.theme_cls.bind(
            primary_color=self._update_primary_color,
            theme_style=self._update_theme_style,
            accent_color=self._update_accent_color,
        )
        self.has_had_text = False
        self._better_texture_size = None
        Clock.schedule_once(self.check_text)

    def check_text(self, interval):
        self.set_text(self, self.text)

    def set_objects_labels(self):
        """Creates labels objects for the parameters
        `helper_text`,`hint_text`, etc."""

        # Label object for `helper_text` parameter.
        self._msg_lbl = TextfieldLabel(
            font_style="Caption",
            halign="left",
            valign="middle",
            text=self.helper_text,
            field=self,
        )
        # Label object for `max_text_length` parameter.
        self._right_msg_lbl = TextfieldLabel(
            font_style="Caption",
            halign="right",
            valign="middle",
            text="",
            field=self,
        )
        # Label object for `hint_text` parameter.
        self._hint_lbl = TextfieldLabel(font_style="Subtitle1",
                                        halign="left",
                                        valign="middle",
                                        field=self)
        # MDIcon object for the icon on the right.
        self._lbl_icon_right = MDIcon(theme_text_color="Custom")

    def on_icon_right(self, instance, value):
        self._lbl_icon_right.icon = value

    def on_icon_right_color(self, instance, value):
        self._lbl_icon_right.text_color = value

    def on_width(self, instance, width):
        """Called when the application window is resized."""

        if (any((self.focus, self.error, self._text_len_error))
                and instance is not None):
            # Bottom line width when active focus.
            self._line_width = width
        self._msg_lbl.width = self.width
        self._right_msg_lbl.width = self.width

    def on_focus(self, *args):
        disabled_hint_text_color = self.theme_cls.disabled_hint_text_color
        Animation.cancel_all(self, "_line_width", "_hint_y",
                             "_hint_lbl_font_size")
        self._set_text_len_error()

        if self.focus:
            _fill_color = self.fill_color
            _fill_color[3] = self.fill_color[3] - 0.1
            if not self._get_has_error():

                def on_progress(*args):
                    self._line_blank_space_right_point = (
                        self._hint_lbl.width + dp(5))

                animation = Animation(
                    _line_blank_space_left_point=self._hint_lbl.x - dp(5),
                    _current_hint_text_color=self.line_color_focus,
                    fill_color=_fill_color,
                    duration=0.2,
                    t="out_quad",
                )
                animation.bind(on_progress=on_progress)
                animation.start(self)
            self.has_had_text = True
            Animation.cancel_all(self, "_line_width", "_hint_y",
                                 "_hint_lbl_font_size")
            if not self.text:
                self._anim_lbl_font_size(dp(14), sp(12))
            Animation(
                _line_width=self.width,
                duration=(0.2 if self.line_anim else 0),
                t="out_quad",
            ).start(self)
            if self._get_has_error():
                self._anim_current_error_color(self.error_color)
                if self.helper_text_mode == "on_error" and (
                        self.error or self._text_len_error):
                    self._anim_current_error_color(self.error_color)
                elif (self.helper_text_mode == "on_error" and not self.error
                      and not self._text_len_error):
                    self._anim_current_error_color((0, 0, 0, 0))
                elif self.helper_text_mode in ("persistent", "on_focus"):
                    self._anim_current_error_color(disabled_hint_text_color)
            else:
                self._anim_current_right_lbl_color(disabled_hint_text_color)
                Animation(
                    duration=0.2,
                    _current_hint_text_color=self.line_color_focus).start(self)
                if self.helper_text_mode == "on_error":
                    self._anim_current_error_color((0, 0, 0, 0))
                if self.helper_text_mode in ("persistent", "on_focus"):
                    self._anim_current_error_color(disabled_hint_text_color)
        else:
            _fill_color = self.fill_color
            _fill_color[3] = self.fill_color[3] + 0.1
            Animation(fill_color=_fill_color, duration=0.2,
                      t="out_quad").start(self)
            if not self.text:
                self._anim_lbl_font_size(dp(38), sp(16))
                Animation(
                    _line_blank_space_right_point=0,
                    _line_blank_space_left_point=0,
                    duration=0.2,
                    t="out_quad",
                ).start(self)
            if self._get_has_error():
                self._anim_get_has_error_color(self.error_color)
                if self.helper_text_mode == "on_error" and (
                        self.error or self._text_len_error):
                    self._anim_current_error_color(self.error_color)
                elif (self.helper_text_mode == "on_error" and not self.error
                      and not self._text_len_error):
                    self._anim_current_error_color((0, 0, 0, 0))
                elif self.helper_text_mode == "persistent":
                    self._anim_current_error_color(disabled_hint_text_color)
                elif self.helper_text_mode == "on_focus":
                    self._anim_current_error_color((0, 0, 0, 0))
            else:
                Animation(duration=0.2,
                          color=(1, 1, 1, 1)).start(self._hint_lbl)
                self._anim_get_has_error_color()
                if self.helper_text_mode == "on_error":
                    self._anim_current_error_color((0, 0, 0, 0))
                elif self.helper_text_mode == "persistent":
                    self._anim_current_error_color(disabled_hint_text_color)
                elif self.helper_text_mode == "on_focus":
                    self._anim_current_error_color((0, 0, 0, 0))
                Animation(
                    _line_width=0,
                    duration=(0.2 if self.line_anim else 0),
                    t="out_quad",
                ).start(self)

    def on_disabled(self, *args):
        if self.disabled:
            self._update_colors(self.theme_cls.disabled_hint_text_color)
        elif not self.disabled:
            if self.color_mode == "primary":
                self._update_primary_color()
            elif self.color_mode == "accent":
                self._update_accent_color()
            elif self.color_mode == "custom":
                self._update_colors(self.line_color_focus)

    def set_text(self, instance, text):
        self.text = re.sub("\n", " ", text) if not self.multiline else text
        if len(text) > 0:
            self.has_had_text = True
        if self.max_text_length is not None:
            self._right_msg_lbl.text = f"{len(text)}/{self.max_text_length}"
        self._set_text_len_error()
        if self.error or self._text_len_error:
            if self.focus:
                self._anim_current_line_color(self.error_color)
                if self.helper_text_mode == "on_error" and (
                        self.error or self._text_len_error):
                    self._anim_current_error_color(self.error_color)
                if self._text_len_error:
                    self._anim_current_right_lbl_color(self.error_color)
        else:
            if self.focus:
                self._anim_current_right_lbl_color(
                    self.theme_cls.disabled_hint_text_color)
                self._anim_current_line_color(self.line_color_focus)
                if self.helper_text_mode == "on_error":
                    self._anim_current_error_color((0, 0, 0, 0))
        if len(self.text) != 0 and not self.focus:
            self._hint_y = dp(14)
            self._hint_lbl_font_size = sp(12)

    def on_text_validate(self):
        self.has_had_text = True
        self._set_text_len_error()

    def on_color_mode(self, instance, mode):
        if mode == "primary":
            self._update_primary_color()
        elif mode == "accent":
            self._update_accent_color()
        elif mode == "custom":
            self._update_colors(self.line_color_focus)

    def on_line_color_focus(self, *args):
        if self.color_mode == "custom":
            self._update_colors(self.line_color_focus)

    def on__hint_text(self, instance, value):
        pass

    def on_hint_text(self, instance, value):
        self._hint_lbl.text = value
        self._hint_lbl.font_size = sp(16)

    def on_height(self, instance, value):
        if value >= self.max_height and self.max_height:
            self.height = self.max_height

    def _anim_get_has_error_color(self, color=None):
        # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_get_has_error.png
        if not color:
            line_color = self.line_color_focus
            hint_text_color = (self.theme_cls.disabled_hint_text_color
                               if not self.current_hint_text_color else
                               self.current_hint_text_color)
            right_lbl_color = (0, 0, 0, 0)
        else:
            line_color = color
            hint_text_color = color
            right_lbl_color = color
        Animation(
            duration=0.2,
            _current_line_color=line_color,
            _current_hint_text_color=hint_text_color,
            _current_right_lbl_color=right_lbl_color,
        ).start(self)

    def _anim_current_line_color(self, color):
        # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_anim_current_line_color.gif
        Animation(
            duration=0.2,
            _current_hint_text_color=color,
            _current_line_color=color,
        ).start(self)

    def _anim_lbl_font_size(self, hint_y, font_size):
        Animation(
            _hint_y=hint_y,
            _hint_lbl_font_size=font_size,
            duration=0.2,
            t="out_quad",
        ).start(self)

    def _anim_current_right_lbl_color(self, color, duration=0.2):
        # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_anim_current_right_lbl_color.png
        Animation(duration=duration,
                  _current_right_lbl_color=color).start(self)

    def _anim_current_error_color(self, color, duration=0.2):
        # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_anim_current_error_color_to_disabled_color.gif
        # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_anim_current_error_color_to_fade.gif
        Animation(duration=duration, _current_error_color=color).start(self)

    def _update_colors(self, color):
        self.line_color_focus = color
        if not self.error and not self._text_len_error:
            self._current_line_color = color
            if self.focus:
                self._current_line_color = color

    def _update_accent_color(self, *args):
        if self.color_mode == "accent":
            self._update_colors(self.theme_cls.accent_color)

    def _update_primary_color(self, *args):
        if self.color_mode == "primary":
            self._update_colors(self.theme_cls.primary_color)

    def _update_theme_style(self, *args):
        self.line_color_normal = self.theme_cls.divider_color
        if not any([self.error, self._text_len_error]):
            if not self.focus:
                self._current_hint_text_color = (
                    self.theme_cls.disabled_hint_text_color)
                self._current_right_lbl_color = (
                    self.theme_cls.disabled_hint_text_color)
                if self.helper_text_mode == "persistent":
                    self._current_error_color = (
                        self.theme_cls.disabled_hint_text_color)

    def _get_has_error(self):
        if self.error or all([
                self.max_text_length is not None
                and len(self.text) > self.max_text_length
        ]):
            has_error = True
        else:
            if all((self.required, len(self.text) == 0, self.has_had_text)):
                has_error = True
            else:
                has_error = False
        return has_error

    def _get_max_text_length(self):
        """Returns the maximum number of characters that can be entered in a
        text field."""

        return (sys.maxsize
                if self.max_text_length is None else self.max_text_length)

    def _set_text_len_error(self):
        if len(self.text) > self._get_max_text_length() or all(
            (self.required, len(self.text) == 0, self.has_had_text)):
            self._text_len_error = True
        else:
            self._text_len_error = False

    def _set_msg(self, instance, text):
        self._msg_lbl.text = text
        self.helper_text = text

    def _set_message_mode(self, instance, text):
        self.helper_text_mode = text
        if self.helper_text_mode == "persistent":
            self._anim_current_error_color(
                self.theme_cls.disabled_hint_text_color, 0.1)

    def _set_max_text_length(self, instance, length):
        self.max_text_length = length
        self._right_msg_lbl.text = f"{len(self.text)}/{length}"

    def _refresh_hint_text(self):
        pass
Esempio n. 26
0
class MDCardSwipe(RelativeLayout):
    """
    :Events:
        :attr:`on_swipe_complete`
            Called when a swipe of card is completed.
    """

    open_progress = NumericProperty(0.0)
    """
    Percent of visible part of side panel. The percent is specified as a
    floating point number in the range 0-1. 0.0 if panel is closed and 1.0 if
    panel is opened.

    :attr:`open_progress` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `0.0`.
    """

    opening_transition = StringProperty("out_cubic")
    """
    The name of the animation transition type to use when animating to
    the :attr:`state` `'opened'`.

    :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'out_cubic'`.
    """

    closing_transition = StringProperty("out_sine")
    """
    The name of the animation transition type to use when animating to
    the :attr:`state` 'closed'.

    :attr:`closing_transition` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'out_sine'`.
    """

    anchor = OptionProperty("left", options=("left", "right"))
    """
    Anchoring screen edge for card. Available options are: `'left'`, `'right'`.

    :attr:`anchor` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `left`.
    """

    swipe_distance = NumericProperty(50)
    """
    The distance of the swipe with which the movement of navigation drawer
    begins.

    :attr:`swipe_distance` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `50`.
    """

    opening_time = NumericProperty(0.2)
    """
    The time taken for the card to slide to the :attr:`state` `'open'`.

    :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `0.2`.
    """

    state = OptionProperty("closed", options=("closed", "opened"))
    """
    Detailed state. Sets before :attr:`state`. Bind to :attr:`state` instead
    of :attr:`status`. Available options are: `'closed'`,  `'opened'`.

    :attr:`status` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `'closed'`.
    """

    max_swipe_x = NumericProperty(0.3)
    """
    If, after the events of :attr:`~on_touch_up` card position exceeds this
    value - will automatically execute the method :attr:`~open_card`,
    and if not - will automatically be :attr:`~close_card` method.

    :attr:`max_swipe_x` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `0.3`.
    """

    max_opened_x = NumericProperty("100dp")
    """
    The value of the position the card shifts to when :attr:`~type_swipe`
    s set to `'hand'`.

    :attr:`max_opened_x` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `100dp`.
    """

    type_swipe = OptionProperty("hand", options=("auto", "hand"))
    """
    Type of card opening when swipe. Shift the card to the edge or to
    a set position :attr:`~max_opened_x`. Available options are:
    `'auto'`, `'hand'`.

    :attr:`type_swipe` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `auto`.
    """

    _opens_process = False
    _to_closed = True

    def __init__(self, **kw):
        self.register_event_type("on_swipe_complete")
        super().__init__(**kw)

    def _on_swipe_complete(self, *args):
        self.dispatch("on_swipe_complete")

    def add_widget(self, widget, index=0, canvas=None):
        if isinstance(widget, (MDCardSwipeFrontBox, MDCardSwipeLayerBox)):
            return super().add_widget(widget)

    def on_swipe_complete(self, *args):
        """Called when a swipe of card is completed."""

    def on_anchor(self, instance, value):
        if value == "right":
            self.open_progress = 1.0
        else:
            self.open_progress = 0.0

    def on_open_progress(self, instance, value):
        if self.anchor == "left":
            self.children[0].x = self.width * value
        else:
            self.children[0].x = self.width * value - self.width

    def on_touch_move(self, touch):
        if self.collide_point(touch.x, touch.y):
            expr = (touch.x < self.swipe_distance if self.anchor == "left" else
                    touch.x > self.width - self.swipe_distance)
            if expr and not self._opens_process:
                self._opens_process = True
                self._to_closed = False
            if self._opens_process:
                self.open_progress = max(
                    min(self.open_progress + touch.dx / self.width, 2.5), 0)
        return super().on_touch_move(touch)

    def on_touch_up(self, touch):
        if self.collide_point(touch.x, touch.y):
            if not self._to_closed:
                self._opens_process = False
                self.complete_swipe()
        return super().on_touch_up(touch)

    def on_touch_down(self, touch):
        if self.collide_point(touch.x, touch.y):
            if self.state == "opened":
                self._to_closed = True
                self.close_card()
        return super().on_touch_down(touch)

    def complete_swipe(self):
        expr = (self.open_progress <= self.max_swipe_x if self.anchor == "left"
                else self.open_progress >= self.max_swipe_x)
        if expr:
            self.close_card()
        else:
            self.open_card()

    def open_card(self):
        if self.type_swipe == "hand":
            swipe_x = self.max_opened_x if self.anchor == "left" else -self.max_opened_x
        else:
            swipe_x = self.width if self.anchor == "left" else 0
        anim = Animation(x=swipe_x,
                         t=self.opening_transition,
                         d=self.opening_time)
        anim.bind(on_complete=self._on_swipe_complete)
        anim.start(self.children[0])
        self.state = "opened"

    def close_card(self):
        anim = Animation(x=0, t=self.closing_transition, d=self.opening_time)
        anim.bind(on_complete=self._reset_open_progress)
        anim.start(self.children[0])
        self.state = "closed"

    def _reset_open_progress(self, *args):
        self.open_progress = 0.0 if self.anchor == "left" else 1.0
        self._to_closed = False
        self.dispatch("on_swipe_complete")
Esempio n. 27
0
class MDDialog(BaseDialog):
    title = StringProperty()
    """
    Title dialog.

    .. code-block:: python

        self.dialog = MDDialog(
            title="Reset settings?",
            buttons=[
                MDFlatButton(
                    text="CANCEL", text_color=self.theme_cls.primary_color
                ),
                MDFlatButton(
                    text="ACCEPT", text_color=self.theme_cls.primary_color
                ),
            ],
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-title.png
        :align: center

    :attr:`title` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    text = StringProperty()
    """
    Text dialog.

    .. code-block:: python

        self.dialog = MDDialog(
            title="Reset settings?",
            text="This will reset your device to its default factory settings.",
            buttons=[
                MDFlatButton(
                    text="CANCEL", text_color=self.theme_cls.primary_color
                ),
                MDFlatButton(
                    text="ACCEPT", text_color=self.theme_cls.primary_color
                ),
            ],
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-text.png
        :align: center

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

    radius = ListProperty([7, 7, 7, 7])
    """
    Dialog corners rounding value.

    .. code-block:: python

        self.dialog = MDDialog(
            text="Oops! Something seems to have gone wrong!",
            radius=[20, 7, 20, 7],
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-radius.png
        :align: center

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

    buttons = ListProperty()
    """
    List of button objects for dialog.
    Objects must be inherited from :class:`~kivymd.uix.button.BaseButton` class.

    .. code-block:: python

        self.dialog = MDDialog(
            text="Discard draft?",
            buttons=[
                MDFlatButton(text="CANCEL"), MDRaisedButton(text="DISCARD"),
            ],
        )

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-buttons.png
        :align: center

    :attr:`buttons` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    items = ListProperty()
    """
    List of items objects for dialog.
    Objects must be inherited from :class:`~kivymd.uix.list.BaseListItem` class.

    With type 'simple'
    -----------------

    .. code-block:: python

        from kivy.lang import Builder
        from kivy.properties import StringProperty

        from kivymd.app import MDApp
        from kivymd.uix.dialog import MDDialog
        from kivymd.uix.list import OneLineAvatarListItem

        KV = '''
        <Item>

            ImageLeftWidget:
                source: root.source


        FloatLayout:

            MDFlatButton:
                text: "ALERT DIALOG"
                pos_hint: {'center_x': .5, 'center_y': .5}
                on_release: app.show_simple_dialog()
        '''


        class Item(OneLineAvatarListItem):
            divider = None
            source = StringProperty()


        class Example(MDApp):
            dialog = None

            def build(self):
                return Builder.load_string(KV)

            def show_simple_dialog(self):
                if not self.dialog:
                    self.dialog = MDDialog(
                        title="Set backup account",
                        type="simple",
                        items=[
                            Item(text="*****@*****.**", source="user-1.png"),
                            Item(text="*****@*****.**", source="user-2.png"),
                            Item(text="Add account", source="add-icon.png"),
                        ],
                    )
                self.dialog.open()


        Example().run()

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-items.png
        :align: center

    With type 'confirmation'
    -----------------------

    .. code-block:: python

        from kivy.lang import Builder

        from kivymd.app import MDApp
        from kivymd.uix.button import MDFlatButton
        from kivymd.uix.dialog import MDDialog
        from kivymd.uix.list import OneLineAvatarIconListItem

        KV = '''
        <ItemConfirm>
            on_release: root.set_icon(check)

            CheckboxLeftWidget:
                id: check
                group: "check"


        FloatLayout:

            MDFlatButton:
                text: "ALERT DIALOG"
                pos_hint: {'center_x': .5, 'center_y': .5}
                on_release: app.show_confirmation_dialog()
        '''


        class ItemConfirm(OneLineAvatarIconListItem):
            divider = None

            def set_icon(self, instance_check):
                instance_check.active = True
                check_list = instance_check.get_widgets(instance_check.group)
                for check in check_list:
                    if check != instance_check:
                        check.active = False


        class Example(MDApp):
            dialog = None

            def build(self):
                return Builder.load_string(KV)

            def show_confirmation_dialog(self):
                if not self.dialog:
                    self.dialog = MDDialog(
                        title="Phone ringtone",
                        type="confirmation",
                        items=[
                            ItemConfirm(text="Callisto"),
                            ItemConfirm(text="Luna"),
                            ItemConfirm(text="Night"),
                            ItemConfirm(text="Solo"),
                            ItemConfirm(text="Phobos"),
                            ItemConfirm(text="Diamond"),
                            ItemConfirm(text="Sirena"),
                            ItemConfirm(text="Red music"),
                            ItemConfirm(text="Allergio"),
                            ItemConfirm(text="Magic"),
                            ItemConfirm(text="Tic-tac"),
                        ],
                        buttons=[
                            MDFlatButton(
                                text="CANCEL", text_color=self.theme_cls.primary_color
                            ),
                            MDFlatButton(
                                text="OK", text_color=self.theme_cls.primary_color
                            ),
                        ],
                    )
                self.dialog.open()


        Example().run()

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-confirmation.png
        :align: center

    :attr:`items` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    type = OptionProperty(
        "alert", options=["alert", "simple", "confirmation", "custom"])
    """
    Dialog type.
    Available option are `'alert'`, `'simple'`, `'confirmation'`, `'custom'`.

    :attr:`type` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'alert'`.
    """

    content_cls = ObjectProperty()
    """
    Custom content class.

    .. code-block::

        from kivy.lang import Builder
        from kivy.uix.boxlayout import BoxLayout

        from kivymd.app import MDApp
        from kivymd.uix.button import MDFlatButton
        from kivymd.uix.dialog import MDDialog

        KV = '''
        <Content>
            orientation: "vertical"
            spacing: "12dp"
            size_hint_y: None
            height: "120dp"

            MDTextField:
                hint_text: "City"

            MDTextField:
                hint_text: "Street"


        FloatLayout:

            MDFlatButton:
                text: "ALERT DIALOG"
                pos_hint: {'center_x': .5, 'center_y': .5}
                on_release: app.show_confirmation_dialog()
        '''


        class Content(BoxLayout):
            pass


        class Example(MDApp):
            dialog = None

            def build(self):
                return Builder.load_string(KV)

            def show_confirmation_dialog(self):
                if not self.dialog:
                    self.dialog = MDDialog(
                        title="Address:",
                        type="custom",
                        content_cls=Content(),
                        buttons=[
                            MDFlatButton(
                                text="CANCEL", text_color=self.theme_cls.primary_color
                            ),
                            MDFlatButton(
                                text="OK", text_color=self.theme_cls.primary_color
                            ),
                        ],
                    )
                self.dialog.open()


        Example().run()

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-custom.png
        :align: center

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

    md_bg_color = ListProperty()
    """
    Background color in the format (r, g, b, a).

    :attr:`md_bg_color` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    _scroll_height = NumericProperty("28dp")
    _spacer_top = NumericProperty("24dp")

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

        self.md_bg_color = (self.theme_cls.bg_dark
                            if not self.md_bg_color else self.md_bg_color)

        if self.size_hint == [1, 1] and DEVICE_TYPE == "mobile":
            self.size_hint = (None, None)
            self.width = dp(280)
        elif self.size_hint == [1, 1] and DEVICE_TYPE == "desktop":
            self.size_hint = (None, None)
            self.width = dp(560)

        if not self.title:
            self._spacer_top = 0

        if not self.buttons:
            self.ids.root_button_box.height = 0
        else:
            self.create_buttons()

        update_height = False
        if self.type in ("simple", "confirmation"):
            if self.type == "confirmation":
                self.ids.spacer_top_box.add_widget(MDSeparator())
                self.ids.spacer_bottom_box.add_widget(MDSeparator())
            self.create_items()
        if self.type == "custom":
            if self.content_cls:
                self.ids.container.remove_widget(self.ids.scroll)
                self.ids.container.remove_widget(self.ids.text)
                self.ids.spacer_top_box.add_widget(self.content_cls)
                self.ids.spacer_top_box.padding = (0, "24dp", "16dp", 0)
                update_height = True
        if self.type == "alert":
            self.ids.scroll.bar_width = 0

        if update_height:
            Clock.schedule_once(self.update_height)

    def update_height(self, *_):
        self._spacer_top = self.content_cls.height + dp(24)

    def on_open(self):
        # TODO: Add scrolling text.
        self.height = self.ids.container.height

    def set_normal_height(self):
        self.size_hint_y = 0.8

    def get_normal_height(self):
        return ((Window.height * 80 / 100) - self._spacer_top - dp(52) -
                self.ids.container.padding[1] -
                self.ids.container.padding[-1] - 100)

    def edit_padding_for_item(self, instance_item):
        instance_item.ids._left_container.x = 0
        instance_item._txt_left_pad = "56dp"

    def create_items(self):
        self.ids.container.remove_widget(self.ids.text)
        height = 0

        for item in self.items:
            if issubclass(item.__class__, BaseListItem):
                height += item.height  # calculate height contents
                self.edit_padding_for_item(item)
                self.ids.box_items.add_widget(item)

        if height > Window.height:
            self.set_normal_height()
            self.ids.scroll.height = self.get_normal_height()
        else:
            self.ids.scroll.height = height

    def create_buttons(self):
        for button in self.buttons:
            if issubclass(button.__class__, BaseButton):
                self.ids.button_box.add_widget(button)
Esempio n. 28
0
class FocusBehavior(object):
    '''Provides keyboard focus behavior. When combined with other
    FocusBehavior widgets it allows one to cycle focus among them by pressing
    tab. Please see the
    :mod:`focus behavior module documentation <kivy.uix.behaviors.focus>`
    for more information.

    .. versionadded:: 1.9.0

    '''

    _requested_keyboard = False
    _keyboard = ObjectProperty(None, allownone=True)
    _keyboards = {}

    ignored_touch = []
    '''A list of touches that should not be used to defocus. After on_touch_up,
    every touch that is not in :attr:`ignored_touch` will defocus all the
    focused widgets if the config keyboard mode is not multi. Touches on
    focusable widgets that were used to focus are automatically added here.

    Example usage::

        class Unfocusable(Widget):

            def on_touch_down(self, touch):
                if self.collide_point(*touch.pos):
                    FocusBehavior.ignored_touch.append(touch)

    Notice that you need to access this as a class, not an instance variable.
    '''
    def _set_keyboard(self, value):
        focus = self.focus
        keyboard = self._keyboard
        keyboards = FocusBehavior._keyboards
        if keyboard:
            self.focus = False  # this'll unbind
            if self._keyboard:  # remove assigned keyboard from dict
                del keyboards[keyboard]
        if value and value not in keyboards:
            keyboards[value] = None
        self._keyboard = value
        self.focus = focus

    def _get_keyboard(self):
        return self._keyboard

    keyboard = AliasProperty(_get_keyboard,
                             _set_keyboard,
                             bind=('_keyboard', ))
    '''The keyboard to bind to (or bound to the widget) when focused.

    When None, a keyboard is requested and released whenever the widget comes
    into and out of focus. If not None, it must be a keyboard, which gets
    bound and unbound from the widget whenever it's in or out of focus. It is
    useful only when more than one keyboard is available, so it is recommended
    to be set to None when only one keyboard is available.

    If more than one keyboard is available, whenever an instance gets focused
    a new keyboard will be requested if None. Unless the other instances lose
    focus (e.g. if tab was used), a new keyboard will appear. When this is
    undesired, the keyboard property can be used. For example, if there are
    two users with two keyboards, then each keyboard can be assigned to
    different groups of instances of FocusBehavior, ensuring that within
    each group, only one FocusBehavior will have focus, and will receive input
    from the correct keyboard. See `keyboard_mode` in :mod:`~kivy.config` for
    more information on the keyboard modes.

    **Keyboard and focus behavior**

    When using the keyboard, there are some important default behaviors you
    should keep in mind.

    * When Config's `keyboard_mode` is multi, each new touch is considered
      a touch by a different user and will set the focus (if clicked on a
      focusable) with a new keyboard. Already focused elements will not lose
      their focus (even if an unfocusable widget is touched).

    * If the keyboard property is set, that keyboard will be used when the
      instance gets focused. If widgets with different keyboards are linked
      through :attr:`focus_next` and :attr:`focus_previous`, then as they are
      tabbed through, different keyboards will become active. Therefore,
      typically it's undesirable to link instances which are assigned
      different keyboards.

    * When a widget has focus, setting its keyboard to None will remove its
      keyboard, but the widget will then immediately try to get
      another keyboard. In order to remove its keyboard, rather set its
      :attr:`focus` to False.

    * When using a software keyboard, typical on mobile and touch devices, the
      keyboard display behavior is determined by the
      :attr:`~kivy.core.window.WindowBase.softinput_mode` property. You can use
      this property to ensure the focused widget is not covered or obscured.

    :attr:`keyboard` is an :class:`~kivy.properties.AliasProperty` and defaults
    to None.

    .. warning:

        When assigning a keyboard, the keyboard must not be released while
        it is still assigned to an instance. Similarly, the keyboard created
        by the instance on focus and assigned to :attr:`keyboard` if None,
        will be released by the instance when the instance loses focus.
        Therefore, it is not safe to assign this keyboard to another instance's
        :attr:`keyboard`.
    '''

    is_focusable = BooleanProperty(_is_desktop)
    '''Whether the instance can become focused. If focused, it'll lose focus
    when set to False.

    :attr:`is_focusable` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to True on a desktop (i.e. `desktop` is True in
    :mod:`~kivy.config`), False otherwise.
    '''

    focus = BooleanProperty(False)
    '''Whether the instance currently has focus.

    Setting it to True will bind to and/or request the keyboard, and input
    will be forwarded to the instance. Setting it to False will unbind
    and/or release the keyboard. For a given keyboard, only one widget can
    have its focus, so focusing one will automatically unfocus the other
    instance holding its focus.

    When using a software keyboard, please refer to the
    :attr:`~kivy.core.window.WindowBase.softinput_mode` property to determine
    how the keyboard display is handled.

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

    focused = focus
    '''An alias of :attr:`focus`.

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

    .. warning::
        :attr:`focused` is an alias of :attr:`focus` and will be removed in
        2.0.0.
    '''

    def _set_on_focus_next(self, instance, value):
        ''' If changing code, ensure following code is not infinite loop:
        widget.focus_next = widget
        widget.focus_previous = widget
        widget.focus_previous = widget2
        '''
        next = self._old_focus_next
        if next is value:  # prevent infinite loop
            return

        if isinstance(next, FocusBehavior):
            next.focus_previous = None
        self._old_focus_next = value
        if value is None or value is StopIteration:
            return
        if not isinstance(value, FocusBehavior):
            raise ValueError('focus_next accepts only objects based on'
                             ' FocusBehavior, or the `StopIteration` class.')
        value.focus_previous = self

    focus_next = ObjectProperty(None, allownone=True)
    '''The :class:`FocusBehavior` instance to acquire focus when
    tab is pressed and this instance has focus, if not `None` or
    `StopIteration`.

    When tab is pressed, focus cycles through all the :class:`FocusBehavior`
    widgets that are linked through :attr:`focus_next` and are focusable. If
    :attr:`focus_next` is `None`, it instead walks the children lists to find
    the next focusable widget. Finally, if :attr:`focus_next` is
    the `StopIteration` class, focus won't move forward, but end here.

    .. note:

        Setting :attr:`focus_next` automatically sets :attr:`focus_previous`
        of the other instance to point to this instance, if not None or
        `StopIteration`. Similarly, if it wasn't None or `StopIteration`, it
        also sets the :attr:`focus_previous` property of the instance
        previously in :attr:`focus_next` to `None`. Therefore, it is only
        required to set one of the :attr:`focus_previous` or
        :attr:`focus_next` links since the other side will be set
        automatically.

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

    def _set_on_focus_previous(self, instance, value):
        prev = self._old_focus_previous
        if prev is value:
            return

        if isinstance(prev, FocusBehavior):
            prev.focus_next = None
        self._old_focus_previous = value
        if value is None or value is StopIteration:
            return
        if not isinstance(value, FocusBehavior):
            raise ValueError('focus_previous accepts only objects based'
                             'on FocusBehavior, or the `StopIteration` class.')
        value.focus_next = self

    focus_previous = ObjectProperty(None, allownone=True)
    '''The :class:`FocusBehavior` instance to acquire focus when
    shift+tab is pressed on this instance, if not None or `StopIteration`.

    When shift+tab is pressed, focus cycles through all the
    :class:`FocusBehavior` widgets that are linked through
    :attr:`focus_previous` and are focusable. If :attr:`focus_previous` is
    `None`, it instead walks the children tree to find the
    previous focusable widget. Finally, if :attr:`focus_previous` is the
    `StopIteration` class, focus won't move backward, but end here.

    .. note:

        Setting :attr:`focus_previous` automatically sets :attr:`focus_next`
        of the other instance to point to this instance, if not None or
        `StopIteration`. Similarly, if it wasn't None or `StopIteration`, it
        also sets the :attr:`focus_next` property of the instance previously in
        :attr:`focus_previous` to `None`. Therefore, it is only required
        to set one of the :attr:`focus_previous` or :attr:`focus_next`
        links since the other side will be set automatically.

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

    keyboard_mode = OptionProperty('auto', options=('auto', 'managed'))
    '''Determines how the keyboard visibility should be managed. 'auto' will
    result in the standard behaviour of showing/hiding on focus. 'managed'
    requires setting the keyboard visibility manually, or calling the helper
    functions :meth:`show_keyboard` and :meth:`hide_keyboard`.

    :attr:`keyboard_mode` is an :class:`~kivy.properties.OptionsProperty` and
    defaults to 'auto'. Can be one of 'auto' or 'managed'.
    '''

    input_type = OptionProperty('text',
                                options=('text', 'number', 'url', 'mail',
                                         'datetime', 'tel', 'address'))
    '''The kind of input keyboard to request.

    .. versionadded:: 1.8.0

    :attr:`input_type` is an :class:`~kivy.properties.OptionsProperty` and
    defaults to 'text'. Can be one of 'text', 'number', 'url', 'mail',
    'datetime', 'tel' or 'address'.
    '''

    unfocus_on_touch = BooleanProperty(
        _keyboard_mode not in ('multi', 'systemandmulti'))
    '''Whether a instance should lose focus when clicked outside the instance.

    When a user clicks on a widget that is focus aware and shares the same
    keyboard as this widget (which in the case of only one keyboard, are
    all focus aware widgets), then as the other widgets gains focus, this
    widget loses focus. In addition to that, if this property is `True`,
    clicking on any widget other than this widget, will remove focus form this
    widget.

    :attr:`unfocus_on_touch` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to `False` if the `keyboard_mode` in :attr:`~kivy.config.Config`
    is `'multi'` or `'systemandmulti'`, otherwise it defaults to `True`.
    '''

    def __init__(self, **kwargs):
        self._old_focus_next = None
        self._old_focus_previous = None
        super(FocusBehavior, self).__init__(**kwargs)

        self._keyboard_mode = _keyboard_mode
        fbind = self.fbind
        fbind('focus', self._on_focus)
        fbind('disabled', self._on_focusable)
        fbind('is_focusable', self._on_focusable)
        fbind('focus_next', self._set_on_focus_next)
        fbind('focus_previous', self._set_on_focus_previous)

    def _on_focusable(self, instance, value):
        if self.disabled or not self.is_focusable:
            self.focus = False

    def _on_focus(self, instance, value, *largs):
        if self.keyboard_mode == 'auto':
            if value:
                self._bind_keyboard()
            else:
                self._unbind_keyboard()

    def _ensure_keyboard(self):
        if self._keyboard is None:
            self._requested_keyboard = True
            keyboard = self._keyboard = \
                EventLoop.window.request_keyboard(
                    self._keyboard_released, self, input_type=self.input_type)
            keyboards = FocusBehavior._keyboards
            if keyboard not in keyboards:
                keyboards[keyboard] = None

    def _bind_keyboard(self):
        self._ensure_keyboard()
        keyboard = self._keyboard

        if not keyboard or self.disabled or not self.is_focusable:
            self.focus = False
            return
        keyboards = FocusBehavior._keyboards
        old_focus = keyboards[keyboard]  # keyboard should be in dict
        if old_focus:
            old_focus.focus = False
            # keyboard shouldn't have been released here, see keyboard warning
        keyboards[keyboard] = self
        keyboard.bind(on_key_down=self.keyboard_on_key_down,
                      on_key_up=self.keyboard_on_key_up,
                      on_textinput=self.keyboard_on_textinput)

    def _unbind_keyboard(self):
        keyboard = self._keyboard
        if keyboard:
            keyboard.unbind(on_key_down=self.keyboard_on_key_down,
                            on_key_up=self.keyboard_on_key_up,
                            on_textinput=self.keyboard_on_textinput)
            if self._requested_keyboard:
                keyboard.release()
                self._keyboard = None
                self._requested_keyboard = False
                del FocusBehavior._keyboards[keyboard]
            else:
                FocusBehavior._keyboards[keyboard] = None

    def keyboard_on_textinput(self, window, text):
        pass

    def _keyboard_released(self):
        self.focus = False

    def on_touch_down(self, touch):
        if not self.collide_point(*touch.pos):
            return
        if (not self.disabled and self.is_focusable
                and ('button' not in touch.profile
                     or not touch.button.startswith('scroll'))):
            self.focus = True
            FocusBehavior.ignored_touch.append(touch)
        return super(FocusBehavior, self).on_touch_down(touch)

    @staticmethod
    def _handle_post_on_touch_up(touch):
        ''' Called by window after each touch has finished.
        '''
        touches = FocusBehavior.ignored_touch
        if touch in touches:
            touches.remove(touch)
            return
        if 'button' in touch.profile and touch.button in \
                ('scrollup', 'scrolldown', 'scrollleft', 'scrollright'):
            return
        for focusable in list(FocusBehavior._keyboards.values()):
            if focusable is None or not focusable.unfocus_on_touch:
                continue
            focusable.focus = False

    def _get_focus_next(self, focus_dir):
        current = self
        walk_tree = 'walk' if focus_dir == 'focus_next' else 'walk_reverse'

        while 1:
            # if we hit a focusable, walk through focus_xxx
            while getattr(current, focus_dir) is not None:
                current = getattr(current, focus_dir)
                if current is self or current is StopIteration:
                    return None  # make sure we don't loop forever
                if current.is_focusable and not current.disabled:
                    return current

            # hit unfocusable, walk widget tree
            itr = getattr(current, walk_tree)(loopback=True)
            if focus_dir == 'focus_next':
                next(itr)  # current is returned first  when walking forward
            for current in itr:
                if isinstance(current, FocusBehavior):
                    break
            # why did we stop
            if isinstance(current, FocusBehavior):
                if current is self:
                    return None
                if current.is_focusable and not current.disabled:
                    return current
            else:
                return None

    def get_focus_next(self):
        '''Returns the next focusable widget using either :attr:`focus_next`
           or the :attr:`children` similar to the order when tabbing forwards
           with the ``tab`` key.
        '''
        return self._get_focus_next('focus_next')

    def get_focus_previous(self):
        '''Returns the previous focusable widget using either
           :attr:`focus_previous` or the :attr:`children` similar to the
           order when ``tab`` + ``shift`` key are triggered together.
        '''
        return self._get_focus_next('focus_previous')

    def keyboard_on_key_down(self, window, keycode, text, modifiers):
        '''The method bound to the keyboard when the instance has focus.

        When the instance becomes focused, this method is bound to the
        keyboard and will be called for every input press. The parameters are
        the same as :meth:`kivy.core.window.WindowBase.on_key_down`.

        When overwriting the method in the derived widget, super should be
        called to enable tab cycling. If the derived widget wishes to use tab
        for its own purposes, it can call super after it has processed the
        character (if it does not wish to consume the tab).

        Similar to other keyboard functions, it should return True if the
        key was consumed.
        '''
        if keycode[1] == 'tab':  # deal with cycle
            if ['shift'] == modifiers:
                next = self.get_focus_previous()
            else:
                next = self.get_focus_next()
            if next:
                self.focus = False

                next.focus = True

            return True
        return False

    def keyboard_on_key_up(self, window, keycode):
        '''The method bound to the keyboard when the instance has focus.

        When the instance becomes focused, this method is bound to the
        keyboard and will be called for every input release. The parameters are
        the same as :meth:`kivy.core.window.WindowBase.on_key_up`.

        When overwriting the method in the derived widget, super should be
        called to enable de-focusing on escape. If the derived widget wishes
        to use escape for its own purposes, it can call super after it has
        processed the character (if it does not wish to consume the escape).

        See :meth:`keyboard_on_key_down`
        '''
        if keycode[1] == 'escape':
            self.focus = False
            return True
        return False

    def show_keyboard(self):
        '''
        Convenience function to show the keyboard in managed mode.
        '''
        if self.keyboard_mode == 'managed':
            self._bind_keyboard()

    def hide_keyboard(self):
        '''
        Convenience function to hide the keyboard in managed mode.
        '''
        if self.keyboard_mode == 'managed':
            self._unbind_keyboard()
Esempio n. 29
0
class BoxLayout(Layout):
    '''Box layout class. See module documentation for more information.
    '''

    spacing = NumericProperty(0)
    '''Spacing between children, in pixels.

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

    padding = VariableListProperty([0, 0, 0, 0])
    '''Padding between layout box and 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].

    .. versionchanged:: 1.7.0
        Replaced NumericProperty with VariableListProperty.

    :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and
    defaults to [0, 0, 0, 0].
    '''

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

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

    minimum_width = NumericProperty(0)
    '''Automatically computed minimum width needed to contain all children.

    .. versionadded:: 1.9.2

    :attr:`minimum_width` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0. It is read only.
    '''

    minimum_height = NumericProperty(0)
    '''Automatically computed minimum height needed to contain all children.

    .. versionadded:: 1.9.2

    :attr:`minimum_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0. It is read only.
    '''

    minimum_size = ReferenceListProperty(minimum_width, minimum_height)
    '''Automatically computed minimum size needed to contain all children.

    .. versionadded:: 1.9.2

    :attr:`minimum_size` is a
    :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`minimum_width`, :attr:`minimum_height`) properties. It is read
    only.
    '''


    def __init__(self, **kwargs):
        super(BoxLayout, self).__init__(**kwargs)
        update = self._trigger_layout
        fbind = self.fbind
        fbind('spacing', update)
        fbind('padding', update)
        fbind('children', update)
        fbind('orientation', update)
        fbind('parent', update)
        fbind('size', update)
        fbind('pos', update)

    def _iterate_layout(self, sizes):
        # optimize layout by preventing looking at the same attribute in a loop
        len_children = len(sizes)
        padding_left, padding_top, padding_right, padding_bottom = self.padding
        spacing = self.spacing
        orientation = self.orientation
        padding_x = padding_left + padding_right
        padding_y = padding_top + padding_bottom

        # calculate maximum space used by size_hint
        stretch_weight_x = 0.
        stretch_weight_y = 0.
        if orientation == 'horizontal':
            minimum_size_x = padding_x + spacing * (len_children - 1)
            minimum_size_y = 0
            for (w, h), (shw, shh), _ in sizes:
                if shw is None:
                    minimum_size_x += w
                else:
                    stretch_weight_x += shw
                if shh is None:
                    minimum_size_y = max(minimum_size_y, h)
            minimum_size_y += padding_y
        else:
            minimum_size_x = 0
            minimum_size_y = padding_y + spacing * (len_children - 1)
            for (w, h), (shw, shh), _ in sizes:
                if shw is None:
                    minimum_size_x = max(minimum_size_x, w)
                if shh is None:
                    minimum_size_y += h
                else:
                    stretch_weight_y += shh
            minimum_size_x += padding_x

        self.minimum_size = minimum_size_x, minimum_size_y
        selfw = self.width
        selfh = self.height
        selfx = self.x
        selfy = self.y

        if orientation == 'horizontal':
            x = padding_left
            stretch_space = max(0.0, selfw - minimum_size_x)
            for i, ((w, h), (shw, shh), pos_hint) in enumerate(
                    reversed(sizes)):
                cx = selfx + x
                cy = selfy + padding_bottom

                if shw:
                    w = stretch_space * shw / stretch_weight_x
                if shh:
                    h = max(0, shh * (selfh - padding_y))

                for key, value in pos_hint.items():
                    posy = value * (selfh - padding_y)
                    if key == 'y':
                        cy += posy
                    elif key == 'top':
                        cy += posy - h
                    elif key == 'center_y':
                        cy += posy - (h / 2.)

                yield len_children - i - 1, cx, cy, w, h
                x += w + spacing

        if orientation == 'vertical':
            y = padding_bottom
            stretch_space = max(0.0, selfh - minimum_size_y)
            for i, ((w, h), (shw, shh), pos_hint) in enumerate(sizes):

                cx = selfx + padding_left
                cy = selfy + y

                if shh:
                    h = stretch_space * shh / stretch_weight_y
                if shw:
                    w = max(0, shw * (selfw - padding_x))

                for key, value in pos_hint.items():
                    posx = value * (selfw - padding_x)
                    if key == 'x':
                        cx += posx
                    elif key == 'right':
                        cx += posx - w
                    elif key == 'center_x':
                        cx += posx - (w / 2.)

                yield i, cx, cy, w, h
                y += h + spacing

    def do_layout(self, *largs):
        children = self.children
        if not children:
            l, t, r, b = self.padding
            self.minimum_size = l + r, t + b
            return

        for i, x, y, w, h in self._iterate_layout(
                [(c.size, c.size_hint, c.pos_hint) for c in children]):
            c = children[i]
            c.pos = x, y
            shw, shh = c.size_hint
            if shw is None:
                if shh is not None:
                    c.height = h
            else:
                if shh is None:
                    c.width = w
                else:
                    c.size = (w, h)

    def add_widget(self, widget, index=0):
        widget.fbind('pos_hint', self._trigger_layout)
        return super(BoxLayout, self).add_widget(widget, index)

    def remove_widget(self, widget):
        widget.funbind('pos_hint', self._trigger_layout)
        return super(BoxLayout, self).remove_widget(widget)
Esempio n. 30
0
class ActionGroup(ActionItem, Spinner):
    '''ActionGroup class, see module documentation for more information.
    '''

    use_separator = BooleanProperty(False)
    '''Specifies whether to use a separator after/before this group or not.

       :data:`use_separator` is a :class:`~kivy.properties.BooleanProperty` and
       defaults to False.
    '''

    separator_image = StringProperty(
        'atlas://data/images/defaulttheme/separator')
    '''Background Image for an ActionSeparator in an ActionView.

       :data:`separator_image` is a :class:`~kivy.properties.StringProperty`
       and defaults to 'atlas://data/images/defaulttheme/separator'.
    '''

    separator_width = NumericProperty(0)
    '''Width of the ActionSeparator in an ActionView.

       :data:`separator_width` is a :class:`~kivy.properties.NumericProperty`
       and defaults to 0.
    '''

    mode = OptionProperty('normal', options=('normal', 'spinner'))
    '''Sets the current mode of an ActionGroup. If mode is 'normal', the
       ActionGroups children will be displayed normally if there is enough
       space, otherwise they will be displayed in a spinner. If mode is
       'spinner', then the children will always be displayed in a spinner.

       :data:`mode` is a :class:`~kivy.properties.OptionProperty` and
       defaults to 'normal'.
    '''
    def __init__(self, **kwargs):
        self.list_action_item = []
        self._list_overflow_items = []
        super(ActionGroup, self).__init__(**kwargs)
        self.dropdown_cls = ActionDropDown

    def add_widget(self, item):
        if isinstance(item, ActionSeparator):
            super(ActionGroup, self).add_widget(item)
            return

        if not isinstance(item, ActionItem):
            raise ActionBarException('ActionGroup only accepts ActionItem')

        self.list_action_item.append(item)

    def show_group(self):
        self.clear_widgets()
        for item in self._list_overflow_items + self.list_action_item:
            item.inside_group = True
            self._dropdown.add_widget(item)

    def _build_dropdown(self, *largs):
        if self._dropdown:
            self._dropdown.unbind(on_dismiss=self._toggle_dropdown)
            self._dropdown.dismiss()
            self._dropdown = None
        self._dropdown = self.dropdown_cls()
        self._dropdown.bind(on_dismiss=self._toggle_dropdown)

    def _update_dropdown(self, *largs):
        pass

    def _toggle_dropdown(self, *largs):
        self.is_open = not self.is_open
        ddn = self._dropdown
        ddn.size_hint_x = None
        if not ddn.container:
            return
        children = ddn.container.children
        ddn.width = max([self.width, children[0].minimum_width])
        for item in children:
            item.size_hint_y = None
            item.height = max([self.height, '48sp'])

    def clear_widgets(self):
        self._dropdown.clear_widgets()