class CustomScreen(Screen): hue = NumericProperty(0)
class KeyValueSpinner(Spinner): __events__ = ["on_select"] sync_height_frac = NumericProperty(1.0) value_refs = ListProperty() selected_index = NumericProperty(0) font_name = StringProperty(Theme.DEFAULT_FONT) def __init__(self, **kwargs): super().__init__(**kwargs) self.build_values() self.bind(size=self.update_dropdown_props, pos=self.update_dropdown_props, value_refs=self.build_values) @property def input_value(self): try: return self.value_refs[self.selected_index] except KeyError: return "" @property def selected(self): try: selected = self.selected_index return selected, self.value_refs[selected], self.values[selected] except (ValueError, IndexError): return 0, "", "" def on_text(self, _widget, text): try: new_index = self.values.index(text) if new_index != self.selected_index: self.selected_index = new_index self.dispatch("on_select") except (ValueError, IndexError): pass def on_select(self, *args): pass def select_key(self, key): try: ix = self.value_refs.index(key) self.text = self.values[ix] except (ValueError, IndexError): pass def build_values(self, *_args): if self.value_refs and self.values: self.text = self.values[self.selected_index] self.font_name = i18n.font_name self.update_dropdown_props() def update_dropdown_props(self, *largs): if not self.sync_height_frac: return dp = self._dropdown if not dp: return container = dp.container if not container: return h = self.height fsz = self.font_size for item in container.children[:]: item.height = h * self.sync_height_frac item.font_size = fsz item.font_name = self.font_name
class CollapsablePanel(MDBoxLayout): __events__ = ["on_option_state"] options = ListProperty([]) options_height = NumericProperty(25) content_height = NumericProperty(100) size_hint_y_open = NumericProperty(None) # total height inc tabs, overrides content_height options_spacing = NumericProperty(6) option_labels = ListProperty([]) option_active = ListProperty([]) option_colors = ListProperty([]) contents = ListProperty([]) closed_label = StringProperty("Closed Panel") state = OptionProperty("open", options=["open", "close"]) close_icon = "Previous-5.png" open_icon = "Next-5.png" def __init__(self, **kwargs): self.open_close_button, self.header = None, None self.option_buttons = [] super().__init__(**kwargs) self.orientation = "vertical" self.bind( options=self.build_options, option_colors=self.build_options, options_height=self.build_options, option_active=self.build_options, options_spacing=self.build_options, ) self.bind(state=self._on_state, content_height=self._on_size, options_height=self._on_size) MDApp.get_running_app().bind(language=lambda *_: Clock.schedule_once(self.build_options, 0)) self.build_options() def _on_state(self, *_args): self.build() self.trigger_select(ix=None) def _on_size(self, *_args): height, size_hint_y = 1, None if self.state == "open" and self.contents: if self.size_hint_y_open is not None: size_hint_y = self.size_hint_y_open else: height = self.content_height + self.options_height else: height = self.header.height self.height, self.size_hint_y = height, size_hint_y @property def option_state(self): return {option: active for option, active in zip(self.options, self.option_active)} def set_option_state(self, state_dict): for ix, (option, button) in enumerate(zip(self.options, self.option_buttons)): if option in state_dict: self.option_active[ix] = state_dict[option] button.state = "down" if state_dict[option] else "normal" self.trigger_select(ix=None) def build_options(self, *args): self.header = CollapsablePanelHeader( height=self.options_height, size_hint_y=None, spacing=self.options_spacing, padding=[1, 0, 0, 0] ) self.option_buttons = [] option_labels = self.option_labels or [i18n._(f"tab:{opt}") for opt in self.options] for ix, (lbl, opt_col, active) in enumerate(zip(option_labels, self.option_colors, self.option_active)): button = CollapsablePanelTab( text=lbl, font_name=i18n.font_name, active_outline_color=opt_col, height=self.options_height, state="down" if active else "normal", ) self.option_buttons.append(button) button.bind(state=lambda *_args, _ix=ix: self.trigger_select(_ix)) self.open_close_button = TransparentIconButton( # << / >> collapse button icon=self.open_close_icon(), icon_size=[0.5 * self.options_height, 0.5 * self.options_height], width=0.75 * self.options_height, size_hint_x=None, on_press=lambda *_args: self.set_state("toggle"), ) self.bind(state=lambda *_args: self.open_close_button.setter("icon")(None, self.open_close_icon())) self.build() def build(self, *args): self.header.clear_widgets() if self.state == "open": for button in self.option_buttons: self.header.add_widget(button) self.header.add_widget(Label()) # spacer else: self.header.add_widget( Label( text=i18n._(self.closed_label), font_name=i18n.font_name, halign="right", height=self.options_height ) ) self.header.add_widget(self.open_close_button) super().clear_widgets() super().add_widget(self.header) if self.state == "open" and self.contents: for w in self.contents: super().add_widget(w) self._on_size() def open_close_icon(self): return self.open_icon if self.state == "open" else self.close_icon def add_widget(self, widget, index=0, **_kwargs): self.contents.append(widget) self.build() def set_state(self, state="toggle"): if state == "toggle": state = "close" if self.state == "open" else "open" self.state = state self.build() if self.state == "open": self.trigger_select(ix=None) def trigger_select(self, ix): if ix is not None and self.option_buttons: self.option_active[ix] = self.option_buttons[ix].state == "down" if self.state == "open": self.dispatch("on_option_state", {opt: btn.active for opt, btn in zip(self.options, self.option_buttons)}) return False def on_option_state(self, options): pass
class BaseRaisedButton(CommonElevationBehavior, BaseButton): """ Abstract base class for raised buttons which elevate from material. Raised buttons are to be used sparingly to emphasise primary/important actions. Implements elevation behavior as well as the recommended down/disabled colors for raised buttons. """ def __init__(self, **kwargs): if self.elevation_raised == 0 and self.elevation_normal + 6 <= 12: self.elevation_raised = self.elevation_normal + 6 elif self.elevation_raised == 0: self.elevation_raised = 12 super(BaseRaisedButton, self).__init__(**kwargs) self.elevation_press_anim = Animation(elevation=self.elevation_raised, duration=.2, t='out_quad') self.elevation_release_anim = Animation( elevation=self.elevation_normal, duration=.2, t='out_quad') _elev_norm = NumericProperty(2) def _get_elev_norm(self): return self._elev_norm def _set_elev_norm(self, value): self._elev_norm = value if value <= 12 else 12 self._elev_raised = (value + 6) if value + 6 <= 12 else 12 self.elevation = self._elev_norm self.elevation_release_anim = Animation(elevation=value, duration=.2, t='out_quad') elevation_normal = AliasProperty(_get_elev_norm, _set_elev_norm, bind=('_elev_norm', )) _elev_raised = NumericProperty(8) def _get_elev_raised(self): return self._elev_raised def _set_elev_raised(self, value): self._elev_raised = value if value + self._elev_norm <= 12 else 12 self.elevation_press_anim = Animation(elevation=value, duration=.2, t='out_quad') elevation_raised = AliasProperty(_get_elev_raised, _set_elev_raised, bind=('_elev_raised', )) def on_disabled(self, instance, value): if self.disabled: self.elevation = 0 else: self.elevation = self.elevation_normal super(BaseRaisedButton, self).on_disabled(instance, value) def on_touch_down(self, touch): if not self.disabled: 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 self.elevation_press_anim.stop(self) self.elevation_press_anim.start(self) return super(BaseRaisedButton, self).on_touch_down(touch) def on_touch_up(self, touch): if not self.disabled: if touch.grab_current is not self: return super(ButtonBehavior, self).on_touch_up(touch) self.elevation_release_anim.stop(self) self.elevation_release_anim.start(self) return super(BaseRaisedButton, self).on_touch_up(touch) def _get_md_bg_color_down(self): t = self.theme_cls c = self.md_bg_color # Default to no change on touch # Material design specifies using darker hue when on Dark theme if t.theme_style == 'Dark': if self.md_bg_color == t.primary_color: c = t.primary_dark elif self.md_bg_color == t.accent_color: c = t.accent_dark return c def _get_md_bg_color_disabled(self): if self.theme_cls.theme_style == 'Dark': c = (1., 1., 1., .12) else: c = (.0, .0, .0, .12) return c
class WindowBase(EventDispatcher): '''WindowBase is an abstract window widget for any window implementation. :Parameters: `borderless`: str, one of ('0', '1') Set the window border state. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. `fullscreen`: str, one of ('0', '1', 'auto', 'fake') Make the window fullscreen. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. `width`: int Width of the window. `height`: int Height of the window. :Events: `on_motion`: etype, motionevent Fired when a new :class:`~kivy.input.motionevent.MotionEvent` is dispatched `on_touch_down`: Fired when a new touch event is initiated. `on_touch_move`: Fired when an existing touch event changes location. `on_touch_up`: Fired when an existing touch event is terminated. `on_draw`: Fired when the :class:`Window` is being drawn. `on_flip`: Fired when the :class:`Window` GL surface is being flipped. `on_rotate`: rotation Fired when the :class:`Window` is being rotated. `on_close`: Fired when the :class:`Window` is closed. `on_request_close`: Fired when the event loop wants to close the window, or if the escape key is pressed and `exit_on_escape` is `True`. If a function bound to this event returns `True`, the window will not be closed. If the the event is triggered because of the keyboard escape key, the keyword argument `source` is dispatched along with a value of `keyboard` to the bound functions. .. versionadded:: 1.9.0 `on_keyboard`: key, scancode, codepoint, modifier Fired when the keyboard is used for input. .. versionchanged:: 1.3.0 The *unicode* parameter has been deprecated in favor of codepoint, and will be removed completely in future versions. `on_key_down`: key, scancode, codepoint Fired when a key pressed. .. versionchanged:: 1.3.0 The *unicode* parameter has been deprecated in favor of codepoint, and will be removed completely in future versions. `on_key_up`: key, scancode, codepoint Fired when a key is released. .. versionchanged:: 1.3.0 The *unicode* parameter has be deprecated in favor of codepoint, and will be removed completely in future versions. `on_dropfile`: str Fired when a file is dropped on the application. ''' __instance = None __initialized = False _fake_fullscreen = False # private properties _size = ListProperty([0, 0]) _modifiers = ListProperty([]) _rotation = NumericProperty(0) _clearcolor = ObjectProperty([0, 0, 0, 1]) children = ListProperty([]) '''List of the children of this window. :attr:`children` is a :class:`~kivy.properties.ListProperty` instance and defaults to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` to manipulate the list of children. Don't manipulate the list directly unless you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this window. :attr:`parent` is a :class:`~kivy.properties.ObjectProperty` instance and defaults to None. When created, the parent is set to the window itself. You must take care of it if you are doing a recursive check. ''' icon = StringProperty() def _get_modifiers(self): return self._modifiers modifiers = AliasProperty(_get_modifiers, None) '''List of keyboard modifiers currently active. ''' def _get_size(self): r = self._rotation w, h = self._size if self.softinput_mode == 'resize': h -= self.keyboard_height if r in (0, 180): return w, h return h, w def _set_size(self, size): if self._size != size: r = self._rotation if r in (0, 180): self._size = size else: self._size = size[1], size[0] self.dispatch('on_resize', *size) return True else: return False size = AliasProperty(_get_size, _set_size, bind=('_size', )) '''Get the rotated size of the window. If :attr:`rotation` is set, then the size will change to reflect the rotation. ''' def _get_clearcolor(self): return self._clearcolor def _set_clearcolor(self, value): if value is not None: if type(value) not in (list, tuple): raise Exception('Clearcolor must be a list or tuple') if len(value) != 4: raise Exception('Clearcolor must contain 4 values') self._clearcolor = value clearcolor = AliasProperty(_get_clearcolor, _set_clearcolor, bind=('_clearcolor', )) '''Color used to clear the window. :: from kivy.core.window import Window # red background color Window.clearcolor = (1, 0, 0, 1) # don't clear background at all Window.clearcolor = None .. versionchanged:: 1.7.2 The clearcolor default value is now: (0, 0, 0, 1). ''' # make some property read-only def _get_width(self): r = self._rotation if r == 0 or r == 180: return self._size[0] return self._size[1] width = AliasProperty(_get_width, None, bind=('_rotation', '_size')) '''Rotated window width. :attr:`width` is a read-only :class:`~kivy.properties.AliasProperty`. ''' def _get_height(self): '''Rotated window height''' r = self._rotation kb = self.keyboard_height if self.softinput_mode == 'resize' else 0 if r == 0 or r == 180: return self._size[1] - kb return self._size[0] - kb height = AliasProperty(_get_height, None, bind=('_rotation', '_size')) '''Rotated window height. :attr:`height` is a read-only :class:`~kivy.properties.AliasProperty`. ''' def _get_center(self): return self.width / 2., self.height / 2. center = AliasProperty(_get_center, None, bind=('width', 'height')) '''Center of the rotated window. :attr:`center` is a :class:`~kivy.properties.AliasProperty`. ''' def _get_rotation(self): return self._rotation def _set_rotation(self, x): x = int(x % 360) if x == self._rotation: return if x not in (0, 90, 180, 270): raise ValueError('can rotate only 0, 90, 180, 270 degrees') self._rotation = x if self.initialized is False: return self.dispatch('on_resize', *self.size) self.dispatch('on_rotate', x) rotation = AliasProperty(_get_rotation, _set_rotation, bind=('_rotation', )) '''Get/set the window content rotation. Can be one of 0, 90, 180, 270 degrees. ''' softinput_mode = OptionProperty('', options=('', 'pan', 'scale', 'resize')) '''This specifies the behavior of window contents on display of soft keyboard on mobile platform. Can be one of '', 'pan', 'scale', 'resize'. When '' The main window is left as it is allowing the user to use :attr:`keyboard_height` to manage the window contents the way they want. when 'pan' The main window pans moving the bottom part of the window to be always on top of the keyboard. when 'resize' The window is resized and the contents scaled to fit the remaining space. ..versionadded::1.9.0 :attr:`softinput_mode` is a :class:`OptionProperty` defaults to None. ''' _keyboard_changed = BooleanProperty(False) def _upd_kbd_height(self, *kargs): self._keyboard_changed = not self._keyboard_changed def _get_ios_kheight(self): return 0 def _get_android_kheight(self): global android if not android: import android return android.get_keyboard_height() def _get_kheight(self): if platform == 'android': return self._get_android_kheight() if platform == 'ios': return self._get_ios_kheight() return 0 keyboard_height = AliasProperty(_get_kheight, None, bind=('_keyboard_changed',)) '''Rerturns the height of the softkeyboard/IME on mobile platforms. Will return 0 if not on mobile platform or if IME is not active. ..versionadded:: 1.9.0 :attr:`keyboard_height` is a read-only :class:`AliasProperty` defaults to 0. ''' def _set_system_size(self, size): self._size = size def _get_system_size(self): if self.softinput_mode == 'resize': return self._size[0], self._size[1] - self.keyboard_height return self._size system_size = AliasProperty( _get_system_size, _set_system_size, bind=('_size', )) '''Real size of the window ignoring rotation. ''' borderless = BooleanProperty(False) '''When set to True, this property removes the window border/decoration. .. versionadded:: 1.9.0 :attr:`borderless` is a :class:`BooleanProperty`, defaults to False. ''' fullscreen = OptionProperty(False, options=(True, False, 'auto', 'fake')) '''This property sets the fullscreen mode of the window. Available options are: True, False, 'auto', 'fake'. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. .. versionadded:: 1.2.0 .. note:: The 'fake' option has been deprecated, use the :attr:`borderless` property instead. ''' mouse_pos = ObjectProperty([0, 0]) '''2d position of the mouse within the window. .. versionadded:: 1.2.0 ''' @property def __self__(self): return self top = NumericProperty(None, allownone=True) left = NumericProperty(None, allownone=True) position = OptionProperty('auto', options=['auto', 'custom']) render_context = ObjectProperty(None) canvas = ObjectProperty(None) title = StringProperty('Kivy') __events__ = ('on_draw', 'on_flip', 'on_rotate', 'on_resize', 'on_close', 'on_motion', 'on_touch_down', 'on_touch_move', 'on_touch_up', 'on_mouse_down', 'on_mouse_move', 'on_mouse_up', 'on_keyboard', 'on_key_down', 'on_key_up', 'on_dropfile', 'on_request_close', 'on_joy_axis', 'on_joy_hat', 'on_joy_ball', 'on_joy_button_down', "on_joy_button_up") def __new__(cls, **kwargs): if cls.__instance is None: cls.__instance = EventDispatcher.__new__(cls) return cls.__instance def __init__(self, **kwargs): kwargs.setdefault('force', False) # don't init window 2 times, # except if force is specified if WindowBase.__instance is not None and not kwargs.get('force'): return self.initialized = False self._is_desktop = Config.getboolean('kivy', 'desktop') # create a trigger for update/create the window when one of window # property changes self.trigger_create_window = Clock.create_trigger( self.create_window, -1) # Create a trigger for updating the keyboard height self.trigger_keyboard_height = Clock.create_trigger( self._upd_kbd_height, .5) # set the default window parameter according to the configuration if 'borderless' not in kwargs: kwargs['borderless'] = Config.getboolean('graphics', 'borderless') if 'fullscreen' not in kwargs: fullscreen = Config.get('graphics', 'fullscreen') if fullscreen not in ('auto', 'fake'): fullscreen = fullscreen.lower() in ('true', '1', 'yes', 'yup') kwargs['fullscreen'] = fullscreen if 'width' not in kwargs: kwargs['width'] = Config.getint('graphics', 'width') if 'height' not in kwargs: kwargs['height'] = Config.getint('graphics', 'height') if 'rotation' not in kwargs: kwargs['rotation'] = Config.getint('graphics', 'rotation') if 'position' not in kwargs: kwargs['position'] = Config.getdefault('graphics', 'position', 'auto') if 'top' in kwargs: kwargs['position'] = 'custom' kwargs['top'] = kwargs['top'] else: kwargs['top'] = Config.getint('graphics', 'top') if 'left' in kwargs: kwargs['position'] = 'custom' kwargs['left'] = kwargs['left'] else: kwargs['left'] = Config.getint('graphics', 'left') kwargs['_size'] = (kwargs.pop('width'), kwargs.pop('height')) super(WindowBase, self).__init__(**kwargs) # bind all the properties that need to recreate the window self._bind_create_window() self.bind(size=self.trigger_keyboard_height, rotation=self.trigger_keyboard_height) self.bind(softinput_mode=lambda *dt: self.update_viewport(), keyboard_height=lambda *dt: self.update_viewport()) # init privates self._system_keyboard = Keyboard(window=self) self._keyboards = {'system': self._system_keyboard} self._vkeyboard_cls = None self.children = [] self.parent = self # before creating the window import kivy.core.gl # NOQA # configure the window self.create_window() # attach modules + listener event EventLoop.set_window(self) Modules.register_window(self) EventLoop.add_event_listener(self) # manage keyboard(s) self.configure_keyboards() # assign the default context of the widget creation if not hasattr(self, '_context'): self._context = get_current_context() # mark as initialized self.initialized = True def _bind_create_window(self): for prop in ( 'fullscreen', 'borderless', 'position', 'top', 'left', '_size', 'system_size'): self.bind(**{prop: self.trigger_create_window}) def _unbind_create_window(self): for prop in ( 'fullscreen', 'borderless', 'position', 'top', 'left', '_size', 'system_size'): self.unbind(**{prop: self.trigger_create_window}) def toggle_fullscreen(self): '''Toggle between fullscreen and windowed mode. .. deprecated:: 1.9.0 Use :attr:`fullscreen` instead. ''' pass def maximize(self): '''Maximizes the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: maximize() is not implemented in the current ' 'window provider.') def minimize(self): '''Minimizes the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: minimize() is not implemented in the current ' 'window provider.') def restore(self): '''Restores the size and position of a maximized or minimized window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: restore() is not implemented in the current ' 'window provider.') def hide(self): '''Hides the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: hide() is not implemented in the current ' 'window provider.') def show(self): '''Shows the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: show() is not implemented in the current ' 'window provider.') def close(self): '''Close the window''' pass def create_window(self, *largs): '''Will create the main window and configure it. .. warning:: This method is called automatically at runtime. If you call it, it will recreate a RenderContext and Canvas. This means you'll have a new graphics tree, and the old one will be unusable. This method exist to permit the creation of a new OpenGL context AFTER closing the first one. (Like using runTouchApp() and stopTouchApp()). This method has only been tested in a unittest environment and is not suitable for Applications. Again, don't use this method unless you know exactly what you are doing! ''' # just to be sure, if the trigger is set, and if this method is # manually called, unset the trigger Clock.unschedule(self.create_window) # ensure the window creation will not be called twice if platform in ('android', 'ios'): self._unbind_create_window() if not self.initialized: from kivy.core.gl import init_gl init_gl() # create the render context and canvas, only the first time. from kivy.graphics import RenderContext, Canvas self.render_context = RenderContext() self.canvas = Canvas() self.render_context.add(self.canvas) else: # if we get initialized more than once, then reload opengl state # after the second time. # XXX check how it's working on embed platform. if platform == 'linux' or Window.__class__.__name__ == 'WindowSDL': # on linux, it's safe for just sending a resize. self.dispatch('on_resize', *self.system_size) else: # on other platform, window are recreated, we need to reload. from kivy.graphics.context import get_context get_context().reload() Clock.schedule_once(lambda x: self.canvas.ask_update(), 0) self.dispatch('on_resize', *self.system_size) # ensure the gl viewport is correct self.update_viewport() def on_flip(self): '''Flip between buffers (event)''' self.flip() def flip(self): '''Flip between buffers''' pass def _update_childsize(self, instance, value): self.update_childsize([instance]) def add_widget(self, widget): '''Add a widget to a window''' widget.parent = self self.children.insert(0, widget) self.canvas.add(widget.canvas) self.update_childsize([widget]) widget.bind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def remove_widget(self, widget): '''Remove a widget from a window ''' if not widget in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None widget.unbind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def clear(self): '''Clear the window with the background color''' # XXX FIXME use late binding from kivy.graphics.opengl import glClearColor, glClear, \ GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_STENCIL_BUFFER_BIT cc = self._clearcolor if cc is not None: glClearColor(*cc) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) def set_title(self, title): '''Set the window title. .. versionadded:: 1.0.5 ''' self.title = title def set_icon(self, filename): '''Set the icon of the window. .. versionadded:: 1.0.5 ''' self.icon = filename def to_widget(self, x, y, initial=True, relative=False): return (x, y) def to_window(self, x, y, initial=True, relative=False): return (x, y) def get_root_window(self): return self def get_parent_window(self): return self def get_parent_layout(self): return None def on_draw(self): self.clear() self.render_context.draw() def on_motion(self, etype, me): '''Event called when a Motion Event is received. :Parameters: `etype`: str One of 'begin', 'update', 'end' `me`: :class:`~kivy.input.motionevent.MotionEvent` The Motion Event currently dispatched. ''' if me.is_touch: w, h = self.system_size me.scale_for_screen(w, h, rotation=self._rotation, smode=self.softinput_mode, kheight=self.keyboard_height) if etype == 'begin': self.dispatch('on_touch_down', me) elif etype == 'update': self.dispatch('on_touch_move', me) elif etype == 'end': self.dispatch('on_touch_up', me) FocusBehavior._handle_post_on_touch_up(me) def on_touch_down(self, touch): '''Event called when a touch down event is initiated. .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Event called when a touch event moves (changes location). .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Event called when a touch event is released (terminated). .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_up', touch): return True def on_resize(self, width, height): '''Event called when the window is resized.''' self.update_viewport() def update_viewport(self): from kivy.graphics.opengl import glViewport from kivy.graphics.transformation import Matrix from math import radians w, h = self.system_size smode = self.softinput_mode kheight = self.keyboard_height w2, h2 = w / 2., h / 2. r = radians(self.rotation) x, y = 0, 0 _h = h if smode: y = kheight if smode == 'scale': _h -= kheight # prepare the viewport glViewport(x, y, w, _h) # do projection matrix projection_mat = Matrix() projection_mat.view_clip(0.0, w, 0.0, h, -1.0, 1.0, 0) self.render_context['projection_mat'] = projection_mat # do modelview matrix modelview_mat = Matrix().translate(w2, h2, 0) modelview_mat = modelview_mat.multiply(Matrix().rotate(r, 0, 0, 1)) w, h = self.size w2, h2 = w / 2., h / 2. modelview_mat = modelview_mat.multiply(Matrix().translate(-w2, -h2, 0)) self.render_context['modelview_mat'] = modelview_mat # redraw canvas self.canvas.ask_update() # and update childs self.update_childsize() def update_childsize(self, childs=None): width, height = self.size if childs is None: childs = self.children for w in childs: shw, shh = w.size_hint if shw and shh: w.size = shw * width, shh * height elif shw: w.width = shw * width elif shh: w.height = shh * height for key, value in w.pos_hint.items(): if key == 'x': w.x = value * width elif key == 'right': w.right = value * width elif key == 'y': w.y = value * height elif key == 'top': w.top = value * height elif key == 'center_x': w.center_x = value * width elif key == 'center_y': w.center_y = value * height def screenshot(self, name='screenshot{:04d}.png'): '''Save the actual displayed image in a file ''' i = 0 path = None if name != 'screenshot{:04d}.png': _ext = name.split('.')[-1] name = ''.join((name[:-(len(_ext) + 1)], '{:04d}.', _ext)) while True: i += 1 path = join(getcwd(), name.format(i)) if not exists(path): break return path def on_rotate(self, rotation): '''Event called when the screen has been rotated. ''' pass def on_close(self, *largs): '''Event called when the window is closed''' Modules.unregister_window(self) EventLoop.remove_event_listener(self) def on_request_close(self, *largs, **kwargs): '''Event called before we close the window. If a bound function returns `True`, the window will not be closed. If the the event is triggered because of the keyboard escape key, the keyword argument `source` is dispatched along with a value of `keyboard` to the bound functions. .. warning:: When the bound function returns True the window will not be closed, so use with care because the user would not be able to close the program, even if the red X is clicked. ''' pass def on_mouse_down(self, x, y, button, modifiers): '''Event called when the mouse is used (pressed/released)''' pass def on_mouse_move(self, x, y, modifiers): '''Event called when the mouse is moved with buttons pressed''' pass def on_mouse_up(self, x, y, button, modifiers): '''Event called when the mouse is moved with buttons pressed''' pass def on_joy_axis(self, stickid, axisid, value): '''Event called when a joystick has a stick or other axis moved .. versionadded:: 1.9.0''' pass def on_joy_hat(self, stickid, hatid, value): '''Event called when a joystick has a hat/dpad moved .. versionadded:: 1.9.0''' pass def on_joy_ball(self, stickid, ballid, value): '''Event called when a joystick has a ball moved .. versionadded:: 1.9.0''' pass def on_joy_button_down(self, stickid, buttonid): '''Event called when a joystick has a button pressed .. versionadded:: 1.9.0''' pass def on_joy_button_up(self, stickid, buttonid): '''Event called when a joystick has a button released .. versionadded:: 1.9.0''' pass def on_keyboard(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when keyboard is used. .. warning:: Some providers may omit `scancode`, `codepoint` and/or `modifier`! ''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") # Quit if user presses ESC or the typical OSX shortcuts CMD+q or CMD+w # TODO If just CMD+w is pressed, only the window should be closed. is_osx = platform == 'darwin' if WindowBase.on_keyboard.exit_on_escape: if key == 27 or all([is_osx, key in [113, 119], modifier == 1024]): if not self.dispatch('on_request_close', source='keyboard'): stopTouchApp() self.close() return True if Config: on_keyboard.exit_on_escape = Config.getboolean('kivy', 'exit_on_escape') def __exit(section, name, value): WindowBase.__dict__['on_keyboard'].exit_on_escape = \ Config.getboolean('kivy', 'exit_on_escape') Config.add_callback(__exit, 'kivy', 'exit_on_escape') def on_key_down(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when a key is down (same arguments as on_keyboard)''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") def on_key_up(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when a key is released (same arguments as on_keyboard) ''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") def on_dropfile(self, filename): '''Event called when a file is dropped on the application. .. warning:: This event currently works with sdl2 window provider, on pygame window provider and MacOSX with a patched version of pygame. This event is left in place for further evolution (ios, android etc.) .. versionadded:: 1.2.0 ''' pass @reify def dpi(self): '''Return the DPI of the screen. If the implementation doesn't support any DPI lookup, it will just return 96. .. warning:: This value is not cross-platform. Use :attr:`kivy.base.EventLoop.dpi` instead. ''' return 96. def configure_keyboards(self): # Configure how to provide keyboards (virtual or not) # register system keyboard to listening keys from window sk = self._system_keyboard self.bind( on_key_down=sk._on_window_key_down, on_key_up=sk._on_window_key_up) # use the device's real keyboard self.use_syskeyboard = True # use the device's real keyboard self.allow_vkeyboard = False # one single vkeyboard shared between all widgets self.single_vkeyboard = True # the single vkeyboard is always sitting at the same position self.docked_vkeyboard = False # now read the configuration mode = Config.get('kivy', 'keyboard_mode') if mode not in ('', 'system', 'dock', 'multi', 'systemanddock', 'systemandmulti'): Logger.critical('Window: unknown keyboard mode %r' % mode) # adapt mode according to the configuration if mode == 'system': self.use_syskeyboard = True self.allow_vkeyboard = False self.single_vkeyboard = True self.docked_vkeyboard = False elif mode == 'dock': self.use_syskeyboard = False self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == 'multi': self.use_syskeyboard = False self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False elif mode == 'systemanddock': self.use_syskeyboard = True self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == 'systemandmulti': self.use_syskeyboard = True self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False Logger.info( 'Window: virtual keyboard %sallowed, %s, %s' % ( '' if self.allow_vkeyboard else 'not ', 'single mode' if self.single_vkeyboard else 'multiuser mode', 'docked' if self.docked_vkeyboard else 'not docked')) def set_vkeyboard_class(self, cls): '''.. versionadded:: 1.0.8 Set the VKeyboard class to use. If set to None, it will use the :class:`kivy.uix.vkeyboard.VKeyboard`. ''' self._vkeyboard_cls = cls def release_all_keyboards(self): '''.. versionadded:: 1.0.8 This will ensure that no virtual keyboard / system keyboard is requested. All instances will be closed. ''' for key in list(self._keyboards.keys())[:]: keyboard = self._keyboards[key] if keyboard: keyboard.release() def request_keyboard(self, callback, target, input_type='text'): '''.. versionadded:: 1.0.4 Internal widget method to request the keyboard. This method is rarely required by the end-user as it is handled automatically by the :class:`~kivy.uix.textinput.TextInput`. We expose it in case you want to handle the keyboard manually for unique input scenarios. A widget can request the keyboard, indicating a callback to call when the keyboard is released (or taken by another widget). :Parameters: `callback`: func Callback that will be called when the keyboard is closed. This can be because somebody else requested the keyboard or the user closed it. `target`: Widget Attach the keyboard to the specified `target`. This should be the widget that requested the keyboard. Ensure you have a different target attached to each keyboard if you're working in a multi user mode. .. versionadded:: 1.0.8 `input_type`: string Choose the type of soft keyboard to request. Can be one of 'text', 'number', 'url', 'mail', 'datetime', 'tel', 'address'. .. note:: `input_type` is currently only honored on mobile devices. .. versionadded:: 1.8.0 :Return: An instance of :class:`Keyboard` containing the callback, target, and if the configuration allows it, a :class:`~kivy.uix.vkeyboard.VKeyboard` instance attached as a *.widget* property. .. note:: The behavior of this function is heavily influenced by the current `keyboard_mode`. Please see the Config's :ref:`configuration tokens <configuration-tokens>` section for more information. ''' # release any previous keyboard attached. self.release_keyboard(target) # if we can use virtual vkeyboard, activate it. if self.allow_vkeyboard: keyboard = None # late import global VKeyboard if VKeyboard is None and self._vkeyboard_cls is None: from kivy.uix.vkeyboard import VKeyboard self._vkeyboard_cls = VKeyboard # if the keyboard doesn't exist, create it. key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: vkeyboard = self._vkeyboard_cls() keyboard = Keyboard(widget=vkeyboard, window=self) vkeyboard.bind( on_key_down=keyboard._on_vkeyboard_key_down, on_key_up=keyboard._on_vkeyboard_key_up) self._keyboards[key] = keyboard else: keyboard = self._keyboards[key] # configure vkeyboard keyboard.target = keyboard.widget.target = target keyboard.callback = keyboard.widget.callback = callback # add to the window self.add_widget(keyboard.widget) # only after add, do dock mode keyboard.widget.docked = self.docked_vkeyboard keyboard.widget.setup_mode() else: # system keyboard, just register the callback. keyboard = self._system_keyboard keyboard.callback = callback keyboard.target = target # use system (hardware) keyboard according to flag if self.allow_vkeyboard and self.use_syskeyboard: self.unbind( on_key_down=keyboard._on_window_key_down, on_key_up=keyboard._on_window_key_up) self.bind( on_key_down=keyboard._on_window_key_down, on_key_up=keyboard._on_window_key_up) return keyboard def release_keyboard(self, target=None): '''.. versionadded:: 1.0.4 Internal method for the widget to release the real-keyboard. Check :meth:`request_keyboard` to understand how it works. ''' if self.allow_vkeyboard: key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: return keyboard = self._keyboards[key] callback = keyboard.callback if callback: keyboard.callback = None callback() keyboard.target = None self.remove_widget(keyboard.widget) if key != 'single' and key in self._keyboards: del self._keyboards[key] elif self._system_keyboard.callback: # this way will prevent possible recursion. callback = self._system_keyboard.callback self._system_keyboard.callback = None callback() return True
class Widget(WidgetBase): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch happens `on_touch_move`: Fired when an existing touch is moved `on_touch_up`: Fired when an existing touch disappears .. versionchanged:: 1.0.9 Everything related to event properties has been moved to :class:`~kivy.event.EventDispatcher`. Event properties can now be used in contructing a simple class, without subclassing :class:`Widget`. .. versionchanged:: 1.5.0 Constructor now accept on_* arguments to automatically bind callbacks to properties or events, as the Kv language. ''' __metaclass__ = WidgetMetaclass __events__ = ('on_touch_down', 'on_touch_move', 'on_touch_up') def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() super(Widget, self).__init__(**kwargs) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas(opacity=self.opacity) # Apply all the styles if '__no_builder' not in kwargs: #current_root = Builder.idmap.get('root') #Builder.idmap['root'] = self Builder.apply(self) #if current_root is not None: # Builder.idmap['root'] = current_root #else: # Builder.idmap.pop('root') # Bind all the events for argument in kwargs: if argument[:3] == 'on_': self.bind(**{argument: kwargs[argument]}) @property def proxy_ref(self): '''Return a proxy reference to the widget, ie, without taking a reference of the widget. See `weakref.proxy <http://docs.python.org/2/library/weakref.html?highlight\ =proxy#weakref.proxy>`_ for more information about it. .. versionadded:: 1.7.2 ''' if hasattr(self, '_proxy_ref'): return self._proxy_ref f = partial(_widget_destructor, self.uid) self._proxy_ref = _proxy_ref = proxy(self, f) # only f should be enough here, but it appears that is a very # specific case, the proxy destructor is not called if both f and # _proxy_ref are not together in a tuple _widget_destructors[self.uid] = (f, _proxy_ref) return _proxy_ref def __eq__(self, other): if not isinstance(other, Widget): return False return self.proxy_ref is other.proxy_ref def __hash__(self): return id(self) @property def __self__(self): return self # # Collision # def collide_point(self, x, y): '''Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True ''' return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): '''Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received :Returns: bool. If True, the dispatching of the touch will stop. ''' if self.disabled and self.collide_point(*touch.pos): return True for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. See :meth:`on_touch_down` for more information ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. See :meth:`on_touch_down` for more information ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True def on_disabled(self, instance, value): for child in self.children: child.disabled = value # # Tree management # def add_widget(self, widget, index=0): '''Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, default to 0 *(this attribute have been added in 1.0.5)* Index to insert the widget in the list >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with Widget classes.') widget = widget.__self__ if widget is self: raise WidgetException('You cannot add yourself in a Widget') parent = widget.parent # check if widget is already a child of another widget if parent: raise WidgetException('Cannot add %r, it already has a parent %r' % (widget, parent)) widget.parent = parent = self # child will be disabled if added to a disabled parent if parent.disabled: widget.disabled = True if index == 0 or len(self.children) == 0: self.children.insert(0, widget) self.canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = 0 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) # we never want to insert widget _before_ canvas.before. if next_index == 0 and canvas.has_before: next_index = 1 canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): '''Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return parent = widget.parent self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self): '''Remove all widgets added to this widget. ''' remove_widget = self.remove_widget for child in self.children[:]: remove_widget(child) def get_root_window(self): '''Return the root window. :Returns: Instance of the root window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window. :Returns: Instance of the parent window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_parent_window() def to_widget(self, x, y, relative=False): '''Convert the given coordinate from window to local widget coordinates. ''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinates to window coordinates.''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinates to parent coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate relative positions from widget to its parent. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinates to local coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate coordinates to relative widget coordinates. ''' if relative: return (x - self.x, y - self.y) return (x, y) x = NumericProperty(0) '''X position of the widget. :data:`x` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' y = NumericProperty(0) '''Y position of the widget. :data:`y` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' width = NumericProperty(100) '''Width of the widget. :data:`width` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' height = NumericProperty(100) '''Height of the widget. :data:`height` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :data:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`x`, :data:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :data:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`width`, :data:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget. :data:`right` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width`) ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget. :data:`top` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height`) ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget. :data:`center_x` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width` / 2.) ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget. :data:`center_y` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height` / 2.) ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget. :data:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`center_x`, :data:`center_y`) ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' id = StringProperty(None, allownone=True) '''Unique identifier of the widget in the tree. :data:`id` is a :class:`~kivy.properties.StringProperty`, default to None. .. warning:: If the :data:`id` is already used in the tree, an exception will be raised. ''' children = ListProperty([]) '''List of children of this widget. :data:`children` is a :class:`~kivy.properties.ListProperty` instance, default to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` for manipulating the children list. Don't manipulate the children list directly until you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this widget. :data:`parent` is a :class:`~kivy.properties.ObjectProperty` instance, default to None. The parent of a widget is set when the widget is added to another one, and unset when the widget is removed from its parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. Represents how much space the widget should use in the direction of the X axis, relative to its parent's width. Only :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` make use of the hint. The value is in percent as a float from 0. to 1., where 1. means the full size of his parent. 0.5 represents 50%. :data:`size_hint_x` is a :class:`~kivy.properties.NumericProperty`, default to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :data:`size_hint_y` is a :class:`~kivy.properties.NumericProperty`, default to 1. See :data:`size_hint_x` for more information ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :data:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`size_hint_x`, :data:`size_hint_y`) See :data:`size_hint_x` for more information ''' pos_hint = ObjectProperty({}) '''Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right', 'center_x', will use the parent width. The keys 'y', 'top', 'center_y', will use the parent height. See :doc:`api-kivy.uix.floatlayout` for further reference. Position hint is only used in :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :data:`pos_hint` is a :class:`~kivy.properties.ObjectProperty` containing a dict. ''' ids = DictProperty({}) '''This is a Dictionary of id's defined in your kv language. This will only be populated if you use id's in your kv language code. .. versionadded:: 1.7.0 :data:`ids` is a :class:`~kivy.properties.DictProperty`, defaults to a empty dict {}. ''' opacity = NumericProperty(1.0) '''Opacity of the widget and all the children. .. versionadded:: 1.4.1 The opacity attribute controls the opacity of the widget and its children. Be careful, it's a cumulative attribute: the value is multiplied to the current global opacity, and the result is applied to the current context color. For example: if your parent have an opacity of 0.5, and one children have an opacity of 0.2, the real opacity of the children will be 0.5 * 0.2 = 0.1. Then, the opacity is applied on the shader as:: frag_color = color * vec4(1.0, 1.0, 1.0, opacity); :data:`opacity` is a :class:`~kivy.properties.NumericProperty`, default to 1.0. ''' def on_opacity(self, instance, value): canvas = self.canvas if canvas is not None: canvas.opacity = value canvas = None '''Canvas of the widget. The canvas is a graphics object that contains all the drawing instructions for the graphical representation of the widget. There are no general properties for the Widget class, such as background color, to keep the design simple and lean. Some derived classes, such as Button, do add such convenience properties, but generally the developer is responsible for implementing the graphics representation for a custom widget from the ground up. See the derived widget classes for patterns to follow and extend. See :class:`~kivy.graphics.Canvas` for more information about the usage. ''' disabled = BooleanProperty(False) '''Indicates whether this widget can interact with input or not.
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 tor_auto_on = BooleanProperty() def toggle_tor_auto_on(self, x): self.tor_auto_on = not self.electrum_config.get('tor_auto_on', True) self.electrum_config.set_key('tor_auto_on', self.tor_auto_on, True) 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_dash 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_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() != 'dash': 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_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 _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 self.pause_time = 0 App.__init__(self) #, **kwargs) title = _('Dash Electrum App') self.electrum_config = config = kwargs.get('config', None) self.language = config.get('language', 'en') self.network = network = kwargs.get('network', None) # type: Network self.tor_auto_on = self.electrum_config.get('tor_auto_on', True) 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', []) self.gui_object = kwargs.get('gui_object', None) self.daemon = self.gui_object.daemon self.fx = self.daemon.fx self.use_change = config.get('use_change', 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.fee_status = self.electrum_config.get_fee_status() def on_pr(self, pr): if not self.wallet: self.show_error(_('No wallet loaded.')) return 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_dash.bitcoin import base_decode, is_address data = data.strip() if is_address(data): self.set_URI(data) return if data.startswith('dash:'): self.set_URI(data) return # try to decode transaction from electrum_dash.transaction import Transaction from electrum_dash.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, addr): self.switch_to('receive') self.receive_screen.screen.address = addr def show_pr_details(self, req, status, is_invoice): from electrum_dash.util import format_time requestor = req.get('requestor') exp = req.get('exp') memo = req.get('memo') amount = req.get('amount') fund = req.get('fund') popup = Builder.load_file( 'electrum_dash/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 popup.fund = fund if fund else 0 txid = req.get('txid') popup.tx_hash = txid or '' popup.on_open = lambda: popup.ids.output_list.update( req.get('outputs', [])) popup.export = self.export_private_keys popup.open() def show_addr_details(self, req, status): from electrum_dash.util import format_time fund = req.get('fund') isaddr = 'y' popup = Builder.load_file( 'electrum_dash/gui/kivy/uix/ui_screens/invoice.kv') popup.isaddr = isaddr popup.is_invoice = False popup.status = status popup.requestor = req.get('address') popup.fund = fund if fund else 0 popup.export = self.export_private_keys 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, on_qr_failure) 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.dash.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) 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_dash/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() 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 dash: 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']) if self.network.tor_auto_on and not self.network.tor_on: self.show_tor_warning() # load wallet self.load_wallet_by_name(self.electrum_config.get_wallet_path()) # URI passed in config uri = self.electrum_config.get('url') if uri: self.set_URI(uri) def show_tor_warning(self): from kivy.uix.button import Button from kivy.uix.image import Image from kivy.uix.label import Label from kivy.uix.popup import Popup from kivy.uix.gridlayout import GridLayout docs_uri = self.network.tor_docs_uri def on_docs_press(a): import webbrowser webbrowser.open(docs_uri) warn_box = GridLayout(rows=4, padding=20, spacing=20) popup = Popup(title='Warning', title_align='center', content=warn_box, auto_dismiss=False) img_error = 'atlas://electrum_dash/gui/kivy/theming/light/error' warn_box.add_widget(Image(source=img_error, size_hint_y=0.1)) warn_box.add_widget( Label(text=self.network.tor_warn_msg, text_size=(Window.size[0] - 40 - 32, None))) docs_btn = Button(text=self.network.tor_docs_title, size_hint_y=0.1) warn_box.add_widget(docs_btn) dismiss_btn = Button(text=_('Close'), size_hint_y=0.1) warn_box.add_widget(dismiss_btn) dismiss_btn.bind(on_press=popup.dismiss) docs_btn.bind(on_press=on_docs_press) popup.open() 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) 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(), ask_if_wizard=True) if getattr(wallet.storage, 'backup_message', None): self.show_info(wallet.storage.backup_message) 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 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 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_dash/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() else: popup = Builder.load_file( 'electrum_dash/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_dash.gui.kivy.uix.dialogs') Factory.register('QRCodeWidget', module='electrum_dash.gui.kivy.uix.qrcodewidget') # preload widgets. Remove this if you want to load the widgets on demand #Cache.append('electrum_dash_widgets', 'AnimatedPopup', Factory.AnimatedPopup()) #Cache.append('electrum_dash_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_dash/gui/icons/electrum-dash.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): 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) 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_dash.transaction import TxOutput if run_hook('abort_send', self): return '' inputs = self.wallet.get_spendable_coins(None, self.electrum_config) if not inputs: return '' addr = str( self.send_screen.screen.address) or self.wallet.dummy_address() outputs = [TxOutput(TYPE_ADDRESS, addr, '!')] try: tx = self.wallet.make_unsigned_transaction(inputs, outputs, self.electrum_config) 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 #@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('Dash Electrum', message, app_icon=icon, app_name='Dash 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, 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 show_error(self, error, width='200dp', pos=None, arrow_pos=None, exit=False, icon='atlas://electrum_dash/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_dash/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_dash/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 invoices_dialog(self, screen): from .uix.dialogs.invoices import InvoicesDialog if len(self.wallet.invoices.sorted_list()) == 0: self.show_info(' '.join([ _('No saved invoices.'), _('Signed invoices are saved automatically when you scan them.' ), _('You may also save unsigned requests or contact addresses using the save button.' ) ])) return popup = InvoicesDialog(self, screen, None) popup.update() popup.open() def requests_dialog(self, screen): from .uix.dialogs.requests import RequestsDialog if len(self.wallet.get_sorted_requests(self.electrum_config)) == 0: self.show_info(_('No saved requests.')) return popup = RequestsDialog(self, screen, None) popup.update() popup.open() def addresses_dialog(self, screen): from .uix.dialogs.addresses import AddressesDialog popup = AddressesDialog(self, screen, None) popup.update() popup.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() 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: {}").format(basename)) new_path = self.electrum_config.get_wallet_path() 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 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 ParticleSystem(Widget): max_num_particles = NumericProperty(200) life_span = NumericProperty(2) texture = ObjectProperty(None) texture_path = StringProperty(None) life_span_variance = NumericProperty(0) start_size = NumericProperty(16) start_size_variance = NumericProperty(0) end_size = NumericProperty(16) end_size_variance = NumericProperty(0) emit_angle = NumericProperty(0) emit_angle_variance = NumericProperty(0) start_rotation = NumericProperty(0) start_rotation_variance = NumericProperty(0) end_rotation = NumericProperty(0) end_rotation_variance = NumericProperty(0) emitter_x_variance = NumericProperty(100) emitter_y_variance = NumericProperty(100) gravity_x = NumericProperty(0) gravity_y = NumericProperty(0) speed = NumericProperty(0) speed_variance = NumericProperty(0) radial_acceleration = NumericProperty(100) radial_acceleration_variance = NumericProperty(0) tangential_acceleration = NumericProperty(0) tangential_acceleration_variance = NumericProperty(0) max_radius = NumericProperty(100) max_radius_variance = NumericProperty(0) min_radius = NumericProperty(50) rotate_per_second = NumericProperty(0) rotate_per_second_variance = NumericProperty(0) start_color = ListProperty([1., 1., 1., 1.]) start_color_variance = ListProperty([1., 1., 1., 1.]) end_color = ListProperty([1., 1., 1., 1.]) end_color_variance = ListProperty([1., 1., 1., 1.]) blend_factor_source = NumericProperty(770) blend_factor_dest = NumericProperty(1) emitter_type = NumericProperty(0) update_interval = NumericProperty(1. / 30.) _is_paused = BooleanProperty(False) def __init__(self, config, **kwargs): super(ParticleSystem, self).__init__(**kwargs) self.capacity = 0 self.particles = list() self.particles_dict = dict() self.emission_time = 0.0 self.frame_time = 0.0 self.num_particles = 0 if config is not None: self._parse_config(config) self.emission_rate = self.max_num_particles / self.life_span self.initial_capacity = self.max_num_particles self.max_capacity = self.max_num_particles self._raise_capacity(self.initial_capacity) with self.canvas.before: Callback(self._set_blend_func) with self.canvas.after: Callback(self._reset_blend_func) Clock.schedule_once(self._update, self.update_interval) def start(self, duration=sys.maxint): if self.emission_rate != 0: self.emission_time = duration def stop(self, clear=False): self.emission_time = 0.0 if clear: self.num_particles = 0 self.particles_dict = dict() self.canvas.clear() def on_max_num_particles(self, instance, value): self.max_capacity = value if self.capacity < value: self._raise_capacity(self.max_capacity - self.capacity) elif self.capacity > value: self._lower_capacity(self.capacity - self.max_capacity) self.emission_rate = self.max_num_particles / self.life_span def on_texture(self, instance, value): for p in self.particles: try: self.particles_dict[p]['rect'].texture = self.texture except KeyError: # if particle isn't initialized yet, you can't change its texture. pass def on_life_span(self, instance, value): self.emission_rate = self.max_num_particles / value def _set_blend_func(self, instruction): glBlendFunc(self.blend_factor_source, self.blend_factor_dest) #glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) #glBlendFunc(GL_SRC_ALPHA, GL_ONE) def _reset_blend_func(self, instruction): glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) def _parse_config(self, config): self._config = parse_xml(config) texture_path = self._parse_data('texture', 'name') config_dir_path = os.path.dirname(os.path.abspath(config)) path = os.path.join(config_dir_path, texture_path) if os.path.exists(path): self.texture_path = path else: self.texture_path = texture_path self.texture = Image(self.texture_path).texture self.emitter_x = float(self._parse_data('sourcePosition', 'x')) self.emitter_y = float(self._parse_data('sourcePosition', 'y')) self.emitter_x_variance = float( self._parse_data('sourcePositionVariance', 'x')) self.emitter_y_variance = float( self._parse_data('sourcePositionVariance', 'y')) self.gravity_x = float(self._parse_data('gravity', 'x')) self.gravity_y = float(self._parse_data('gravity', 'y')) self.emitter_type = int(self._parse_data('emitterType')) self.max_num_particles = int(float(self._parse_data('maxParticles'))) self.life_span = max(0.01, float(self._parse_data('particleLifeSpan'))) self.life_span_variance = float( self._parse_data('particleLifespanVariance')) self.start_size = float(self._parse_data('startParticleSize')) self.start_size_variance = float( self._parse_data('startParticleSizeVariance')) self.end_size = float(self._parse_data('finishParticleSize')) self.end_size_variance = float( self._parse_data('FinishParticleSizeVariance')) self.emit_angle = math.radians(float(self._parse_data('angle'))) self.emit_angle_variance = math.radians( float(self._parse_data('angleVariance'))) self.start_rotation = math.radians( float(self._parse_data('rotationStart'))) self.start_rotation_variance = math.radians( float(self._parse_data('rotationStartVariance'))) self.end_rotation = math.radians(float( self._parse_data('rotationEnd'))) self.end_rotation_variance = math.radians( float(self._parse_data('rotationEndVariance'))) self.speed = float(self._parse_data('speed')) self.speed_variance = float(self._parse_data('speedVariance')) self.radial_acceleration = float( self._parse_data('radialAcceleration')) self.radial_acceleration_variance = float( self._parse_data('radialAccelVariance')) self.tangential_acceleration = float( self._parse_data('tangentialAcceleration')) self.tangential_acceleration_variance = float( self._parse_data('tangentialAccelVariance')) self.max_radius = float(self._parse_data('maxRadius')) self.max_radius_variance = float(self._parse_data('maxRadiusVariance')) self.min_radius = float(self._parse_data('minRadius')) self.rotate_per_second = math.radians( float(self._parse_data('rotatePerSecond'))) self.rotate_per_second_variance = math.radians( float(self._parse_data('rotatePerSecondVariance'))) self.start_color = self._parse_color('startColor') self.start_color_variance = self._parse_color('startColorVariance') self.end_color = self._parse_color('finishColor') self.end_color_variance = self._parse_color('finishColorVariance') self.blend_factor_source = self._parse_blend('blendFuncSource') self.blend_factor_dest = self._parse_blend('blendFuncDestination') def _parse_data(self, name, attribute='value'): return self._config.getElementsByTagName(name)[0].getAttribute( attribute) def _parse_color(self, name): return [ float(self._parse_data(name, 'red')), float(self._parse_data(name, 'green')), float(self._parse_data(name, 'blue')), float(self._parse_data(name, 'alpha')) ] def _parse_blend(self, name): value = int(self._parse_data(name)) return BLEND_FUNC[value] def pause(self): self._is_paused = True def resume(self): self._is_paused = False Clock.schedule_once(self._update, self.update_interval) def _update(self, dt): self._advance_time(dt) self._render() if not self._is_paused: Clock.schedule_once(self._update, self.update_interval) def _create_particle(self): return Particle() def _init_particle(self, particle): life_span = random_variance(self.life_span, self.life_span_variance) if life_span <= 0.0: return particle.current_time = 0.0 particle.total_time = life_span particle.x = random_variance(self.emitter_x, self.emitter_x_variance) particle.y = random_variance(self.emitter_y, self.emitter_y_variance) particle.start_x = self.emitter_x particle.start_y = self.emitter_y angle = random_variance(self.emit_angle, self.emit_angle_variance) speed = random_variance(self.speed, self.speed_variance) particle.velocity_x = speed * math.cos(angle) particle.velocity_y = speed * math.sin(angle) particle.emit_radius = random_variance(self.max_radius, self.max_radius_variance) particle.emit_radius_delta = (self.max_radius - self.min_radius) / life_span particle.emit_rotation = random_variance(self.emit_angle, self.emit_angle_variance) particle.emit_rotation_delta = random_variance( self.rotate_per_second, self.rotate_per_second_variance) particle.radial_acceleration = random_variance( self.radial_acceleration, self.radial_acceleration_variance) particle.tangent_acceleration = random_variance( self.tangential_acceleration, self.tangential_acceleration_variance) start_size = random_variance(self.start_size, self.start_size_variance) end_size = random_variance(self.end_size, self.end_size_variance) start_size = max(0.1, start_size) end_size = max(0.1, end_size) particle.scale = start_size / self.texture.width particle.scale_delta = ( (end_size - start_size) / life_span) / self.texture.width # colors start_color = random_color_variance(self.start_color, self.start_color_variance) end_color = random_color_variance(self.end_color, self.end_color_variance) particle.color_delta = [(end_color[i] - start_color[i]) / life_span for i in range(4)] particle.color = start_color # rotation start_rotation = random_variance(self.start_rotation, self.start_rotation_variance) end_rotation = random_variance(self.end_rotation, self.end_rotation_variance) particle.rotation = start_rotation particle.rotation_delta = (end_rotation - start_rotation) / life_span def _advance_particle(self, particle, passed_time): passed_time = min(passed_time, particle.total_time - particle.current_time) particle.current_time += passed_time if self.emitter_type == EMITTER_TYPE_RADIAL: particle.emit_rotation += particle.emit_rotation_delta * passed_time particle.emit_radius -= particle.emit_radius_delta * passed_time particle.x = self.emitter_x - math.cos( particle.emit_rotation) * particle.emit_radius particle.y = self.emitter_y - math.sin( particle.emit_rotation) * particle.emit_radius if particle.emit_radius < self.min_radius: particle.current_time = particle.total_time else: distance_x = particle.x - particle.start_x distance_y = particle.y - particle.start_y distance_scalar = math.sqrt(distance_x * distance_x + distance_y * distance_y) if distance_scalar < 0.01: distance_scalar = 0.01 radial_x = distance_x / distance_scalar radial_y = distance_y / distance_scalar tangential_x = radial_x tangential_y = radial_y radial_x *= particle.radial_acceleration radial_y *= particle.radial_acceleration new_y = tangential_x tangential_x = -tangential_y * particle.tangent_acceleration tangential_y = new_y * particle.tangent_acceleration particle.velocity_x += passed_time * (self.gravity_x + radial_x + tangential_x) particle.velocity_y += passed_time * (self.gravity_y + radial_y + tangential_y) particle.x += particle.velocity_x * passed_time particle.y += particle.velocity_y * passed_time particle.scale += particle.scale_delta * passed_time particle.rotation += particle.rotation_delta * passed_time particle.color = [ particle.color[i] + particle.color_delta[i] * passed_time for i in range(4) ] def _raise_capacity(self, by_amount): old_capacity = self.capacity new_capacity = min(self.max_capacity, self.capacity + by_amount) for i in range(int(new_capacity - old_capacity)): self.particles.append(self._create_particle()) self.num_particles = int(new_capacity) self.capacity = new_capacity def _lower_capacity(self, by_amount): old_capacity = self.capacity new_capacity = max(0, self.capacity - by_amount) for i in range(int(old_capacity - new_capacity)): try: self.canvas.remove( self.particles_dict[self.particles.pop()]['rect']) except: pass self.num_particles = int(new_capacity) self.capacity = new_capacity def _advance_time(self, passed_time): particle_index = 0 # advance existing particles while particle_index < self.num_particles: particle = self.particles[particle_index] if particle.current_time < particle.total_time: self._advance_particle(particle, passed_time) particle_index += 1 else: if particle_index != self.num_particles - 1: next_particle = self.particles[self.num_particles - 1] self.particles[self.num_particles - 1] = particle self.particles[particle_index] = next_particle self.num_particles -= 1 if self.num_particles == 0: Logger.debug('Particle: COMPLETE') # create and advance new particles if self.emission_time > 0: time_between_particles = 1.0 / self.emission_rate self.frame_time += passed_time while self.frame_time > 0: if self.num_particles < self.max_capacity: if self.num_particles == self.capacity: self._raise_capacity(self.capacity) particle = self.particles[self.num_particles] self.num_particles += 1 self._init_particle(particle) self._advance_particle(particle, self.frame_time) self.frame_time -= time_between_particles if self.emission_time != sys.maxint: self.emission_time = max(0.0, self.emission_time - passed_time) def _render(self): if self.num_particles == 0: return for i in range(self.num_particles): particle = self.particles[i] size = (self.texture.size[0] * particle.scale, self.texture.size[1] * particle.scale) if particle not in self.particles_dict: self.particles_dict[particle] = dict() color = particle.color[:] with self.canvas: self.particles_dict[particle]['color'] = Color( color[0], color[1], color[2], color[3]) PushMatrix() self.particles_dict[particle]['translate'] = Translate() self.particles_dict[particle]['rotate'] = Rotate() self.particles_dict[particle]['rotate'].set( particle.rotation, 0, 0, 1) self.particles_dict[particle]['rect'] = Quad( texture=self.texture, points=(-size[0] * 0.5, -size[1] * 0.5, size[0] * 0.5, -size[1] * 0.5, size[0] * 0.5, size[1] * 0.5, -size[0] * 0.5, size[1] * 0.5)) self.particles_dict[particle]['translate'].xy = ( particle.x, particle.y) PopMatrix() else: self.particles_dict[particle][ 'rotate'].angle = particle.rotation self.particles_dict[particle]['translate'].xy = (particle.x, particle.y) self.particles_dict[particle]['color'].rgba = particle.color self.particles_dict[particle]['rect'].points = (-size[0] * 0.5, -size[1] * 0.5, size[0] * 0.5, -size[1] * 0.5, size[0] * 0.5, size[1] * 0.5, -size[0] * 0.5, size[1] * 0.5)
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 BaseButton(ThemableBehavior, ButtonBehavior, SpecificBackgroundColorBehavior, AnchorLayout): """ Abstract base class for all MD buttons. This class handles the button's colors (disabled/down colors handled in children classes as those depend on type of button) as well as the disabled state. """ _md_bg_color_down = ListProperty(None, allownone=True) _md_bg_color_disabled = ListProperty(None, allownone=True) _current_button_color = ListProperty([.0, .0, .0, .0]) theme_text_color = OptionProperty(None, allownone=True, options=['Primary', 'Secondary', 'Hint', 'Error', 'Custom', 'ContrastParentBackground']) text_color = ListProperty(None, allownone=True) opposite_colors = BooleanProperty(False) font_name = StringProperty() font_size = NumericProperty(14) def on_font_name(self, instance, value): instance.ids.lbl_txt.font_name = value def __init__(self, **kwargs): super().__init__(**kwargs) Clock.schedule_once(self._finish_init) def _finish_init(self, dt): self._update_color() def on_md_bg_color(self, instance, value): self._update_color() def _update_color(self): if not self.disabled: self._current_button_color = self.md_bg_color else: self._current_button_color = self.md_bg_color_disabled def _call_get_bg_color_down(self): return self._get_md_bg_color_down() def _get_md_bg_color_down(self): if self._md_bg_color_down: return self._md_bg_color_down else: raise NotImplementedError def _set_md_bg_color_down(self, value): self._md_bg_color_down = value md_bg_color_down = AliasProperty(_call_get_bg_color_down, _set_md_bg_color_down) def _call_get_bg_color_disabled(self): return self._get_md_bg_color_disabled() def _get_md_bg_color_disabled(self): if self._md_bg_color_disabled: return self._md_bg_color_disabled else: raise NotImplementedError def _set_md_bg_color_disabled(self, value): self._md_bg_color_disabled = value md_bg_color_disabled = AliasProperty(_call_get_bg_color_disabled, _set_md_bg_color_disabled) def on_disabled(self, instance, value): if self.disabled: self._current_button_color = self.md_bg_color_disabled else: self._current_button_color = self.md_bg_color
class EngineWidget(Widget): """ Manages all aspects of gameplay and game display. """ score = NumericProperty(0) def __init__(self, settings, **kwargs): super(EngineWidget, self).__init__(**kwargs) # Register game-ending events. self.register_event_type('on_game_over') self.register_event_type('on_win') # Store some important values. self.win_score = settings['num_gold'] self.start_time = time.time() # Create maze widget. maze = Maze(2 * settings['maze_width'] + 1, 2 * settings['maze_height'] + 1) maze.generate() self.maze_widget = MazeWidget(maze) self.add_widget(self.maze_widget, 1) # Get a cycling list of the empty cells in the maze in a random order; # this is used to place game objects in the maze so that they do not # overlap unless necessary. empty_cells = itertools.cycle(maze.get_empty_cells()) # Create player widget. x, y = next(empty_cells) player = Player(x, y, maze) self.player_widget = PlayerWidget(player, self) self.add_widget(self.player_widget, 2) self.non_player_object_widgets = [] # Create enemy widgets. for i in range(0, settings['num_enemies']): x, y = next(empty_cells) enemy_widget = EnemyWidget(Enemy(x, y, maze), self, speed=settings['enemy_speed']) self.non_player_object_widgets.append(enemy_widget) self.add_widget(enemy_widget, 3) # Create gold widgets. for i in range(0, settings['num_gold']): x, y = next(empty_cells) gold_widget = GoldWidget(Gold(x, y, maze), self) self.non_player_object_widgets.append(gold_widget) self.add_widget(gold_widget, 4) # Create crystal widgets. for i in range(0, settings['num_crystals']): x, y = next(empty_cells) crystal_widget = CrystalWidget(Crystal(x, y, maze), self) self.non_player_object_widgets.append(crystal_widget) self.add_widget(crystal_widget, 5) # reposition player when window is resized self.center_player() self.bind(size=self.center_player) # bind relevant keyboard events self._keyboard = Window.request_keyboard(self._keyboard_closed, self) self._keyboard.bind(on_key_down=self._on_keyboard_down, on_key_up=self._on_keyboard_up) self.update_event = Clock.schedule_interval(self.update, 0.01) def _keyboard_closed(self): self._keyboard.unbind(on_key_down=self._on_keyboard_down) self._keyboard = None def _on_keyboard_down(self, keyboard, keycode, text, modifiers): if keycode[1] in ['up', 'down', 'left', 'right']: self.player_widget.direction.add(keycode[1]) self.player_widget.move() def _on_keyboard_up(self, keyboard, keycode, *args): if keycode[1] in ['up', 'down', 'left', 'right']: self.player_widget.direction.discard(keycode[1]) def remove_game_object(self, game_object_widget): self.non_player_object_widgets.remove(game_object_widget) self.remove_widget(game_object_widget) def check_collisions(self): for widget in self.non_player_object_widgets: if widget.collide_widget(self.player_widget): widget.game_object.collide(self.player_widget.game_object) def update(self, *args): self.maze_widget.x = self.player_widget.x - self.player_widget.rel_x self.maze_widget.y = self.player_widget.y - self.player_widget.rel_y self.maze_widget.redraw() for widget in self.non_player_object_widgets: widget.pos = (widget.rel_x + self.maze_widget.x, widget.rel_y + self.maze_widget.y) self.check_collisions() self.player_widget.check_state() for widget in self.non_player_object_widgets: widget.check_state() def on_game_over(self, *args): print("you lose") self.update_event.cancel() def on_win(self, *args): self.update_event.cancel() def center_player(self, *args): self.player_widget.pos = (self.width / 2, self.height / 2)
class PongPaddle(Widget): score = NumericProperty(0) def bounce_ball(self, ball): if self.collide_widget(ball): ball.velocity_x *= -1.1
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_sum = 0. has_bound = False hint = [None] * len_children # min size from all the None hint, and from those with sh_min minimum_size_bounded = 0 if orientation == 'horizontal': minimum_size_y = 0 minimum_size_none = padding_x + spacing * (len_children - 1) for i, ((w, h), (shw, shh), _, (shw_min, shh_min), (shw_max, _)) in enumerate(sizes): if shw is None: minimum_size_none += w else: hint[i] = shw if shw_min: has_bound = True minimum_size_bounded += shw_min elif shw_max is not None: has_bound = True stretch_sum += shw if shh is None: minimum_size_y = max(minimum_size_y, h) elif shh_min: minimum_size_y = max(minimum_size_y, shh_min) minimum_size_x = minimum_size_bounded + minimum_size_none minimum_size_y += padding_y else: minimum_size_x = 0 minimum_size_none = padding_y + spacing * (len_children - 1) for i, ((w, h), (shw, shh), _, (shw_min, shh_min), (_, shh_max)) in enumerate(sizes): if shh is None: minimum_size_none += h else: hint[i] = shh if shh_min: has_bound = True minimum_size_bounded += shh_min elif shh_max is not None: has_bound = True stretch_sum += shh if shw is None: minimum_size_x = max(minimum_size_x, w) elif shw_min: minimum_size_x = max(minimum_size_x, shw_min) minimum_size_y = minimum_size_bounded + minimum_size_none minimum_size_x += padding_x self.minimum_size = minimum_size_x, minimum_size_y # do not move the w/h get above, it's likely to change on above line selfx = self.x selfy = self.y if orientation == 'horizontal': stretch_space = max(0.0, self.width - minimum_size_none) dim = 0 else: stretch_space = max(0.0, self.height - minimum_size_none) dim = 1 if has_bound: # make sure the size_hint_min/max are not violated if stretch_space < 1e-9: # there's no space, so just set to min size or zero stretch_sum = stretch_space = 1. for i, val in enumerate(sizes): sh = val[1][dim] if sh is None: continue sh_min = val[3][dim] if sh_min is not None: hint[i] = sh_min else: hint[i] = 0. # everything else is zero else: # hint gets updated in place self.layout_hint_with_bounds(stretch_sum, stretch_space, minimum_size_bounded, (val[3][dim] for val in sizes), (elem[4][dim] for elem in sizes), hint) if orientation == 'horizontal': x = padding_left + selfx size_y = self.height - padding_y for i, (sh, ((w, h), (_, shh), pos_hint, _, _)) in enumerate(zip(reversed(hint), reversed(sizes))): cy = selfy + padding_bottom if sh: w = max(0., stretch_space * sh / stretch_sum) if shh: h = max(0, shh * size_y) for key, value in pos_hint.items(): posy = value * size_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, x, cy, w, h x += w + spacing else: y = padding_bottom + selfy size_x = self.width - padding_x for i, (sh, ((w, h), (shw, _), pos_hint, _, _)) in enumerate(zip(hint, sizes)): cx = selfx + padding_left if sh: h = max(0., stretch_space * sh / stretch_sum) if shw: w = max(0, shw * size_x) for key, value in pos_hint.items(): posx = value * size_x if key == 'x': cx += posx elif key == 'right': cx += posx - w elif key == 'center_x': cx += posx - (w / 2.) yield i, cx, y, 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, c.size_hint_min, c.size_hint_max) 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 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). :data:`background_color` is a :class:`~kivy.properties.ListProperty`, default 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 :data:`background_image`. Can be changed for a custom background. It must be a list of four values: (top, right, bottom, left). Read the BorderImage instructions for more information. :data:`border` is a :class:`~kivy.properties.ListProperty`, default to (16, 16, 16, 16) ''' background_image = StringProperty('atlas://data/images/defaulttheme/tab') '''Background image of the main shared content object. :data:`background_image` is a :class:`~kivy.properties.StringProperty`, default 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 :data:`background_disabled_image` is a :class:`~kivy.properties.StringProperty`, default to 'atlas://data/images/defaulttheme/tab'. ''' _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 select or active tab. .. versionadded:: 1.4.0 :data:`current_tab` is a :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`. :data:`tab_pos` is a :class:`~kivy.properties.OptionProperty`, default to 'bottom_mid'. ''' tab_height = NumericProperty('40dp') '''Specifies the height of the tab header. :data:`tab_height` is a :class:`~kivy.properties.NumericProperty`, default to 40. ''' tab_width = NumericProperty('100dp', allownone=True) '''Specifies the width of the tab header. :data:`tab_width` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' do_default_tab = BooleanProperty(True) '''Specifies weather a default_tab head is provided. .. versionadded:: 1.5.0 :data:`do_default_tab` is a :class:`~kivy.properties.BooleanProperty`, defaults to 'True'. ''' default_tab_text = StringProperty('Default tab') '''Specifies the text displayed on the default tab header. :data:`default_tab_text` is a :class:`~kivy.properties.StringProperty`, 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` :data:`default_tab_cls` is a :class:`~kivy.properties.ObjectProperty`, default to `TabbedPanelHeader`. ''' 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. :data:`tab_list` is a :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. :data:`content` is a :class:`~kivy.properties.ObjectProperty`, default 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 not just the auto provided one. :data:`default_tab` is a :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. :data:`default_tab_content` is a :class:`~kivy.properties.AliasProperty` ''' 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._tab_layout = GridLayout(rows=1) self.rows = 1 self._tab_strip = TabbedPanelStrip(tabbed_panel=self, rows=1, cols=99, 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.bind(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): '''Switch to a specific panel header. ''' 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) 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) widget.group = '__tab%r__' % self_tabs.uid self.on_tab_width() else: widget.pos_hint = {'x': 0, 'top': 1} 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: 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_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): Clock.unschedule(self._update_tab_width) Clock.schedule_once(self._update_tab_width, 0) 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 not issubclass(cls, TabbedPanelHeader): raise TabbedPanelException('`default_tab_class` should be\ subclassed from `TabbedPanelHeader`') # no need to instanciate 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): Clock.unschedule(self._update_tabs) Clock.schedule_once(self._update_tabs, 0) 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 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 Clock.unschedule(partial(self._updt_top, sctr, top, scrl_v_width)) 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 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 `10`. """ 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 TransitionBase(EventDispatcher): '''TransitionBase is used to animate 2 screens within the :class:`ScreenManager`. This class acts as a base for other implementations like the :class:`SlideTransition` and :class:`SwapTransition`. :Events: `on_progress`: Transition object, progression float Fired during the animation of the transition. `on_complete`: Transition object Fired when the transition is fininshed. ''' screen_out = ObjectProperty() '''Property that contains the screen to hide. Automatically set by the :class:`ScreenManager`. :class:`screen_out` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' screen_in = ObjectProperty() '''Property that contains the screen to show. Automatically set by the :class:`ScreenManager`. :class:`screen_in` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' duration = NumericProperty(.4) '''Duration in seconds of the transition. :class:`duration` is a :class:`~kivy.properties.NumericProperty` and defaults to .4 (= 400ms). .. versionchanged:: 1.8.0 Default duration has been changed from 700ms to 400ms. ''' manager = ObjectProperty() ''':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. ''' is_active = BooleanProperty(False) '''Indicate whether the transition is currently active or not. :attr:`is_active` is a :class:`~kivy.properties.BooleanProperty` and defaults to False, read-only. ''' # privates _anim = ObjectProperty(allownone=True) __events__ = ('on_progress', 'on_complete') def start(self, manager): '''(internal) Starts the transition. This is automatically called by the :class:`ScreenManager`. ''' if self.is_active: raise ScreenManagerException('start() is called twice!') self.manager = manager self._anim = Animation(d=self.duration, s=0) self._anim.bind(on_progress=self._on_progress, on_complete=self._on_complete) self.add_screen(self.screen_in) self.screen_in.transition_progress = 0. self.screen_in.transition_state = 'in' self.screen_out.transition_progress = 0. self.screen_out.transition_state = 'out' self.screen_in.dispatch('on_pre_enter') self.screen_out.dispatch('on_pre_leave') self.is_active = True self._anim.start(self) self.dispatch('on_progress', 0) def stop(self): '''(internal) Stops the transition. This is automatically called by the :class:`ScreenManager`. ''' if self._anim: self._anim.cancel(self) self.dispatch('on_complete') self._anim = None self.is_active = False def add_screen(self, screen): '''(internal) Used to add a screen to the :class:`ScreenManager`. ''' self.manager.real_add_widget(screen) def remove_screen(self, screen): '''(internal) Used to remove a screen from the :class:`ScreenManager`. ''' self.manager.real_remove_widget(screen) def on_complete(self): self.remove_screen(self.screen_out) def on_progress(self, progression): pass def _on_progress(self, *l): progress = l[-1] self.screen_in.transition_progress = progress self.screen_out.transition_progress = 1. - progress self.dispatch('on_progress', progress) def _on_complete(self, *l): self.is_active = False self.dispatch('on_complete') self.screen_in.dispatch('on_enter') self.screen_out.dispatch('on_leave') self._anim = None
class TestGame(Widget): entity_id = NumericProperty(None) def on_kv_post(self, *args): self.uuids = {} self.gameworld.init_gameworld(['position', 'poly_renderer'], callback=self.init_game) def init_game(self): self.setup_states() self.set_state() self.load_svg() def on_touch_move(self, touch): if self.entity_id is not None: entity = self.gameworld.entities[self.entity_id] position = entity.position position.x += touch.dx position.y += touch.dy def load_svg(self): model_manager = self.gameworld.model_manager data = model_manager.get_model_info_for_svg("tiger.svg") load_model_from_model_info = model_manager.load_model_from_model_info init_entity = self.gameworld.init_entity model_data = data['model_info'] svg_name = data['svg_name'] model_infos = [] entity_to_copy = None final_infos = model_manager.combine_model_infos(model_data) svg_bounds = model_manager.get_center_and_bbox_from_infos(final_infos) center = svg_bounds['center'] neg_center = [-center[0], -center[1]] for model_info in final_infos: model_name = load_model_from_model_info(model_info, svg_name) model = model_manager.models[model_name] Logger.info(model.vertex_count) model.add_all_vertex_attribute('pos', neg_center) create_dict = { 'position': (300, 300), 'poly_renderer': { 'model_key': model_name }, } if entity_to_copy is not None: create_dict['position'] = entity_to_copy ent = init_entity(create_dict, ['position', 'poly_renderer']) if entity_to_copy is None: entity_to_copy = self.gameworld.entities[ent] self.entity_id = ent def setup_states(self): self.gameworld.add_state(state_name='main', systems_added=[], systems_removed=[], systems_paused=[], systems_unpaused=[], screenmanager_screen='main') def set_state(self): self.gameworld.state = 'main'
class Switch(Widget): '''Switch class. See module documentation for more information. ''' active = BooleanProperty(False) '''Indicate whether the switch is active or inactive. :data:`active` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' touch_control = ObjectProperty(None, allownone=True) '''(internal) Contains the touch that currently interacts with the switch. :data:`touch_control` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' touch_distance = NumericProperty(0) '''(internal) Contains the distance between the initial position of the touch and the current position to determine if the swipe is from the left or right. :data:`touch_distance` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' active_norm_pos = NumericProperty(0) '''(internal) Contains the normalized position of the movable element inside the switch, in the 0-1 range. :data:`active_norm_pos` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' def on_touch_down(self, touch): if self.disabled or self.touch_control is not None: return if not self.collide_point(*touch.pos): return touch.grab(self) self.touch_distance = 0 self.touch_control = touch return True def on_touch_move(self, touch): if touch.grab_current is not self: return self.touch_distance = touch.x - touch.ox return True def on_touch_up(self, touch): if touch.grab_current is not self: return touch.ungrab(self) # depending of the distance, activate by norm pos or invert if abs(touch.ox - touch.x) < 5: self.active = not self.active else: self.active = self.active_norm_pos > 0.5 Animation(active_norm_pos=int(self.active), t='out_quad', d=.2).start(self) self.touch_control = None return True
class AKFloatingWindow(ThemableBehavior, RectangularElevationBehavior, BoxLayout): _window_active = BooleanProperty(False) header_height = NumericProperty("20dp") header_color_normal = ListProperty() header_color_active = ListProperty() header_text_color = ListProperty() title_font_size = NumericProperty("10dp") window_title = StringProperty() window_elevation = NumericProperty(10) fade_exit = BooleanProperty(True) fade_open = BooleanProperty(True) animation_transition = StringProperty("out_quad") animation_duration = NumericProperty(0.1) open_position = ListProperty() maximize_animation = BooleanProperty(True) minimize_animation = BooleanProperty(True) window_radius = NumericProperty("8dp") bg_color = ListProperty() exit_button_icon = StringProperty("close") exit_button_color = ListProperty() exit_button_text_color = ListProperty() max_button_icon = StringProperty("window-maximize") max_button_color = ListProperty() max_button_text_color = ListProperty() bottom_widget = ObjectProperty() left_widget = ObjectProperty() right_widget = ObjectProperty() top_widget = ObjectProperty() _state = "close" _maximized = False _resize_click_dis = NumericProperty("10dp") _allow_resize = False _size_before = False _pos_before = False def __init__(self, **kwargs): super().__init__(**kwargs) Clock.schedule_once(lambda x: self._update()) @property def state(self): return self._state @property def maximized(self): return self._maximized def _update(self): if not self.header_color_active: self.header_color_active = self.theme_cls.primary_color if not self.header_color_normal: self.header_color_normal = self.theme_cls.primary_light self.elevation = self.window_elevation exit_pos = [-self.width, -self.height] self.pos = exit_pos self.opacity = 0 self._state = "close" def _update_open_pos(self): self.open_position = [ Window.size[0] / 2 - self.width / 2, Window.size[1] / 2 - self.height / 2, ] def add_widget(self, widget, index=0, canvas=None): if issubclass(widget.__class__, WindowContent) or issubclass( widget.__class__, WindowHeader): return super().add_widget(widget, index=index, canvas=canvas) else: return self.ids.content.add_widget(widget) def dismiss(self): if self._state == "close": return exit_pos = [-self.width, -self.height] if self.fade_exit: (Animation( opacity=0, t=self.animation_transition, duration=self.animation_duration, ) + Animation(pos=exit_pos, duration=0)).start(self) else: self.opacity = 0 self.pos = exit_pos self._state = "close" def open(self): self.parent._bring_to_front(self) self.parent._update_header_color(self) self._update_open_pos() if self._state == "open": return if self.fade_open: anim = Animation(pos=self.open_position, duration=0) + Animation( opacity=1, t=self.animation_transition, duration=self.animation_duration, ) anim.start(self) else: self.opacity = 1 self.pos = self.open_position self._state = "open" def minimize_to_normal(self): pos = self._pos_before size = self._size_before if self.minimize_animation: anim = Animation( pos=pos, size=size, duration=self.animation_duration, t=self.animation_transition, ) anim.start(self) else: self.pos = pos self.size = size self._maximized = False def maximize(self): if self._maximized: self.minimize_to_normal() return self._size_before = [] self._pos_before = [] self._size_before += [self.size[0], self.size[1]] self._pos_before += [self.pos[0], self.pos[1]] if not self.bottom_widget: y = 0 else: y = self.bottom_widget.y + self.bottom_widget.height if not self.top_widget: top = Window.size[1] else: top = self.top_widget.y if not self.left_widget: x = 0 else: x = self.left_widget.x + self.left_widget.width if not self.right_widget: right = Window.size[0] else: right = self.right_widget.x pos = [x, y] size = [right - x, top - y] if self.maximize_animation: anim = Animation( pos=pos, size=size, duration=self.animation_duration, t=self.animation_transition, ) anim.start(self) else: self.pos = pos self.size = size self._maximized = True def on_touch_down(self, touch): touch_pos = touch.pos window_right = self.x + self.width window_bottom = self.y x = window_right - self._resize_click_dis right = window_right + self._resize_click_dis y = window_bottom - self._resize_click_dis top = window_bottom + self._resize_click_dis if x < touch_pos[0] < right and y < touch_pos[1] < top: self._allow_resize = True Window.set_system_cursor("size_nwse") return super().on_touch_down(touch) def on_touch_up(self, touch): self._allow_resize = False Window.set_system_cursor("arrow") return super().on_touch_up(touch) def on_touch_move(self, touch): if self._allow_resize: touch_pos = touch.pos width = touch_pos[0] - self.x height = self.y + self.height - touch_pos[1] self.size = [width, height] self.pos = [self.x, touch_pos[1]] return super().on_touch_move(touch)
class ShowcaseApp(App): index = NumericProperty(-1) current_title = StringProperty() time = NumericProperty(0) show_sourcecode = BooleanProperty(False) sourcecode = StringProperty() def build(self): Clock.schedule_interval(self._update_clock, 1 / 60.) self.screens = {} self.available_screens = [ 'buttons', 'togglebutton', 'sliders', 'progressbar', 'switchs', 'checkboxs', 'textinputs', 'accordions', 'filechoosers', 'carousels', 'bubbles', 'codeinput', 'dropdown', 'spinner', 'scatter', 'splitter', 'tabbedpanel', 'rstdocument', 'screenmanager' ] curdir = dirname(__file__) self.available_screens = [ join(curdir, 'data', 'screens', '{}.kv'.format(fn)) for fn in self.available_screens ] self.go_next_screen() def go_previous_screen(self): self.index = (self.index - 1) % len(self.available_screens) screen = self.load_screen(self.index) sm = self.root.ids.sm sm.switch_to(screen, direction='right') self.current_title = screen.name self.update_sourcecode() def go_next_screen(self): self.index = (self.index + 1) % len(self.available_screens) screen = self.load_screen(self.index) sm = self.root.ids.sm sm.switch_to(screen, direction='left') self.current_title = screen.name self.update_sourcecode() def load_screen(self, index): if index in self.screens: return self.screens[index] screen = Builder.load_file(self.available_screens[index]) self.screens[index] = screen return screen def read_sourcecode(self): fn = self.available_screens[self.index] with open(fn) as fd: return fd.read() def toggle_source_code(self): self.show_sourcecode = not self.show_sourcecode if self.show_sourcecode: height = self.root.height * .3 else: height = 0 Animation(height=height, d=.3, t='out_quart').start(self.root.ids.sv) self.update_sourcecode() def update_sourcecode(self): if not self.show_sourcecode: return self.root.ids.sourcecode.text = self.read_sourcecode() self.root.ids.sv.scroll_y = 1 def _update_clock(self, dt): self.time = time()
class Car(Widget): angle = NumericProperty(0) rotation = NumericProperty(0) velocity_x = NumericProperty(0) velocity_y = NumericProperty(0) velocity = ReferenceListProperty(velocity_x, velocity_y) sensor1_x = NumericProperty(0) sensor1_y = NumericProperty(0) sensor1 = ReferenceListProperty(sensor1_x, sensor1_y) sensor2_x = NumericProperty(0) sensor2_y = NumericProperty(0) sensor2 = ReferenceListProperty(sensor2_x, sensor2_y) sensor3_x = NumericProperty(0) sensor3_y = NumericProperty(0) sensor3 = ReferenceListProperty(sensor3_x, sensor3_y) signal1 = NumericProperty(0) signal2 = NumericProperty(0) signal3 = NumericProperty(0) def move(self, rotation): self.pos = Vector(*self.velocity) + self.pos self.rotation = rotation self.angle = self.angle + self.rotation self.sensor1 = Vector(30, 0).rotate(self.angle) + self.pos self.sensor2 = Vector(30, 0).rotate((self.angle + 30) % 360) + self.pos self.sensor3 = Vector(30, 0).rotate((self.angle - 30) % 360) + self.pos self.signal1 = int( np.sum(sand[int(self.sensor1_x) - 10:int(self.sensor1_x) + 10, int(self.sensor1_y) - 10:int(self.sensor1_y) + 10])) / 400. self.signal2 = int( np.sum(sand[int(self.sensor2_x) - 10:int(self.sensor2_x) + 10, int(self.sensor2_y) - 10:int(self.sensor2_y) + 10])) / 400. self.signal3 = int( np.sum(sand[int(self.sensor3_x) - 10:int(self.sensor3_x) + 10, int(self.sensor3_y) - 10:int(self.sensor3_y) + 10])) / 400. if self.sensor1_x > longueur - 10 or self.sensor1_x < 10 or self.sensor1_y > largeur - 10 or self.sensor1_y < 10: self.signal1 = 1. if self.sensor2_x > longueur - 10 or self.sensor2_x < 10 or self.sensor2_y > largeur - 10 or self.sensor2_y < 10: self.signal2 = 1. if self.sensor3_x > longueur - 10 or self.sensor3_x < 10 or self.sensor3_y > largeur - 10 or self.sensor3_y < 10: self.signal3 = 1.
class ColorWheel(Widget): '''Chromatic wheel for the ColorPicker. .. versionchanged:: 1.7.1 `font_size`, `font_name` and `foreground_color` have been removed. The sizing is now the same as others widget, based on 'sp'. Orientation is also automatically determined according to the width/height ratio. ''' r = BoundedNumericProperty(0, min=0, max=1) '''The Red value of the color currently selected. :attr:`r` is a :class:`~kivy.properties.BoundedNumericProperty` and can be a value from 0 to 1. It defaults to 0. ''' g = BoundedNumericProperty(0, min=0, max=1) '''The Green value of the color currently selected. :attr:`g` is a :class:`~kivy.properties.BoundedNumericProperty` and can be a value from 0 to 1. ''' b = BoundedNumericProperty(0, min=0, max=1) '''The Blue value of the color currently selected. :attr:`b` is a :class:`~kivy.properties.BoundedNumericProperty` and can be a value from 0 to 1. ''' a = BoundedNumericProperty(0, min=0, max=1) '''The Alpha value of the color currently selected. :attr:`a` is a :class:`~kivy.properties.BoundedNumericProperty` and can be a value from 0 to 1. ''' color = ReferenceListProperty(r, g, b, a) '''The holds the color currently selected. :attr:`color` is a :class:`~kivy.properties.ReferenceListProperty` and contains a list of `r`, `g`, `b`, `a` values. ''' _origin = ListProperty((100, 100)) _radius = NumericProperty(100) _piece_divisions = NumericProperty(10) _pieces_of_pie = NumericProperty(16) _inertia_slowdown = 1.25 _inertia_cutoff = .25 _num_touches = 0 _pinch_flag = False _hsv = ListProperty([1, 1, 1, 0]) def __init__(self, **kwargs): super(ColorWheel, self).__init__(**kwargs) pdv = self._piece_divisions self.sv_s = [(float(x) / pdv, 1) for x in range(pdv)] + [(1, float(y) / pdv) for y in reversed(range(pdv))] def on__origin(self, instance, value): self.init_wheel(None) def on__radius(self, instance, value): self.init_wheel(None) def init_wheel(self, dt): # initialize list to hold all meshes self.canvas.clear() self.arcs = [] self.sv_idx = 0 pdv = self._piece_divisions ppie = self._pieces_of_pie for r in range(pdv): for t in range(ppie): self.arcs.append( _ColorArc(self._radius * (float(r) / float(pdv)), self._radius * (float(r + 1) / float(pdv)), 2 * pi * (float(t) / float(ppie)), 2 * pi * (float(t + 1) / float(ppie)), origin=self._origin, color=(float(t) / ppie, self.sv_s[self.sv_idx + r][0], self.sv_s[self.sv_idx + r][1], 1))) self.canvas.add(self.arcs[-1]) def recolor_wheel(self): ppie = self._pieces_of_pie for idx, segment in enumerate(self.arcs): segment.change_color(sv=self.sv_s[int(self.sv_idx + idx / ppie)]) def change_alpha(self, val): for idx, segment in enumerate(self.arcs): segment.change_color(a=val) def inertial_incr_sv_idx(self, dt): # if its already zoomed all the way out, cancel the inertial zoom if self.sv_idx == len(self.sv_s) - self._piece_divisions: return False self.sv_idx += 1 self.recolor_wheel() if dt * self._inertia_slowdown > self._inertia_cutoff: return False else: Clock.schedule_once(self.inertial_incr_sv_idx, dt * self._inertia_slowdown) def inertial_decr_sv_idx(self, dt): # if its already zoomed all the way in, cancel the inertial zoom if self.sv_idx == 0: return False self.sv_idx -= 1 self.recolor_wheel() if dt * self._inertia_slowdown > self._inertia_cutoff: return False else: Clock.schedule_once(self.inertial_decr_sv_idx, dt * self._inertia_slowdown) def on_touch_down(self, touch): r = self._get_touch_r(touch.pos) if r > self._radius: return False # code is still set up to allow pinch to zoom, but this is # disabled for now since it was fiddly with small wheels. # Comment out these lines and adjust on_touch_move to reenable # this. if self._num_touches != 0: return False touch.grab(self) self._num_touches += 1 touch.ud['anchor_r'] = r touch.ud['orig_sv_idx'] = self.sv_idx touch.ud['orig_time'] = Clock.get_time() def on_touch_move(self, touch): if touch.grab_current is not self: return r = self._get_touch_r(touch.pos) goal_sv_idx = (touch.ud['orig_sv_idx'] - int( (r - touch.ud['anchor_r']) / (float(self._radius) / self._piece_divisions))) if (goal_sv_idx != self.sv_idx and goal_sv_idx >= 0 and goal_sv_idx <= len(self.sv_s) - self._piece_divisions): # this is a pinch to zoom self._pinch_flag = True self.sv_idx = goal_sv_idx self.recolor_wheel() def on_touch_up(self, touch): if touch.grab_current is not self: return touch.ungrab(self) self._num_touches -= 1 if self._pinch_flag: if self._num_touches == 0: # user was pinching, and now both fingers are up. Return # to normal if self.sv_idx > touch.ud['orig_sv_idx']: Clock.schedule_once( self.inertial_incr_sv_idx, (Clock.get_time() - touch.ud['orig_time']) / (self.sv_idx - touch.ud['orig_sv_idx'])) if self.sv_idx < touch.ud['orig_sv_idx']: Clock.schedule_once( self.inertial_decr_sv_idx, (Clock.get_time() - touch.ud['orig_time']) / (self.sv_idx - touch.ud['orig_sv_idx'])) self._pinch_flag = False return else: # user was pinching, and at least one finger remains. We # don't want to treat the remaining fingers as touches return else: r, theta = rect_to_polar(self._origin, *touch.pos) # if touch up is outside the wheel, ignore if r >= self._radius: return # compute which ColorArc is being touched (they aren't # widgets so we don't get collide_point) and set # _hsv based on the selected ColorArc piece = int((theta / (2 * pi)) * self._pieces_of_pie) division = int((r / self._radius) * self._piece_divisions) self._hsv = \ self.arcs[self._pieces_of_pie * division + piece].color def on__hsv(self, instance, value): c_hsv = Color(*value, mode='hsv') self.r = c_hsv.r self.g = c_hsv.g self.b = c_hsv.b self.a = c_hsv.a self.rgba = (self.r, self.g, self.b, self.a) def _get_touch_r(self, pos): return distance(pos, self._origin)
class Car(Widget): angle = NumericProperty(0) #between x axis and car direction rotation = NumericProperty(0) #latest rotation (0, 20, -20) velocity_x = NumericProperty(0) #x coord velocity_y = NumericProperty(0) #Y coord velocity = ReferenceListProperty(velocity_x, velocity_y) # 3 sensors #sensor 1 = front sensor1_x = NumericProperty(0) sensor1_y = NumericProperty(0) sensor1 = ReferenceListProperty(sensor1_x, sensor1_y) #sensor 2 = left sensor2_x = NumericProperty(0) sensor2_y = NumericProperty(0) sensor2 = ReferenceListProperty(sensor2_x, sensor2_y) #sensor 3 = right sensor3_x = NumericProperty(0) sensor3_y = NumericProperty(0) sensor3 = ReferenceListProperty(sensor3_x, sensor3_y) #signals received from each sensor signal1 = NumericProperty(0) #density around sensor signal2 = NumericProperty(0) signal3 = NumericProperty(0) #density = number of 1 squares in the 200x200 block, divided by total number of squares # EG: 5 squares, so 5 / (20**2) = 5 / 400 #handles car movement def move(self, rotation): self.pos = Vector( *self.velocity) + self.pos #updating position with velocity vector self.rotation = rotation self.angle = self.angle + self.rotation #when car rotates, sensors rotate as well #30 is the distance between car and sensors self.sensor1 = Vector(30, 0).rotate(self.angle) + self.pos self.sensor2 = Vector(30, 0).rotate((self.angle + 30) % 360) + self.pos self.sensor3 = Vector(30, 0).rotate((self.angle - 30) % 360) + self.pos #get x coord of sensor, then take all cells from -10 to 10, getting a square of 20x20, same for y #Sum of all 1's around a sensor, divide by 400 to get density of sand around sensor self.signal1 = int( np.sum(sand[int(self.sensor1_x) - 10:int(self.sensor1_x) + 10, int(self.sensor1_y) - 10:int(self.sensor1_y) + 10])) / 400. self.signal2 = int( np.sum(sand[int(self.sensor2_x) - 10:int(self.sensor2_x) + 10, int(self.sensor2_y) - 10:int(self.sensor2_y) + 10])) / 400. self.signal3 = int( np.sum(sand[int(self.sensor3_x) - 10:int(self.sensor3_x) + 10, int(self.sensor3_y) - 10:int(self.sensor3_y) + 10])) / 400. #if sensor 1 approaches RIGHT or LEFT edge of the map, or if approaching UPPER or LOWER edge of map if self.sensor1_x > longueur - 10 or self.sensor1_x < 10 or self.sensor1_y > largeur - 10 or self.sensor1_y < 10: self.signal1 = 1. #maximum sand density, signal to stop- NEGATIVE REWARD if self.sensor2_x > longueur - 10 or self.sensor2_x < 10 or self.sensor2_y > largeur - 10 or self.sensor2_y < 10: self.signal2 = 1. if self.sensor3_x > longueur - 10 or self.sensor3_x < 10 or self.sensor3_y > largeur - 10 or self.sensor3_y < 10: self.signal3 = 1.
class ModifiedOneLineIconListItem(ContainerSupport, ModifiedOneLineListItem): _txt_left_pad = NumericProperty(dp(72))
class CircleWithText(Widget): text = StringProperty("0") player = OptionProperty("B", options=["B", "W"]) min_size = NumericProperty(50)
class Car(Widget): angle = NumericProperty( 0 ) # initializing the angle of the car (angle between the x-axis of the map and the axis of the car) rotation = NumericProperty( 0 ) # initializing the last rotation of the car (after playing the action, the car does a rotation of 0, 20 or -20 degrees) velocity_x = NumericProperty( 0) # initializing the x-coordinate of the velocity vector velocity_y = NumericProperty( 0) # initializing the y-coordinate of the velocity vector velocity = ReferenceListProperty(velocity_x, velocity_y) # velocity vector sensor1_x = NumericProperty( 0 ) # initializing the x-coordinate of the first sensor (the one that looks forward) sensor1_y = NumericProperty( 0 ) # initializing the y-coordinate of the first sensor (the one that looks forward) sensor1 = ReferenceListProperty(sensor1_x, sensor1_y) # first sensor vector sensor2_x = NumericProperty( 0 ) # initializing the x-coordinate of the second sensor (the one that looks 30 degrees to the left) sensor2_y = NumericProperty( 0 ) # initializing the y-coordinate of the second sensor (the one that looks 30 degrees to the left) sensor2 = ReferenceListProperty(sensor2_x, sensor2_y) # second sensor vector sensor3_x = NumericProperty( 0 ) # initializing the x-coordinate of the third sensor (the one that looks 30 degrees to the right) sensor3_y = NumericProperty( 0 ) # initializing the y-coordinate of the third sensor (the one that looks 30 degrees to the right) sensor3 = ReferenceListProperty(sensor3_x, sensor3_y) # third sensor vector signal1 = NumericProperty( 0) # initializing the signal received by sensor 1 signal2 = NumericProperty( 0) # initializing the signal received by sensor 2 signal3 = NumericProperty( 0) # initializing the signal received by sensor 3 def move(self, rotation): self.pos = Vector( *self.velocity ) + self.pos # updating the position of the car according to its last position and velocity self.rotation = rotation # getting the rotation of the car self.angle = self.angle + self.rotation # updating the angle self.sensor1 = Vector(30, 0).rotate( self.angle) + self.pos # updating the position of sensor 1 self.sensor2 = Vector(30, 0).rotate( (self.angle + 30) % 360) + self.pos # updating the position of sensor 2 self.sensor3 = Vector(30, 0).rotate( (self.angle - 30) % 360) + self.pos # updating the position of sensor 3 self.signal1 = int( np.sum(sand[int(self.sensor1_x) - 10:int(self.sensor1_x) + 10, int(self.sensor1_y) - 10:int(self.sensor1_y) + 10]) ) / 400. # getting the signal received by sensor 1 (density of sand around sensor 1) self.signal2 = int( np.sum(sand[int(self.sensor2_x) - 10:int(self.sensor2_x) + 10, int(self.sensor2_y) - 10:int(self.sensor2_y) + 10]) ) / 400. # getting the signal received by sensor 2 (density of sand around sensor 2) self.signal3 = int( np.sum(sand[int(self.sensor3_x) - 10:int(self.sensor3_x) + 10, int(self.sensor3_y) - 10:int(self.sensor3_y) + 10]) ) / 400. # getting the signal received by sensor 3 (density of sand around sensor 3) if self.sensor1_x > longueur - 10 or self.sensor1_x < 10 or self.sensor1_y > largeur - 10 or self.sensor1_y < 10: # if sensor 1 is out of the map (the car is facing one edge of the map) self.signal1 = 1. # sensor 1 detects full sand if self.sensor2_x > longueur - 10 or self.sensor2_x < 10 or self.sensor2_y > largeur - 10 or self.sensor2_y < 10: # if sensor 2 is out of the map (the car is facing one edge of the map) self.signal2 = 1. # sensor 2 detects full sand if self.sensor3_x > longueur - 10 or self.sensor3_x < 10 or self.sensor3_y > largeur - 10 or self.sensor3_y < 10: # if sensor 3 is out of the map (the car is facing one edge of the map) self.signal3 = 1. # sensor 3 detects full sand
class BackgroundMixin(Widget): # -- mixins background_color = ListProperty([0, 0, 0, 0]) background_radius = NumericProperty(0) outline_color = ListProperty([0, 0, 0, 0]) outline_width = NumericProperty(1)
class GestureDatabase(GridLayout): selected_count = NumericProperty(0) recognizer = ObjectProperty(None) export_popup = ObjectProperty(GestureExportPopup()) import_popup = ObjectProperty(GestureImportPopup()) info_popup = ObjectProperty(InformationPopup()) def __init__(self, **kwargs): super(GestureDatabase, self).__init__(**kwargs) self.redraw_all = Clock.create_trigger(self._redraw_gesture_list, 0) self.export_popup.ids.save_btn.bind(on_press=self.perform_export) self.import_popup.ids.filechooser.bind(on_submit=self.perform_import) def import_gdb(self): self.gdict = {} for gesture in self.recognizer.db: if gesture.name not in self.gdict: self.gdict[gesture.name] = [] self.gdict[gesture.name].append(gesture) self.selected_count = 0 self.ids.gesture_list.clear_widgets() for k in sorted(self.gdict, key=lambda n: n.lower()): gitem = GestureDatabaseItem(name=k, gesture_list=self.gdict[k]) gitem.bind(on_select=self.select_item) gitem.bind(on_deselect=self.deselect_item) self.ids.gesture_list.add_widget(gitem) def select_item(self, *l): self.selected_count += 1 def deselect_item(self, *l): self.selected_count -= 1 def mass_select(self, *l): if self.selected_count: for i in self.ids.gesture_list.children: if i.ids.select.state == 'down': i.ids.select.state = 'normal' i.draw_item() else: for i in self.ids.gesture_list.children: if i.ids.select.state == 'normal': i.ids.select.state = 'down' i.draw_item() def unload_gestures(self, *l): if not self.selected_count: self.recognizer.db = [] self.ids.gesture_list.clear_widgets() self.selected_count = 0 return for i in self.ids.gesture_list.children[:]: if i.ids.select.state == 'down': self.selected_count -= 1 for g in i.gesture_list: # if g in self.recognizer.db: # not needed, for testing self.recognizer.db.remove(g) self.ids.gesture_list.remove_widget(i) def perform_export(self, *l): path = self.export_popup.ids.filename.text if not path: self.export_popup.dismiss() self.info_popup.text = 'Missing filename' self.info_popup.open() return elif not path.lower().endswith('.kg'): path += '.kg' self.save_selection_to_file(path) self.export_popup.dismiss() self.info_popup.text = 'Gestures exported!' self.info_popup.open() def perform_import(self, filechooser, *l): count = len(self.recognizer.db) for f in filechooser.selection: self.recognizer.import_gesture(filename=f) self.import_gdb() self.info_popup.text = ("Imported %d gestures.\n" % (len(self.recognizer.db) - count)) self.import_popup.dismiss() self.info_popup.open() def save_selection_to_file(self, filename, *l): if not self.selected_count: self.recognizer.export_gesture(filename=filename) else: tmpgdb = Recognizer() for i in self.ids.gesture_list.children: if i.ids.select.state == 'down': for g in i.gesture_list: tmpgdb.db.append(g) tmpgdb.export_gesture(filename=filename) def _redraw_gesture_list(self, *l): for child in self.ids.gesture_list.children: child._draw_trigger()
class StatsBox(MDBoxLayout, BackgroundMixin): winrate = StringProperty("...") score = StringProperty("...") points_lost = NumericProperty(None, allownone=True) player = StringProperty("")
class ScrollView(StencilView): '''ScrollView class. See module documentation for more information. :Events: `on_scroll_start` Generic event fired when scrolling starts from touch. `on_scroll_move` Generic event fired when scrolling move from touch. `on_scroll_stop` Generic event fired when scrolling stops from touch. .. versionchanged:: 1.9.0 `on_scroll_start`, `on_scroll_move` and `on_scroll_stop` events are now dispatched when scrolling to handle nested ScrollViews. .. versionchanged:: 1.7.0 `auto_scroll`, `scroll_friction`, `scroll_moves`, `scroll_stoptime' has been deprecated, use :attr:`effect_cls` instead. ''' scroll_distance = NumericProperty(_scroll_distance) '''Distance to move before scrolling the :class:`ScrollView`, in pixels. As soon as the distance has been traveled, the :class:`ScrollView` will start to scroll, and no touch event will go to children. It is advisable that you base this value on the dpi of your target device's screen. :attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and defaults to 20 (pixels), according to the default value in user configuration. ''' scroll_wheel_distance = NumericProperty('20sp') '''Distance to move when scrolling with a mouse wheel. It is advisable that you base this value on the dpi of your target device's screen. .. versionadded:: 1.8.0 :attr:`scroll_wheel_distance` is a :class:`~kivy.properties.NumericProperty` , defaults to 20 pixels. ''' scroll_timeout = NumericProperty(_scroll_timeout) '''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds. If the user has not moved :attr:`scroll_distance` within the timeout, the scrolling will be disabled, and the touch event will go to the children. :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and defaults to 55 (milliseconds) according to the default value in user configuration. .. versionchanged:: 1.5.0 Default value changed from 250 to 55. ''' scroll_x = NumericProperty(0.) '''X scrolling value, between 0 and 1. If 0, the content's left side will touch the left side of the ScrollView. If 1, the content's right side will touch the right side. This property is controled by :class:`ScrollView` only if :attr:`do_scroll_x` is True. :attr:`scroll_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' scroll_y = NumericProperty(1.) '''Y scrolling value, between 0 and 1. If 0, the content's bottom side will touch the bottom side of the ScrollView. If 1, the content's top side will touch the top side. This property is controled by :class:`ScrollView` only if :attr:`do_scroll_y` is True. :attr:`scroll_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' do_scroll_x = BooleanProperty(True) '''Allow scroll on X axis. :attr:`do_scroll_x` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' do_scroll_y = BooleanProperty(True) '''Allow scroll on Y axis. :attr:`do_scroll_y` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' def _get_do_scroll(self): return (self.do_scroll_x, self.do_scroll_y) def _set_do_scroll(self, value): if type(value) in (list, tuple): self.do_scroll_x, self.do_scroll_y = value else: self.do_scroll_x = self.do_scroll_y = bool(value) do_scroll = AliasProperty(_get_do_scroll, _set_do_scroll, bind=('do_scroll_x', 'do_scroll_y')) '''Allow scroll on X or Y axis. :attr:`do_scroll` is a :class:`~kivy.properties.AliasProperty` of (:attr:`do_scroll_x` + :attr:`do_scroll_y`) ''' def _get_vbar(self): # must return (y, height) in % # calculate the viewport size / scrollview size % if self._viewport is None: return 0, 1. vh = self._viewport.height h = self.height if vh < h or vh == 0: return 0, 1. ph = max(0.01, h / float(vh)) sy = min(1.0, max(0.0, self.scroll_y)) py = (1. - ph) * sy return (py, ph) vbar = AliasProperty(_get_vbar, None, bind=( 'scroll_y', '_viewport', 'viewport_size')) '''Return a tuple of (position, size) of the vertical scrolling bar. .. versionadded:: 1.2.0 The position and size are normalized between 0-1, and represent a percentage of the current scrollview height. This property is used internally for drawing the little vertical bar when you're scrolling. :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly. ''' def _get_hbar(self): # must return (x, width) in % # calculate the viewport size / scrollview size % if self._viewport is None: return 0, 1. vw = self._viewport.width w = self.width if vw < w or vw == 0: return 0, 1. pw = max(0.01, w / float(vw)) sx = min(1.0, max(0.0, self.scroll_x)) px = (1. - pw) * sx return (px, pw) hbar = AliasProperty(_get_hbar, None, bind=( 'scroll_x', '_viewport', 'viewport_size')) '''Return a tuple of (position, size) of the horizontal scrolling bar. .. versionadded:: 1.2.0 The position and size are normalized between 0-1, and represent a percentage of the current scrollview height. This property is used internally for drawing the little horizontal bar when you're scrolling. :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly. ''' bar_color = ListProperty([.7, .7, .7, .9]) '''Color of horizontal / vertical scroll bar, in RGBA format. .. versionadded:: 1.2.0 :attr:`bar_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .9]. ''' bar_inactive_color = ListProperty([.7, .7, .7, .2]) '''Color of horizontal / vertical scroll bar (in RGBA format), when no scroll is happening. .. versionadded:: 1.9.0 :attr:`bar_inactive_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .2]. ''' bar_width = NumericProperty('2dp') '''Width of the horizontal / vertical scroll bar. The width is interpreted as a height for the horizontal bar. .. versionadded:: 1.2.0 :attr:`bar_width` is a :class:`~kivy.properties.NumericProperty` and defaults to 2. ''' bar_pos_x = OptionProperty('bottom', options=('top', 'bottom')) '''Which side of the ScrollView the horizontal scroll bar should go on. Possible values are 'top' and 'bottom'. .. versionadded:: 1.8.0 :attr:`bar_pos_x` is an :class:`~kivy.properties.OptionProperty`, defaults to 'bottom'. ''' bar_pos_y = OptionProperty('right', options=('left', 'right')) '''Which side of the ScrollView the vertical scroll bar should go on. Possible values are 'left' and 'right'. .. versionadded:: 1.8.0 :attr:`bar_pos_y` is an :class:`~kivy.properties.OptionProperty` and defaults to 'right'. ''' bar_pos = ReferenceListProperty(bar_pos_x, bar_pos_y) '''Which side of the scroll view to place each of the bars on. :attr:`bar_pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`bar_pos_x`, :attr:`bar_pos_y`) ''' bar_margin = NumericProperty(0) '''Margin between the bottom / right side of the scrollview when drawing the horizontal / vertical scroll bar. .. versionadded:: 1.2.0 :attr:`bar_margin` is a :class:`~kivy.properties.NumericProperty`, default to 0 ''' effect_cls = ObjectProperty(DampedScrollEffect, allownone=True) '''Class effect to instantiate for X and Y axis. .. versionadded:: 1.7.0 :attr:`effect_cls` is an :class:`~kivy.properties.ObjectProperty` and defaults to :class:`DampedScrollEffect`. .. versionchanged:: 1.8.0 If you set a string, the :class:`~kivy.factory.Factory` will be used to resolve the class. ''' effect_x = ObjectProperty(None, allownone=True) '''Effect to apply for the X axis. If None is set, an instance of :attr:`effect_cls` will be created. .. versionadded:: 1.7.0 :attr:`effect_x` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' effect_y = ObjectProperty(None, allownone=True) '''Effect to apply for the Y axis. If None is set, an instance of :attr:`effect_cls` will be created. .. versionadded:: 1.7.0 :attr:`effect_y` is an :class:`~kivy.properties.ObjectProperty` and defaults to None, read-only. ''' viewport_size = ListProperty([0, 0]) '''(internal) Size of the internal viewport. This is the size of your only child in the scrollview. ''' scroll_type = OptionProperty(['content'], options=(['content'], ['bars'], ['bars', 'content'], ['content', 'bars'])) '''Sets the type of scrolling to use for the content of the scrollview. Available options are: ['content'], ['bars'], ['bars', 'content']. .. versionadded:: 1.8.0 :attr:`scroll_type` is a :class:`~kivy.properties.OptionProperty`, defaults to ['content']. ''' # private, for internal use only _viewport = ObjectProperty(None, allownone=True) _bar_color = ListProperty([0, 0, 0, 0]) _effect_x_start_width = None _effect_y_start_height = None _update_effect_bounds_ev = None _bind_inactive_bar_color_ev = None def _set_viewport_size(self, instance, value): self.viewport_size = value def on__viewport(self, instance, value): if value: value.bind(size=self._set_viewport_size) self.viewport_size = value.size __events__ = ('on_scroll_start', 'on_scroll_move', 'on_scroll_stop') def __init__(self, **kwargs): self._touch = None self._trigger_update_from_scroll = Clock.create_trigger( self.update_from_scroll, -1) # create a specific canvas for the viewport from kivy.graphics import PushMatrix, Translate, PopMatrix, Canvas self.canvas_viewport = Canvas() self.canvas = Canvas() with self.canvas_viewport.before: PushMatrix() self.g_translate = Translate(0, 0) with self.canvas_viewport.after: PopMatrix() super(ScrollView, self).__init__(**kwargs) self.register_event_type('on_scroll_start') self.register_event_type('on_scroll_move') self.register_event_type('on_scroll_stop') # now add the viewport canvas to our canvas self.canvas.add(self.canvas_viewport) effect_cls = self.effect_cls if isinstance(effect_cls, string_types): effect_cls = Factory.get(effect_cls) if self.effect_x is None and effect_cls is not None: self.effect_x = effect_cls(target_widget=self._viewport) if self.effect_y is None and effect_cls is not None: self.effect_y = effect_cls(target_widget=self._viewport) trigger_update_from_scroll = self._trigger_update_from_scroll update_effect_widget = self._update_effect_widget update_effect_x_bounds = self._update_effect_x_bounds update_effect_y_bounds = self._update_effect_y_bounds fbind = self.fbind fbind('width', update_effect_x_bounds) fbind('height', update_effect_y_bounds) fbind('viewport_size', self._update_effect_bounds) fbind('_viewport', update_effect_widget) fbind('scroll_x', trigger_update_from_scroll) fbind('scroll_y', trigger_update_from_scroll) fbind('pos', trigger_update_from_scroll) fbind('size', trigger_update_from_scroll) 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 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): touch.grab(self) 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'): e.value = max(e.value - m, e.min) e.velocity = 0 elif btn in ('scrollup', 'scrollright'): e.value = min(e.value + m, e.max) e.velocity = 0 touch.ud[self._get_uid('svavoid')] = True e.trigger_velocity_update() return True in_bar = ud['in_bar_x'] or ud['in_bar_y'] if scroll_type == ['bars'] and not in_bar: return self.simulate_touch_down(touch) if in_bar: if (ud['in_bar_y'] and not self._touch_in_handle( self._handle_y_pos, self._handle_y_size, touch)): self.scroll_y = (touch.y - self.y) / self.height elif (ud['in_bar_x'] and not self._touch_in_handle( self._handle_x_pos, self._handle_x_size, touch)): self.scroll_x = (touch.x - self.x) / self.width # no mouse scrolling, so the user is going to drag the scrollview with # this touch. self._touch = touch uid = self._get_uid() ud[uid] = { 'mode': 'unknown', 'dx': 0, 'dy': 0, 'user_stopped': in_bar, 'frames': Clock.frames, 'time': touch.time_start} if self.do_scroll_x and self.effect_x and not ud['in_bar_x']: self._effect_x_start_width = self.width self.effect_x.start(touch.x) self._scroll_x_mouse = self.scroll_x if self.do_scroll_y and self.effect_y and not ud['in_bar_y']: self._effect_y_start_height = self.height self.effect_y.start(touch.y) self._scroll_y_mouse = self.scroll_y if not in_bar: Clock.schedule_once(self._change_touch_mode, self.scroll_timeout / 1000.) return True def on_touch_move(self, touch): if self._touch is not touch: # touch is in parent touch.push() touch.apply_transform_2d(self.to_local) super(ScrollView, self).on_touch_move(touch) touch.pop() return self._get_uid() in touch.ud if touch.grab_current is not self: return True if touch.ud.get(self._get_uid()) is None: return super(ScrollView, self).on_touch_move(touch) touch.ud['sv.handled'] = {'x': False, 'y': False} if self.dispatch('on_scroll_move', touch): return True def on_scroll_move(self, touch): if self._get_uid('svavoid') in touch.ud: return False touch.push() touch.apply_transform_2d(self.to_local) if self.dispatch_children('on_scroll_move', touch): touch.pop() return True touch.pop() rv = True # By default this touch can be used to defocus currently focused # widget, like any touch outside of ScrollView. touch.ud['sv.can_defocus'] = True uid = self._get_uid() if 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: # 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