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
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()
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)
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)
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])
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
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)
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
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
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))
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
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)
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)
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()
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()
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))
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
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
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()
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)
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()
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)
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
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")
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)
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()
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)
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()