class Atlas(EventDispatcher): '''Manage texture atlas. See module documentation for more information. ''' original_textures = ListProperty([]) '''List of original atlas textures (which contain the :attr:`textures`). :attr:`original_textures` is a :class:`~kivy.properties.ListProperty` and defaults to []. .. versionadded:: 1.9.1 ''' textures = DictProperty({}) '''List of available textures within the atlas. :attr:`textures` is a :class:`~kivy.properties.DictProperty` and defaults to {}. ''' def _get_filename(self): return self._filename filename = AliasProperty(_get_filename, None) '''Filename of the current Atlas. :attr:`filename` is an :class:`~kivy.properties.AliasProperty` and defaults to None. ''' def __init__(self, filename): self._filename = filename super(Atlas, self).__init__() self._load() def __getitem__(self, key): return self.textures[key] def _load(self): # late import to prevent recursive import. global CoreImage if CoreImage is None: from kivy.core.image import Image as CoreImage # must be a name finished by .atlas ? filename = self._filename assert(filename.endswith('.atlas')) filename = filename.replace('/', os.sep) Logger.debug('Atlas: Load <%s>' % filename) with open(filename, 'r') as fd: meta = json.load(fd) Logger.debug('Atlas: Need to load %d images' % len(meta)) d = dirname(filename) textures = {} for subfilename, ids in meta.items(): subfilename = join(d, subfilename) Logger.debug('Atlas: Load <%s>' % subfilename) # load the image ci = CoreImage(subfilename) atlas_texture = ci.texture self.original_textures.append(atlas_texture) # for all the uid, load the image, get the region, and put # it in our dict. for meta_id, meta_coords in ids.items(): x, y, w, h = meta_coords textures[meta_id] = atlas_texture.get_region(*meta_coords) self.textures = textures @staticmethod def create(outname, filenames, size, padding=2, use_path=False): '''This method can be used to create an atlas manually from a set of images. :Parameters: `outname`: str Basename to use for ``.atlas`` creation and ``-<idx>.png`` associated images. `filenames`: list List of filenames to put in the atlas. `size`: int or list (width, height) Size of the atlas image. `padding`: int, defaults to 2 Padding to put around each image. Be careful. If you're using a padding < 2, you might have issues with the borders of the images. Because of the OpenGL linearization, it might use the pixels of the adjacent image. If you're using a padding >= 2, we'll automatically generate a "border" of 1px around your image. If you look at the result, don't be scared if the image inside is not exactly the same as yours :). `use_path`: bool, defaults to False If True, the relative path of the source png file names will be included in the atlas ids rather that just in the file names. Leading dots and slashes will be excluded and all other slashes in the path will be replaced with underscores. For example, if `use_path` is False (the default) and the file name is ``../data/tiles/green_grass.png``, the id will be ``green_grass``. If `use_path` is True, it will be ``data_tiles_green_grass``. .. versionchanged:: 1.8.0 Parameter use_path added ''' # Thanks to # omnisaurusgames.com/2011/06/texture-atlas-generation-using-python/ # for its initial implementation. try: from PIL import Image except ImportError: Logger.critical('Atlas: Imaging/PIL are missing') raise if isinstance(size, (tuple, list)): size_w, size_h = list(map(int, size)) else: size_w = size_h = int(size) # open all of the images ims = list() for f in filenames: fp = open(f, 'rb') im = Image.open(fp) im.load() fp.close() ims.append((f, im)) # sort by image area ims = sorted(ims, key=lambda im: im[1].size[0] * im[1].size[1], reverse=True) # free boxes are empty space in our output image set # the freebox tuple format is: outidx, x, y, w, h freeboxes = [(0, 0, 0, size_w, size_h)] numoutimages = 1 # full boxes are areas where we have placed images in the atlas # the full box tuple format is: image, outidx, x, y, w, h, filename fullboxes = [] # do the actual atlasing by sticking the largest images we can # have into the smallest valid free boxes for imageinfo in ims: im = imageinfo[1] imw, imh = im.size imw += padding imh += padding if imw > size_w or imh > size_h: Logger.error( 'Atlas: image %s (%d by %d) is larger than the atlas size!' % (imageinfo[0], imw, imh)) return inserted = False while not inserted: for idx, fb in enumerate(freeboxes): # find the smallest free box that will contain this image if fb[3] >= imw and fb[4] >= imh: # we found a valid spot! Remove the current # freebox, and split the leftover space into (up to) # two new freeboxes del freeboxes[idx] if fb[3] > imw: freeboxes.append(( fb[0], fb[1] + imw, fb[2], fb[3] - imw, imh)) if fb[4] > imh: freeboxes.append(( fb[0], fb[1], fb[2] + imh, fb[3], fb[4] - imh)) # keep this sorted! freeboxes = sorted(freeboxes, key=lambda fb: fb[3] * fb[4]) fullboxes.append((im, fb[0], fb[1] + padding, fb[2] + padding, imw - padding, imh - padding, imageinfo[0])) inserted = True break if not inserted: # oh crap - there isn't room in any of our free # boxes, so we have to add a new output image freeboxes.append((numoutimages, 0, 0, size_w, size_h)) numoutimages += 1 # now that we've figured out where everything goes, make the output # images and blit the source images to the appropriate locations Logger.info('Atlas: create an {0}x{1} rgba image'.format(size_w, size_h)) outimages = [Image.new('RGBA', (size_w, size_h)) for i in range(0, int(numoutimages))] for fb in fullboxes: x, y = fb[2], fb[3] out = outimages[fb[1]] out.paste(fb[0], (fb[2], fb[3])) w, h = fb[0].size if padding > 1: out.paste(fb[0].crop((0, 0, w, 1)), (x, y - 1)) out.paste(fb[0].crop((0, h - 1, w, h)), (x, y + h)) out.paste(fb[0].crop((0, 0, 1, h)), (x - 1, y)) out.paste(fb[0].crop((w - 1, 0, w, h)), (x + w, y)) # save the output images for idx, outimage in enumerate(outimages): outimage.save('%s-%d.png' % (outname, idx)) # write out an json file that says where everything ended up meta = {} for fb in fullboxes: fn = '%s-%d.png' % (basename(outname), fb[1]) if fn not in meta: d = meta[fn] = {} else: d = meta[fn] # fb[6] contain the filename if use_path: # use the path with separators replaced by _ # example '../data/tiles/green_grass.png' becomes # 'data_tiles_green_grass' uid = splitext(fb[6])[0] # remove leading dots and slashes uid = uid.lstrip('./\\') # replace remaining slashes with _ uid = uid.replace('/', '_').replace('\\', '_') else: # for example, '../data/tiles/green_grass.png' # just get only 'green_grass' as the uniq id. uid = splitext(basename(fb[6]))[0] x, y, w, h = fb[2:6] d[uid] = x, size_h - y - h, w, h outfn = '%s.atlas' % outname with open(outfn, 'w') as fd: json.dump(meta, fd) return outfn, meta
class WindowBase(EventDispatcher): '''WindowBase is an abstract window widget for any window implementation. :Parameters: `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_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 # 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 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 :class:`~kivy.properties.AliasProperty`. ''' def _get_height(self): '''Rotated window height''' r = self._rotation if r == 0 or r == 180: return self._size[1] return self._size[0] height = AliasProperty(_get_height, None, bind=('_rotation', '_size')) '''Rotated window height. :attr:`height` is a :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. ''' def _set_system_size(self, size): self._size = size def _get_system_size(self): return self._size system_size = AliasProperty( _get_system_size, _set_system_size, bind=('_size', )) '''Real size of the window ignoring rotation. ''' fullscreen = OptionProperty(False, options=(True, False, 'auto', 'fake')) '''If True, the window will be put in fullscreen mode, "auto". That means the screen size will not change and will use the current size to set the app fullscreen. .. versionadded:: 1.2.0 ''' mouse_pos = ObjectProperty([0, 0]) '''2d position of the mouse within the window. .. versionadded:: 1.2.0 ''' 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') 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 # 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) # set the default window parameter according to the configuration 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 for prop in ( 'fullscreen', 'position', 'top', 'left', '_size', 'system_size'): self.bind(**{prop: self.trigger_create_window}) # 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 toggle_fullscreen(self): '''Toggle fullscreen on window''' pass 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) 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': # 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 cc = self._clearcolor if cc is not None: glClearColor(*cc) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_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: 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) def on_touch_down(self, touch): '''Event called when a touch down event is initiated. ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) 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). ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) 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). ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) 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 w2, h2 = w / 2., h / 2. r = radians(self.rotation) # prepare the viewport glViewport(0, 0, 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_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_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.") 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 is currently used only on MacOSX with a patched version of pygame, but 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. ''' # 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 CoreBluetoothScanner(EventDispatcher): __events__ = ('on_state', ) app = AliasProperty(lambda *args: App.get_running_app(), None) def __init__(self, **kwargs): super(CoreBluetoothScanner, self).__init__(**kwargs) self.decoder = decoder.BluetoothDecoder() self.previous_scan_results = [] self.state = "none" self.start_button_pressed = False self.central = None self.last_central = None self.CBCentralManager = CBCentralManager self.check_bluetooth_enabled() @protocol('CBCentralManagerDelegate') def centralManager_didConnectPeripheral_(self, central, peripheral): global visited_connecting visited_connecting = False if DEBUG: print '-> connected to device', peripheral print peripheral.state print '-> GREAT!' # peripheral.discoverServices_(None) central.cancelPeripheralConnection_(peripheral) uuid = peripheral.identifier.UUIDString().cString() visited[uuid] = True @protocol('CBCentralManagerDelegate') def centralManager_didFailToConnectPeripheral_error_(self, central, peripheral, error): global visited_connecting visited_connecting = False uuid = peripheral.identifier.UUIDString().cString() if DEBUG: print uuid, '! err: failed to connect to periph' if uuid not in visited_failed: visited_failed[uuid] = 1 else: visited_failed[uuid] += 1 @protocol('CBCentralManagerDelegate') def centralManager_didDisconnectPeripheral_error_(self, central, peripheral, error): global visited_connecting visited_connecting = False uuid = peripheral.identifier.UUIDString().cString() if DEBUG: print uuid, '! err: disconnect to periph' if uuid not in visited_failed: visited_failed[uuid] = 1 else: visited_failed[uuid] += 1 @protocol('CBCentralManagerDelegate') def centralManager_didDiscoverPeripheral_advertisementData_RSSI_( self, central, peripheral, data, rssi): # print "Incoming central:", central.description().UTF8String() # print "Incoming peripheral:", peripheral.description().UTF8String() # time1 = time.time() data_object = data.objectForKey_("kCBAdvDataServiceData") if data_object: # time2 = time.time() ble_data_string = data_object.description().UTF8String() print ble_data_string # validating key if self.app.appix_beacon_key == ble_data_string[18:24].replace(" ", "").upper(): # time3 = time.time() if not self.app.ADMIN_APP: self.process_ble_packets(ble_data_string) else: rssi = str(rssi.description().UTF8String()) self.process_ble_packets(ble_data_string, rssi) # else # self.process_ble_packets(ble_data_string) # time4 = time.time() # print "TIME TO EDDYSTONE:", (time2-time1)*1000. # print "TIME TO VALIDATE: ", (time3-time2)*1000. # print "TIME TO PROCESS: ", (time4-time3)*1000. # print "TOTAL TIME: ", (time4-time1)*1000. # print " " @protocol('CBCentralManagerDelegate') def centralManagerDidUpdateState_(self, central): state = central.state() if callable(central.state) else central.state # print "centralManagerDidUpdateState_, state is", state if state == CBCentralManagerStateUnknown: kstate = 'unknown' elif state == CBCentralManagerStatePoweredOn: kstate = 'ready' elif state == CBCentralManagerStatePoweredOff: # place an alert box here with uialertcontroller? kstate = 'poweroff' elif state == CBCentralManagerStateUnauthorized: kstate = 'nopermission' elif state == CBCentralManagerStateUnsupported: kstate = 'nohardware' elif state == CBCentralManagerStateResetting: kstate = 'resetting' if DEBUG: print '---------> CENTRAL UPDATE', kstate self.schedule_dispatch('on_state', kstate) def on_state(self, state, *args): '''Event called when the Bluetooth manager state change. Can be one of: 'unknown', 'ready', 'poweroff', 'nopermission', 'nohardware' ''' self.state = state if self.last_central != str(self.central): if state == "ready": # start the scanning when bluetooth is ready to use self.start() if self.start_button_pressed: Clock.schedule_once(self.app.appix_base.start_show, 0.1) self.last_central = str(self.central) else: # print "Bluetooth error, state is ", state pass def schedule_dispatch(self, eventname, *args, **kwargs): def _dispatch(dt): self.dispatch(eventname, *args, **kwargs) Clock.schedule_once(_dispatch) def check_bluetooth_enabled(self): # print "check_bluetooth_enabled():", self.state, self.central if self.state in ["none", "poweroff"]: self.central = self.CBCentralManager.alloc().initWithDelegate_queue_(self, None) elif self.state == "ready": Clock.schedule_once(self.app.appix_base.start_show, 0.1) else: print "Bluetooth Error:", self.state def process_ble_packets(self, ble_data_string, *args): # print args start_index, end_index = ble_data_string.index("FEAA = <")+12, ble_data_string.index(">;")-4 scan_results = ble_data_string[start_index:end_index] print scan_results if self.app.ADMIN_APP: sr = scan_results.replace(" ", "") self.app.appix_base.update_data_labels(sr, args[0]) if scan_results != self.previous_scan_results: # print "bluetooth in:", scan_results decoded_data = self.decoder.decode_beacon_data(scan_results) self.app.appix_base.run_bluetooth_commands(decoded_data) self.previous_scan_results = scan_results def start(self): key = NSString.alloc().initWithUTF8String_('kCBScanOptionAllowDuplicates') value = NSNumber.alloc().initWithInt_(1) options = NSMutableDictionary.alloc().init() options.setObject_forKey_(value, key) # comment following line if you do not have notifications compiled in kivy-ios # notifications.IOSNotif().show('Appix', 'App is running') # scan_list = NSArray.arrayWithObject_(CBUUID.UUIDWithString_('78B4')) # set scan list to None to blankly scan for every beacon/bluetooth peripheral # This only works when app is in foreground. scan_list = None self.central.scanForPeripheralsWithServices_options_(scan_list, options) def stop(self): self.central.stopScan()
class FileChooser(FileChooserController): '''Implementation of a :class:`FileChooserController` which supports switching between multiple, synced layout views. The FileChooser can be used as follows: .. code-block:: kv BoxLayout: orientation: 'vertical' BoxLayout: size_hint_y: None height: sp(52) Button: text: 'Icon View' on_press: fc.view_mode = 'icon' Button: text: 'List View' on_press: fc.view_mode = 'list' FileChooser: id: fc FileChooserIconLayout FileChooserListLayout .. versionadded:: 1.9.0 ''' manager = ObjectProperty() ''' Reference to the :class:`~kivy.uix.screenmanager.ScreenManager` instance. manager is an :class:`~kivy.properties.ObjectProperty`. ''' _view_list = ListProperty() def get_view_list(self): return self._view_list view_list = AliasProperty(get_view_list, bind=('_view_list',)) ''' List of views added to this FileChooser. view_list is an :class:`~kivy.properties.AliasProperty` of type :class:`list`. ''' _view_mode = StringProperty() def get_view_mode(self): return self._view_mode def set_view_mode(self, mode): if mode not in self._view_list: raise ValueError('unknown view mode %r' % mode) self._view_mode = mode view_mode = AliasProperty( get_view_mode, set_view_mode, bind=('_view_mode',)) ''' Current layout view mode. view_mode is an :class:`~kivy.properties.AliasProperty` of type :class:`str`. ''' @property def _views(self): return [screen.children[0] for screen in self.manager.screens] def __init__(self, **kwargs): super(FileChooser, self).__init__(**kwargs) self.manager = ScreenManager() super(FileChooser, self).add_widget(self.manager) self.trigger_update_view = Clock.create_trigger(self.update_view) self.fbind('view_mode', self.trigger_update_view) def add_widget(self, widget, *args, **kwargs): if widget is self._progress: super(FileChooser, self).add_widget(widget, *args, **kwargs) elif hasattr(widget, 'VIEWNAME'): name = widget.VIEWNAME + 'view' screen = Screen(name=name) widget.controller = self screen.add_widget(widget) self.manager.add_widget(screen) self.trigger_update_view() else: raise ValueError( 'widget must be a FileChooserLayout,' ' not %s' % type(widget).__name__) def rebuild_views(self): views = [view.VIEWNAME for view in self._views] if views != self._view_list: self._view_list = views if self._view_mode not in self._view_list: self._view_mode = self._view_list[0] self._trigger_update() def update_view(self, *args): self.rebuild_views() sm = self.manager viewlist = self._view_list view = self.view_mode current = sm.current[:-4] viewindex = viewlist.index(view) if view in viewlist else 0 currentindex = viewlist.index(current) if current in viewlist else 0 direction = 'left' if currentindex < viewindex else 'right' sm.transition.direction = direction sm.current = view + 'view' def _create_entry_widget(self, ctx): return [Builder.template(view._ENTRY_TEMPLATE, **ctx) for view in self._views] def _get_file_paths(self, items): if self._views: return [file[0].path for file in items] return [] def _update_item_selection(self, *args): for viewitem in self._items: selected = viewitem[0].path in self.selection for item in viewitem: item.selected = selected def on_entry_added(self, node, parent=None): for index, view in enumerate(self._views): view.dispatch( 'on_entry_added', node[index], parent[index] if parent else None) def on_entries_cleared(self): for view in self._views: view.dispatch('on_entries_cleared') def on_subentry_to_entry(self, subentry, entry): for index, view in enumerate(self._views): view.dispatch('on_subentry_to_entry', subentry[index], entry) def on_remove_subentry(self, subentry, entry): for index, view in enumerate(self._views): view.dispatch('on_remove_subentry', subentry[index], entry) def on_submit(self, selected, touch=None): view_mode = self.view_mode for view in self._views: if view_mode == view.VIEWNAME: view.dispatch('on_submit', selected, touch) return
class Vertical_Gauge(Tickline): __events__ = ('on_centered', ) #=========================================================================== # overrides #=========================================================================== background_image = StringProperty(join(dirname(__file__), 'roulettebackground.png'), allownone=True) '''background image, overriding the default of None in :class:`Tickline`. .. versionadded:: 0.1.1 ''' background_color = ListProperty([1, 1, 1, 1]) '''background color, defaulting to [1, 1, 1, 1], overriding default of [0, 0, 0] in :class:`Tickline`. .. versionadded:: 0.1.1 ''' cover_background = BooleanProperty(False) '''determines whether to draw a Rectangle covering the background. Overriding :class:`Tickline` default to give False. .. versionadded:: 0.1.1 ''' size_hint_x = NumericProperty(None, allownone=True) labeller_cls = ObjectProperty(SlotLabeller) zoomable = BooleanProperty(False) draw_line = BooleanProperty(False) font_size = NumericProperty('20sp') width = NumericProperty('60dp') # doesn't make sense to have more than 1 tick tick = ObjectProperty(None) def get_ticks(self): if self.tick: return [self.tick] else: return [] def set_ticks(self, val): self.tick = val[0] ticks = AliasProperty(get_ticks, set_ticks, bind=['tick']) #=========================================================================== # public attributes #=========================================================================== selected_value = ObjectProperty(None) '''the currently selected value.''' format_str = StringProperty('{}') '''formatting spec string for the values displayed.''' tick_cls = ObjectProperty(Slot) '''The class of the tick in this vertical_gauge. Defaults to :class:`Slot`. Should be overriden as needed by child class.''' int_valued = BooleanProperty(True) '''indicates whether the values should be displayed as integers.''' scroll_effect_cls = ObjectProperty(RouletteScrollEffect) # has to be negative so that ``ScrollEffect.trigger_velocity_update`` # is always called drag_threshold = NumericProperty(-1) '''this is passed to the ``drag_threshold`` of :attr:`scroll_effect_cls`. It is by default set to -1 to turn off the drag threshold. ''' center_duration = NumericProperty(.3) '''duration for the animation of :meth:`center_on`.''' density = NumericProperty(4.2) '''determines how many slots are shown at a time.''' def get_rolling_value(self): return self.tick.slot_value(self.tick.localize(self.index_mid)) def set_rolling_value(self, val): self.index_mid = self.tick.globalize(val) rolling_value = AliasProperty(get_rolling_value, set_rolling_value, bind=['index_mid']) '''the val indicated by whatever slot is in the middle of the vertical_gauge. If the vertical_gauge is still, then :attr:`rolling_value` is equal to :attr:`selected_value`. Otherwise, they shouldn't be equal. .. note:: This property is not stable under resizing, since often that will change the slot in the middle.''' def __init__(self, **kw): self.tick = Slot() self._trigger_set_selection = \ Clock.create_trigger(self.set_selected_value) super(Vertical_Gauge, self).__init__(**kw) self.scale = dp(10) self.tick = self.tick_cls() self._trigger_calibrate() def on_tick_cls(self, *args): self.tick = self.tick_cls() def on_tick(self, *args): tick = self.tick if tick: tick.font_size = self.font_size tick.int_valued = self.int_valued tick.format_str = self.format_str def on_size(self, *args): self.scale = self.line_length / self.density self.recenter() def on_int_valued(self, *args): if self.tick: self.tick.int_valued = self.int_valued def on_format_str(self, *args): if self.tick: self.tick.format_str = self.format_str def get_anchor(self): '''returns a legal stopping value for the :class:`RouletteScrollEffect`. Should be overriden if necessary.''' return 0 def _update_effect_constants(self, *args): if not super(Vertical_Gauge, self)._update_effect_constants(*args): return effect = self.scroll_effect scale = self.scale effect.pull_back_velocity = sp(50) / scale def calibrate_scroll_effect(self, *args, **kw): if not super(Vertical_Gauge, self).calibrate_scroll_effect( *args, **kw): return anchor = self.get_anchor() effect = self.scroll_effect effect.interval = 1. / self.tick.scale_factor effect.anchor = anchor effect.on_coasted_to_stop = self._trigger_set_selection def set_selected_value(self, *args): '''set :attr:`selected_value` to the currently slot.''' self.selected_value = self.round_(self.rolling_value) def round_(self, val): '''round an arbitrary rolling value to a legal selection value. Should be overriden if necessary.''' if self.int_valued: return int(round(val)) return round(val) def recenter(self, *args): if self.selected_value is not None: self.center_on(self.selected_value) self._trigger_calibrate() def index_of(self, val): '''returns the index that should be equivalent to a selection value ``val``. Should be overriden if necessary.''' return val def center_on(self, val, animate=True): Animation.stop_all(self) center_index = self.index_of(val) half_length = self.line_length / 2. / self.scale index_0 = center_index - half_length index_1 = center_index + half_length if animate: anim = Animation(index_0=index_0, index_1=index_1, duration=self.center_duration) anim.on_complete = lambda *args: self._centered() anim.start(self) else: self.index_0 = index_0 self.index_1 = index_1 self._centered() def on_centered(self, *args): '''event that fires when the operation :meth:`center_on` completes. (and by extension, when :meth:`center` or :meth:`select_and_center` completes). By default it doesn't do anything.''' pass def _centered(self, *args): self._trigger_calibrate() self.dispatch('on_centered') def center(self, animate=True): self.center_on(self.selected_value, animate) def select_and_center(self, val, *args, **kw): '''set :attr:`selected_value` to ``val`` and center on it. If :attr:`selected_value` is already ``val``, return False; else return True.''' if self.selected_value == val: return False self.selected_value = val self.center(*args, **kw) return True def is_rolling(self): return self.scroll_effect.velocity != 0
class cScrollableLabelLarge(Widget): """ Main Widget to display a large text By default, x and y scrolling is enabled Horizontal scrolling can be disabled by passing noxscroll = False Supports background color for the Label As implementaion it is a Widget which contains a Background (if color is given) and a customized RecycleView """ text = StringProperty('') #font_size = Property('20sp') def __init__(self, **kwargs): kwargsInner = {} for k in kwargs: if k not in ["size_hint", "size", "pos", "pos_hint"]: kwargsInner[k] = kwargs[k] self.oScrollableLabelLargeInner = cScrollableLabelLargeInner( **kwargsInner) super(self.__class__, self).__init__(**RemoveNoClassArgs(kwargs, Widget)) self.oBackGround = None if "background_color" in kwargs: self.oBackGround = cTouchRectangle( size=self.size, pos=self.pos, background_color=kwargs["background_color"]) self.add_widget(self.oBackGround) del kwargs["background_color"] self.oScrollableLabelLargeInner.size = self.size self.oScrollableLabelLargeInner.pos = self.pos self.add_widget(self.oScrollableLabelLargeInner) self.bind(pos=self.update_graphics_pos, size=self.update_graphics_size) def update_graphics_pos(self, instance, value): """ Updates the child widget position (Backgrund and Recycleview) """ if self.oBackGround is not None: self.oBackGround.pos = value self.oScrollableLabelLargeInner.pos = value def update_graphics_size(self, instance, value): """ Updates the child widget size (Backgrund and Recycleview) """ if self.oBackGround is not None: self.oBackGround.size = value self.oScrollableLabelLargeInner.size = value def IncreaseFontSize(self, *args): """ Pass through function for the Recycleview """ self.oScrollableLabelLargeInner.IncreaseFontSize(args) def DecreaseFontSize(self, *args): """ Pass through function for the Recycleview """ self.oScrollableLabelLargeInner.DecreaseFontSize(args) def on_text(self, instance, value): """ Pass through function for the Recycleview """ self.oScrollableLabelLargeInner.text = value def on_oOrcaWidget(self, instance, value): """ Passes the OrcaWidget to the Childs """ if self.oBackGround is not None: self.oBackGround.oOrcaWidget = value self.oScrollableLabelLargeInner.oOrcaWidget = value def _get_font_size(self): """Returns the Font Size """ return self.oScrollableLabelLargeInner.fFontSize def _set_font_size(self, value): """Passes the change of font size """ self.oScrollableLabelLargeInner.font_size = value font_size = AliasProperty(_get_font_size, _set_font_size)
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) def __init__(self, **kwargs): super(BaseButton, self).__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 value: self._current_button_color = self.md_bg_color_disabled else: self._current_button_color = self.md_bg_color
class ScrollView(StencilView): '''ScrollView class. See module documentation for more information. :Events: `on_scroll_start` Generic event fired when scrolling starts from touch. `on_scroll_move` Generic event fired when scrolling move from touch. `on_scroll_stop` Generic event fired when scrolling stops from touch. .. versionchanged:: 1.9.0 `on_scroll_start`, `on_scroll_move` and `on_scroll_stop` events are now dispatched when scrolling to handle nested ScrollViews. .. versionchanged:: 1.7.0 `auto_scroll`, `scroll_friction`, `scroll_moves`, `scroll_stoptime' has been deprecated, use :attr:`effect_cls` instead. ''' scroll_distance = NumericProperty(_scroll_distance) '''Distance to move before scrolling the :class:`ScrollView`, in pixels. As soon as the distance has been traveled, the :class:`ScrollView` will start to scroll, and no touch event will go to children. It is advisable that you base this value on the dpi of your target device's screen. :attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and defaults to 20 (pixels), according to the default value in user configuration. ''' scroll_wheel_distance = NumericProperty('20sp') '''Distance to move when scrolling with a mouse wheel. It is advisable that you base this value on the dpi of your target device's screen. .. versionadded:: 1.8.0 :attr:`scroll_wheel_distance` is a :class:`~kivy.properties.NumericProperty` , defaults to 20 pixels. ''' scroll_timeout = NumericProperty(_scroll_timeout) '''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds. If the user has not moved :attr:`scroll_distance` within the timeout, the scrolling will be disabled, and the touch event will go to the children. :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and defaults to 55 (milliseconds) according to the default value in user configuration. .. versionchanged:: 1.5.0 Default value changed from 250 to 55. ''' scroll_x = NumericProperty(0.) '''X scrolling value, between 0 and 1. If 0, the content's left side will touch the left side of the ScrollView. If 1, the content's right side will touch the right side. This property is controled by :class:`ScrollView` only if :attr:`do_scroll_x` is True. :attr:`scroll_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' scroll_y = NumericProperty(1.) '''Y scrolling value, between 0 and 1. If 0, the content's bottom side will touch the bottom side of the ScrollView. If 1, the content's top side will touch the top side. This property is controled by :class:`ScrollView` only if :attr:`do_scroll_y` is True. :attr:`scroll_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' do_scroll_x = BooleanProperty(True) '''Allow scroll on X axis. :attr:`do_scroll_x` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' do_scroll_y = BooleanProperty(True) '''Allow scroll on Y axis. :attr:`do_scroll_y` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' def _get_do_scroll(self): return (self.do_scroll_x, self.do_scroll_y) def _set_do_scroll(self, value): if type(value) in (list, tuple): self.do_scroll_x, self.do_scroll_y = value else: self.do_scroll_x = self.do_scroll_y = bool(value) do_scroll = AliasProperty(_get_do_scroll, _set_do_scroll, bind=('do_scroll_x', 'do_scroll_y'), cache=True) '''Allow scroll on X or Y axis. :attr:`do_scroll` is a :class:`~kivy.properties.AliasProperty` of (:attr:`do_scroll_x` + :attr:`do_scroll_y`) ''' def _get_vbar(self): # must return (y, height) in % # calculate the viewport size / scrollview size % if self._viewport is None: return 0, 1. vh = self._viewport.height h = self.height if vh < h or vh == 0: return 0, 1. ph = max(0.01, h / float(vh)) sy = min(1.0, max(0.0, self.scroll_y)) py = (1. - ph) * sy return (py, ph) vbar = AliasProperty(_get_vbar, bind=('scroll_y', '_viewport', 'viewport_size', 'height'), cache=True) '''Return a tuple of (position, size) of the vertical scrolling bar. .. versionadded:: 1.2.0 The position and size are normalized between 0-1, and represent a percentage of the current scrollview height. This property is used internally for drawing the little vertical bar when you're scrolling. :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly. ''' def _get_hbar(self): # must return (x, width) in % # calculate the viewport size / scrollview size % if self._viewport is None: return 0, 1. vw = self._viewport.width w = self.width if vw < w or vw == 0: return 0, 1. pw = max(0.01, w / float(vw)) sx = min(1.0, max(0.0, self.scroll_x)) px = (1. - pw) * sx return (px, pw) hbar = AliasProperty(_get_hbar, bind=('scroll_x', '_viewport', 'viewport_size', 'width'), cache=True) '''Return a tuple of (position, size) of the horizontal scrolling bar. .. versionadded:: 1.2.0 The position and size are normalized between 0-1, and represent a percentage of the current scrollview height. This property is used internally for drawing the little horizontal bar when you're scrolling. :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly. ''' bar_color = ListProperty([.7, .7, .7, .9]) '''Color of horizontal / vertical scroll bar, in RGBA format. .. versionadded:: 1.2.0 :attr:`bar_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .9]. ''' bar_inactive_color = ListProperty([.7, .7, .7, .2]) '''Color of horizontal / vertical scroll bar (in RGBA format), when no scroll is happening. .. versionadded:: 1.9.0 :attr:`bar_inactive_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .2]. ''' bar_width = NumericProperty('2dp') '''Width of the horizontal / vertical scroll bar. The width is interpreted as a height for the horizontal bar. .. versionadded:: 1.2.0 :attr:`bar_width` is a :class:`~kivy.properties.NumericProperty` and defaults to 2. ''' bar_pos_x = OptionProperty('bottom', options=('top', 'bottom')) '''Which side of the ScrollView the horizontal scroll bar should go on. Possible values are 'top' and 'bottom'. .. versionadded:: 1.8.0 :attr:`bar_pos_x` is an :class:`~kivy.properties.OptionProperty`, defaults to 'bottom'. ''' bar_pos_y = OptionProperty('right', options=('left', 'right')) '''Which side of the ScrollView the vertical scroll bar should go on. Possible values are 'left' and 'right'. .. versionadded:: 1.8.0 :attr:`bar_pos_y` is an :class:`~kivy.properties.OptionProperty` and defaults to 'right'. ''' bar_pos = ReferenceListProperty(bar_pos_x, bar_pos_y) '''Which side of the scroll view to place each of the bars on. :attr:`bar_pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`bar_pos_x`, :attr:`bar_pos_y`) ''' bar_margin = NumericProperty(0) '''Margin between the bottom / right side of the scrollview when drawing the horizontal / vertical scroll bar. .. versionadded:: 1.2.0 :attr:`bar_margin` is a :class:`~kivy.properties.NumericProperty`, default to 0 ''' effect_cls = ObjectProperty(DampedScrollEffect, allownone=True) '''Class effect to instantiate for X and Y axis. .. versionadded:: 1.7.0 :attr:`effect_cls` is an :class:`~kivy.properties.ObjectProperty` and defaults to :class:`DampedScrollEffect`. .. versionchanged:: 1.8.0 If you set a string, the :class:`~kivy.factory.Factory` will be used to resolve the class. ''' effect_x = ObjectProperty(None, allownone=True) '''Effect to apply for the X axis. If None is set, an instance of :attr:`effect_cls` will be created. .. versionadded:: 1.7.0 :attr:`effect_x` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' effect_y = ObjectProperty(None, allownone=True) '''Effect to apply for the Y axis. If None is set, an instance of :attr:`effect_cls` will be created. .. versionadded:: 1.7.0 :attr:`effect_y` is an :class:`~kivy.properties.ObjectProperty` and defaults to None, read-only. ''' viewport_size = ListProperty([0, 0]) '''(internal) Size of the internal viewport. This is the size of your only child in the scrollview. ''' scroll_type = OptionProperty(['content'], options=(['content'], ['bars'], ['bars', 'content'], ['content', 'bars'])) '''Sets the type of scrolling to use for the content of the scrollview. Available options are: ['content'], ['bars'], ['bars', 'content']. +---------------------+------------------------------------------------+ | ['content'] | Content is scrolled by dragging or swiping the | | | content directly. | +---------------------+------------------------------------------------+ | ['bars'] | Content is scrolled by dragging or swiping the | | | scoll bars. | +---------------------+------------------------------------------------+ | ['bars', 'content'] | Content is scrolled by either of the above | | | methods. | +---------------------+------------------------------------------------+ .. versionadded:: 1.8.0 :attr:`scroll_type` is an :class:`~kivy.properties.OptionProperty` and defaults to ['content']. ''' smooth_scroll_end = NumericProperty(None, allownone=True) '''Whether smooth scroll end should be used when scrolling with the mouse-wheel and the factor of transforming the scroll distance to velocity. This option also enables velocity addition meaning if you scroll more, you will scroll faster and further. The recommended value is `10`. The velocity is calculated as :attr:`scroll_wheel_distance` * :attr:`smooth_scroll_end`. .. versionadded:: 1.11.0 :attr:`smooth_scroll_end` is a :class:`~kivy.properties.NumericProperty` and defaults to None. ''' # private, for internal use only _viewport = ObjectProperty(None, allownone=True) _bar_color = ListProperty([0, 0, 0, 0]) _effect_x_start_width = None _effect_y_start_height = None _update_effect_bounds_ev = None _bind_inactive_bar_color_ev = None def _set_viewport_size(self, instance, value): self.viewport_size = value def on__viewport(self, instance, value): if value: value.bind(size=self._set_viewport_size) self.viewport_size = value.size __events__ = ('on_scroll_start', 'on_scroll_move', 'on_scroll_stop') def __init__(self, **kwargs): self._touch = None self._trigger_update_from_scroll = Clock.create_trigger( self.update_from_scroll, -1) # create a specific canvas for the viewport from kivy.graphics import PushMatrix, Translate, PopMatrix, Canvas self.canvas_viewport = Canvas() self.canvas = Canvas() with self.canvas_viewport.before: PushMatrix() self.g_translate = Translate(0, 0) with self.canvas_viewport.after: PopMatrix() super(ScrollView, self).__init__(**kwargs) self.register_event_type('on_scroll_start') self.register_event_type('on_scroll_move') self.register_event_type('on_scroll_stop') # now add the viewport canvas to our canvas self.canvas.add(self.canvas_viewport) effect_cls = self.effect_cls if isinstance(effect_cls, string_types): effect_cls = Factory.get(effect_cls) if self.effect_x is None and effect_cls is not None: self.effect_x = effect_cls(target_widget=self._viewport) if self.effect_y is None and effect_cls is not None: self.effect_y = effect_cls(target_widget=self._viewport) trigger_update_from_scroll = self._trigger_update_from_scroll update_effect_widget = self._update_effect_widget update_effect_x_bounds = self._update_effect_x_bounds update_effect_y_bounds = self._update_effect_y_bounds fbind = self.fbind fbind('width', update_effect_x_bounds) fbind('height', update_effect_y_bounds) fbind('viewport_size', self._update_effect_bounds) fbind('_viewport', update_effect_widget) fbind('scroll_x', trigger_update_from_scroll) fbind('scroll_y', trigger_update_from_scroll) fbind('pos', trigger_update_from_scroll) fbind('size', trigger_update_from_scroll) fbind('scroll_y', self._update_effect_bounds) fbind('scroll_x', self._update_effect_bounds) trigger_update_from_scroll() update_effect_widget() update_effect_x_bounds() update_effect_y_bounds() def on_effect_x(self, instance, value): if value: value.bind(scroll=self._update_effect_x) value.target_widget = self._viewport def on_effect_y(self, instance, value): if value: value.bind(scroll=self._update_effect_y) value.target_widget = self._viewport def on_effect_cls(self, instance, cls): if isinstance(cls, string_types): cls = Factory.get(cls) self.effect_x = cls(target_widget=self._viewport) self.effect_x.bind(scroll=self._update_effect_x) self.effect_y = cls(target_widget=self._viewport) self.effect_y.bind(scroll=self._update_effect_y) def _update_effect_widget(self, *args): if self.effect_x: self.effect_x.target_widget = self._viewport if self.effect_y: self.effect_y.target_widget = self._viewport def _update_effect_x_bounds(self, *args): if not self._viewport or not self.effect_x: return self.effect_x.min = -(self.viewport_size[0] - self.width) self.effect_x.max = 0 self.effect_x.value = self.effect_x.min * self.scroll_x def _update_effect_y_bounds(self, *args): if not self._viewport or not self.effect_y: return self.effect_y.min = -(self.viewport_size[1] - self.height) self.effect_y.max = 0 self.effect_y.value = self.effect_y.min * self.scroll_y def _update_effect_bounds(self, *args): if not self._viewport: return if self.effect_x: self._update_effect_x_bounds() if self.effect_y: self._update_effect_y_bounds() def _update_effect_x(self, *args): vp = self._viewport if not vp or not self.effect_x: return if self.effect_x.is_manual: sw = vp.width - self._effect_x_start_width else: sw = vp.width - self.width if sw < 1: return sx = self.effect_x.scroll / float(sw) self.scroll_x = -sx self._trigger_update_from_scroll() def _update_effect_y(self, *args): vp = self._viewport if not vp or not self.effect_y: return if self.effect_y.is_manual: sh = vp.height - self._effect_y_start_height else: sh = vp.height - self.height if sh < 1: return sy = self.effect_y.scroll / float(sh) self.scroll_y = -sy self._trigger_update_from_scroll() def to_local(self, x, y, **k): tx, ty = self.g_translate.xy return x - tx, y - ty def to_parent(self, x, y, **k): tx, ty = self.g_translate.xy return x + tx, y + ty def _apply_transform(self, m, pos=None): tx, ty = self.g_translate.xy m.translate(tx, ty, 0) return super(ScrollView, self)._apply_transform(m, (0, 0)) def simulate_touch_down(self, touch): # at this point the touch is in parent coords touch.push() touch.apply_transform_2d(self.to_local) ret = super(ScrollView, self).on_touch_down(touch) touch.pop() return ret def on_touch_down(self, touch): if self.dispatch('on_scroll_start', touch): self._touch = touch touch.grab(self) return True def _touch_in_handle(self, pos, size, touch): x, y = pos width, height = size return x <= touch.x <= x + width and y <= touch.y <= y + height def on_scroll_start(self, touch, check_children=True): if check_children: touch.push() touch.apply_transform_2d(self.to_local) if self.dispatch_children('on_scroll_start', touch): touch.pop() return True touch.pop() if not self.collide_point(*touch.pos): touch.ud[self._get_uid('svavoid')] = True return if self.disabled: return True if self._touch or (not (self.do_scroll_x or self.do_scroll_y)): return self.simulate_touch_down(touch) # handle mouse scrolling, only if the viewport size is bigger than the # scrollview size, and if the user allowed to do it vp = self._viewport if not vp: return True scroll_type = self.scroll_type ud = touch.ud scroll_bar = 'bars' in scroll_type # check if touch is in bar_x(horizontal) or bar_y(vertical) width_scrollable = vp.width > self.width height_scrollable = vp.height > self.height d = { 'bottom': touch.y - self.y - self.bar_margin, 'top': self.top - touch.y - self.bar_margin, 'left': touch.x - self.x - self.bar_margin, 'right': self.right - touch.x - self.bar_margin } ud['in_bar_x'] = (scroll_bar and width_scrollable and (0 <= d[self.bar_pos_x] <= self.bar_width)) ud['in_bar_y'] = (scroll_bar and height_scrollable and (0 <= d[self.bar_pos_y] <= self.bar_width)) if vp and 'button' in touch.profile and \ touch.button.startswith('scroll'): btn = touch.button m = self.scroll_wheel_distance e = None if ((btn == 'scrolldown' and self.scroll_y >= 1) or (btn == 'scrollup' and self.scroll_y <= 0) or (btn == 'scrollleft' and self.scroll_x >= 1) or (btn == 'scrollright' and self.scroll_x <= 0)): return False if (self.effect_x and self.do_scroll_y and height_scrollable and btn in ('scrolldown', 'scrollup')): e = self.effect_x if ud['in_bar_x'] else self.effect_y elif (self.effect_y and self.do_scroll_x and width_scrollable and btn in ('scrollleft', 'scrollright')): e = self.effect_y if ud['in_bar_y'] else self.effect_x if e: if btn in ('scrolldown', 'scrollleft'): if self.smooth_scroll_end: e.velocity -= m * self.smooth_scroll_end else: e.value = max(e.value - m, e.min) e.velocity = 0 elif btn in ('scrollup', 'scrollright'): if self.smooth_scroll_end: e.velocity += m * self.smooth_scroll_end else: e.value = min(e.value + m, e.max) e.velocity = 0 touch.ud[self._get_uid('svavoid')] = True e.trigger_velocity_update() return True in_bar = ud['in_bar_x'] or ud['in_bar_y'] if scroll_type == ['bars'] and not in_bar: return self.simulate_touch_down(touch) if in_bar: if (ud['in_bar_y'] and not self._touch_in_handle( self._handle_y_pos, self._handle_y_size, touch)): self.scroll_y = (touch.y - self.y) / self.height elif (ud['in_bar_x'] and not self._touch_in_handle( self._handle_x_pos, self._handle_x_size, touch)): self.scroll_x = (touch.x - self.x) / self.width # no mouse scrolling, so the user is going to drag the scrollview with # this touch. self._touch = touch uid = self._get_uid() ud[uid] = { 'mode': 'unknown', 'dx': 0, 'dy': 0, 'user_stopped': in_bar, 'frames': Clock.frames, 'time': touch.time_start } if self.do_scroll_x and self.effect_x and not ud['in_bar_x']: self._effect_x_start_width = self.width self.effect_x.start(touch.x) self._scroll_x_mouse = self.scroll_x if self.do_scroll_y and self.effect_y and not ud['in_bar_y']: self._effect_y_start_height = self.height self.effect_y.start(touch.y) self._scroll_y_mouse = self.scroll_y if not in_bar: Clock.schedule_once(self._change_touch_mode, self.scroll_timeout / 1000.) return True def on_touch_move(self, touch): if self._touch is not touch: # don't pass on touch to children if outside the sv if self.collide_point(*touch.pos): # touch is in parent touch.push() touch.apply_transform_2d(self.to_local) super(ScrollView, self).on_touch_move(touch) touch.pop() return self._get_uid() in touch.ud if touch.grab_current is not self: return True if not any( isinstance(key, str) and key.startswith('sv.') for key in touch.ud): # don't pass on touch to children if outside the sv if self.collide_point(*touch.pos): # touch is in window coordinates touch.push() touch.apply_transform_2d(self.to_local) res = super(ScrollView, self).on_touch_move(touch) touch.pop() return res return False touch.ud['sv.handled'] = {'x': False, 'y': False} if self.dispatch('on_scroll_move', touch): return True def on_scroll_move(self, touch): if self._get_uid('svavoid') in touch.ud: return False touch.push() touch.apply_transform_2d(self.to_local) if self.dispatch_children('on_scroll_move', touch): touch.pop() return True touch.pop() rv = True # By default this touch can be used to defocus currently focused # widget, like any touch outside of ScrollView. touch.ud['sv.can_defocus'] = True uid = self._get_uid() if uid not in touch.ud: self._touch = False return self.on_scroll_start(touch, False) ud = touch.ud[uid] # check if the minimum distance has been travelled if ud['mode'] == 'unknown': if not self.do_scroll_x and not self.do_scroll_y: # touch is in parent, but _change expects window coords touch.push() touch.apply_transform_2d(self.to_local) touch.apply_transform_2d(self.to_window) self._change_touch_mode() touch.pop() return ud['dx'] += abs(touch.dx) ud['dy'] += abs(touch.dy) if ((ud['dx'] > self.scroll_distance and self.do_scroll_x) or (ud['dy'] > self.scroll_distance and self.do_scroll_y)): ud['mode'] = 'scroll' if ud['mode'] == 'scroll': if not touch.ud['sv.handled']['x'] and self.do_scroll_x \ and self.effect_x: width = self.width if touch.ud.get('in_bar_x', False): dx = touch.dx / float(width - width * self.hbar[1]) self.scroll_x = min(max(self.scroll_x + dx, 0.), 1.) self._trigger_update_from_scroll() else: if self.scroll_type != ['bars']: self.effect_x.update(touch.x) if self.scroll_x < 0 or self.scroll_x > 1: rv = False else: touch.ud['sv.handled']['x'] = True # Touch resulted in scroll should not defocus focused widget touch.ud['sv.can_defocus'] = False if not touch.ud['sv.handled']['y'] and self.do_scroll_y \ and self.effect_y: height = self.height if touch.ud.get('in_bar_y', False): dy = touch.dy / float(height - height * self.vbar[1]) self.scroll_y = min(max(self.scroll_y + dy, 0.), 1.) self._trigger_update_from_scroll() else: if self.scroll_type != ['bars']: self.effect_y.update(touch.y) if self.scroll_y < 0 or self.scroll_y > 1: rv = False else: touch.ud['sv.handled']['y'] = True # Touch resulted in scroll should not defocus focused widget touch.ud['sv.can_defocus'] = False ud['dt'] = touch.time_update - ud['time'] ud['time'] = touch.time_update ud['user_stopped'] = True return rv def on_touch_up(self, touch): uid = self._get_uid('svavoid') if self._touch is not touch and uid not in touch.ud: # don't pass on touch to children if outside the sv if self.collide_point(*touch.pos): # touch is in parents touch.push() touch.apply_transform_2d(self.to_local) if super(ScrollView, self).on_touch_up(touch): touch.pop() return True touch.pop() return False if self.dispatch('on_scroll_stop', touch): touch.ungrab(self) if not touch.ud.get('sv.can_defocus', True): # Focused widget should stay focused FocusBehavior.ignored_touch.append(touch) return True def on_scroll_stop(self, touch, check_children=True): self._touch = None if check_children: touch.push() touch.apply_transform_2d(self.to_local) if self.dispatch_children('on_scroll_stop', touch): touch.pop() return True touch.pop() if self._get_uid('svavoid') in touch.ud: return if self._get_uid() not in touch.ud: return False self._touch = None uid = self._get_uid() ud = touch.ud[uid] if self.do_scroll_x and self.effect_x: if not touch.ud.get('in_bar_x', False) and\ self.scroll_type != ['bars']: self.effect_x.stop(touch.x) if self.do_scroll_y and self.effect_y and\ self.scroll_type != ['bars']: if not touch.ud.get('in_bar_y', False): self.effect_y.stop(touch.y) if ud['mode'] == 'unknown': # we must do the click at least.. # only send the click if it was not a click to stop # autoscrolling if not ud['user_stopped']: self.simulate_touch_down(touch) Clock.schedule_once(partial(self._do_touch_up, touch), .2) ev = self._update_effect_bounds_ev if ev is None: ev = self._update_effect_bounds_ev = Clock.create_trigger( self._update_effect_bounds) ev() # if we do mouse scrolling, always accept it if 'button' in touch.profile and touch.button.startswith('scroll'): return True return self._get_uid() in touch.ud def scroll_to(self, widget, padding=10, animate=True): '''Scrolls the viewport to ensure that the given widget is visible, optionally with padding and animation. If animate is True (the default), then the default animation parameters will be used. Otherwise, it should be a dict containing arguments to pass to :class:`~kivy.animation.Animation` constructor. .. versionadded:: 1.9.1 ''' if not self.parent: return # if _viewport is layout and has pending operation, reschedule if hasattr(self._viewport, 'do_layout'): if self._viewport._trigger_layout.is_triggered: Clock.schedule_once( lambda *dt: self.scroll_to(widget, padding, animate)) return if isinstance(padding, (int, float)): padding = (padding, padding) pos = self.parent.to_widget(*widget.to_window(*widget.pos)) cor = self.parent.to_widget( *widget.to_window(widget.right, widget.top)) dx = dy = 0 if pos[1] < self.y: dy = self.y - pos[1] + dp(padding[1]) elif cor[1] > self.top: dy = self.top - cor[1] - dp(padding[1]) if pos[0] < self.x: dx = self.x - pos[0] + dp(padding[0]) elif cor[0] > self.right: dx = self.right - cor[0] - dp(padding[0]) dsx, dsy = self.convert_distance_to_scroll(dx, dy) sxp = min(1, max(0, self.scroll_x - dsx)) syp = min(1, max(0, self.scroll_y - dsy)) if animate: if animate is True: animate = {'d': 0.2, 't': 'out_quad'} Animation.stop_all(self, 'scroll_x', 'scroll_y') Animation(scroll_x=sxp, scroll_y=syp, **animate).start(self) else: self.scroll_x = sxp self.scroll_y = syp def convert_distance_to_scroll(self, dx, dy): '''Convert a distance in pixels to a scroll distance, depending on the content size and the scrollview size. The result will be a tuple of scroll distance that can be added to :data:`scroll_x` and :data:`scroll_y` ''' if not self._viewport: return 0, 0 vp = self._viewport if vp.width > self.width: sw = vp.width - self.width sx = dx / float(sw) else: sx = 0 if vp.height > self.height: sh = vp.height - self.height sy = dy / float(sh) else: sy = 1 return sx, sy def update_from_scroll(self, *largs): '''Force the reposition of the content, according to current value of :attr:`scroll_x` and :attr:`scroll_y`. This method is automatically called when one of the :attr:`scroll_x`, :attr:`scroll_y`, :attr:`pos` or :attr:`size` properties change, or if the size of the content changes. ''' if not self._viewport: self.g_translate.xy = self.pos return vp = self._viewport # update from size_hint if vp.size_hint_x is not None: w = vp.size_hint_x * self.width if vp.size_hint_min_x is not None: w = max(w, vp.size_hint_min_x) if vp.size_hint_max_x is not None: w = min(w, vp.size_hint_max_x) vp.width = w if vp.size_hint_y is not None: h = vp.size_hint_y * self.height if vp.size_hint_min_y is not None: h = max(h, vp.size_hint_min_y) if vp.size_hint_max_y is not None: h = min(h, vp.size_hint_max_y) vp.height = h if vp.width > self.width: sw = vp.width - self.width x = self.x - self.scroll_x * sw else: x = self.x if vp.height > self.height: sh = vp.height - self.height y = self.y - self.scroll_y * sh else: y = self.top - vp.height # from 1.8.0, we now use a matrix by default, instead of moving the # widget position behind. We set it here, but it will be a no-op most # of the time. vp.pos = 0, 0 self.g_translate.xy = x, y # New in 1.2.0, show bar when scrolling happens and (changed in 1.9.0) # fade to bar_inactive_color when no scroll is happening. ev = self._bind_inactive_bar_color_ev if ev is None: ev = self._bind_inactive_bar_color_ev = Clock.create_trigger( self._bind_inactive_bar_color, .5) self.funbind('bar_inactive_color', self._change_bar_color) Animation.stop_all(self, '_bar_color') self.fbind('bar_color', self._change_bar_color) self._bar_color = self.bar_color ev() def _bind_inactive_bar_color(self, *l): self.funbind('bar_color', self._change_bar_color) self.fbind('bar_inactive_color', self._change_bar_color) Animation(_bar_color=self.bar_inactive_color, d=.5, t='out_quart').start(self) def _change_bar_color(self, inst, value): self._bar_color = value # # Private # def add_widget(self, widget, index=0): if self._viewport: raise Exception('ScrollView accept only one widget') canvas = self.canvas self.canvas = self.canvas_viewport super(ScrollView, self).add_widget(widget, index) self.canvas = canvas self._viewport = widget widget.bind(size=self._trigger_update_from_scroll, size_hint_min=self._trigger_update_from_scroll) self._trigger_update_from_scroll() def remove_widget(self, widget): canvas = self.canvas self.canvas = self.canvas_viewport super(ScrollView, self).remove_widget(widget) self.canvas = canvas if widget is self._viewport: self._viewport = None def _get_uid(self, prefix='sv'): return '{0}.{1}'.format(prefix, self.uid) def _change_touch_mode(self, *largs): if not self._touch: return uid = self._get_uid() touch = self._touch if uid not in touch.ud: self._touch = False return ud = touch.ud[uid] if ud['mode'] != 'unknown' or ud['user_stopped']: return diff_frames = Clock.frames - ud['frames'] # in order to be able to scroll on very slow devices, let at least 3 # frames displayed to accumulate some velocity. And then, change the # touch mode. Otherwise, we might never be able to compute velocity, # and no way to scroll it. See #1464 and #1499 if diff_frames < 3: Clock.schedule_once(self._change_touch_mode, 0) return if self.do_scroll_x and self.effect_x: self.effect_x.cancel() if self.do_scroll_y and self.effect_y: self.effect_y.cancel() # XXX the next line was in the condition. But this stop # the possibility to "drag" an object out of the scrollview in the # non-used direction: if you have an horizontal scrollview, a # vertical gesture will not "stop" the scroll view to look for an # horizontal gesture, until the timeout is done. # and touch.dx + touch.dy == 0: touch.ungrab(self) self._touch = None # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) touch.apply_transform_2d(self.to_parent) self.simulate_touch_down(touch) touch.pop() return def _do_touch_up(self, touch, *largs): # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) super(ScrollView, self).on_touch_up(touch) touch.pop() # don't forget about grab event! for x in touch.grab_list[:]: touch.grab_list.remove(x) x = x() if not x: continue touch.grab_current = x # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) super(ScrollView, self).on_touch_up(touch) touch.pop() touch.grab_current = None
class CheckBox(ToggleButtonBehavior, Widget): '''CheckBox class, see module documentation for more information. ''' def _get_active(self): return self.state == 'down' def _set_active(self, value): self.state = 'down' if value else 'normal' active = AliasProperty(_get_active, _set_active, bind=('state', ), cache=True) '''Indicates if the switch is active or inactive. :attr:`active` is a boolean and reflects and sets whether the underlying :attr:`~kivy.uix.button.Button.state` is 'down' (True) or 'normal' (False). It is a :class:`~kivy.properties.AliasProperty`, which accepts boolean values and defaults to False. .. versionchanged:: 1.11.0 It changed from a BooleanProperty to a AliasProperty. ''' background_checkbox_normal = StringProperty( 'atlas://data/images/defaulttheme/checkbox_off') '''Background image of the checkbox used for the default graphical representation when the checkbox is not active. .. versionadded:: 1.9.0 :attr:`background_checkbox_normal` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_off'. ''' background_checkbox_down = StringProperty( 'atlas://data/images/defaulttheme/checkbox_on') '''Background image of the checkbox used for the default graphical representation when the checkbox is active. .. versionadded:: 1.9.0 :attr:`background_checkbox_down` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_on'. ''' background_checkbox_disabled_normal = StringProperty( 'atlas://data/images/defaulttheme/checkbox_disabled_off') '''Background image of the checkbox used for the default graphical representation when the checkbox is disabled and not active. .. versionadded:: 1.9.0 :attr:`background_checkbox_disabled_normal` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_disabled_off'. ''' background_checkbox_disabled_down = StringProperty( 'atlas://data/images/defaulttheme/checkbox_disabled_on') '''Background image of the checkbox used for the default graphical representation when the checkbox is disabled and active. .. versionadded:: 1.9.0 :attr:`background_checkbox_disabled_down` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_disabled_on'. ''' background_radio_normal = StringProperty( 'atlas://data/images/defaulttheme/checkbox_radio_off') '''Background image of the radio button used for the default graphical representation when the radio button is not active. .. versionadded:: 1.9.0 :attr:`background_radio_normal` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_radio_off'. ''' background_radio_down = StringProperty( 'atlas://data/images/defaulttheme/checkbox_radio_on') '''Background image of the radio button used for the default graphical representation when the radio button is active. .. versionadded:: 1.9.0 :attr:`background_radio_down` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_radio_on'. ''' background_radio_disabled_normal = StringProperty( 'atlas://data/images/defaulttheme/checkbox_radio_disabled_off') '''Background image of the radio button used for the default graphical representation when the radio button is disabled and not active. .. versionadded:: 1.9.0 :attr:`background_radio_disabled_normal` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_radio_disabled_off'. ''' background_radio_disabled_down = StringProperty( 'atlas://data/images/defaulttheme/checkbox_radio_disabled_on') '''Background image of the radio button used for the default graphical representation when the radio button is disabled and active. .. versionadded:: 1.9.0 :attr:`background_radio_disabled_down` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_radio_disabled_on'. ''' color = ListProperty([1, 1, 1, 1]) '''Color is used for tinting the default graphical representation of checkbox and radio button (images). Color is in the format (r, g, b, a). .. versionadded:: 1.10.0 :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to '[1, 1, 1, 1]'. ''' def __init__(self, **kwargs): self.fbind('state', self._on_state) super(CheckBox, self).__init__(**kwargs) def _on_state(self, instance, value): if self.group and self.state == 'down': self._release_group(self) def on_group(self, *largs): super(CheckBox, self).on_group(*largs) if self.active: self._release_group(self)
class ShutterZone(AnchorLayout): aspect = NumericProperty(1.) quadrant = NumericProperty(0) selected = ListProperty([]) theapp = ObjectProperty(None) update = BooleanProperty(False) def on_touch_down(self, touch): if not touch.button == 'left' or self.disabled: return x, y = touch.pos if not self.collide_point(x, y): return tmp = self.selected[:] lx, ly = self.to_local(x, y, relative=True) col = 364 - int(lx / self.width * 365) row = 170 - int(ly / self.height * 171) val = self.theapp.all_shutters[self.quadrant][(col, row)] if (col, row) in self.selected: tmp.remove((col, row)) elif val == '0': tmp.append((col, row)) self.selected = tmp def _get_shutter_texture(self): txtr = Texture.create(size=(365 * 3, 171 * 3), colorfmt='rgba', mipmap=True) data = np.zeros((171, 365, 4), dtype=np.uint8) if self.theapp is not None and self.theapp.all_shutters[self.quadrant]: color = { "0": [255, 155, 5, 255], "1": [0, 0, 0, 0], "x": [68, 68, 68, 255], "s": [135, 44, 44, 255] } s_color = [29, 226, 226, 255] for j in range(171): for i in range(365): val = self.theapp.all_shutters[self.quadrant][(i, j)] colr = color[val] if (i, j) not in self.selected else s_color data[170 - j, 364 - i] = colr data = np.repeat(np.repeat(data, 3, axis=0), 3, axis=1) txtr.blit_buffer(data.tostring(), colorfmt='rgba', bufferfmt='ubyte') return txtr shutter_texture = AliasProperty(_get_shutter_texture, None, bind=['update', 'selected', 'quadrant']) def _get_zposhint(self): if self.quadrant in [0, 1]: xhint = 1.24 / 2.24 else: xhint = 0.005 if self.quadrant in [0, 2]: yhint = 1.38 / 2.38 else: yhint = 0.005 #xhint = 0.5 * (1 - self.quadrant // 2) + 0.01 #yhint = 0.25 - 0.24 / self.aspect + 0.5 * (1 - self.quadrant % 2) return {'x': xhint, 'y': yhint} zposhint = AliasProperty(_get_zposhint, None, bind=['quadrant', 'aspect'])
class ShutterScreen(Screen): msafile = StringProperty('') selected = ListProperty([]) filtname = StringProperty('') gratname = StringProperty('') recent_select = ListProperty([]) def on_pre_enter(self): self.ids.shutterpane.transform_with_touch(False) def on_leave(self): self.ids.shutterpane.scale = 1.0 self.ids.shutterpane.transform_with_touch(False) def _recent_select_text(self): if not self.recent_select: return "" return "Q {}, I {}, J {}".format(*self.recent_select) recent_select_text = AliasProperty(_recent_select_text, None, bind=['recent_select']) def save_dialog(self): suggested, ext = os.path.splitext(self.msafile) suggested += '_' + self.filtname + '_' + self.gratname + '_shutters.png' popup = MSAFilePopup(title="Save...", allowed_ext=["*.png"], suggested_file=suggested) popup.bind(on_dismiss=self.save_png) popup.open() def find_dialog(self): sel = [(q + 1, i + 1, j + 1) for q, quad in enumerate(self.selected) for i, j in quad] popup = FindShutterPopup(selected=sel) popup.bind(on_dismiss=self.zoom_to) popup.open() def zoom_to(self, instance): if instance.canceled: return stencil = self.ids.stencil #parent of the scatter scatter = self.ids.shutterpane q, i, j = instance.quad, instance.col, instance.row #set the scatter scaling correctly scatter.scale = 4.0 #determine the fractional shutter position, w/r/t to quadrant placement #in the scatter quadrant = self.ids.shutterlayout.ids['q' + str(q)] qw, qh = quadrant.size_hint qx, qy = [quadrant.pos_hint[c] for c in 'xy'] sx0 = (364 - i) / 365 #fractional x in the quadrant sy0 = (170 - j) / 171 #fractional y in the quadrant sfx = sx0 * qw + qx #fractional x position in the scatter sfy = sy0 * qh + qy #fractional y position in the scatter #Now find the offsets from the scatter center, and move the center so #that the shutter is on the target (which is the stencil center) scx, scy = scatter.center (bx, by), (bw, bh) = scatter.bbox stx, sty = stencil.center sx = sfx * bw + bx #pixel x position in scatter sy = sfy * bh + by #pixel y position in scatter x1 = (stx - sx) + scx y1 = (sty - sy) + scy scatter.center = [x1, y1] scatter.transform_with_touch(False) #enforce the border lock def save_png(self, instance): if instance.canceled: return png_out = os.path.join(instance.selected_path, instance.selected_file) filebase, ext = os.path.splitext(png_out) png_out = filebase + '.png' self.ids.shutterpane.export_to_png(png_out)
class MeshLinePlot(Plot): '''MeshLinePlot class which displays a set of points similar to a mesh. ''' # mesh which forms the plot _mesh = ObjectProperty(None) # color of the plot _color = ObjectProperty(None) _trigger = ObjectProperty(None) # most recent values of the params used to draw the plot _params = DictProperty({ 'xlog': False, 'xmin': 0, 'xmax': 100, 'ylog': False, 'ymin': 0, 'ymax': 100, 'size': (0, 0, 0, 0) }) def __init__(self, **kwargs): self._color = Color(1, 1, 1, group='LinePlot%d' % id(self)) self._mesh = Mesh(mode='line_strip', group='LinePlot%d' % id(self)) super(MeshLinePlot, self).__init__(**kwargs) self._trigger = Clock.create_trigger(self._redraw) self.bind(_params=self._trigger, points=self._trigger) def _update(self, xlog, xmin, xmax, ylog, ymin, ymax, size): self._params = { 'xlog': xlog, 'xmin': xmin, 'xmax': xmax, 'ylog': ylog, 'ymin': ymin, 'ymax': ymax, 'size': size } def _redraw(self, *args): points = self.points mesh = self._mesh vert = mesh.vertices ind = mesh.indices params = self._params funcx = log10 if params['xlog'] else lambda x: x funcy = log10 if params['ylog'] else lambda x: x xmin = funcx(params['xmin']) ymin = funcy(params['ymin']) diff = len(points) - len(vert) / 4 size = params['size'] ratiox = (size[2] - size[0]) / float(funcx(params['xmax']) - xmin) ratioy = (size[3] - size[1]) / float(funcy(params['ymax']) - ymin) if diff < 0: del vert[4 * len(points):] del ind[len(points):] elif diff > 0: ind.extend(xrange(len(ind), len(ind) + diff)) vert.extend([0] * (diff * 4)) for k in xrange(len(points)): vert[k * 4] = (funcx(points[k][0]) - xmin) * ratiox + size[0] vert[k * 4 + 1] = (funcy(points[k][1]) - ymin) * ratioy + size[1] mesh.vertices = vert def _get_group(self): return 'LinePlot%d' % id(self) def _get_drawings(self): return [self._color, self._mesh] def _set_mode(self, value): self._mesh.mode = value mode = AliasProperty(lambda self: self._mesh.mode, _set_mode) '''VBO Mode used for drawing the points. Can be one of: 'points', 'line_strip', 'line_loop', 'lines', 'triangle_strip', 'triangle_fan'. See :class:`~kivy.graphics.Mesh` for more details. Defaults to 'line_strip'. ''' def _set_color(self, value): self._color.rgba = value color = AliasProperty(lambda self: self._color.rgba, _set_color) '''Plot color, in the format [r, g, b, a] with values between 0-1. Defaults to [1, 1, 1, 1]. ''' points = ListProperty([]) '''List of x, y points to be displayed in the plot.
class Sound(EventDispatcher): '''Represent a sound to play. This class is abstract, and cannot be used directly. Use SoundLoader to load a sound ! :Events: `on_play` : None Fired when the sound is played `on_stop` : None Fired when the sound is stopped ''' source = StringProperty(None) '''Filename / source of your image. .. versionadded:: 1.3.0 :data:`source` a :class:`~kivy.properties.StringProperty`, default to None, read-only. Use the :meth:`SoundLoader.load` for loading audio. ''' volume = NumericProperty(1.) '''Volume, in the range 0-1. 1 mean full volume, 0 mean mute. .. versionadded:: 1.3.0 :data:`volume` is a :class:`~kivy.properties.NumericProperty`, default to 1. ''' state = OptionProperty('stop', options=('stop', 'play')) '''State of the sound, one of 'stop' or 'play' .. versionadded:: 1.3.0 :data:`state` is an :class:`~kivy.properties.OptionProperty`, read-only. ''' # # deprecated # def _get_status(self): return self.state status = AliasProperty(_get_status, None, bind=('state', )) ''' .. deprecated:: 1.3.0 Use :data:`state` instead ''' def _get_filename(self): return self.source filename = AliasProperty(_get_filename, None, bind=('source', )) ''' .. deprecated:: 1.3.0 Use :data:`source` instead ''' __events__ = ('on_play', 'on_stop') def on_source(self, instance, filename): self.unload() if filename is None: return self.load() def get_pos(self): ''' get the current position of the audio file. returns 0 if not playing .. versionadded:: 1.4.1 ''' return 0 def _get_length(self): return 0 length = property(lambda self: self._get_length(), doc='Get length of the sound (in seconds)') def load(self): '''Load the file into memory''' pass def unload(self): '''Unload the file from memory''' pass def play(self): '''Play the file''' self.state = 'play' self.dispatch('on_play') def stop(self): '''Stop playback''' self.state = 'stop' self.dispatch('on_stop') def seek(self, position): '''Seek to the <position> (in seconds)''' pass def on_play(self): pass def on_stop(self): pass
class ElectrumWindow(App): electrum_config = ObjectProperty(None) language = StringProperty('en') def set_URI(self, uri): self.switch_to('send') self.send_screen.set_URI(uri) def on_new_intent(self, intent): if intent.getScheme() != 'bitcoin': return uri = intent.getDataString() self.set_URI(uri) def on_language(self, instance, language): Logger.info('language: {}'.format(language)) _.switch_lang(language) def on_quotes(self, d): #Logger.info("on_quotes") pass def on_history(self, d): #Logger.info("on_history") if self.history_screen: Clock.schedule_once(lambda dt: self.history_screen.update()) def _get_bu(self): return self.electrum_config.get('base_unit', 'mBTC') def _set_bu(self, value): assert value in base_units.keys() self.electrum_config.set_key('base_unit', value, True) self.update_status() if self.history_screen: self.history_screen.update() base_unit = AliasProperty(_get_bu, _set_bu) status = StringProperty('') fiat_unit = StringProperty('') def on_fiat_unit(self, a, b): if self.history_screen: self.history_screen.update() def decimal_point(self): return base_units[self.base_unit] def btc_to_fiat(self, amount_str): if not amount_str: return '' rate = run_hook('exchange_rate') if not rate: return '' fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8) return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.') def fiat_to_btc(self, fiat_amount): if not fiat_amount: return '' rate = run_hook('exchange_rate') if not rate: return '' satoshis = int(pow(10, 8) * Decimal(fiat_amount) / Decimal(rate)) return format_satoshis_plain(satoshis, self.decimal_point()) def get_amount(self, amount_str): a, u = amount_str.split() assert u == self.base_unit try: x = Decimal(a) except: return None p = pow(10, self.decimal_point()) return int(p * x) _orientation = OptionProperty('landscape', options=('landscape', 'portrait')) def _get_orientation(self): return self._orientation orientation = AliasProperty(_get_orientation, None, bind=('_orientation', )) '''Tries to ascertain the kind of device the app is running on. Cane be one of `tablet` or `phone`. :data:`orientation` is a read only `AliasProperty` Defaults to 'landscape' ''' _ui_mode = OptionProperty('phone', options=('tablet', 'phone')) def _get_ui_mode(self): return self._ui_mode ui_mode = AliasProperty(_get_ui_mode, None, bind=('_ui_mode', )) '''Defines tries to ascertain the kind of device the app is running on. Cane be one of `tablet` or `phone`. :data:`ui_mode` is a read only `AliasProperty` Defaults to 'phone' ''' wallet = ObjectProperty(None) '''Holds the electrum wallet :attr:`wallet` is a `ObjectProperty` defaults to None. ''' def __init__(self, **kwargs): # initialize variables self._clipboard = Clipboard self.info_bubble = None self.qrscanner = None self.nfcscanner = None self.tabs = None self.is_exit = False super(ElectrumWindow, self).__init__(**kwargs) title = _('Electrum App') self.electrum_config = config = kwargs.get('config', None) self.language = config.get('language', 'en') self.network = network = kwargs.get('network', None) self.plugins = kwargs.get('plugins', []) self.gui_object = kwargs.get('gui_object', None) #self.config = self.gui_object.config self.contacts = Contacts(self.electrum_config) self.invoices = InvoiceStore(self.electrum_config) # create triggers so as to minimize updation a max of 2 times a sec self._trigger_update_wallet =\ Clock.create_trigger(self.update_wallet, .5) self._trigger_update_status =\ Clock.create_trigger(self.update_status, .5) self._trigger_notify_transactions = \ Clock.create_trigger(self.notify_transactions, 5) # cached dialogs self._settings_dialog = None self._password_dialog = None def on_pr(self, pr): if pr.verify(self.contacts): key = self.invoices.add(pr) if self.invoices_screen: self.invoices_screen.update() status = self.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): if data.startswith('bitcoin:'): self.set_URI(data) return # try to decode transaction from electrum.bitcoin import base_decode from electrum.transaction import Transaction try: text = base_decode(data, None, base=43).encode('hex') tx = Transaction(text) except: tx = None if tx: self.tx_dialog(tx) return # show error self.show_error("Unable to decode QR data") def update_tab(self, name): s = getattr(self, name + '_screen', None) if s: s.update() @profiler def update_tabs(self): for tab in ['invoices', 'send', 'history', 'receive', 'requests']: self.update_tab(tab) def switch_to(self, name): s = getattr(self, name + '_screen', None) if self.send_screen 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.util import format_time requestor = req.get('requestor') exp = req.get('exp') memo = req.get('memo') amount = req.get('amount') popup = Builder.load_file('gui/kivy/uix/ui_screens/invoice.kv') popup.is_invoice = is_invoice popup.amount = amount popup.requestor = requestor if is_invoice else req.get('address') popup.exp = format_time(exp) if exp else '' popup.description = memo if memo else '' popup.signature = req.get('signature', '') popup.status = status txid = req.get('txid') popup.tx_hash = txid or '' popup.on_open = lambda: popup.ids.output_list.update( req.get('outputs', [])) popup.open() def qr_dialog(self, title, data, show_text=False): from uix.dialogs.qr_dialog import QRDialog popup = QRDialog(title, data, show_text) popup.open() def scan_qr(self, on_complete): if platform != 'android': return from jnius import autoclass from android import activity PythonActivity = autoclass('org.renpy.android.PythonActivity') Intent = autoclass('android.content.Intent') intent = Intent("com.google.zxing.client.android.SCAN") intent.putExtra("SCAN_MODE", "QR_CODE_MODE") def on_qr_result(requestCode, resultCode, intent): if requestCode == 0: if resultCode == -1: # RESULT_OK: contents = intent.getStringExtra("SCAN_RESULT") if intent.getStringExtra( "SCAN_RESULT_FORMAT") == 'QR_CODE': on_complete(contents) else: self.show_error( "wrong format " + intent.getStringExtra("SCAN_RESULT_FORMAT")) activity.bind(on_activity_result=on_qr_result) try: PythonActivity.mActivity.startActivityForResult(intent, 0) except: self.show_error( _('Could not start Barcode Scanner.') + ' ' + _('Please install the Barcode Scanner app from ZXing')) def build(self): return Builder.load_file('gui/kivy/main.kv') def _pause(self): if platform == 'android': # move activity to back from jnius import autoclass python_act = autoclass('org.renpy.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())) Logger.info("dpi: {} {}".format(metrics.dpi, metrics.dpi_rounded)) win = Window win.bind(size=self.on_size, on_keyboard=self.on_keyboard) win.bind(on_key_down=self.on_key_down) win.softinput_mode = 'below_target' self.on_size(win, win.size) self.init_ui() self.load_wallet_by_name(self.electrum_config.get_wallet_path()) # init plugins run_hook('init_kivy', self) # default tab self.switch_to('history') # bind intent for bitcoin: URI scheme if platform == 'android': from android import activity from jnius import autoclass PythonActivity = autoclass('org.renpy.android.PythonActivity') mactivity = PythonActivity.mActivity self.on_new_intent(mactivity.getIntent()) activity.bind(on_new_intent=self.on_new_intent) # URI passed in config uri = self.electrum_config.get('url') if uri: self.set_URI(uri) def get_wallet_path(self): if self.wallet: return self.wallet.storage.path else: return '' def load_wallet_by_name(self, wallet_path): if not wallet_path: return config = self.electrum_config try: storage = WalletStorage(wallet_path) except IOError: self.show_error("Cannot read wallet file") return if storage.file_exists: wallet = Wallet(storage) action = wallet.get_action() else: action = 'new' if action is not None: # start installation wizard Logger.debug( 'Electrum: Wallet not found. Launching install wizard') wizard = Factory.InstallWizard(config, self.network, storage) wizard.bind(on_wizard_complete=lambda instance, wallet: self. load_wallet(wallet)) wizard.run(action) else: self.load_wallet(wallet) self.on_resume() def on_stop(self): self.stop_wallet() def stop_wallet(self): if self.wallet: self.wallet.stop_threads() 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 self.is_exit = False # override settings button if key in (319, 282): #f1/settings button on android #self.gui.main_gui.toggle_settings(self) return True def settings_dialog(self): if self._settings_dialog is None: from uix.dialogs.settings import SettingsDialog self._settings_dialog = SettingsDialog(self) self._settings_dialog.update() self._settings_dialog.open() def popup_dialog(self, name): if name == 'settings': self.settings_dialog() elif name == 'wallets': from uix.dialogs.wallets import WalletDialog d = WalletDialog() d.open() else: popup = Builder.load_file('gui/kivy/uix/ui_screens/' + name + '.kv') popup.open() @profiler def init_ui(self): ''' Initialize The Ux part of electrum. This function performs the basic tasks of setting up the ui. ''' from weakref import ref self.funds_error = False # setup UX self.screens = {} #setup lazy imports for mainscreen Factory.register('AnimatedPopup', module='electrum_gui.kivy.uix.dialogs') Factory.register('QRCodeWidget', module='electrum_gui.kivy.uix.qrcodewidget') # preload widgets. Remove this if you want to load the widgets on demand #Cache.append('electrum_widgets', 'AnimatedPopup', Factory.AnimatedPopup()) #Cache.append('electrum_widgets', 'QRCodeWidget', Factory.QRCodeWidget()) # load and focus the ui self.root.manager = self.root.ids['manager'] self.history_screen = None self.contacts_screen = None self.send_screen = None self.invoices_screen = None self.receive_screen = None self.requests_screen = None self.icon = "icons/electrum.png" # connect callbacks if self.network: interests = ['updated', 'status', 'new_transaction'] self.network.register_callback(self.on_network, interests) #self.wallet = None self.tabs = self.root.ids['tabs'] def on_network(self, event, *args): if event == 'updated': self._trigger_update_wallet() elif event == 'status': self._trigger_update_status() elif event == 'new_transaction': self._trigger_notify_transactions(*args) @profiler def load_wallet(self, wallet): self.stop_wallet() self.wallet = wallet self.wallet.start_threads(self.network) self.current_account = self.wallet.storage.get('current_account', None) 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() self.notify_transactions() run_hook('load_wallet', wallet, self) def update_status(self, *dt): if not self.wallet: self.status = _("No Wallet") return if self.network is None or not self.network.is_running(): self.status = _("Offline") elif self.network.is_connected(): server_height = self.network.get_server_height() server_lag = self.network.get_local_height() - server_height if not self.wallet.up_to_date or server_height == 0: self.status = _("Synchronizing...") elif server_lag > 1: self.status = _("Server lagging (%d blocks)" % server_lag) else: c, u, x = self.wallet.get_account_balance(self.current_account) text = self.format_amount(c + x + u) self.status = str(text.strip() + ' ' + self.base_unit) else: self.status = _("Not connected") def get_max_amount(self): inputs = self.wallet.get_spendable_coins(None) addr = str( self.send_screen.screen.address) or self.wallet.dummy_address() amount, fee = self.wallet.get_max_amount(self.electrum_config, inputs, addr, None) return format_satoshis_plain(amount, self.decimal_point()) def format_amount(self, x, is_diff=False, whitespaces=False): return format_satoshis(x, is_diff, 0, self.decimal_point(), whitespaces) def format_amount_and_units(self, x): return format_satoshis_plain( x, self.decimal_point()) + ' ' + self.base_unit @profiler def update_wallet(self, *dt): self._trigger_update_status() #if self.wallet.up_to_date or not self.network or not self.network.is_connected(): self.update_tabs() @profiler def notify_transactions(self, *dt): if not self.network or not self.network.is_connected(): return # temporarily disabled for merge return iface = self.network ptfn = iface.pending_transactions_for_notifications if len(ptfn) > 0: # Combine the transactions if there are more then three tx_amount = len(ptfn) if (tx_amount >= 3): total_amount = 0 for tx in ptfn: is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx) if (v > 0): total_amount += v self.notify( _("{txs}s new transactions received. Total amount" "received in the new transactions {amount}s" "{unit}s").format( txs=tx_amount, amount=self.format_amount(total_amount), unit=self.base_unit())) iface.pending_transactions_for_notifications = [] else: for tx in iface.pending_transactions_for_notifications: if tx: iface.pending_transactions_for_notifications.remove(tx) is_relevant, is_mine, v, fee = self.wallet.get_tx_value( tx) if (v > 0): self.notify( _("{txs} new transaction received. {amount} {unit}" ).format(txs=tx_amount, amount=self.format_amount(v), unit=self.base_unit)) def notify(self, message): try: global notification, os if not notification: from plyer import notification icon = (os.path.dirname(os.path.realpath(__file__)) + '/../../' + self.icon) notification.notify('Electrum', message, app_icon=icon, app_name='Electrum') except ImportError: Logger.Error('Notification: needs plyer; `sudo pip install plyer`') def on_pause(self): # pause nfc if self.qrscanner: self.qrscanner.stop() if self.nfcscanner: self.nfcscanner.nfc_disable() return True def on_resume(self): if self.qrscanner and qrscanner.get_parent_window(): self.qrscanner.start() 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 set_send(self, address, amount, label, message): self.send_payment(address, amount=amount, label=label, message=message) def show_error(self, error, width='200dp', pos=None, arrow_pos=None, exit=False, icon='atlas://gui/kivy/theming/light/error', duration=0, modal=False): ''' Show a error Message Bubble. ''' self.show_info_bubble(text=error, icon=icon, width=width, pos=pos or Window.center, arrow_pos=arrow_pos, exit=exit, duration=duration, modal=modal) def show_info(self, error, width='200dp', pos=None, arrow_pos=None, exit=False, duration=0, modal=False): ''' Show a Info Message Bubble. ''' self.show_error(error, icon='atlas://gui/kivy/theming/light/important', duration=duration, modal=modal, exit=exit, pos=pos, arrow_pos=arrow_pos) def show_info_bubble(self, text=_('Hello World'), pos=None, duration=0, arrow_pos='bottom_mid', width=None, icon='', modal=False, exit=False): '''Method to show a Information Bubble .. parameters:: text: Message to be displayed pos: position for the bubble duration: duration the bubble remains on screen. 0 = click to hide width: width of the Bubble arrow_pos: arrow position for the bubble ''' info_bubble = self.info_bubble if not info_bubble: info_bubble = self.info_bubble = Factory.InfoBubble() win = Window if info_bubble.parent: win.remove_widget(info_bubble if not info_bubble.modal else info_bubble._modal_view) if not arrow_pos: info_bubble.show_arrow = False else: info_bubble.show_arrow = True info_bubble.arrow_pos = arrow_pos img = info_bubble.ids.img if text == 'texture': # icon holds a texture not a source image # display the texture in full screen text = '' img.texture = icon info_bubble.fs = True info_bubble.show_arrow = False img.allow_stretch = True info_bubble.dim_background = True info_bubble.background_image = 'atlas://gui/kivy/theming/light/card' else: info_bubble.fs = False info_bubble.icon = icon #if img.texture and img._coreimage: # img.reload() img.allow_stretch = False info_bubble.dim_background = False info_bubble.background_image = 'atlas://data/images/defaulttheme/bubble' info_bubble.message = text if not pos: pos = (win.center[0], win.center[1] - (info_bubble.height / 2)) info_bubble.show(pos, duration, width, modal=modal, exit=exit) def tx_dialog(self, tx): from uix.dialogs.tx_dialog import TxDialog d = TxDialog(self, tx) d.open() def sign_tx(self, *args): threading.Thread(target=self._sign_tx, args=args).start() def _sign_tx(self, tx, password, on_success, on_failure): try: self.wallet.sign_transaction(tx, password) except InvalidPassword: Clock.schedule_once(lambda dt: on_failure(_("Invalid PIN"))) return Clock.schedule_once(lambda dt: on_success(tx)) def _broadcast_thread(self, tx, on_complete): ok, txid = self.network.broadcast(tx) Clock.schedule_once(lambda dt: on_complete(ok, txid)) def broadcast(self, tx, pr=None): def on_complete(ok, txid): self.show_info(txid) if ok and pr: pr.set_paid(tx.hash()) self.invoices.save() self.update_tab('invoices') if self.network and self.network.is_connected(): self.show_info(_('Sending')) threading.Thread(target=self._broadcast_thread, args=(tx, on_complete)).start() else: self.show_info( _('Cannot broadcast transaction') + ':\n' + _('Not connected')) def description_dialog(self, screen): from uix.dialogs.label_dialog import LabelDialog text = screen.message def callback(text): screen.message = text d = LabelDialog(_('Enter description'), text, callback) d.open() @profiler def amount_dialog(self, screen, show_max): from uix.dialogs.amount_dialog import AmountDialog amount = screen.amount if amount: amount, u = str(amount).split() assert u == self.base_unit def cb(amount): screen.amount = amount popup = AmountDialog(show_max, amount, cb) popup.open() def protected(self, msg, f, args): if self.wallet.use_encryption: self.password_dialog(msg, f, args) else: apply(f, args + (None, )) 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.use_encryption and password is None: return try: seed = self.wallet.get_seed(password) except: self.show_error("Invalid PIN") return label.text = _('Seed') + ':\n' + seed def change_password(self, cb): if self.wallet.use_encryption: self.protected( _("Changing PIN code.") + '\n' + _("Enter your current PIN:"), self._change_password, (cb, )) else: self._change_password(cb, None) def _change_password(self, cb, old_password): if self.wallet.use_encryption: if old_password is None: return try: self.wallet.check_password(old_password) except InvalidPassword: self.show_error("Invalid PIN") return self.password_dialog(_('Enter new PIN'), self._change_password2, ( cb, old_password, )) def _change_password2(self, cb, old_password, new_password): self.password_dialog(_('Confirm new PIN'), self._change_password3, (cb, old_password, new_password)) def _change_password3(self, cb, old_password, new_password, confirmed_password): if new_password == confirmed_password: self.wallet.update_password(old_password, new_password) cb() else: self.show_error("PIN numbers do not match") def password_dialog(self, msg, f, args): def callback(pw): Clock.schedule_once(lambda x: apply(f, args + (pw, )), 0.1) if self._password_dialog is None: from uix.dialogs.password_dialog import PasswordDialog self._password_dialog = PasswordDialog() self._password_dialog.init(msg, callback) self._password_dialog.open()
class CircularTimePicker(BoxLayout): """Widget that makes use of :class:`CircularHourPicker` and :class:`CircularMinutePicker` to create a user-friendly, animated time picker like the one seen on Android. See module documentation for more details. """ hours = NumericProperty(0) """The hours, in military format (0-23). :attr:`hours` is a :class:`~kivy.properties.NumericProperty` and defaults to 0 (12am). """ minutes = NumericProperty(0) """The minutes. :attr:`minutes` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. """ time_list = ReferenceListProperty(hours, minutes) """Packs :attr:`hours` and :attr:`minutes` in a list for convenience. :attr:`time_list` is a :class:`~kivy.properties.ReferenceListProperty`. """ # military = BooleanProperty(False) time_format = StringProperty("[color={hours_color}][ref=hours]{hours}[/ref][/color]:[color={minutes_color}][ref=minutes]{minutes:02d}[/ref][/color]") """String that will be formatted with the time and shown in the time label. Can be anything supported by :meth:`str.format`. Make sure you don't remove the refs. See the default for the arguments passed to format. :attr:`time_format` is a :class:`~kivy.properties.StringProperty` and defaults to "[color={hours_color}][ref=hours]{hours}[/ref][/color]:[color={minutes_color}][ref=minutes]{minutes:02d}[/ref][/color]". """ ampm_format = StringProperty("[color={am_color}][ref=am]AM[/ref][/color]\n[color={pm_color}][ref=pm]PM[/ref][/color]") """String that will be formatted and shown in the AM/PM label. Can be anything supported by :meth:`str.format`. Make sure you don't remove the refs. See the default for the arguments passed to format. :attr:`ampm_format` is a :class:`~kivy.properties.StringProperty` and defaults to "[color={am_color}][ref=am]AM[/ref][/color]\n[color={pm_color}][ref=pm]PM[/ref][/color]". """ picker = OptionProperty("hours", options=("minutes", "hours")) """Currently shown time picker. Can be one of "minutes", "hours". :attr:`picker` is a :class:`~kivy.properties.OptionProperty` and defaults to "hours". """ selector_color = ListProperty([.337, .439, .490]) """Color of the number selector and of the highlighted text. RGB. :attr:`selector_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.337, .439, .490] (material green). """ color = ListProperty([1, 1, 1]) """Color of the number labels and of the center dot. RGB. :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1] (white). """ selector_alpha = BoundedNumericProperty(.3, min=0, max=1) """Alpha value for the transparent parts of the selector. :attr:`selector_alpha` is a :class:`~kivy.properties.BoundedNumericProperty` and defaults to 0.3 (min=0, max=1). """ _am = BooleanProperty(True) _h_picker = ObjectProperty(None) _m_picker = ObjectProperty(None) _bound = DictProperty({}) def _get_time(self): return datetime.time(*self.time_list) def _set_time(self, dt): self.time_list = [dt.hour, dt.minute] time = AliasProperty(_get_time, _set_time, bind=("time_list",)) """Selected time as a datetime.time object. :attr:`time` is an :class:`~kivy.properties.AliasProperty`. """ def _get_picker(self): if self.picker == "hours": return self._h_picker return self._m_picker _picker = AliasProperty(_get_picker, None) def _get_time_text(self): hc = rgb_to_hex(*self.selector_color) if self.picker == "hours" else rgb_to_hex(*self.color) mc = rgb_to_hex(*self.selector_color) if self.picker == "minutes" else rgb_to_hex(*self.color) h = self.hours == 0 and 12 or self.hours <= 12 and self.hours or self.hours - 12 m = self.minutes return self.time_format.format(hours_color=hc, minutes_color=mc, hours=h, minutes=m) time_text = AliasProperty(_get_time_text, None, bind=("hours", "minutes", "time_format", "picker")) def _get_ampm_text(self): amc = rgb_to_hex(*self.selector_color) if self._am else rgb_to_hex(*self.color) pmc = rgb_to_hex(*self.selector_color) if not self._am else rgb_to_hex(*self.color) return self.ampm_format.format(am_color=amc, pm_color=pmc) ampm_text = AliasProperty(_get_ampm_text, None, bind=("hours", "ampm_format", "_am")) def __init__(self, **kw): super(CircularTimePicker, self).__init__(**kw) if self.hours >= 12: self._am = False self.bind(time_list=self.on_time_list, picker=self._switch_picker, _am=self.on_ampm) self._h_picker = CircularHourPicker() self._m_picker = CircularMinutePicker() Clock.schedule_once(self.on_selected) Clock.schedule_once(self.on_time_list) Clock.schedule_once(self._init_later) Clock.schedule_once(lambda *a: self._switch_picker(noanim=True)) #print "TIMEee", self.time def _init_later(self, *args): self.ids.timelabel.bind(on_ref_press=self.on_ref_press) self.ids.ampmlabel.bind(on_ref_press=self.on_ref_press) def on_ref_press(self, ign, ref): if ref == "hours": self.picker = "hours" elif ref == "minutes": self.picker = "minutes" elif ref == "am": self._am = True elif ref == "pm": self._am = False def on_selected(self, *a): if not self._picker: return if self.picker == "hours": hours = self._picker.selected if self._am else self._picker.selected + 12 if hours == 24 and not self._am: hours = 12 elif hours == 12 and self._am: hours = 0 self.hours = hours elif self.picker == "minutes": self.minutes = self._picker.selected def on_time_list(self, *a): #print "TIME", self.time if not self._picker: return if self.picker == "hours": self._picker.selected = self.hours == 0 and 12 or self._am and self.hours or self.hours - 12 elif self.picker == "minutes": self._picker.selected = self.minutes def on_ampm(self, *a): if self._am: self.hours = self.hours if self.hours < 12 else self.hours - 12 else: self.hours = self.hours if self.hours >= 12 else self.hours + 12 def _switch_picker(self, *a, **kw): noanim = "noanim" in kw if noanim: noanim = kw["noanim"] try: container = self.ids.picker_container except (AttributeError, NameError): Clock.schedule_once(lambda *a: self._switch_picker(noanim=noanim)) if self.picker == "hours": picker = self._h_picker prevpicker = self._m_picker elif self.picker == "minutes": picker = self._m_picker prevpicker = self._h_picker if len(self._bound) > 0: prevpicker.unbind(selected=self.on_selected) self.unbind(**self._bound) picker.bind(selected=self.on_selected) self._bound = {"selector_color": picker.setter("selector_color"), "color": picker.setter("color"), "selector_alpha": picker.setter("selector_alpha")} self.bind(**self._bound) if len(container._bound) > 0: container.unbind(**container._bound) container._bound = {"size": picker.setter("size"), "pos": picker.setter("pos")} container.bind(**container._bound) picker.pos = container.pos picker.size = container.size picker.selector_color = self.selector_color picker.color = self.color picker.selector_alpha = self.selector_alpha if noanim: # print "noanim" if prevpicker in container.children: container.remove_widget(prevpicker) if picker.parent: picker.parent.remove_widget(picker) container.add_widget(picker) else: if prevpicker in container.children: anim = Animation(scale=1.5, d=.5, t="in_back") & Animation(opacity=0, d=.5, t="in_cubic") anim.start(prevpicker) Clock.schedule_once(lambda *a: container.remove_widget(prevpicker), .5)#.31) picker.scale = 1.5 picker.opacity = 0 if picker.parent: picker.parent.remove_widget(picker) container.add_widget(picker) anim = Animation(scale=1, d=.5, t="out_back") & Animation(opacity=1, d=.5, t="out_cubic") Clock.schedule_once(lambda *a: anim.start(picker), .3)
class ScreenManager(FloatLayout): '''Screen manager. This is the main class that will control your :class:`Screen` stack and memory. By default, the manager will show only one screen at a time. ''' current = StringProperty(None, allownone=True) ''' Name of the screen currently shown, or the screen to show. :: from kivy.uix.screenmanager import ScreenManager, Screen sm = ScreenManager() sm.add_widget(Screen(name='first')) sm.add_widget(Screen(name='second')) # By default, the first added screen will be shown. If you want to # show another one, just set the 'current' property. sm.current = 'second' :attr:`current` is a :class:`~kivy.properties.StringProperty` and defaults to None. ''' transition = ObjectProperty(baseclass=TransitionBase) '''Transition object to use for animating the transition from the current screen to the next one being shown. For example, if you want to use a :class:`WipeTransition` between slides:: from kivy.uix.screenmanager import ScreenManager, Screen, WipeTransition sm = ScreenManager(transition=WipeTransition()) sm.add_widget(Screen(name='first')) sm.add_widget(Screen(name='second')) # by default, the first added screen will be shown. If you want to # show another one, just set the 'current' property. sm.current = 'second' :attr:`transition` is an :class:`~kivy.properties.ObjectProperty` and defaults to a :class:`SlideTransition`. .. versionchanged:: 1.8.0 Default transition has been changed from :class:`SwapTransition` to :class:`SlideTransition`. ''' screens = ListProperty() '''List of all the :class:`Screen` widgets added. You should not change this list manually. Use the :meth:`add_widget <kivy.uix.widget.Widget.add_widget>` method instead. :attr:`screens` is a :class:`~kivy.properties.ListProperty` and defaults to [], read-only. ''' current_screen = ObjectProperty(None, allownone=True) '''Contains the currently displayed screen. You must not change this property manually, use :attr:`current` instead. :attr:`current_screen` is an :class:`~kivy.properties.ObjectProperty` and defaults to None, read-only. ''' def _get_screen_names(self): return [s.name for s in self.screens] screen_names = AliasProperty(_get_screen_names, bind=('screens',)) '''List of the names of all the :class:`Screen` widgets added. The list is read only. :attr:`screens_names` is an :class:`~kivy.properties.AliasProperty` and is read-only. It is updated if the screen list changes or the name of a screen changes. ''' def __init__(self, **kwargs): if 'transition' not in kwargs: self.transition = SlideTransition() super(ScreenManager, self).__init__(**kwargs) self.fbind('pos', self._update_pos) def _screen_name_changed(self, screen, name): self.property('screen_names').dispatch(self) if screen == self.current_screen: self.current = name def add_widget(self, screen): if not isinstance(screen, Screen): raise ScreenManagerException( 'ScreenManager accepts only Screen widget.') if screen.manager: if screen.manager is self: raise ScreenManagerException( 'Screen already managed by this ScreenManager (are you ' 'calling `switch_to` when you should be setting ' '`current`?)') raise ScreenManagerException( 'Screen already managed by another ScreenManager.') screen.manager = self screen.bind(name=self._screen_name_changed) self.screens.append(screen) if self.current is None: self.current = screen.name def remove_widget(self, *l): screen = l[0] if not isinstance(screen, Screen): raise ScreenManagerException( 'ScreenManager uses remove_widget only for removing Screens.') if screen not in self.screens: return if self.current_screen == screen: other = next(self) if screen.name == other: self.current = None screen.parent.real_remove_widget(screen) else: self.current = other screen.manager = None screen.unbind(name=self._screen_name_changed) self.screens.remove(screen) def clear_widgets(self, screens=None): if not screens: screens = self.screens remove_widget = self.remove_widget for screen in screens: remove_widget(screen) def real_add_widget(self, screen, *args): # ensure screen is removed from its previous parent parent = screen.parent if parent: parent.real_remove_widget(screen) super(ScreenManager, self).add_widget(screen) def real_remove_widget(self, screen, *args): super(ScreenManager, self).remove_widget(screen) def on_current(self, instance, value): if value is None: self.transition.stop() self.current_screen = None return screen = self.get_screen(value) if screen == self.current_screen: return self.transition.stop() previous_screen = self.current_screen self.current_screen = screen if previous_screen: self.transition.screen_in = screen self.transition.screen_out = previous_screen self.transition.start(self) else: self.real_add_widget(screen) screen.pos = self.pos self.do_layout() screen.dispatch('on_pre_enter') screen.dispatch('on_enter') def get_screen(self, name): '''Return the screen widget associated with the name or raise a :class:`ScreenManagerException` if not found. ''' matches = [s for s in self.screens if s.name == name] num_matches = len(matches) if num_matches == 0: raise ScreenManagerException('No Screen with name "%s".' % name) if num_matches > 1: Logger.warn('Multiple screens named "%s": %s' % (name, matches)) return matches[0] def has_screen(self, name): '''Return True if a screen with the `name` has been found. .. versionadded:: 1.6.0 ''' return bool([s for s in self.screens if s.name == name]) def __next__(self): '''Py2K backwards compatibility without six or other lib. ''' screens = self.screens if not screens: return try: index = screens.index(self.current_screen) index = (index + 1) % len(screens) return screens[index].name except ValueError: return def next(self): '''Return the name of the next screen from the screen list.''' return self.__next__() def previous(self): '''Return the name of the previous screen from the screen list. ''' screens = self.screens if not screens: return try: index = screens.index(self.current_screen) index = (index - 1) % len(screens) return screens[index].name except ValueError: return def switch_to(self, screen, **options): '''Add a new or existing screen to the ScreenManager and switch to it. The previous screen will be "switched away" from. `options` are the :attr:`transition` options that will be changed before the animation happens. If no previous screens are available, the screen will be used as the main one:: sm = ScreenManager() sm.switch_to(screen1) # later sm.switch_to(screen2, direction='left') # later sm.switch_to(screen3, direction='right', duration=1.) If any animation is in progress, it will be stopped and replaced by this one: you should avoid this because the animation will just look weird. Use either :meth:`switch_to` or :attr:`current` but not both. The `screen` name will be changed if there is any conflict with the current screen. .. versionadded: 1.8.0 ''' assert(screen is not None) if not isinstance(screen, Screen): raise ScreenManagerException( 'ScreenManager accepts only Screen widget.') # stop any transition that might be happening already self.transition.stop() # ensure the screen name will be unique if screen not in self.screens: if self.has_screen(screen.name): screen.name = self._generate_screen_name() # change the transition if given explicitly old_transition = self.transition specified_transition = options.pop("transition", None) if specified_transition: self.transition = specified_transition # change the transition options for key, value in iteritems(options): setattr(self.transition, key, value) # add and leave if we are set as the current screen if screen.manager is not self: self.add_widget(screen) if self.current_screen is screen: return old_current = self.current_screen def remove_old_screen(transition): if old_current in self.children: self.remove_widget(old_current) self.transition = old_transition transition.unbind(on_complete=remove_old_screen) self.transition.bind(on_complete=remove_old_screen) self.current = screen.name def _generate_screen_name(self): i = 0 while True: name = '_screen{}'.format(i) if not self.has_screen(name): return name i += 1 def _update_pos(self, instance, value): for child in self.children: if self.transition.is_active and \ (child == self.transition.screen_in or child == self.transition.screen_out): continue child.pos = value def on_touch_down(self, touch): if self.transition.is_active: return False return super(ScreenManager, self).on_touch_down(touch) def on_touch_move(self, touch): if self.transition.is_active: return False return super(ScreenManager, self).on_touch_move(touch) def on_touch_up(self, touch): if self.transition.is_active: return False return super(ScreenManager, self).on_touch_up(touch)
class Widget(EventDispatcher): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch appear `on_touch_move`: Fired when an existing touch is moved `on_touch_up`: Fired when an existing touch disappears .. versionchanged:: In 1.0.9, everything related to properties have been moved in :class:`~kivy.event.EventDispatcher`. Properties can now be used for contruct simple class, without inherit of :class:`Widget`. ''' def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # Register touch events self.register_event_type('on_touch_down') self.register_event_type('on_touch_move') self.register_event_type('on_touch_up') super(Widget, self).__init__(**kwargs) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas() # 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') # # 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. ''' 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 :func:`on_touch_down` for more information ''' 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 :func:`on_touch_down` for more information ''' for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True # # 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.parent = self 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) 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 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. ''' def get_uid(self): return self.__dict__['__uid'] uid = AliasProperty(get_uid, None) '''Unique identifier of the widget in the whole Kivy instance. .. versionadded:: 1.0.7 :data:`uid` is a :class:`~kivy.properties.AliasProperty`, read-only. ''' 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 :func:`add_widget` and :func:`remove_widget` for manipulate children list. Don't manipulate 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 his parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. It 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, i.e. 100%. 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. Check :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. ''' canvas = None '''Canvas of the widget.
class TextInput(Widget): '''TextInput class, see module documentation for more information. :Events: `on_text_validate` Fired only in multiline=False mode, when the user hits 'enter'. This will also unfocus the textinput. ''' def __init__(self, **kwargs): self._win = None self._cursor_blink_time = Clock.get_time() self._cursor = [0, 0] self._selection = False self._selection_finished = True self._selection_touch = None self.selection_text = '' self.selection_from = None self.selection_to = None self._lines_flags = [] self._lines_labels = [] self._lines_rects = [] self._line_spacing = 0 self._label_cached = None self._line_options = None self._keyboard = None self.interesting_keys = { 8: 'backspace', 13: 'enter', 127: 'del', 271: 'enter', 273: 'cursor_up', 274: 'cursor_down', 275: 'cursor_right', 276: 'cursor_left', 278: 'cursor_home', 279: 'cursor_end', 280: 'cursor_pgup', 281: 'cursor_pgdown', 303: 'shift_L', 304: 'shift_R' } self.register_event_type('on_text_validate') super(TextInput, self).__init__(**kwargs) self.bind(font_size=self._trigger_refresh_line_options, font_name=self._trigger_refresh_line_options) self.bind(padding_x=self._trigger_refresh_text, padding_y=self._trigger_refresh_text, tab_width=self._trigger_refresh_text, font_size=self._trigger_refresh_text, font_name=self._trigger_refresh_text, size=self._trigger_refresh_text) self.bind(pos=self._trigger_update_graphics) self._trigger_refresh_line_options() self._trigger_refresh_text() def on_text_validate(self): pass def cursor_index(self): '''Return the cursor index in the text/value. ''' try: l = self._lines if len(l) == 0: return 0 lf = self._lines_flags index, cr = self.cursor for row in xrange(cr): if row >= len(l): continue index += len(l[row]) if lf[row] & FL_IS_NEWLINE: index += 1 if lf[cr] & FL_IS_NEWLINE: index += 1 return index except IndexError: return 0 def cursor_offset(self): '''Get the cursor x offset on the current line ''' offset = 0 if self.cursor_col: offset = self._get_text_width( self._lines[self.cursor_row][:self.cursor_col]) return offset def get_cursor_from_index(self, index): '''Return the (row, col) of the cursor from text index ''' index = boundary(index, 0, len(self.text)) if index <= 0: return 0, 0 lf = self._lines_flags l = self._lines i = 0 for row in xrange(len(l)): ni = i + len(l[row]) if lf[row] & FL_IS_NEWLINE: ni += 1 i += 1 if ni >= index: return index - i, row i = ni return index, row def insert_text(self, substring): '''Insert new text on the current cursor position ''' cc, cr = self.cursor ci = self.cursor_index() text = self._lines[cr] new_text = text[:cc] + substring + text[cc:] self._set_line_text(cr, new_text) self._trigger_refresh_text() self.cursor = self.get_cursor_from_index(ci + len(substring)) def do_backspace(self): '''Do backspace operation from the current cursor position. This action might do lot of things like: - removing the current selection if available - removing the previous char, and back the cursor - do nothing, if we are at the start. ''' cc, cr = self.cursor text = self._lines[cr] cursor_index = self.cursor_index() if cc == 0 and cr == 0: return if cc == 0: text_last_line = self._lines[cr - 1] self._set_line_text(cr - 1, text_last_line + text) self._delete_line(cr) else: ch = text[cc - 1] new_text = text[:cc - 1] + text[cc:] self._set_line_text(cr, new_text) self._refresh_text_from_property() self.cursor = self.get_cursor_from_index(cursor_index - 1) def do_cursor_movement(self, action): '''Move the cursor relative to it's current position. Action can be one of : - cursor_left: move the cursor to the left - cursor_right: move the cursor to the right - cursor_up: move the cursor on the previous line - cursor_down: move the cursor on the next line - cursor_home: move the cursor at the start of the current line - cursor_end: move the cursor at the end of current line - cursor_pgup: move one "page" before - cursor_pgdown: move one "page" after .. warning:: Current page are 3 lines before/after ''' pgmove_speed = 3 col, row = self.cursor if action == 'cursor_up': row = max(row - 1, 0) col = min(len(self._lines[row]), col) elif action == 'cursor_down': row = min(row + 1, len(self._lines) - 1) col = min(len(self._lines[row]), col) elif action == 'cursor_left': col, row = self.get_cursor_from_index(self.cursor_index() - 1) elif action == 'cursor_right': col, row = self.get_cursor_from_index(self.cursor_index() + 1) elif action == 'cursor_home': col = 0 elif action == 'cursor_end': col = len(self._lines[row]) elif action == 'cursor_pgup': row /= pgmove_speed col = min(len(self._lines[row]), col) elif action == 'cursor_pgdown': row = min((row + 1) * pgmove_speed, len(self._lines) - 1) col = min(len(self._lines[row]), col) self.cursor = (col, row) def get_cursor_from_xy(self, x, y): '''Return the (row, col) of the cursor from an (x, y) position. ''' l = self._lines dy = self.line_height + self._line_spacing cx = x - self.x cy = (self.top - self.padding_y + self.scroll_y * dy) - y cy = int(boundary(round(cy / dy), 0, len(l) - 1)) dcx = 0 for i in xrange(1, len(l[cy]) + 1): if self._get_text_width(l[cy][:i]) >= cx: break dcx = i cx = dcx return cx, cy # # Selection control # def cancel_selection(self): '''Cancel current selection (if any) ''' self._selection = False self._selection_finished = True self._selection_touch = None self._trigger_update_graphics() def delete_selection(self): '''Delete the current text selection (if any) ''' if not self._selection: return v = self.text a, b = self.selection_from, self.selection_to if a > b: a, b = b, a text = v[:a] + v[b:] self.text = text self.cursor = self.get_cursor_from_index(a) self.cancel_selection() def _update_selection(self, finished=False): '''Update selection text and order of from/to if finished is True. Can be called multiple times until finished=True. ''' a, b = self.selection_from, self.selection_to if a > b: a, b = b, a self._selection_finished = finished self.selection_text = self.text[a:b] if not finished: self._selection = True else: self._selection = bool(len(self.selection_text)) self._selection_touch = None self._trigger_update_graphics() # # Touch control # def on_touch_down(self, touch): if not self.collide_point(touch.x, touch.y): return False if not self.focus: self.focus = True touch.grab(self) self.cursor = self.get_cursor_from_xy(touch.x, touch.y) if not self._selection_touch: self.cancel_selection() self._selection_touch = touch self.selection_from = self.selection_to = self.cursor_index() self._update_selection() return True def on_touch_move(self, touch): if touch.grab_current is not self: return if not self.focus: touch.ungrab(self) if self._selection_touch is touch: self._selection_touch = None return False if self._selection_touch is touch: self.cursor = self.get_cursor_from_xy(touch.x, touch.y) self.selection_to = self.cursor_index() self._update_selection() return True def on_touch_up(self, touch): if touch.grab_current is not self: return touch.ungrab(self) if not self.focus: return False if self._selection_touch is touch: self.selection_to = self.cursor_index() self._update_selection(True) return True # # Private # def on_focus(self, instance, value, *largs): win = self._win if not win: self._win = win = self.get_root_window() if not win: # we got argument, it could be the previous schedule # cancel focus. if len(largs): Logger.warning( 'Textinput: ' 'Cannot focus the element, unable to get root window') return else: Clock.schedule_once(partial(self.on_focus, self, value), 0) return if value: keyboard = win.request_keyboard(self._keyboard_released, self) self._keyboard = keyboard keyboard.bind(on_key_down=self._keyboard_on_key_down, on_key_up=self._keyboard_on_key_up) Clock.schedule_interval(self._do_blink_cursor, 1 / 2.) else: keyboard = self._keyboard keyboard.unbind(on_key_down=self._keyboard_on_key_down, on_key_up=self._keyboard_on_key_up) keyboard.release() self.cancel_selection() Clock.unschedule(self._do_blink_cursor) self._win = None def _keyboard_released(self): # Callback called when the real keyboard is taken by someone else # called by the window if the keyboard is taken by somebody else # FIXME: handle virtual keyboard. self.focus = False def _get_text_width(self, text): # Return the width of a text, according to the current line options if not self._label_cached: self._get_line_options() text = text.replace('\t', ' ' * self.tab_width) return self._label_cached.get_extents(text)[0] def _do_blink_cursor(self, dt): # Callback called by the timer to blink the cursor, according to the # last activity in the widget b = (Clock.get_time() - self._cursor_blink_time) self.cursor_blink = int(b * 2) % 2 def on_cursor(self, instance, value): # When the cursor is moved, reset the activity timer, and update all # the graphics. self._cursor_blink_time = Clock.get_time() self._trigger_update_graphics() def _delete_line(self, idx): # Delete current line, and fix cursor position assert (idx < len(self._lines)) self._lines.pop(idx) self._lines_flags.pop(idx) self._lines_labels.pop(idx) self.cursor = self.cursor def _set_line_text(self, line_num, text): # Set current line with other text than the default one. self._lines[line_num] = text self._lines_labels[line_num] = self._create_line_label(text) def _trigger_refresh_line_options(self, *largs): Clock.unschedule(self._refresh_line_options) Clock.schedule_once(self._refresh_line_options, 0) def _refresh_line_options(self, *largs): self._line_options = None self._get_line_options() self._refresh_text(self.text) self.cursor = self.get_cursor_from_index(len(self.text)) def _trigger_refresh_text(self, *largs): Clock.unschedule(self._refresh_text_from_property) Clock.schedule_once(self._refresh_text_from_property) def _refresh_text_from_property(self, *largs): self._refresh_text(self.text) def _refresh_text(self, text): # Refresh all the lines from a new text. # By using cache in internal functions, this method should be fast. _lines, self._lines_flags = self._split_smart(text) self._lines = _lines self._lines_labels = [self._create_line_label(x) for x in self._lines] self._lines_rects = [Rectangle(texture=x, size=( \ x.size if x else (0, 0))) \ for x in self._lines_labels] line_label = self._lines_labels[0] if line_label is None: self.line_height = max(1, self.font_size + self.padding_y) else: self.line_height = line_label.height self._line_spacing = 2 # now, if the text change, maybe the cursor is not as the same place as # before. so, try to set the cursor on the good place row = self.cursor_row self.cursor = self.get_cursor_from_index(self.cursor_index()) # if we back to a new line, reset the scroll, otherwise, the effect is # ugly if self.cursor_row != row: self.scroll_x = 0 # with the new text don't forget to update graphics again self._trigger_update_graphics() def _trigger_update_graphics(self, *largs): Clock.unschedule(self._update_graphics) Clock.schedule_once(self._update_graphics, -1) def _update_graphics(self, *largs): # Update all the graphics according to the current internal values. # # This is a little bit complex, cause we have to : # - handle scroll_x # - handle padding # - create rectangle for the lines matching the viewport # - crop the texture coordinates to match the viewport # # This is the first step of graphics, the second is the selection. self.canvas.clear() add = self.canvas.add cursor_row = self.cursor_row lh = self.line_height dy = self.line_height + self._line_spacing # adjust view if the cursor is going outside the bounds sx = self.scroll_x sy = self.scroll_y # draw labels rects = self._lines_rects labels = self._lines_labels x = self.x + self.padding_x y = self.top - self.padding_y + sy miny = self.y + self.padding_y maxy = self.top - self.padding_y for line_num, value in enumerate(self._lines): if miny <= y <= maxy + dy: texture = labels[line_num] if not texture: y -= dy continue size = list(texture.size) texc = texture.tex_coords[:] # calcul coordinate viewport_pos = sx, 0 vw = self.width - self.padding_x * 2 vh = self.height - self.padding_y * 2 tw, th = map(float, size) oh, ow = tch, tcw = texc[1:3] tcx, tcy = 0, 0 # adjust size/texcoord according to viewport if vw < tw: tcw = (vw / tw) * tcw size[0] = vw if vh < th: tch = (vh / th) * tch size[1] = vh if viewport_pos: tcx, tcy = viewport_pos tcx = tcx / tw * ow tcy = tcy / th * oh # cropping mlh = lh if y > maxy: vh = (maxy - y + lh) tch = (vh / float(lh)) * oh tcy = oh - tch size[1] = vh if y - lh < miny: diff = miny - (y - lh) y += diff vh = lh - diff tch = (vh / float(lh)) * oh size[1] = vh texc = (tcx, tcy + tch, tcx + tcw, tcy + tch, tcx + tcw, tcy, tcx, tcy) # add rectangle. r = rects[line_num] r.pos = int(x), int(y - mlh) r.size = size r.texture = texture r.tex_coords = texc add(r) y -= dy self._update_graphics_selection() def _update_graphics_selection(self): if not self._selection: return self.canvas.remove_group('selection') dy = self.line_height + self._line_spacing rects = self._lines_rects labels = self._lines_labels x = self.x + self.padding_x y = self.top - self.padding_y + self.scroll_y miny = self.y + self.padding_y maxy = self.top - self.padding_y draw_selection = self._draw_selection for line_num, value in enumerate(self._lines): if miny <= y <= maxy + dy: r = rects[line_num] draw_selection(r.pos, r.size, line_num) y -= dy def _draw_selection(self, pos, size, line_num): # Draw the current selection on the widget. a, b = self.selection_from, self.selection_to if a > b: a, b = b, a s1c, s1r = self.get_cursor_from_index(a) s2c, s2r = self.get_cursor_from_index(b) if line_num < s1r or line_num > s2r: return lh = self.line_height x, y = pos w, h = size x1 = x x2 = x + w if line_num == s1r: lines = self._lines[line_num] x1 += self._get_text_width(lines[:s1c]) if line_num == s2r: lines = self._lines[line_num] x2 = x + self._get_text_width(lines[:s2c]) maxx = x + self.width - self.padding_x if x1 > maxx: return x2 = min(x2, self.x + self.width - self.padding_x) self.canvas.add(Color(*self.selection_color, group='selection')) self.canvas.add( Rectangle(pos=(x1, pos[1]), size=(x2 - x1, size[1]), group='selection')) def on_size(self, instance, value): # if the size change, we might do invalid scrolling / text split # size the text maybe be put after size_hint have been resolved. self._trigger_refresh_text() self.scroll_x = self.scroll_y = 0 def _get_cursor_pos(self): # return the current cursor x/y from the row/col dy = self.line_height + self._line_spacing x = self.x + self.padding_x y = self.top - self.padding_y + self.scroll_y y -= self.cursor_row * dy x, y = x + self.cursor_offset() - self.scroll_x, y return x, y def _get_line_options(self): # Get or create line options, to be used for Label creation if self._line_options is None: self._line_options = kw = { 'font_size': self.font_size, 'font_name': self.font_name, 'anchor_x': 'left', 'anchor_y': 'top', 'padding_x': 0, 'padding_y': 0, 'padding': (0, 0) } self._label_cached = Label(**kw) return self._line_options def _create_line_label(self, text): # Create a label from a text, using line options ntext = text.replace('\n', '').replace('\t', ' ' * self.tab_width) kw = self._get_line_options() cid = '%s\0%s' % (ntext, str(kw)) texture = Cache.get('textinput.label', cid) if not texture: label = Label(text=ntext, **kw) label.refresh() texture = label.texture Cache.append('textinput.label', cid, texture) return texture def _tokenize(self, text): # Tokenize a text string from some delimiters if text is None: return delimiters = ' ,\'".;:\n\r\t' oldindex = 0 for index, char in enumerate(text): if char not in delimiters: continue if oldindex != index: yield text[oldindex:index] yield text[index:index + 1] oldindex = index + 1 yield text[oldindex:] def _split_smart(self, text): # Do a "smart" split. If autowidth or autosize is set, # we are not doing smart split, just a split on line break. # Otherwise, we are trying to split as soon as possible, to prevent # overflow on the widget. # depend of the options, split the text on line, or word if not self.multiline: lines = text.split('\n') lines_flags = [0] + [FL_IS_NEWLINE] * (len(lines) - 1) return lines, lines_flags # no autosize, do wordwrap. x = flags = 0 line = [] lines = [] lines_flags = [] width = self.width - self.padding_x * 2 text_width = self._get_text_width # try to add each word on current line. for word in self._tokenize(text): is_newline = (word == '\n') w = text_width(word) # if we have more than the width, or if it's a newline, # push the current line, and create a new one if (x + w > width and line) or is_newline: lines.append(''.join(line)) lines_flags.append(flags) flags = 0 line = [] x = 0 if is_newline: flags |= FL_IS_NEWLINE else: x += w line.append(word) if line or flags & FL_IS_NEWLINE: lines.append(''.join(line)) lines_flags.append(flags) return lines, lines_flags def _key_down(self, key, repeat=False): displayed_str, internal_str, internal_action, scale = key if internal_action is None: if self._selection: self.delete_selection() self.insert_text(displayed_str) elif internal_action in ('shift', 'shift_L', 'shift_R'): if not self._selection: self.selection_from = self.selection_to = self.cursor_index() self._selection = True self._selection_finished = False elif internal_action.startswith('cursor_'): self.do_cursor_movement(internal_action) if self._selection and not self._selection_finished: self.selection_to = self.cursor_index() self._update_selection() else: self.cancel_selection() elif self._selection and internal_action in ('del', 'backspace'): self.delete_selection() elif internal_action == 'del': # Move cursor one char to the right. If that was successful, # do a backspace (effectively deleting char right of cursor) cursor = self.cursor self.do_cursor_movement('cursor_right') if cursor != self.cursor: self.do_backspace() elif internal_action == 'backspace': self.do_backspace() elif internal_action == 'enter': if self.multiline: self.insert_text('\n') else: self.dispatch('on_text_validate') self.focus = False elif internal_action == 'escape': self.focus = False if internal_action != 'escape': #self._recalc_size() pass def _key_up(self, key, repeat=False): displayed_str, internal_str, internal_action, scale = key if internal_action in ('shift', 'shift_L', 'shift_R'): self._update_selection(True) def _keyboard_on_key_down(self, window, keycode, text, modifiers): global Clipboard if Clipboard is None: from kivy.core.clipboard import Clipboard is_osx = sys.platform == 'darwin' # Keycodes on OSX: ctrl, cmd = 64, 1024 key, key_str = keycode if text and not key in (self.interesting_keys.keys() + [27]): # This allows *either* ctrl *or* cmd, but not both. if modifiers == ['ctrl'] or (is_osx and modifiers == ['meta']): if key == ord('x'): # cut selection Clipboard.put(self.selection_text, 'text/plain') self.delete_selection() elif key == ord('c'): # copy selection Clipboard.put(self.selection_text, 'text/plain') elif key == ord('v'): # paste selection data = Clipboard.get('text/plain') if data: self.delete_selection() self.insert_text(data) elif key == ord('a'): # select all self.selection_from = 0 self.selection_to = len(self.text) self._update_selection(True) else: if self._selection: self.delete_selection() self.insert_text(text) #self._recalc_size() return if key == 27: # escape self.focus = False return True elif key == 9: # tab self.insert_text('\t') return True k = self.interesting_keys.get(key) if k: key = (None, None, k, 1) self._key_down(key) def _keyboard_on_key_up(self, window, keycode): key, key_str = keycode k = self.interesting_keys.get(key) if k: key = (None, None, k, 1) self._key_up(key) # # Properties # _lines = ListProperty([]) multiline = BooleanProperty(True) '''If True, the widget will be able show multiple lines of text. If false, "enter" action will defocus the textinput instead of adding a new line. :data:`multiline` is a :class:`~kivy.properties.BooleanProperty`, default to True ''' cursor_blink = BooleanProperty(False) '''This property is used to blink the cursor graphics. The value of :data:`cursor_blink` is automatically computed, setting a value on it will have no impact. :data:`cursor_blink` is a :class:`~kivy.properties.BooleanProperty`, default to False ''' def _get_cursor(self): return self._cursor def _set_cursor(self, pos): if not self._lines: self._trigger_refresh_text() return l = self._lines cr = boundary(pos[1], 0, len(l) - 1) cc = boundary(pos[0], 0, len(l[cr])) cursor = cc, cr if self._cursor == cursor: return self._cursor = cursor # adjust scrollview to ensure that the cursor will be always inside our # viewport. viewport_width = self.width - self.padding_x * 2 sx = self.scroll_x offset = self.cursor_offset() # if offset is outside the current bounds, reajust if offset > viewport_width + sx: self.scroll_x = offset - viewport_width if offset < sx: self.scroll_x = offset # do the same for Y # this algo try to center the cursor as much as possible dy = self.line_height + self._line_spacing offsety = cr * dy sy = self.scroll_y viewport_height = self.height - self.padding_y * 2 - dy if offsety > viewport_height + sy: sy = offsety - viewport_height if offsety < sy: sy = offsety self.scroll_y = sy return True cursor = AliasProperty(_get_cursor, _set_cursor) '''Tuple of (row, col) of the current cursor position. You can set a new (row, col) if you want to move the cursor. The scrolling area will be automatically updated to ensure that the cursor will be visible inside the viewport. :data:`cursor` is a :class:`~kivy.properties.AliasProperty`. ''' def _get_cursor_col(self): return self._cursor[0] cursor_col = AliasProperty(_get_cursor_col, None, bind=('cursor', )) '''Current column of the cursor. :data:`cursor_col` is a :class:`~kivy.properties.AliasProperty` to cursor[0], read-only. ''' def _get_cursor_row(self): return self._cursor[1] cursor_row = AliasProperty(_get_cursor_row, None, bind=('cursor', )) '''Current row of the cursor. :data:`cursor_row` is a :class:`~kivy.properties.AliasProperty` to cursor[1], read-only. ''' cursor_pos = AliasProperty(_get_cursor_pos, None, bind=('cursor', 'padding', 'pos', 'size', 'focus', 'scroll_x', 'scroll_y')) '''Current position of the cursor, in (x, y). :data:`cursor_pos` is a :class:`~kivy.properties.AliasProperty`, read-only. ''' line_height = NumericProperty(1) '''Height of a line. This property is automatically computed from the :data:`font_name`, :data:`font_size`. Changing the line_height will have no impact. :data:`line_height` is a :class:`~kivy.properties.NumericProperty`, read-only. ''' tab_width = NumericProperty(4) '''By default, each tab will be replaced by the size of 4 spaces on the text input widget. You can set a lower or higher value. :data:`tab_width` is a :class:`~kivy.properties.NumericProperty`, default to 4. ''' padding_x = NumericProperty(0) '''Horizontal padding of the text, inside the widget box. :data:`padding_x` is a :class:`~kivy.properties.NumericProperty`, default to 0. This might be changed by the current theme. ''' padding_y = NumericProperty(0) '''Vertical padding of the text, inside the widget box. :data:`padding_x` is a :class:`~kivy.properties.NumericProperty`, default to 0. This might be changed by the current theme. ''' padding = ReferenceListProperty(padding_x, padding_y) '''Padding of the text, in the format (padding_x, padding_y) :data:`padding` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`padding_x`, :data:`padding_y`) properties. ''' scroll_x = NumericProperty(0) '''X scrolling value of the viewport. The scrolling is automatically updated when the cursor is moving or text is changing. If you are not doing any action, you can still change the scroll_x and scroll_y properties. :data:`scroll_x` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' scroll_y = NumericProperty(0) '''Y scrolling value of the viewport. See :data:`scroll_x` for more information. :data:`scroll_y` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' selection_color = ListProperty([0.1843, 0.6549, 0.8313, .5]) '''Current color of the selection, in (r, g, b, a) format. .. warning:: The color should always have "alpha" component different from 1, since the selection is drawed after the text. :data:`selection_color` is a :class:`~kivy.properties.ListProperty`, default to [0.1843, 0.6549, 0.8313, .5] ''' selection_from = NumericProperty(None, allownone=True) '''If a selection is happening, or finished, this property will represent the cursor index where the selection start. :data:`selection_from` is a :class:`~kivy.properties.NumericProperty`, default to None ''' selection_to = NumericProperty(None, allownone=True) '''If a selection is happening, or finished, this property will represent the cursor index where the selection end. :data:`selection_to` is a :class:`~kivy.properties.NumericProperty`, default to None ''' selection_text = StringProperty('') '''Current content selection. :data:`selection_text` is a :class:`~kivy.properties.StringProperty`, default to '' ''' focus = BooleanProperty(False) '''If focus is true, the keyboard will be requested, and you can start to write on the textinput. :data:`focus` is a :class:`~kivy.properties.BooleanProperty`, default to False ''' def _get_text(self): lf = self._lines_flags l = self._lines text = ''.join([('\n' if (lf[i] & FL_IS_NEWLINE) else '') + l[i] \ for i in xrange(len(l))]) return text def _set_text(self, text): if self.text == text: return self._refresh_text(text) self.cursor = self.get_cursor_from_index(len(text)) text = AliasProperty(_get_text, _set_text, bind=('_lines', )) '''Text of the widget. Creation of a simple hello world :: widget = TextInput(text='Hello world') If you want to create the widget with an unicode string, use :: widget = TextInput(text=u'My unicode string') :data:`text` a :class:`~kivy.properties.StringProperty`. ''' font_name = StringProperty('fonts/DroidSans.ttf') '''Filename of the font to use, the path can be absolute or relative. Relative paths are resolved by the :func:`~kivy.resources.resource_find` function. .. warning:: Depending of your text provider, the font file can be ignored. But you can mostly use this without trouble. :data:`font_name` is a :class:`~kivy.properties.StringProperty`, default to 'fonts/DroidSans.ttf'. ''' font_size = NumericProperty(10) '''Font size of the text, in pixels.
class Scatter(Widget): '''Scatter class. See module documentation for more information. :Events: `on_transform_with_touch`: Fired when the scatter has been transformed by user touch or multitouch such as panning or zooming .. versionchanged:: 1.8.0 Event `on_transform_with_touch` added. ''' __events__ = ('on_transform_with_touch', ) auto_bring_to_front = BooleanProperty(True) '''If True, the widget will be automatically pushed on the top of parent widget list for drawing. :data:`auto_bring_to_front` is a :class:`~kivy.properties.BooleanProperty`, default to True. ''' do_translation_x = BooleanProperty(True) '''Allow translation on X axis :data:`do_translation_x` is a :class:`~kivy.properties.BooleanProperty`, default to True. ''' do_translation_y = BooleanProperty(True) '''Allow translation on Y axis. :data:`do_translation_y` is a :class:`~kivy.properties.BooleanProperty`, default to True. ''' def _get_do_translation(self): return (self.do_translation_x, self.do_translation_y) def _set_do_translation(self, value): if type(value) in (list, tuple): self.do_translation_x, self.do_translation_y = value else: self.do_translation_x = self.do_translation_y = bool(value) do_translation = AliasProperty(_get_do_translation, _set_do_translation, bind=('do_translation_x', 'do_translation_y')) '''Allow translation on X or Y axis. :data:`do_translation` is a :class:`~kivy.properties.AliasProperty` of (:data:`do_translation_x` + :data:`do_translation_y`) ''' translation_touches = BoundedNumericProperty(1, min=1) '''Change whether translation is triggered by a single or multiple touch. This only matters when :data:`do_translation` = True :data:`translation_touches` is a :class:`~kivy.properties.NumericProperty`, default to 1. .. versionadded:: 1.7.0 ''' do_rotation = BooleanProperty(True) '''Allow rotation. :data:`do_rotation` is a :class:`~kivy.properties.BooleanProperty`, default to True. ''' do_scale = BooleanProperty(True) '''Allow scaling. :data:`do_scale` is a :class:`~kivy.properties.BooleanProperty`, default to True. ''' do_collide_after_children = BooleanProperty(False) '''If True, the collision detection for limiting the touch inside the scatter will be done after dispaching the touch to the children. You can put children outside the bounding box of the scatter, and be able to touch them. .. versionadded:: 1.3.0 ''' scale_min = NumericProperty(0.01) '''Minimum scaling factor allowed. :data:`scale_min` is a :class:`~kivy.properties.NumericProperty`, default to 0.01 ''' scale_max = NumericProperty(1e20) '''Maximum scaling factor allowed. :data:`scale_max` is a :class:`~kivy.properties.NumericProperty`, default to 1e20 ''' transform = ObjectProperty(Matrix()) '''Transformation matrix. :data:`transform` is a :class:`~kivy.properties.ObjectProperty`, default to the identity matrix. ''' transform_inv = ObjectProperty(Matrix()) '''Inverse of the transformation matrix. :data:`transform_inv` is a :class:`~kivy.properties.ObjectProperty`, default to the identity matrix. ''' def _get_bbox(self): xmin, ymin = xmax, ymax = self.to_parent(0, 0) for point in [(self.width, 0), (0, self.height), self.size]: x, y = self.to_parent(*point) if x < xmin: xmin = x if y < ymin: ymin = y if x > xmax: xmax = x if y > ymax: ymax = y return (xmin, ymin), (xmax - xmin, ymax - ymin) bbox = AliasProperty(_get_bbox, None, bind=('transform', 'width', 'height')) '''Bounding box of the widget in parent space:: ((x, y), (w, h)) # x, y = lower left corner :data:`bbox` is a :class:`~kivy.properties.AliasProperty`. ''' def _get_rotation(self): v1 = Vector(0, 10) tp = self.to_parent v2 = Vector(*tp(*self.pos)) - tp(self.x, self.y + 10) return -1.0 * (v1.angle(v2) + 180) % 360 def _set_rotation(self, rotation): angle_change = self.rotation - rotation r = Matrix().rotate(-radians(angle_change), 0, 0, 1) self.apply_transform(r, post_multiply=True, anchor=self.to_local(*self.center)) rotation = AliasProperty(_get_rotation, _set_rotation, bind=('x', 'y', 'transform')) '''Rotation value of the scatter. :data:`rotation` is a :class:`~kivy.properties.AliasProperty`. ''' def _get_scale(self): p1 = Vector(*self.to_parent(0, 0)) p2 = Vector(*self.to_parent(1, 0)) scale = p1.distance(p2) return float(scale) def _set_scale(self, scale): rescale = scale * 1.0 / self.scale self.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=self.to_local(*self.center)) scale = AliasProperty(_get_scale, _set_scale, bind=('x', 'y', 'transform')) '''Scale value of the scatter. :data:`scale` is a :class:`~kivy.properties.AliasProperty`. ''' def _get_center(self): return (self.bbox[0][0] + self.bbox[1][0] / 2.0, self.bbox[0][1] + self.bbox[1][1] / 2.0) def _set_center(self, center): if center == self.center: return False t = Vector(*center) - self.center trans = Matrix().translate(t.x, t.y, 0) self.apply_transform(trans) center = AliasProperty(_get_center, _set_center, bind=('bbox', )) def _get_pos(self): return self.bbox[0] def _set_pos(self, pos): _pos = self.bbox[0] if pos == _pos: return t = Vector(*pos) - _pos trans = Matrix().translate(t.x, t.y, 0) self.apply_transform(trans) pos = AliasProperty(_get_pos, _set_pos, bind=('bbox', )) def _get_x(self): return self.bbox[0][0] def _set_x(self, x): if x == self.bbox[0][0]: return False self.pos = (x, self.y) return True x = AliasProperty(_get_x, _set_x, bind=('bbox', )) def _get_y(self): return self.bbox[0][1] def _set_y(self, y): if y == self.bbox[0][1]: return False self.pos = (self.x, y) return True y = AliasProperty(_get_y, _set_y, bind=('bbox', )) def get_right(self): return self.x + self.bbox[1][0] def set_right(self, value): self.x = value - self.bbox[1][0] right = AliasProperty(get_right, set_right, bind=('x', 'width')) def get_top(self): return self.y + self.bbox[1][1] def set_top(self, value): self.y = value - self.bbox[1][1] top = AliasProperty(get_top, set_top, bind=('y', 'height')) def get_center_x(self): return self.x + self.bbox[1][0] / 2. def set_center_x(self, value): self.x = value - self.bbox[1][0] / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) def get_center_y(self): return self.y + self.bbox[1][1] / 2. def set_center_y(self, value): self.y = value - self.bbox[1][1] / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) def __init__(self, **kwargs): self._touches = [] self._last_touch_pos = {} super(Scatter, self).__init__(**kwargs) def on_transform(self, instance, value): self.transform_inv = value.inverse() def collide_point(self, x, y): x, y = self.to_local(x, y) return 0 <= x <= self.width and 0 <= y <= self.height def to_parent(self, x, y, **k): p = self.transform.transform_point(x, y, 0) return (p[0], p[1]) def to_local(self, x, y, **k): p = self.transform_inv.transform_point(x, y, 0) return (p[0], p[1]) def apply_transform(self, trans, post_multiply=False, anchor=(0, 0)): ''' Transforms scatter by trans (on top of its current transformation state). :Parameters: `trans`: transformation matrix from transformation lib. Transformation to be applied to the scatter widget `anchor`: tuple, default to (0, 0) The point to use as the origin of the transformation (uses local widget space) `post_multiply`: bool, default to False If true the transform matrix is post multiplied (as if applied before the current transform) ''' t = Matrix().translate(anchor[0], anchor[1], 0) t = t.multiply(trans) t = t.multiply(Matrix().translate(-anchor[0], -anchor[1], 0)) if post_multiply: self.transform = self.transform.multiply(t) else: self.transform = t.multiply(self.transform) def transform_with_touch(self, touch): # just do a simple one finger drag changed = False if len(self._touches) == self.translation_touches: # _last_touch_pos has last pos in correct parent space, # just like incoming touch dx = (touch.x - self._last_touch_pos[touch][0]) \ * self.do_translation_x dy = (touch.y - self._last_touch_pos[touch][1]) \ * self.do_translation_y dx = dx / self.translation_touches dy = dy / self.translation_touches self.apply_transform(Matrix().translate(dx, dy, 0)) changed = True if len(self._touches) == 1: return changed # we have more than one touch... points = [Vector(self._last_touch_pos[t]) for t in self._touches] # we only want to transform if the touch is part of the two touches # furthest apart! So first we find anchor, the point to transform # around as the touch farthest away from touch anchor = max(points, key=lambda p: p.distance(touch.pos)) # now we find the touch farthest away from anchor, if its not the # same as touch. Touch is not one of the two touches used to transform farthest = max(points, key=anchor.distance) if points.index(farthest) != self._touches.index(touch): return changed # ok, so we have touch, and anchor, so we can actually compute the # transformation old_line = Vector(*touch.ppos) - anchor new_line = Vector(*touch.pos) - anchor angle = radians(new_line.angle(old_line)) * self.do_rotation self.apply_transform(Matrix().rotate(angle, 0, 0, 1), anchor=anchor) if self.do_scale: scale = new_line.length() / old_line.length() new_scale = scale * self.scale if new_scale < self.scale_min: scale = self.scale_min / self.scale elif new_scale > self.scale_max: scale = self.scale_max / self.scale self.apply_transform(Matrix().scale(scale, scale, scale), anchor=anchor) changed = True return changed def _bring_to_front(self): # auto bring to front if self.auto_bring_to_front and self.parent: parent = self.parent parent.remove_widget(self) parent.add_widget(self) def on_touch_down(self, touch): x, y = touch.x, touch.y # if the touch isnt on the widget we do nothing if not self.do_collide_after_children: if not self.collide_point(x, y): return False # let the child widgets handle the event if they want touch.push() touch.apply_transform_2d(self.to_local) if super(Scatter, self).on_touch_down(touch): touch.pop() self._bring_to_front() return True touch.pop() # if our child didn't do anything, and if we don't have any active # interaction control, then don't accept the touch. if not self.do_translation_x and \ not self.do_translation_y and \ not self.do_rotation and \ not self.do_scale: return False if self.do_collide_after_children: if not self.collide_point(x, y): return False # grab the touch so we get all it later move events for sure self._bring_to_front() touch.grab(self) self._touches.append(touch) self._last_touch_pos[touch] = touch.pos return True def on_touch_move(self, touch): x, y = touch.x, touch.y # let the child widgets handle the event if they want if self.collide_point(x, y) and not touch.grab_current == self: touch.push() touch.apply_transform_2d(self.to_local) if super(Scatter, self).on_touch_move(touch): touch.pop() return True touch.pop() # rotate/scale/translate if touch in self._touches and touch.grab_current == self: if self.transform_with_touch(touch): self.dispatch('on_transform_with_touch', touch) self._last_touch_pos[touch] = touch.pos # stop propagating if its within our bounds if self.collide_point(x, y): return True def on_transform_with_touch(self, touch): ''' Called when a touch event has transformed the scatter widget. By default this does nothing, but can be overriden by derived classes that need to react to transformations caused by user input. :Parameters: `touch`: the touch object which triggered the transformation .. versionadded:: 1.8.0 ''' pass def on_touch_up(self, touch): x, y = touch.x, touch.y # if the touch isnt on the widget we do nothing, just try children if not touch.grab_current == self: touch.push() touch.apply_transform_2d(self.to_local) if super(Scatter, self).on_touch_up(touch): touch.pop() return True touch.pop() # remove it from our saved touches if touch in self._touches and touch.grab_state: touch.ungrab(self) del self._last_touch_pos[touch] self._touches.remove(touch) # stop propagating if its within our bounds if self.collide_point(x, y): return True
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'. ''' def get_current_tab(self): return self._current_tab current_tab = AliasProperty(get_current_tab, None) '''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 the content of the current tab. :data:`content` is a :class:`~kivy.properties.ObjectProperty`, default to 'None'. ''' 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 if hasattr(self, '_original_tab') and self._original_tab == oltab: 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) '''Holds the default tab. .. Note:: For convenience, the automatically provided default tab is deleted when you change default_tab to something else. :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 initialised before the kv lang is processed # setup the base layout for the tabbed panel self._tab_layout = GridLayout(rows=1) self.rows = 1 # bakground_image self._bk_img = Image(source=self.background_image, allow_stretch=True, keep_ratio=False, color=self.background_color) self._tab_strip = _tabs = 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 = content = TabbedPanelContent() self._current_tab = default_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.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 widget != self._default_tab: self_tabs = self._tab_strip self_tabs.width -= widget.width self_tabs.remove_widget(widget) if widget.state == 'down': if 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_background_image(self, *l): self._bk_img.source = self.background_image def on_background_color(self, *l): if self.content is None: return self._bk_img.color = self.background_color def on_do_default_tab(self, instance, value): if not value: dft = self.default_tab if dft in self.tab_list: self._default_tab = None self.remove_widget(dft) self._switch_to_first_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 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 value: 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., 0.12) else: c = (0., 0., 0., 0.12) return c
class Widget(WidgetBase): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch event occurs `on_touch_move`: Fired when an existing touch moves `on_touch_up`: Fired when an existing touch disappears .. warning:: Adding a `__del__` method to a class derived from Widget with Python prior to 3.4 will disable automatic garbage collection for instances of that class. This is because the Widget class creates reference cycles, thereby `preventing garbage collection <https://docs.python.org/2/library/gc.html#gc.garbage>`_. .. versionchanged:: 1.0.9 Everything related to event properties has been moved to the :class:`~kivy.event.EventDispatcher`. Event properties can now be used when contructing a simple class without subclassing :class:`Widget`. .. versionchanged:: 1.5.0 The constructor now accepts on_* arguments to automatically bind callbacks to properties or events, as in the Kv language. ''' __metaclass__ = WidgetMetaclass __events__ = ('on_touch_down', 'on_touch_move', 'on_touch_up') _proxy_ref = None def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # Assign the default context of the widget creation. if not hasattr(self, '_context'): self._context = get_current_context() no_builder = '__no_builder' in kwargs if no_builder: del kwargs['__no_builder'] on_args = {k: v for k, v in kwargs.items() if k[:3] == 'on_'} for key in on_args: del kwargs[key] super(Widget, self).__init__(**kwargs) # Create the default canvas if it does not exist. if self.canvas is None: self.canvas = Canvas(opacity=self.opacity) # Apply all the styles. if not no_builder: #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. if on_args: self.bind(**on_args) @property def proxy_ref(self): '''Return a proxy reference to the widget, i.e. without creating a reference to the widget. See `weakref.proxy <http://docs.python.org/2/library/weakref.html?highlight\ =proxy#weakref.proxy>`_ for more information. .. versionadded:: 1.7.2 ''' _proxy_ref = self._proxy_ref if _proxy_ref is not None: return _proxy_ref f = partial(_widget_destructor, self.uid) self._proxy_ref = _proxy_ref = WeakProxy(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 __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. .. code-block:: python >>> 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. .. code-block:: python >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received. The touch is in parent coordinates. See :mod:`~kivy.uix.relativelayout` for a discussion on coordinate systems. :Returns: bool. If True, the dispatching of the touch event will stop. ''' if self.disabled and self.collide_point(*touch.pos): return True for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. The touch is in parent coordinates. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. The touch is in parent coordinates. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True def on_disabled(self, instance, value): for child in self.children: child.disabled = value # # Tree management # def add_widget(self, widget, index=0, canvas=None): '''Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, defaults to 0 Index to insert the widget in the list. .. versionadded:: 1.0.5 `canvas`: str, defaults to None Canvas to add widget's canvas to. Can be 'before', 'after' or None for the default canvas. .. versionadded:: 1.9.0 .. code-block:: python >>> from kivy.uix.button import Button >>> from kivy.uix.slider import Slider >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with instances' ' of the Widget class.') widget = widget.__self__ if widget is self: raise WidgetException( 'Widget instances cannot be added to themselves.') parent = widget.parent # Check if the 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 canvas = self.canvas.before if canvas == 'before' else \ self.canvas.after if canvas == 'after' else self.canvas if index == 0 or len(self.children) == 0: self.children.insert(0, widget) 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. .. code-block:: python >>> from kivy.uix.button import Button >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return self.children.remove(widget) if widget.canvas in self.canvas.children: self.canvas.remove(widget.canvas) elif widget.canvas in self.canvas.after.children: self.canvas.after.remove(widget.canvas) elif widget.canvas in self.canvas.before.children: self.canvas.before.remove(widget.canvas) widget.parent = None def clear_widgets(self, children=None): '''Remove all widgets added to this widget. .. versionchanged:: 1.8.0 `children` argument can be used to select the children we want to remove. It should be a list of children (or filtered list) of the current widget. ''' if not children: children = self.children remove_widget = self.remove_widget for child in children[:]: remove_widget(child) def export_to_png(self, filename, *args): '''Saves an image of the widget and its children in png format at the specified filename. Works by removing the widget canvas from its parent, rendering to an :class:`~kivy.graphics.fbo.Fbo`, and calling :meth:`~kivy.graphics.texture.Texture.save`. .. note:: The image includes only this widget and its children. If you want to include widgets elsewhere in the tree, you must call :meth:`~Widget.export_to_png` from their common parent, or use :meth:`~kivy.core.window.Window.screenshot` to capture the whole window. .. note:: The image will be saved in png format, you should include the extension in your filename. .. versionadded:: 1.9.0 ''' if self.parent is not None: canvas_parent_index = self.parent.canvas.indexof(self.canvas) self.parent.canvas.remove(self.canvas) fbo = Fbo(size=self.size, with_stencilbuffer=True) with fbo: ClearColor(0, 0, 0, 1) ClearBuffers() Scale(1, -1, 1) Translate(-self.x, -self.y - self.height, 0) fbo.add(self.canvas) fbo.draw() fbo.texture.save(filename, flipped=False) fbo.remove(self.canvas) if self.parent is not None: self.parent.canvas.insert(canvas_parent_index, self.canvas) return True def get_root_window(self): '''Return the root window. :Returns: Instance of the root window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window. :Returns: Instance of the parent window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_parent_window() def _walk(self, restrict=False, loopback=False, index=None): # We pass index only when we are going on the parent # so don't yield the parent as well. if index is None: index = len(self.children) yield self for child in reversed(self.children[:index]): for walk_child in child._walk(restrict=True): yield walk_child # If we want to continue with our parent, just do it. if not restrict: parent = self.parent try: if parent is None or not isinstance(parent, Widget): raise ValueError index = parent.children.index(self) except ValueError: # Self is root, if we want to loopback from the first element: if not loopback: return # If we started with root (i.e. index==None), then we have to # start from root again, so we return self again. Otherwise, we # never returned it, so return it now starting with it. parent = self index = None for walk_child in parent._walk(loopback=loopback, index=index): yield walk_child def walk(self, restrict=False, loopback=False): ''' Iterator that walks the widget tree starting with this widget and goes forward returning widgets in the order in which layouts display them. :Parameters: `restrict`: bool, defaults to False If True, it will only iterate through the widget and its children (or children of its children etc.). Defaults to False. `loopback`: bool, defaults to False If True, when the last widget in the tree is reached, it'll loop back to the uppermost root and start walking until we hit this widget again. Naturally, it can only loop back when `restrict` is False. Defaults to False. :return: A generator that walks the tree, returning widgets in the forward layout order. For example, given a tree with the following structure:: GridLayout: Button BoxLayout: id: box Widget Button Widget walking this tree: .. code-block:: python >>> # Call walk on box with loopback True, and restrict False >>> [type(widget) for widget in box.walk(loopback=True)] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>, <class 'GridLayout'>, <class 'Button'>] >>> # Now with loopback False, and restrict False >>> [type(widget) for widget in box.walk()] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>] >>> # Now with restrict True >>> [type(widget) for widget in box.walk(restrict=True)] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>] .. versionadded:: 1.9.0 ''' gen = self._walk(restrict, loopback) yield next(gen) for node in gen: if node is self: return yield node def _walk_reverse(self, loopback=False, go_up=False): # process is walk up level, walk down its children tree, then walk up # next level etc. # default just walk down the children tree root = self index = 0 # we need to go up a level before walking tree if go_up: root = self.parent try: if root is None or not isinstance(root, Widget): raise ValueError index = root.children.index(self) + 1 except ValueError: if not loopback: return index = 0 go_up = False root = self # now walk children tree starting with last-most child for child in islice(root.children, index, None): for walk_child in child._walk_reverse(loopback=loopback): yield walk_child # we need to return ourself last, in all cases yield root # if going up, continue walking up the parent tree if go_up: for walk_child in root._walk_reverse(loopback=loopback, go_up=go_up): yield walk_child def walk_reverse(self, loopback=False): ''' Iterator that walks the widget tree backwards starting with the widget before this, and going backwards returning widgets in the reverse order in which layouts display them. This walks in the opposite direction of :meth:`walk`, so a list of the tree generated with :meth:`walk` will be in reverse order compared to the list generated with this, provided `loopback` is True. :Parameters: `loopback`: bool, defaults to False If True, when the uppermost root in the tree is reached, it'll loop back to the last widget and start walking back until after we hit widget again. Defaults to False. :return: A generator that walks the tree, returning widgets in the reverse layout order. For example, given a tree with the following structure:: GridLayout: Button BoxLayout: id: box Widget Button Widget walking this tree: .. code-block:: python >>> # Call walk on box with loopback True >>> [type(widget) for widget in box.walk_reverse(loopback=True)] [<class 'Button'>, <class 'GridLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>, <class 'BoxLayout'>] >>> # Now with loopback False >>> [type(widget) for widget in box.walk_reverse()] [<class 'Button'>, <class 'GridLayout'>] >>> forward = [w for w in box.walk(loopback=True)] >>> backward = [w for w in box.walk_reverse(loopback=True)] >>> forward == backward[::-1] True .. versionadded:: 1.9.0 ''' for node in self._walk_reverse(loopback=loopback, go_up=True): yield node if node is self: return def to_widget(self, x, y, relative=False): '''Convert the given coordinate from window to local widget coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. ''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinates to window coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. ''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinates to parent coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate relative positions from a widget to its parent coordinates. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinates to local coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate coordinates to relative widget coordinates. ''' if relative: return (x - self.x, y - self.y) return (x, y) def _apply_transform(self, m, pos=None): if self.parent: x, y = self.parent.to_widget(relative=True, *self.to_window(*(pos or self.pos))) m.translate(x, y, 0) m = self.parent._apply_transform(m) if self.parent else m return m def get_window_matrix(self, x=0, y=0): '''Calculate the transformation matrix to convert between window and widget coordinates. :Parameters: `x`: float, defaults to 0 Translates the matrix on the x axis. `y`: float, defaults to 0 Translates the matrix on the y axis. ''' m = Matrix() m.translate(x, y, 0) m = self._apply_transform(m) return m x = NumericProperty(0) '''X position of the widget. :attr:`x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' y = NumericProperty(0) '''Y position of the widget. :attr:`y` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' width = NumericProperty(100) '''Width of the widget. :attr:`width` is a :class:`~kivy.properties.NumericProperty` and defaults to 100. .. warning:: Keep in mind that the `width` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' height = NumericProperty(100) '''Height of the widget. :attr:`height` is a :class:`~kivy.properties.NumericProperty` and defaults to 100. .. warning:: Keep in mind that the `height` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :attr:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`x`, :attr:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :attr:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`width`, :attr:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget. :attr:`right` is an :class:`~kivy.properties.AliasProperty` of (:attr:`x` + :attr:`width`). ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget. :attr:`top` is an :class:`~kivy.properties.AliasProperty` of (:attr:`y` + :attr:`height`). ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget. :attr:`center_x` is an :class:`~kivy.properties.AliasProperty` of (:attr:`x` + :attr:`width` / 2.). ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget. :attr:`center_y` is an :class:`~kivy.properties.AliasProperty` of (:attr:`y` + :attr:`height` / 2.). ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget. :attr:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`center_x`, :attr:`center_y`) properties. ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' id = StringProperty(None, allownone=True) '''Unique identifier of the widget in the tree. :attr:`id` is a :class:`~kivy.properties.StringProperty` and defaults to None. .. warning:: If the :attr:`id` is already used in the tree, an exception will be raised. ''' children = ListProperty([]) '''List of children of this widget. :attr:`children` is a :class:`~kivy.properties.ListProperty` and defaults to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` for manipulating the children list. Don't manipulate the children list directly unless you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this widget. :attr:`parent` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. The parent of a widget is set when the widget is added to another widget and unset when the widget is removed from its parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. Represents how much space the widget should use in the direction of the X axis relative to its parent's width. Only the :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` classes make use of the hint. The size_hint is used by layouts for two purposes: - When the layout considers widgets on their own rather than in relation to its other children, the size_hint_x is a direct proportion of the parent width, normally between 0.0 and 1.0. For instance, a widget with ``size_hint_x=0.5`` in a vertical BoxLayout will take up half the BoxLayout's width, or a widget in a FloatLayout with ``size_hint_x=0.2`` will take up 20% of the FloatLayout width. If the size_hint is greater than 1, the widget will be wider than the parent. - When multiple widgets can share a row of a layout, such as in a horizontal BoxLayout, their widths will be their size_hint_x as a fraction of the sum of widget size_hints. For instance, if the size_hint_xs are (0.5, 1.0, 0.5), the first widget will have a width of 25% of the parent width. :attr:`size_hint_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :attr:`size_hint_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. See :attr:`size_hint_x` for more information, but with widths and heights swapped. ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :attr:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`size_hint_x`, :attr:`size_hint_y`) properties. See :attr:`size_hint_x` for more information. ''' pos_hint = ObjectProperty({}) '''Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write:: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right' and 'center_x' will use the parent width. The keys 'y', 'top' and 'center_y' will use the parent height. See :doc:`api-kivy.uix.floatlayout` for further reference. .. note:: :attr:`pos_hint` is not used by all layouts. Check the documentation of the layout in question to see if it supports pos_hint. :attr:`pos_hint` is an :class:`~kivy.properties.ObjectProperty` containing a dict. ''' ids = DictProperty({}) '''This is a dictionary of ids defined in your kv language. This will only be populated if you use ids in your kv language code. .. versionadded:: 1.7.0 :attr:`ids` is a :class:`~kivy.properties.DictProperty` and defaults to an empty dict {}. The :attr:`ids` are populated for each root level widget definition. For example:: # in kv <MyWidget@Widget>: id: my_widget Label: id: label_widget Widget: id: inner_widget Label: id: inner_label TextInput: id: text_input OtherWidget: id: other_widget <OtherWidget@Widget> id: other_widget Label: id: other_label TextInput: id: other_textinput Then, in python: .. code-block:: python >>> widget = MyWidget() >>> print(widget.ids) {'other_widget': <weakproxy at 041CFED0 to OtherWidget at 041BEC38>, 'inner_widget': <weakproxy at 04137EA0 to Widget at 04138228>, 'inner_label': <weakproxy at 04143540 to Label at 04138260>, 'label_widget': <weakproxy at 04137B70 to Label at 040F97A0>, 'text_input': <weakproxy at 041BB5D0 to TextInput at 041BEC00>} >>> print(widget.ids['other_widget'].ids) {'other_textinput': <weakproxy at 041DBB40 to TextInput at 041BEF48>, 'other_label': <weakproxy at 041DB570 to Label at 041BEEA0>} >>> print(widget.ids['label_widget'].ids) {} ''' opacity = NumericProperty(1.0) '''Opacity of the widget and all its children. .. versionadded:: 1.4.1 The opacity attribute controls the opacity of the widget and its children. Be careful, it's a cumulative attribute: the value is multiplied by the current global opacity and the result is applied to the current context color. For example, if the parent has an opacity of 0.5 and a child has an opacity of 0.2, the real opacity of the child will be 0.5 * 0.2 = 0.1. Then, the opacity is applied by the shader as: .. code-block:: python frag_color = color * vec4(1.0, 1.0, 1.0, opacity); :attr:`opacity` is a :class:`~kivy.properties.NumericProperty` and defaults to 1.0. ''' def on_opacity(self, instance, value): canvas = self.canvas if canvas is not None: canvas.opacity = value canvas = None '''Canvas of the widget. The canvas is a graphics object that contains all the drawing instructions for the graphical representation of the widget. There are no general properties for the Widget class, such as background color, to keep the design simple and lean. Some derived classes, such as Button, do add such convenience properties but generally the developer is responsible for implementing the graphics representation for a custom widget from the ground up. See the derived widget classes for patterns to follow and extend. See :class:`~kivy.graphics.Canvas` for more information about the usage. ''' disabled = BooleanProperty(False) '''Indicates whether this widget can interact with input or not.
class DeckBuilderScrollBar(FloatLayout): """A widget that looks a lot like one of the scrollbars on the sides of eg. :class:`kivy.uix.ScrollView`, which moves a single deck within a :class:`DeckBuilderLayout`. """ orientation = OptionProperty('vertical', options=['horizontal', 'vertical']) """Which way to scroll? Options are 'horizontal' and 'vertical'.""" deckbuilder = ObjectProperty() """The :class:`DeckBuilderLayout` of the deck to scroll.""" deckidx = NumericProperty(0) """The index of the deck to scroll, within its :class:`DeckBuilderLayout`'s ``decks`` property. """ scrolling = BooleanProperty(False) """Has the user grabbed me?""" scroll_min = NumericProperty(-1) """How far left (if horizontal) or down (if vertical) I can move my deck, expressed as a proportion of the :class:`DeckBuilderLayout`'s width or height, respectively. """ scroll_max = NumericProperty(1) """How far right (if horizontal) or up (if vertical) I can move my deck, expressed as a proportion of the :class:`DeckBuilderLayout`'s width or height, respectively. """ scroll_hint = AliasProperty( lambda self: abs(self.scroll_max - self.scroll_min), lambda self, v: None, bind=('scroll_min', 'scroll_max')) """The distance between ``scroll_max`` and ``scroll_min``.""" _scroll = NumericProperty(0) """Private. The current adjustment to the deck's ``pos_hint_x`` or ``pos_hint_y``. """ def _get_scroll(self): zero = self._scroll - self.scroll_min return zero / self.scroll_hint def _set_scroll(self, v): if v < 0: v = 0 if v > 1: v = 1 normal = v * self.scroll_hint self._scroll = self.scroll_min + normal scroll = AliasProperty(_get_scroll, _set_scroll, bind=('_scroll', 'scroll_min', 'scroll_max')) """A number between 0 and 1 representing how far beyond ``scroll_min`` toward ``scroll_max`` I am presently scrolled. """ def _get_vbar(self): if self.deckbuilder is None: return (0, 1) vh = self.deckbuilder.height * (self.scroll_hint + 1) h = self.height if vh < h or vh == 0: return (0, 1) ph = max(0.01, h / vh) sy = min(1.0, max(0.0, self.scroll)) py = (1 - ph) * sy return (py, ph) vbar = AliasProperty(_get_vbar, None, bind=('_scroll', 'scroll_min', 'scroll_max')) """A tuple of ``(y, height)`` for my scroll bar, if it's vertical.""" def _get_hbar(self): if self.deckbuilder is None: return (0, 1) vw = self.deckbuilder.width * self.scroll_hint w = self.width if vw < w or vw == 0: return (0, 1) pw = max(0.01, w / vw) sx = min(1.0, max(0.0, self.scroll)) px = (1 - pw) * sx return (px, pw) hbar = AliasProperty(_get_hbar, None, bind=('_scroll', 'scroll_min', 'scroll_max')) """A tuple of ``(x, width)`` for my scroll bar, if it's horizontal.""" bar_color = ListProperty([.7, .7, .7, .9]) """Color to use for the scroll bar when scrolling. RGBA format.""" bar_inactive_color = ListProperty([.7, .7, .7, .2]) """Color to use for the scroll bar when not scrolling. RGBA format.""" bar_texture = ObjectProperty(None, allownone=True) """Texture for the scroll bar, normally ``None``.""" def __init__(self, **kwargs): """Arrange to be laid out whenever I'm scrolled or the range of my scrolling changes. """ super().__init__(**kwargs) self.bind(_scroll=self._trigger_layout, scroll_min=self._trigger_layout, scroll_max=self._trigger_layout) def do_layout(self, *args): """Put the bar where it's supposed to be, and size it in proportion to the size of the scrollable area. """ if 'bar' not in self.ids: Clock.schedule_once(self.do_layout) return if self.orientation == 'horizontal': self.ids.bar.size_hint_x = self.hbar[1] self.ids.bar.pos_hint = {'x': self.hbar[0], 'y': 0} else: self.ids.bar.size_hint_y = self.vbar[1] self.ids.bar.pos_hint = {'x': 0, 'y': self.vbar[0]} super().do_layout(*args) def upd_scroll(self, *args): """Update my own ``scroll`` property to where my deck is actually scrolled. """ att = 'deck_{}_hint_offsets'.format('x' if self.orientation == 'horizontal' else 'y') self._scroll = getattr(self.deckbuilder, att)[self.deckidx] def on_deckbuilder(self, *args): """Bind my deckbuilder to update my ``scroll``, and my ``scroll`` to update my deckbuilder. """ if self.deckbuilder is None: return att = 'deck_{}_hint_offsets'.format('x' if self.orientation == 'horizontal' else 'y') offs = getattr(self.deckbuilder, att) if len(offs) <= self.deckidx: Clock.schedule_once(self.on_deckbuilder, 0) return self.bind(scroll=self.handle_scroll) self.deckbuilder.bind(**{att: self.upd_scroll}) self.upd_scroll() self.deckbuilder._trigger_layout() def handle_scroll(self, *args): """When my ``scroll`` changes, tell my deckbuilder how it's scrolled now. """ if 'bar' not in self.ids: Clock.schedule_once(self.handle_scroll, 0) return att = 'deck_{}_hint_offsets'.format('x' if self.orientation == 'horizontal' else 'y') offs = list(getattr(self.deckbuilder, att)) if len(offs) <= self.deckidx: Clock.schedule_once(self.on_scroll, 0) return offs[self.deckidx] = self._scroll setattr(self.deckbuilder, att, offs) self.deckbuilder._trigger_layout() def bar_touched(self, bar, touch): """Start scrolling, and record where I started scrolling.""" self.scrolling = True self._start_bar_pos_hint = get_pos_hint(bar.pos_hint, *bar.size_hint) self._start_touch_pos_hint = (touch.x / self.width, touch.y / self.height) self._start_bar_touch_hint = (self._start_touch_pos_hint[0] - self._start_bar_pos_hint[0], self._start_touch_pos_hint[1] - self._start_bar_pos_hint[1]) touch.grab(self) def on_touch_move(self, touch): """Move the scrollbar to the touch, and update my ``scroll`` accordingly. """ if not self.scrolling or 'bar' not in self.ids: touch.ungrab(self) return touch.push() touch.apply_transform_2d(self.parent.to_local) touch.apply_transform_2d(self.to_local) if self.orientation == 'horizontal': hint_right_of_bar = (touch.x - self.ids.bar.x) / self.width hint_correction = hint_right_of_bar - self._start_bar_touch_hint[0] self.scroll += hint_correction else: # self.orientation == 'vertical' hint_above_bar = (touch.y - self.ids.bar.y) / self.height hint_correction = hint_above_bar - self._start_bar_touch_hint[1] self.scroll += hint_correction touch.pop() def on_touch_up(self, touch): """Stop scrolling.""" self.scrolling = False
class ActionItem(object): ''' ActionItem class, an abstract class for all ActionBar widgets. To create a custom widget for an ActionBar, inherit from this class. See module documentation for more information. ''' minimum_width = NumericProperty('90sp') ''' Minimum Width required by an ActionItem. :attr:`minimum_width` is a :class:`~kivy.properties.NumericProperty` and defaults to '90sp'. ''' def get_pack_width(self): return max(self.minimum_width, self.width) pack_width = AliasProperty(get_pack_width, bind=('minimum_width', 'width')) ''' (read-only) The actual width to use when packing the items. Equal to the greater of minimum_width and width. :attr:`pack_width` is an :class:`~kivy.properties.AliasProperty`. ''' important = BooleanProperty(False) ''' Determines if an ActionItem is important or not. If an item is important and space is limited, this item will be displayed in preference to others. :attr:`important` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' inside_group = BooleanProperty(False) ''' (internal) Determines if an ActionItem is displayed inside an ActionGroup or not. :attr:`inside_group` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' background_normal = StringProperty( 'atlas://data/images/defaulttheme/action_item') ''' Background image of the ActionItem used for the default graphical representation when the ActionItem is not pressed. :attr:`background_normal` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/action_item'. ''' background_down = StringProperty( 'atlas://data/images/defaulttheme/action_item_down') ''' Background image of the ActionItem used for the default graphical representation when an ActionItem is pressed. :attr:`background_down` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/action_item_down'. ''' mipmap = BooleanProperty(True) '''
class MDSwitch(ThemableBehavior, ButtonBehavior, FloatLayout): active = BooleanProperty(False) _thumb_color = ListProperty(get_color_from_hex(colors['Grey']['50'])) def _get_thumb_color(self): return self._thumb_color def _set_thumb_color(self, color, alpha=None): if len(color) == 2: self._thumb_color = get_color_from_hex(colors[color[0]][color[1]]) if alpha: self._thumb_color[3] = alpha elif len(color) == 4: self._thumb_color = color thumb_color = AliasProperty(_get_thumb_color, _set_thumb_color, bind=['_thumb_color']) _thumb_color_down = ListProperty([1, 1, 1, 1]) def _get_thumb_color_down(self): return self._thumb_color_down def _set_thumb_color_down(self, color, alpha=None): if len(color) == 2: self._thumb_color_down = get_color_from_hex( colors[color[0]][color[1]]) if alpha: self._thumb_color_down[3] = alpha else: self._thumb_color_down[3] = 1 elif len(color) == 4: self._thumb_color_down = color thumb_color_down = AliasProperty(_get_thumb_color_down, _set_thumb_color_down, bind=['_thumb_color_down']) _thumb_color_disabled = ListProperty( get_color_from_hex(colors['Grey']['400'])) def _get_thumb_color_disabled(self): return self._thumb_color_disabled def _set_thumb_color_disabled(self, color, alpha=None): if len(color) == 2: self._thumb_color_disabled = get_color_from_hex( colors[color[0]][color[1]]) if alpha: self._thumb_color_disabled[3] = alpha elif len(color) == 4: self._thumb_color_disabled = color thumb_color_down = AliasProperty(_get_thumb_color_disabled, _set_thumb_color_disabled, bind=['_thumb_color_disabled']) _track_color_active = ListProperty() _track_color_normal = ListProperty() _track_color_disabled = ListProperty() _thumb_pos = ListProperty([0, 0]) def __init__(self, **kwargs): super(MDSwitch, self).__init__(**kwargs) self.theme_cls.bind(theme_style=self._set_colors, primary_color=self._set_colors, primary_palette=self._set_colors) self._set_colors() def _set_colors(self, *args): self._track_color_normal = self.theme_cls.disabled_hint_text_color if self.theme_cls.theme_style == 'Dark': self._track_color_active = self.theme_cls.primary_color self._track_color_active[3] = .5 self._track_color_disabled = get_color_from_hex('FFFFFF') self._track_color_disabled[3] = .1 self.thumb_color = get_color_from_hex(colors['Grey']['400']) self.thumb_color_down = get_color_from_hex( colors[self.theme_cls.primary_palette]['200']) self.thumb_color_disabled = get_color_from_hex( colors['Grey']['800']) else: self._track_color_active = get_color_from_hex( colors[self.theme_cls.primary_palette]['200']) self._track_color_active[3] = .5 self._track_color_disabled = self.theme_cls.disabled_hint_text_color self.thumb_color_down = self.theme_cls.primary_color def on_pos(self, *args): if self.active: self._thumb_pos = (self.right - dp(12), self.center_y - dp(12)) else: self._thumb_pos = (self.x - dp(12), self.center_y - dp(12)) self.bind(active=self._update_thumb) def _update_thumb(self, *args): if self.active: Animation.cancel_all(self, '_thumb_pos') anim = Animation(_thumb_pos=(self.right - dp(12), self.center_y - dp(12)), duration=.2, t='out_quad') else: Animation.cancel_all(self, '_thumb_pos') anim = Animation(_thumb_pos=(self.x - dp(12), self.center_y - dp(12)), duration=.2, t='out_quad') anim.start(self)
class ThemeManager(EventDispatcher): primary_palette = OptionProperty("Blue", options=palette) """ The name of the color scheme that the application will use. All major `material` components will have the color of the specified color theme. Available options are: `'Red'`, `'Pink'`, `'Purple'`, `'DeepPurple'`, `'Indigo'`, `'Blue'`, `'LightBlue'`, `'Cyan'`, `'Teal'`, `'Green'`, `'LightGreen'`, `'Lime'`, `'Yellow'`, `'Amber'`, `'Orange'`, `'DeepOrange'`, `'Brown'`, `'Gray'`, `'BlueGray'`. To change the color scheme of an application: .. code-block:: python from kivymd.app import MDApp from kivymd.uix.screen import MDScreen from kivymd.uix.button import MDRectangleFlatButton class MainApp(MDApp): def build(self): self.theme_cls.primary_palette = "Green" # "Purple", "Red" screen = MDScreen() screen.add_widget( MDRectangleFlatButton( text="Hello, World", pos_hint={"center_x": 0.5, "center_y": 0.5}, ) ) return screen MainApp().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-palette.png :attr:`primary_palette` is an :class:`~kivy.properties.OptionProperty` and defaults to `'Blue'`. """ primary_hue = OptionProperty("500", options=hue) """ The color hue of the application. Available options are: `'50'`, `'100'`, `'200'`, `'300'`, `'400'`, `'500'`, `'600'`, `'700'`, `'800'`, `'900'`, `'A100'`, `'A200'`, `'A400'`, `'A700'`. To change the hue color scheme of an application: .. code-block:: python from kivymd.app import MDApp from kivymd.uix.screen import MDScreen from kivymd.uix.button import MDRectangleFlatButton class MainApp(MDApp): def build(self): self.theme_cls.primary_palette = "Green" # "Purple", "Red" self.theme_cls.primary_hue = "200" # "500" screen = MDScreen() screen.add_widget( MDRectangleFlatButton( text="Hello, World", pos_hint={"center_x": 0.5, "center_y": 0.5}, ) ) return screen MainApp().run() With a value of ``self.theme_cls.primary_hue = "500"``: .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-palette.png With a value of ``self.theme_cls.primary_hue = "200"``: .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-hue.png :attr:`primary_hue` is an :class:`~kivy.properties.OptionProperty` and defaults to `'500'`. """ primary_light_hue = OptionProperty("200", options=hue) """ Hue value for :attr:`primary_light`. :attr:`primary_light_hue` is an :class:`~kivy.properties.OptionProperty` and defaults to `'200'`. """ primary_dark_hue = OptionProperty("700", options=hue) """ Hue value for :attr:`primary_dark`. :attr:`primary_light_hue` is an :class:`~kivy.properties.OptionProperty` and defaults to `'700'`. """ def _get_primary_color(self) -> list: return get_color_from_hex( self.colors[self.primary_palette][self.primary_hue]) primary_color = AliasProperty(_get_primary_color, bind=("primary_palette", "primary_hue")) """ The color of the current application theme in ``rgba`` format. :attr:`primary_color` is an :class:`~kivy.properties.AliasProperty` that returns the value of the current application theme, property is readonly. """ def _get_primary_light(self) -> list: return get_color_from_hex( self.colors[self.primary_palette][self.primary_light_hue]) primary_light = AliasProperty(_get_primary_light, bind=("primary_palette", "primary_light_hue")) """ Colors of the current application color theme in ``rgba`` format (in lighter color). .. code-block:: python from kivy.lang import Builder from kivymd.app import MDApp KV = ''' MDScreen: MDRaisedButton: text: "primary_light" pos_hint: {"center_x": 0.5, "center_y": 0.7} md_bg_color: app.theme_cls.primary_light MDRaisedButton: text: "primary_color" pos_hint: {"center_x": 0.5, "center_y": 0.5} MDRaisedButton: text: "primary_dark" pos_hint: {"center_x": 0.5, "center_y": 0.3} md_bg_color: app.theme_cls.primary_dark ''' class MainApp(MDApp): def build(self): self.theme_cls.primary_palette = "Green" return Builder.load_string(KV) MainApp().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-colors-light-dark.png :align: center :attr:`primary_light` is an :class:`~kivy.properties.AliasProperty` that returns the value of the current application theme (in lighter color), property is readonly. """ def _get_primary_dark(self) -> list: return get_color_from_hex( self.colors[self.primary_palette][self.primary_dark_hue]) primary_dark = AliasProperty(_get_primary_dark, bind=("primary_palette", "primary_dark_hue")) """ Colors of the current application color theme in ``rgba`` format (in darker color). :attr:`primary_dark` is an :class:`~kivy.properties.AliasProperty` that returns the value of the current application theme (in darker color), property is readonly. """ accent_palette = OptionProperty("Amber", options=palette) """ The application color palette used for items such as the tab indicator in the :attr:`MDTabsBar` class and so on... The image below shows the color schemes with the values ``self.theme_cls.accent_palette = 'Blue'``, ``Red'`` and ``Yellow'``: .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/accent-palette.png :attr:`accent_palette` is an :class:`~kivy.properties.OptionProperty` and defaults to `'Amber'`. """ accent_hue = OptionProperty("500", options=hue) """ Similar to :attr:`primary_hue`, but returns a value for :attr:`accent_palette`. :attr:`accent_hue` is an :class:`~kivy.properties.OptionProperty` and defaults to `'500'`. """ accent_light_hue = OptionProperty("200", options=hue) """ Hue value for :attr:`accent_light`. :attr:`accent_light_hue` is an :class:`~kivy.properties.OptionProperty` and defaults to `'200'`. """ accent_dark_hue = OptionProperty("700", options=hue) """ Hue value for :attr:`accent_dark`. :attr:`accent_dark_hue` is an :class:`~kivy.properties.OptionProperty` and defaults to `'700'`. """ def _get_accent_color(self) -> list: return get_color_from_hex( self.colors[self.accent_palette][self.accent_hue]) accent_color = AliasProperty(_get_accent_color, bind=["accent_palette", "accent_hue"]) """ Similar to :attr:`primary_color`, but returns a value for :attr:`accent_color`. :attr:`accent_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`accent_color`, property is readonly. """ def _get_accent_light(self) -> list: return get_color_from_hex( self.colors[self.accent_palette][self.accent_light_hue]) accent_light = AliasProperty(_get_accent_light, bind=["accent_palette", "accent_light_hue"]) """ Similar to :attr:`primary_light`, but returns a value for :attr:`accent_light`. :attr:`accent_light` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`accent_light`, property is readonly. """ def _get_accent_dark(self) -> list: return get_color_from_hex( self.colors[self.accent_palette][self.accent_dark_hue]) accent_dark = AliasProperty(_get_accent_dark, bind=["accent_palette", "accent_dark_hue"]) """ Similar to :attr:`primary_dark`, but returns a value for :attr:`accent_dark`. :attr:`accent_dark` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`accent_dark`, property is readonly. """ material_style = OptionProperty("M2", options=["M2", "M3"]) """ Material design style. Available options are: 'M2', 'M3'. .. versionadded:: 1.0.0 .. seealso:: `Material Design 2 <https://material.io/>`_ and `Material Design 3 <https://m3.material.io>`_ :attr:`material_style` is an :class:`~kivy.properties.OptionProperty` and defaults to `'M2'`. """ theme_style = OptionProperty("Light", options=["Light", "Dark"]) """ App theme style. .. code-block:: python from kivymd.app import MDApp from kivymd.uix.screen import MDScreen from kivymd.uix.button import MDRectangleFlatButton class MainApp(MDApp): def build(self): self.theme_cls.theme_style = "Dark" # "Light" screen = MDScreen() screen.add_widget( MDRectangleFlatButton( text="Hello, World", pos_hint={"center_x": 0.5, "center_y": 0.5}, ) ) return screen MainApp().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/theme-style.png :attr:`theme_style` is an :class:`~kivy.properties.OptionProperty` and defaults to `'Light'`. """ def _get_theme_style(self, opposite: bool) -> str: if opposite: return "Light" if self.theme_style == "Dark" else "Dark" else: return self.theme_style def _get_bg_darkest(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": return get_color_from_hex(self.colors["Light"]["StatusBar"]) elif theme_style == "Dark": return get_color_from_hex(self.colors["Dark"]["StatusBar"]) bg_darkest = AliasProperty(_get_bg_darkest, bind=["theme_style"]) """ Similar to :attr:`bg_dark`, but the color values are a tone lower (darker) than :attr:`bg_dark`. .. code-block:: python KV = ''' MDBoxLayout: MDBoxLayout: md_bg_color: app.theme_cls.bg_light MDBoxLayout: md_bg_color: app.theme_cls.bg_normal MDBoxLayout: md_bg_color: app.theme_cls.bg_dark MDBoxLayout: md_bg_color: app.theme_cls.bg_darkest ''' from kivy.lang import Builder from kivymd.app import MDApp class MainApp(MDApp): def build(self): self.theme_cls.theme_style = "Dark" # "Light" return Builder.load_string(KV) MainApp().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bg-normal-dark-darkest.png :attr:`bg_darkest` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`bg_darkest`, property is readonly. """ def _get_op_bg_darkest(self) -> list: return self._get_bg_darkest(True) opposite_bg_darkest = AliasProperty(_get_op_bg_darkest, bind=["theme_style"]) """ The opposite value of color in the :attr:`bg_darkest`. :attr:`opposite_bg_darkest` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_bg_darkest`, property is readonly. """ def _get_bg_dark(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": return get_color_from_hex(self.colors["Light"]["AppBar"]) elif theme_style == "Dark": return get_color_from_hex(self.colors["Dark"]["AppBar"]) bg_dark = AliasProperty(_get_bg_dark, bind=["theme_style"]) """ Similar to :attr:`bg_normal`, but the color values are one tone lower (darker) than :attr:`bg_normal`. :attr:`bg_dark` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`bg_dark`, property is readonly. """ def _get_op_bg_dark(self) -> list: return self._get_bg_dark(True) opposite_bg_dark = AliasProperty(_get_op_bg_dark, bind=["theme_style"]) """ The opposite value of color in the :attr:`bg_dark`. :attr:`opposite_bg_dark` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_bg_dark`, property is readonly. """ def _get_bg_normal(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": return get_color_from_hex(self.colors["Light"]["Background"]) elif theme_style == "Dark": return get_color_from_hex(self.colors["Dark"]["Background"]) bg_normal = AliasProperty(_get_bg_normal, bind=["theme_style"]) """ Similar to :attr:`bg_light`, but the color values are one tone lower (darker) than :attr:`bg_light`. :attr:`bg_normal` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`bg_normal`, property is readonly. """ def _get_op_bg_normal(self) -> list: return self._get_bg_normal(True) opposite_bg_normal = AliasProperty(_get_op_bg_normal, bind=["theme_style"]) """ The opposite value of color in the :attr:`bg_normal`. :attr:`opposite_bg_normal` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_bg_normal`, property is readonly. """ def _get_bg_light(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": return get_color_from_hex(self.colors["Light"]["CardsDialogs"]) elif theme_style == "Dark": return get_color_from_hex(self.colors["Dark"]["CardsDialogs"]) bg_light = AliasProperty(_get_bg_light, bind=["theme_style"]) """" Depending on the style of the theme (`'Dark'` or `'Light`') that the application uses, :attr:`bg_light` contains the color value in ``rgba`` format for the widgets background. :attr:`bg_light` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`bg_light`, property is readonly. """ def _get_op_bg_light(self) -> list: return self._get_bg_light(True) opposite_bg_light = AliasProperty(_get_op_bg_light, bind=["theme_style"]) """ The opposite value of color in the :attr:`bg_light`. :attr:`opposite_bg_light` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_bg_light`, property is readonly. """ def _get_divider_color(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": color = get_color_from_hex("000000") elif theme_style == "Dark": color = get_color_from_hex("FFFFFF") color[3] = 0.12 return color divider_color = AliasProperty(_get_divider_color, bind=["theme_style"]) """ Color for dividing lines such as :class:`~kivymd.uix.card.MDSeparator`. :attr:`divider_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`divider_color`, property is readonly. """ def _get_op_divider_color(self) -> list: return self._get_divider_color(True) opposite_divider_color = AliasProperty(_get_op_divider_color, bind=["theme_style"]) """ The opposite value of color in the :attr:`divider_color`. :attr:`opposite_divider_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_divider_color`, property is readonly. """ def _get_disabled_primary_color(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) lum = sum(self.primary_color[0:3]) / 3.0 if theme_style == "Light": a = 0.38 elif theme_style == "Dark": a = 0.50 return [lum, lum, lum, a] disabled_primary_color = AliasProperty(_get_disabled_primary_color, bind=["theme_style"]) """ The greyscale disabled version of the current application theme color in ``rgba`` format. .. versionadded:: 1.0.0 :attr:`disabled_primary_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`disabled_primary_color`, property is readonly. """ def _get_op_disabled_primary_color(self) -> list: return self._get_disabled_primary_color(True) opposite_disabled_primary_color = AliasProperty( _get_op_disabled_primary_color, bind=["theme_style"]) """ The opposite value of color in the :attr:`disabled_primary_color`. .. versionadded:: 1.0.0 :attr:`opposite_disabled_primary_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_disabled_primary_color`, property is readonly. """ def _get_text_color(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": color = get_color_from_hex("000000") color[3] = 0.87 elif theme_style == "Dark": color = get_color_from_hex("FFFFFF") return color text_color = AliasProperty(_get_text_color, bind=["theme_style"]) """ Color of the text used in the :class:`~kivymd.uix.label.MDLabel`. :attr:`text_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`text_color`, property is readonly. """ def _get_op_text_color(self) -> list: return self._get_text_color(True) opposite_text_color = AliasProperty(_get_op_text_color, bind=["theme_style"]) """ The opposite value of color in the :attr:`text_color`. :attr:`opposite_text_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_text_color`, property is readonly. """ def _get_secondary_text_color(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": color = get_color_from_hex("000000") color[3] = 0.54 elif theme_style == "Dark": color = get_color_from_hex("FFFFFF") color[3] = 0.70 return color secondary_text_color = AliasProperty(_get_secondary_text_color, bind=["theme_style"]) """ The color for the secondary text that is used in classes from the module :class:`~kivymd/uix/list.TwoLineListItem`. :attr:`secondary_text_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`secondary_text_color`, property is readonly. """ def _get_op_secondary_text_color(self) -> list: return self._get_secondary_text_color(True) opposite_secondary_text_color = AliasProperty(_get_op_secondary_text_color, bind=["theme_style"]) """ The opposite value of color in the :attr:`secondary_text_color`. :attr:`opposite_secondary_text_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_secondary_text_color`, property is readonly. """ def _get_icon_color(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": color = get_color_from_hex("000000") color[3] = 0.54 elif theme_style == "Dark": color = get_color_from_hex("FFFFFF") return color icon_color = AliasProperty(_get_icon_color, bind=["theme_style"]) """ Color of the icon used in the :class:`~kivymd.uix.button.MDIconButton`. :attr:`icon_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`icon_color`, property is readonly. """ def _get_op_icon_color(self) -> list: return self._get_icon_color(True) opposite_icon_color = AliasProperty(_get_op_icon_color, bind=["theme_style"]) """ The opposite value of color in the :attr:`icon_color`. :attr:`opposite_icon_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_icon_color`, property is readonly. """ def _get_disabled_hint_text_color(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": color = get_color_from_hex("000000") color[3] = 0.38 elif theme_style == "Dark": color = get_color_from_hex("FFFFFF") color[3] = 0.50 return color disabled_hint_text_color = AliasProperty(_get_disabled_hint_text_color, bind=["theme_style"]) """ Color of the disabled text used in the :class:`~kivymd.uix.textfield.MDTextField`. :attr:`disabled_hint_text_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`disabled_hint_text_color`, property is readonly. """ def _get_op_disabled_hint_text_color(self) -> list: return self._get_disabled_hint_text_color(True) opposite_disabled_hint_text_color = AliasProperty( _get_op_disabled_hint_text_color, bind=["theme_style"]) """ The opposite value of color in the :attr:`disabled_hint_text_color`. :attr:`opposite_disabled_hint_text_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_disabled_hint_text_color`, property is readonly. """ # Hardcoded because muh standard def _get_error_color(self) -> list: return get_color_from_hex(self.colors["Red"]["A700"]) error_color = AliasProperty(_get_error_color, bind=["theme_style"]) """ Color of the error text used in the :class:`~kivymd.uix.textfield.MDTextField`. :attr:`error_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`error_color`, property is readonly. """ def _get_ripple_color(self) -> list: return self._ripple_color def _set_ripple_color(self, value) -> None: self._ripple_color = value _ripple_color = ColorProperty(get_color_from_hex(colors["Gray"]["400"])) """Private value.""" ripple_color = AliasProperty(_get_ripple_color, _set_ripple_color, bind=["_ripple_color"]) """ Color of ripple effects. :attr:`ripple_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`ripple_color`, property is readonly. """ def _determine_device_orientation(self, _, window_size) -> None: if window_size[0] > window_size[1]: self.device_orientation = "landscape" elif window_size[1] >= window_size[0]: self.device_orientation = "portrait" device_orientation = StringProperty("") """ Device orientation. :attr:`device_orientation` is an :class:`~kivy.properties.StringProperty`. """ def _get_standard_increment(self) -> float: if DEVICE_TYPE == "mobile": if self.device_orientation == "landscape": return dp(48) else: return dp(56) else: return dp(64) standard_increment = AliasProperty(_get_standard_increment, bind=["device_orientation"]) """ Value of standard increment. :attr:`standard_increment` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`standard_increment`, property is readonly. """ def _get_horizontal_margins(self) -> float: if DEVICE_TYPE == "mobile": return dp(16) else: return dp(24) horizontal_margins = AliasProperty(_get_horizontal_margins) """ Value of horizontal margins. :attr:`horizontal_margins` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`horizontal_margins`, property is readonly. """ def on_theme_style(self, interval: int, theme_style: str) -> None: if (hasattr(App.get_running_app(), "theme_cls") and App.get_running_app().theme_cls == self): self.set_clearcolor_by_theme_style(theme_style) set_clearcolor = BooleanProperty(True) def set_clearcolor_by_theme_style(self, theme_style): if not self.set_clearcolor: return Window.clearcolor = get_color_from_hex( self.colors[theme_style]["Background"]) # Font name, size (sp), always caps, letter spacing (sp). font_styles = DictProperty({ "H1": ["RobotoLight", 96, False, -1.5], "H2": ["RobotoLight", 60, False, -0.5], "H3": ["Roboto", 48, False, 0], "H4": ["Roboto", 34, False, 0.25], "H5": ["Roboto", 24, False, 0], "H6": ["RobotoMedium", 20, False, 0.15], "Subtitle1": ["Roboto", 16, False, 0.15], "Subtitle2": ["RobotoMedium", 14, False, 0.1], "Body1": ["Roboto", 16, False, 0.5], "Body2": ["Roboto", 14, False, 0.25], "Button": ["RobotoMedium", 14, True, 1.25], "Caption": ["Roboto", 12, False, 0.4], "Overline": ["Roboto", 10, True, 1.5], "Icon": ["Icons", 24, False, 0], }) """ Data of default font styles. Add custom font: .. code-block:: python KV = ''' MDScreen: MDLabel: text: "JetBrainsMono" halign: "center" font_style: "JetBrainsMono" ''' from kivy.core.text import LabelBase from kivy.lang import Builder from kivymd.app import MDApp from kivymd.font_definitions import theme_font_styles class MainApp(MDApp): def build(self): LabelBase.register( name="JetBrainsMono", fn_regular="JetBrainsMono-Regular.ttf") theme_font_styles.append('JetBrainsMono') self.theme_cls.font_styles["JetBrainsMono"] = [ "JetBrainsMono", 16, False, 0.15, ] return Builder.load_string(KV) MainApp().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/font-styles.png :attr:`font_styles` is an :class:`~kivy.properties.DictProperty`. """ def set_colors( self, primary_palette: str, primary_hue: str, primary_light_hue: str, primary_dark_hue: str, accent_palette: str, accent_hue: str, accent_light_hue: str, accent_dark_hue: str, ) -> None: """ Courtesy method to allow all of the theme color attributes to be set in one call. :attr:`set_colors` allows all of the following to be set in one method call: * primary palette color, * primary hue, * primary light hue, * primary dark hue, * accent palette color, * accent hue, * accent ligth hue, and * accent dark hue. Note that all values *must* be provided. If you only want to set one or two values use the appropriate method call for that. .. code-block:: python from kivymd.app import MDApp from kivymd.uix.screen import MDScreen from kivymd.uix.button import MDRectangleFlatButton class MainApp(MDApp): def build(self): self.theme_cls.set_colors( "Blue", "600", "50", "800", "Teal", "600", "100", "800" ) screen = MDScreen() screen.add_widget( MDRectangleFlatButton( text="Hello, World", pos_hint={"center_x": 0.5, "center_y": 0.5}, ) ) return screen MainApp().run() """ self.primary_palette = primary_palette self.primary_hue = primary_hue self.primary_light_hue = primary_light_hue self.primary_dark_hue = primary_dark_hue self.accent_palette = accent_palette self.accent_hue = accent_hue self.accent_light_hue = accent_light_hue self.accent_dark_hue = accent_dark_hue def __init__(self, **kwargs): super().__init__(**kwargs) self.rec_shadow = Atlas(f"{images_path}rec_shadow.atlas") self.rec_st_shadow = Atlas(f"{images_path}rec_st_shadow.atlas") self.quad_shadow = Atlas(f"{images_path}quad_shadow.atlas") self.round_shadow = Atlas(f"{images_path}round_shadow.atlas") Clock.schedule_once(lambda x: self.on_theme_style(0, self.theme_style)) self._determine_device_orientation(None, Window.size) Window.bind(size=self._determine_device_orientation) self.bind(font_styles=self.sync_theme_styles) self.colors = colors Clock.schedule_once(self.sync_theme_styles) def sync_theme_styles(self, *args) -> None: # Syncs the values from self.font_styles to theme_font_styles # this will ensure continuity when someone registers a new font_style. for num, style in enumerate(theme_font_styles): if style not in self.font_styles: theme_font_styles.pop(num) for style in self.font_styles.keys(): theme_font_styles.append(style)
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_checkpoint = NumericProperty(0) auto_connect = BooleanProperty(False) def on_auto_connect(self, instance, x): host, port, protocol, proxy, auto_connect = self.network.get_parameters( ) self.network.set_parameters(host, port, protocol, proxy, self.auto_connect) def toggle_auto_connect(self, x): self.auto_connect = not self.auto_connect def choose_server_dialog(self, popup): from .uix.dialogs.choice_dialog import ChoiceDialog protocol = 's' def cb2(host): from electrum 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): for index, b in self.network.blockchains.items(): if name == self.network.get_blockchain_name(b): self.network.follow_chain(index) #self.block names = [self.network.blockchains[b].get_name() for b in chains] if len(names) > 1: ChoiceDialog(_('Choose your chain'), names, '', cb).open() use_rbf = BooleanProperty(False) def on_use_rbf(self, instance, x): self.electrum_config.set_key('use_rbf', self.use_rbf, True) use_change = BooleanProperty(False) def on_use_change(self, instance, x): self.electrum_config.set_key('use_change', self.use_change, True) use_unconfirmed = BooleanProperty(False) def on_use_unconfirmed(self, instance, x): self.electrum_config.set_key('confirmed_only', not self.use_unconfirmed, True) def set_URI(self, uri): self.switch_to('send') self.send_screen.set_URI(uri) def on_new_intent(self, intent): if intent.getScheme() != 'xrjv1': return uri = intent.getDataString() self.set_URI(uri) def on_language(self, instance, language): Logger.info('language: {}'.format(language)) _.switch_lang(language) def update_history(self, *dt): if self.history_screen: self.history_screen.update() def on_quotes(self, d): Logger.info("on_quotes") self._trigger_update_history() def on_history(self, d): Logger.info("on_history") self._trigger_update_history() def _get_bu(self): return self.electrum_config.get('base_unit', 'mXRJ') def _set_bu(self, value): assert value in base_units.keys() self.electrum_config.set_key('base_unit', value, True) self._trigger_update_status() self._trigger_update_history() base_unit = AliasProperty(_get_bu, _set_bu) status = StringProperty('') fiat_unit = StringProperty('') def on_fiat_unit(self, a, b): self._trigger_update_history() def decimal_point(self): return base_units[self.base_unit] def btc_to_fiat(self, amount_str): if not amount_str: return '' 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 App.__init__(self) #, **kwargs) title = _('Electrum App') self.electrum_config = config = kwargs.get('config', None) self.language = config.get('language', 'en') self.network = network = kwargs.get('network', None) if self.network: self.num_blocks = self.network.get_local_height() self.num_nodes = len(self.network.get_interfaces()) host, port, protocol, proxy_config, auto_connect = self.network.get_parameters( ) self.server_host = host self.server_port = port self.auto_connect = auto_connect self.proxy_config = proxy_config if proxy_config else {} self.plugins = kwargs.get('plugins', []) self.gui_object = kwargs.get('gui_object', None) self.daemon = self.gui_object.daemon self.fx = self.daemon.fx self.use_rbf = config.get('use_rbf', True) self.use_change = config.get('use_change', True) self.use_unconfirmed = not config.get('confirmed_only', False) # create triggers so as to minimize updation a max of 2 times a sec self._trigger_update_wallet = Clock.create_trigger( self.update_wallet, .5) self._trigger_update_status = Clock.create_trigger( self.update_status, .5) self._trigger_update_history = Clock.create_trigger( self.update_history, .5) self._trigger_update_interfaces = Clock.create_trigger( self.update_interfaces, .5) # cached dialogs self._settings_dialog = None self._password_dialog = None self.fee_status = self.electrum_config.get_fee_status() def wallet_name(self): return os.path.basename( self.wallet.storage.path) if self.wallet else ' ' def on_pr(self, pr): if pr.verify(self.wallet.contacts): key = self.wallet.invoices.add(pr) if self.invoices_screen: self.invoices_screen.update() status = self.wallet.invoices.get_status(key) if status == PR_PAID: self.show_error("invoice already paid") self.send_screen.do_clear() else: if pr.has_expired(): self.show_error(_('Payment request has expired')) else: self.switch_to('send') self.send_screen.set_request(pr) else: self.show_error("invoice error:" + pr.error) self.send_screen.do_clear() def on_qr(self, data): from electrum.bitcoin import base_decode, is_address data = data.strip() if is_address(data): self.set_URI(data) return if data.startswith('xrjv1:'): self.set_URI(data) return # try to decode transaction from electrum.transaction import Transaction from electrum.util import bh2u try: text = bh2u(base_decode(data, None, base=43)) tx = Transaction(text) tx.deserialize() except: tx = None if tx: self.tx_dialog(tx) return # show error self.show_error("Unable to decode QR data") def update_tab(self, name): s = getattr(self, name + '_screen', None) if s: s.update() @profiler def update_tabs(self): for tab in ['invoices', 'send', 'history', 'receive', 'address']: self.update_tab(tab) def switch_to(self, name): s = getattr(self, name + '_screen', None) if s is None: s = self.tabs.ids[name + '_screen'] s.load_screen() panel = self.tabs.ids.panel tab = self.tabs.ids[name + '_tab'] panel.switch_to(tab) def show_request(self, addr): self.switch_to('receive') self.receive_screen.screen.address = addr def show_pr_details(self, req, status, is_invoice): from electrum.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('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.util import format_time fund = req.get('fund') isaddr = 'y' popup = Builder.load_file('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): from .uix.dialogs.qr_dialog import QRDialog popup = QRDialog(title, data, show_text) popup.open() def scan_qr(self, on_complete): if platform != 'android': return from jnius import autoclass, cast from android import activity PythonActivity = autoclass('org.kivy.android.PythonActivity') SimpleScannerActivity = autoclass( "org.electrum.qr.SimpleScannerActivity") Intent = autoclass('android.content.Intent') intent = Intent(PythonActivity.mActivity, SimpleScannerActivity) def on_qr_result(requestCode, resultCode, intent): 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) 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('gui/kivy/main.kv') def _pause(self): if platform == 'android': # move activity to back from jnius import autoclass python_act = autoclass('org.kivy.android.PythonActivity') mActivity = python_act.mActivity mActivity.moveTaskToBack(True) def on_start(self): ''' This is the start point of the kivy ui ''' import time Logger.info('Time to on_start: {} <<<<<<<<'.format(time.clock())) win = Window win.bind(size=self.on_size, on_keyboard=self.on_keyboard) win.bind(on_key_down=self.on_key_down) #win.softinput_mode = 'below_target' self.on_size(win, win.size) self.init_ui() self.load_wallet_by_name(self.electrum_config.get_wallet_path()) # init plugins run_hook('init_kivy', self) # fiat currency self.fiat_unit = self.fx.ccy if self.fx.is_enabled() else '' # default tab self.switch_to('history') # bind intent for bitcoin: URI scheme if platform == 'android': from android import activity from jnius import autoclass PythonActivity = autoclass('org.kivy.android.PythonActivity') mactivity = PythonActivity.mActivity self.on_new_intent(mactivity.getIntent()) activity.bind(on_new_intent=self.on_new_intent) # connect callbacks if self.network: interests = [ 'updated', 'status', 'new_transaction', 'verified', 'interfaces' ] self.network.register_callback(self.on_network_event, interests) self.network.register_callback(self.on_fee, ['fee']) self.network.register_callback(self.on_quotes, ['on_quotes']) self.network.register_callback(self.on_history, ['on_history']) # URI passed in config uri = self.electrum_config.get('url') if uri: self.set_URI(uri) def get_wallet_path(self): if self.wallet: return self.wallet.storage.path else: return '' def on_wizard_complete(self, instance, wallet): if wallet: wallet.start_threads(self.daemon.network) self.daemon.add_wallet(wallet) self.load_wallet(wallet) self.on_resume() def load_wallet_by_name(self, path): if not path: return wallet = self.daemon.load_wallet(path, None) if wallet: if wallet != self.wallet: self.stop_wallet() self.load_wallet(wallet) self.on_resume() else: Logger.debug( 'Electrum: Wallet not found. Launching install wizard') storage = WalletStorage(path) wizard = Factory.InstallWizard(self.electrum_config, storage) wizard.bind(on_wizard_complete=self.on_wizard_complete) action = wizard.storage.get_action() wizard.run(action) def on_stop(self): self.stop_wallet() def stop_wallet(self): if self.wallet: self.daemon.stop_wallet(self.wallet.storage.path) self.wallet = None def on_key_down(self, instance, key, keycode, codepoint, modifiers): if 'ctrl' in modifiers: # q=24 w=25 if keycode in (24, 25): self.stop() elif keycode == 27: # r=27 # force update wallet self.update_wallet() elif keycode == 112: # pageup #TODO move to next tab pass elif keycode == 117: # pagedown #TODO move to prev tab pass #TODO: alt+tab_number to activate the particular tab def on_keyboard(self, instance, key, keycode, codepoint, modifiers): if key == 27 and self.is_exit is False: self.is_exit = True self.show_info(_('Press again to exit')) return True # override settings button if key in (319, 282): #f1/settings button on android #self.gui.main_gui.toggle_settings(self) return True def settings_dialog(self): 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() else: popup = Builder.load_file('gui/kivy/uix/ui_screens/' + name + '.kv') popup.open() @profiler def init_ui(self): ''' Initialize The Ux part of electrum. This function performs the basic tasks of setting up the ui. ''' #from weakref import ref self.funds_error = False # setup UX self.screens = {} #setup lazy imports for mainscreen Factory.register('AnimatedPopup', module='electrum_gui.kivy.uix.dialogs') Factory.register('QRCodeWidget', module='electrum_gui.kivy.uix.qrcodewidget') # preload widgets. Remove this if you want to load the widgets on demand #Cache.append('electrum_widgets', 'AnimatedPopup', Factory.AnimatedPopup()) #Cache.append('electrum_widgets', 'QRCodeWidget', Factory.QRCodeWidget()) # load and focus the ui self.root.manager = self.root.ids['manager'] self.history_screen = None self.contacts_screen = None self.send_screen = None self.invoices_screen = None self.receive_screen = None self.requests_screen = None self.address_screen = None self.icon = "icons/electrum.png" self.tabs = self.root.ids['tabs'] def update_interfaces(self, dt): self.num_nodes = len(self.network.get_interfaces()) self.num_chains = len(self.network.get_blockchains()) chain = self.network.blockchain() self.blockchain_checkpoint = chain.get_checkpoint() self.blockchain_name = chain.get_name() if self.network.interface: self.server_host = self.network.interface.host def on_network_event(self, event, *args): Logger.info('network event: ' + event) if event == 'interfaces': self._trigger_update_interfaces() elif event == 'updated': self._trigger_update_wallet() self._trigger_update_status() elif event == 'status': self._trigger_update_status() elif event == 'new_transaction': self._trigger_update_wallet() elif event == 'verified': self._trigger_update_wallet() @profiler def load_wallet(self, wallet): self.wallet = wallet self.update_wallet() # Once GUI has been initialized check if we want to announce something # since the callback has been called before the GUI was initialized if self.receive_screen: self.receive_screen.clear() self.update_tabs() run_hook('load_wallet', wallet, self) def update_status(self, *dt): self.num_blocks = self.network.get_local_height() if not self.wallet: self.status = _("No Wallet") return if self.network is None or not self.network.is_running(): status = _("Offline") elif self.network.is_connected(): server_height = self.network.get_server_height() server_lag = self.network.get_local_height() - server_height if not self.wallet.up_to_date or server_height == 0: status = _("Synchronizing...") elif server_lag > 1: status = _("Server lagging") else: status = '' else: status = _("Disconnected") self.status = self.wallet.basename() + (' [size=15dp](%s)[/size]' % status if status else '') # balance 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 get_max_amount(self): inputs = self.wallet.get_spendable_coins(None, self.electrum_config) addr = str( self.send_screen.screen.address) or self.wallet.dummy_address() outputs = [(TYPE_ADDRESS, addr, '!')] tx = self.wallet.make_unsigned_transaction(inputs, outputs, self.electrum_config) amount = tx.output_value() return format_satoshis_plain(amount, self.decimal_point()) def format_amount(self, x, is_diff=False, whitespaces=False): return format_satoshis(x, is_diff, 0, self.decimal_point(), whitespaces) def format_amount_and_units(self, x): return format_satoshis_plain( x, self.decimal_point()) + ' ' + self.base_unit #@profiler def update_wallet(self, *dt): self._trigger_update_status() if self.wallet and (self.wallet.up_to_date or not self.network or not self.network.is_connected()): self.update_tabs() def notify(self, message): try: global notification, os if not notification: from plyer import notification icon = (os.path.dirname(os.path.realpath(__file__)) + '/../../' + self.icon) notification.notify('Electrum', message, app_icon=icon, app_name='Electrum') except ImportError: Logger.Error('Notification: needs plyer; `sudo pip install plyer`') def on_pause(self): # pause nfc if self.nfcscanner: self.nfcscanner.nfc_disable() return True def on_resume(self): if self.nfcscanner: self.nfcscanner.nfc_enable() def on_size(self, instance, value): width, height = value self._orientation = 'landscape' if width > height else 'portrait' self._ui_mode = 'tablet' if min(width, height) > inch(3.51) else 'phone' def on_ref_label(self, label, touch): if label.touched: label.touched = False self.qr_dialog(label.name, label.data, True) else: label.touched = True self._clipboard.copy(label.data) Clock.schedule_once(lambda dt: self.show_info( _('Text copied to clipboard.\nTap again to display it as QR code.' ))) def set_send(self, address, amount, label, message): self.send_payment(address, amount=amount, label=label, message=message) def show_error(self, error, width='200dp', pos=None, arrow_pos=None, exit=False, icon='atlas://gui/kivy/theming/light/error', duration=0, modal=False): ''' Show a error Message Bubble. ''' self.show_info_bubble(text=error, icon=icon, width=width, pos=pos or Window.center, arrow_pos=arrow_pos, exit=exit, duration=duration, modal=modal) def show_info(self, error, width='200dp', pos=None, arrow_pos=None, exit=False, duration=0, modal=False): ''' Show a Info Message Bubble. ''' self.show_error(error, icon='atlas://gui/kivy/theming/light/important', duration=duration, modal=modal, exit=exit, pos=pos, arrow_pos=arrow_pos) def show_info_bubble(self, text=_('Hello World'), pos=None, duration=0, arrow_pos='bottom_mid', width=None, icon='', modal=False, exit=False): '''Method to show a Information Bubble .. parameters:: text: Message to be displayed pos: position for the bubble duration: duration the bubble remains on screen. 0 = click to hide width: width of the Bubble arrow_pos: arrow position for the bubble ''' info_bubble = self.info_bubble if not info_bubble: info_bubble = self.info_bubble = Factory.InfoBubble() win = Window if info_bubble.parent: win.remove_widget(info_bubble if not info_bubble.modal else info_bubble._modal_view) if not arrow_pos: info_bubble.show_arrow = False else: info_bubble.show_arrow = True info_bubble.arrow_pos = arrow_pos img = info_bubble.ids.img if text == 'texture': # icon holds a texture not a source image # display the texture in full screen text = '' img.texture = icon info_bubble.fs = True info_bubble.show_arrow = False img.allow_stretch = True info_bubble.dim_background = True info_bubble.background_image = 'atlas://gui/kivy/theming/light/card' else: info_bubble.fs = False info_bubble.icon = icon #if img.texture and img._coreimage: # img.reload() img.allow_stretch = False info_bubble.dim_background = False info_bubble.background_image = 'atlas://data/images/defaulttheme/bubble' info_bubble.message = text if not pos: pos = (win.center[0], win.center[1] - (info_bubble.height / 2)) info_bubble.show(pos, duration, width, modal=modal, exit=exit) def tx_dialog(self, tx): from .uix.dialogs.tx_dialog import TxDialog d = TxDialog(self, tx) d.open() def sign_tx(self, *args): threading.Thread(target=self._sign_tx, args=args).start() def _sign_tx(self, tx, password, on_success, on_failure): try: self.wallet.sign_transaction(tx, password) except InvalidPassword: Clock.schedule_once(lambda dt: on_failure(_("Invalid PIN"))) return Clock.schedule_once(lambda dt: on_success(tx)) def _broadcast_thread(self, tx, on_complete): ok, txid = self.network.broadcast(tx) Clock.schedule_once(lambda dt: on_complete(ok, txid)) def broadcast(self, tx, pr=None): def on_complete(ok, msg): if ok: self.show_info(_('Payment sent.')) if self.send_screen: self.send_screen.do_clear() if pr: self.wallet.invoices.set_paid(pr, tx.txid()) self.wallet.invoices.save() self.update_tab('invoices') else: self.show_error(msg) if self.network and self.network.is_connected(): self.show_info(_('Sending')) threading.Thread(target=self._broadcast_thread, args=(tx, on_complete)).start() else: self.show_info( _('Cannot broadcast transaction') + ':\n' + _('Not connected')) def description_dialog(self, screen): from .uix.dialogs.label_dialog import LabelDialog text = screen.message def callback(text): screen.message = text d = LabelDialog(_('Enter description'), text, callback) d.open() 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(): self.password_dialog(msg, f, args) else: f(*(args + (None, ))) def delete_wallet(self): from .uix.dialogs.question import Question basename = os.path.basename(self.wallet.storage.path) d = Question( _('Delete wallet?') + '\n' + basename, self._delete_wallet) d.open() def _delete_wallet(self, b): if b: basename = os.path.basename(self.wallet.storage.path) self.protected( _("Enter your PIN code to confirm deletion of {}").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:" + basename) d = os.listdir(dirname) name = 'default_wallet' new_path = os.path.join(dirname, name) self.load_wallet_by_name(new_path) def show_seed(self, label): self.protected(_("Enter your PIN code in order to decrypt your seed"), self._show_seed, (label, )) def _show_seed(self, label, password): if self.wallet.has_password() and password is None: return keystore = self.wallet.keystore try: seed = keystore.get_seed(password) passphrase = keystore.get_passphrase(password) except: self.show_error("Invalid PIN") return label.text = _('Seed') + ':\n' + seed if passphrase: label.text += '\n\n' + _('Passphrase') + ': ' + passphrase def change_password(self, cb): if self.wallet.has_password(): self.protected( _("Changing PIN code.") + '\n' + _("Enter your current PIN:"), self._change_password, (cb, )) else: self._change_password(cb, None) def _change_password(self, cb, old_password): if self.wallet.has_password(): if old_password is None: return try: self.wallet.check_password(old_password) except InvalidPassword: self.show_error("Invalid PIN") return self.password_dialog(_('Enter new PIN'), self._change_password2, ( cb, old_password, )) def _change_password2(self, cb, old_password, new_password): self.password_dialog(_('Confirm new PIN'), self._change_password3, (cb, old_password, new_password)) def _change_password3(self, cb, old_password, new_password, confirmed_password): if new_password == confirmed_password: self.wallet.update_password(old_password, new_password) cb() else: self.show_error("PIN numbers do not match") def password_dialog(self, msg, f, args): from .uix.dialogs.password_dialog import PasswordDialog def callback(pw): Clock.schedule_once(lambda x: f(*(args + (pw, ))), 0.1) if self._password_dialog is None: self._password_dialog = PasswordDialog() self._password_dialog.init(msg, callback) 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 CircularNumberPicker(CircularLayout): """A circular number picker based on CircularLayout. A selector will help you pick a number. You can also set :attr:`multiples_of` to make it show only some numbers and use the space in between for the other numbers. """ min = NumericProperty(0) """The first value of the range. :attr:`min` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. """ max = NumericProperty(0) """The last value of the range. Note that it behaves like xrange, so the actual last displayed value will be :attr:`max` - 1. :attr:`max` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. """ range = ReferenceListProperty(min, max) """Packs :attr:`min` and :attr:`max` into a list for convenience. See their documentation for further information. :attr:`range` is a :class:`~kivy.properties.ReferenceListProperty`. """ multiples_of = NumericProperty(1) """Only show numbers that are multiples of this number. The other numbers will be selectable, but won't have their own label. :attr:`multiples_of` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. """ selector_color = ListProperty([.337, .439, .490]) """Color of the number selector. RGB. :attr:`selector_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.337, .439, .490] (material green). """ color = ListProperty([1, 1, 1]) """Color of the number labels and of the center dot. RGB. :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1] (white). """ selector_alpha = BoundedNumericProperty(.3, min=0, max=1) """Alpha value for the transparent parts of the selector. :attr:`selector_alpha` is a :class:`~kivy.properties.BoundedNumericProperty` and defaults to 0.3 (min=0, max=1). """ selected = NumericProperty(None) """Currently selected number. :attr:`selected` is a :class:`~kivy.properties.NumericProperty` and defaults to :attr:`min`. """ number_size_factor = NumericProperty(.5) """Font size scale factor fot the :class:`Number`s. :attr:`number_size_factor` is a :class:`~kivy.properties.NumericProperty` and defaults to 0.5. """ number_format_string = StringProperty("{}") """String that will be formatted with the selected number as the first argument. Can be anything supported by :meth:`str.format` (es. "{:02d}"). :attr:`number_format_string` is a :class:`~kivy.properties.StringProperty` and defaults to "{}". """ scale = NumericProperty(1) """Canvas scale factor. Used in :class:`CircularTimePicker` transitions. :attr:`scale` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. """ _selection_circle = ObjectProperty(None) _selection_line = ObjectProperty(None) _selection_dot = ObjectProperty(None) _selection_dot_color = ObjectProperty(None) _selection_color = ObjectProperty(None) _center_dot = ObjectProperty(None) _center_color = ObjectProperty(None) def _get_items(self): return self.max - self.min items = AliasProperty(_get_items, None) def _get_shown_items(self): c = 0 for i in xrange(*self.range): if i % self.multiples_of == 0: c += 1 return c shown_items = AliasProperty(_get_shown_items, None) def __init__(self, **kw): self._trigger_genitems = Clock.create_trigger(self._genitems, -1) self.bind(min=self._trigger_genitems, max=self._trigger_genitems, multiples_of=self._trigger_genitems) super(CircularNumberPicker, self).__init__(**kw) self.selected = self.min self.bind(selected=self.on_selected, pos=self.on_selected, size=self.on_selected) cx = self.center_x + self.padding[0] - self.padding[2] cy = self.center_y + self.padding[3] - self.padding[1] sx, sy = self.pos_for_number(self.selected) epos = [i - (self.delta_radii * self.number_size_factor) for i in (sx, sy)] esize = [self.delta_radii * self.number_size_factor * 2]*2 dsize = [i * .3 for i in esize] dpos = [i + esize[0] / 2. - dsize[0] / 2. for i in epos] csize = [i * .05 for i in esize] cpos = [i - csize[0] / 2. for i in (cx, cy)] dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1 color = list(self.selector_color) with self.canvas: self._selection_color = Color(*(color + [self.selector_alpha])) self._selection_circle = Ellipse(pos=epos, size=esize) self._selection_line = Line(points=[cx, cy, sx, sy]) self._selection_dot_color = Color(*(color + [dot_alpha])) self._selection_dot = Ellipse(pos=dpos, size=dsize) self._center_color = Color(*self.color) self._center_dot = Ellipse(pos=cpos, size=csize) self.bind(selector_color=lambda ign, c: setattr(self._selection_color, "rgba", c + [self.selector_alpha])) self.bind(selector_color=lambda ign, c: setattr(self._selection_dot_color, "rgb", c)) self.bind(color=lambda ign, c: setattr(self._center_color, "rgb", c)) Clock.schedule_once(self._genitems) Clock.schedule_once(self.on_selected) # Just to make sure pos/size are set def _genitems(self, *a): self.clear_widgets() for i in xrange(*self.range): if i % self.multiples_of != 0: continue n = Number(text=self.number_format_string.format(i), size_factor=self.number_size_factor, color=self.color) self.bind(color=n.setter("color")) self.add_widget(n) def on_touch_down(self, touch): if not self.collide_point(*touch.pos): return touch.grab(self) self.selected = self.number_at_pos(*touch.pos) def on_touch_move(self, touch): if touch.grab_current is not self: return super(CircularNumberPicker, self).on_touch_move(touch) self.selected = self.number_at_pos(*touch.pos) def on_touch_up(self, touch): if touch.grab_current is not self: return super(CircularNumberPicker, self).on_touch_up(touch) touch.ungrab(self) def on_selected(self, *a): cx = self.center_x + self.padding[0] - self.padding[2] cy = self.center_y + self.padding[3] - self.padding[1] sx, sy = self.pos_for_number(self.selected) epos = [i - (self.delta_radii * self.number_size_factor) for i in (sx, sy)] esize = [self.delta_radii * self.number_size_factor * 2]*2 dsize = [i * .3 for i in esize] dpos = [i + esize[0] / 2. - dsize[0] / 2. for i in epos] csize = [i * .05 for i in esize] cpos = [i - csize[0] / 2. for i in (cx, cy)] dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1 if self._selection_circle: self._selection_circle.pos = epos self._selection_circle.size = esize if self._selection_line: self._selection_line.points = [cx, cy, sx, sy] if self._selection_dot: self._selection_dot.pos = dpos self._selection_dot.size = dsize if self._selection_dot_color: self._selection_dot_color.a = dot_alpha if self._center_dot: self._center_dot.pos = cpos self._center_dot.size = csize # print self.selected def pos_for_number(self, n): """Returns the center x, y coordinates for a given number. """ if self.items == 0: return 0, 0 radius = min(self.width-self.padding[0]-self.padding[2], self.height-self.padding[1]-self.padding[3]) / 2. middle_r = radius * sum(self.radius_hint) / 2. cx = self.center_x + self.padding[0] - self.padding[2] cy = self.center_y + self.padding[3] - self.padding[1] sign = +1. angle_offset = radians(self.start_angle) if self.direction == 'cw': angle_offset = 2 * pi - angle_offset sign = -1. quota = 2*pi / self.items mult_quota = 2*pi / self.shown_items angle = angle_offset + n * sign * quota if self.items == self.shown_items: angle += quota / 2 else: angle -= mult_quota / 2 # kived: looking it up, yes. x = cos(angle) * radius + centerx; y = sin(angle) * radius + centery x = cos(angle) * middle_r + cx y = sin(angle) * middle_r + cy return x, y def number_at_pos(self, x, y): """Returns the number at a given x, y position. The number is found using the widget's center as a starting point for angle calculations. Not thoroughly tested, may yield wrong results. """ if self.items == 0: return self.min cx = self.center_x + self.padding[0] - self.padding[2] cy = self.center_y + self.padding[3] - self.padding[1] lx = x - cx ly = y - cy quota = 2*pi / self.items mult_quota = 2*pi / self.shown_items if lx == 0 and ly > 0: angle = pi/2 elif lx == 0 and ly < 0: angle = 3*pi/2 else: angle = atan(ly/lx) if lx < 0 and ly > 0: angle += pi if lx > 0 and ly < 0: angle += 2*pi if lx < 0 and ly < 0: angle += pi angle += radians(self.start_angle) if self.direction == "cw": angle = 2*pi - angle if mult_quota != quota: angle -= mult_quota / 2 if angle < 0: angle += 2*pi elif angle > 2*pi: angle -= 2*pi return int(angle / quota) + self.min
class FloatLabel(AnchorLayout): text = StringProperty("") position = OptionProperty("bottom", options=["bottom", "top"]) duration = NumericProperty(3) clicable = BooleanProperty(False) bg_color = ObjectProperty((0, 1, .2, 1)) color = ObjectProperty((0, 0, 0, 1)) __show__ = BooleanProperty(False) __contador__ = NumericProperty(0) def __get_pos_widget__(self): if not self.__show__: if self.position == 'bottom': return 0, -self.height else: return 0, self.height __pos_widget__ = AliasProperty(__get_pos_widget__, bind=["position", "size"]) def on_color(self, w, val): if "#" in val: val = "".join(val) self.color = get_color_from_hex(val) else: self.color = val def on_bg_color(self, w, val): if "#" in val: val = "".join(val) self.bg_color = get_color_from_hex(val) else: self.bg_color = val def __init__(self, **kargs): super(FloatLabel, self).__init__(**kargs) def show(self): if not self.__show__: self.__show__ = True y = self.height if self.position == 'bottom' else -self.height ani = Animation(y=self.__label_widget__.y + y, duration=.5) ani.start(self.__label_widget__) Clock.schedule_once(self.hide_label, self.duration + .5) else: self.__contador__ += 1 Clock.schedule_once(self.hide_label, self.duration + .5) def hide_label(self, dt): if self.__contador__ <= 0: self.__contador__ = 0 y = -self.height if self.position == 'bottom' else self.height ani = Animation(y=self.__label_widget__.y + y, duration=.5) ani.start(self.__label_widget__) Clock.schedule_once(self.clear_show, .5) else: self.__contador__ -= 1 def clear_show(self, dt): self.__show__ = False
class DrawerNavigator(MDCard): bg_color = ListProperty([1, 1, 1, 1]) anchor = OptionProperty("left", options=("left", "right")) close_on_click = BooleanProperty(True) state = OptionProperty("close", options=("close", "open")) status = OptionProperty( "closed", options=( "closed", "opening_with_swipe", "opening_with_animation", "opened", "closing_with_swipe", "closing_with_animation", ), ) open_progress = NumericProperty(0.0) swipe_distance = NumericProperty(10) swipe_edge_width = NumericProperty(20) scrim_color = ListProperty([0, 0, 0, 0.5]) def _get_scrim_alpha(self): _scrim_alpha = self._scrim_alpha_transition(self.open_progress) if isinstance(self.parent, NavigationLayout): self.parent._scrim_color.rgba = self.scrim_color[:3] + [ self.scrim_color[3] * _scrim_alpha ] return _scrim_alpha _scrim_alpha = AliasProperty( _get_scrim_alpha, None, bind=("_scrim_alpha_transition", "open_progress", "scrim_color"), ) """ Multiplier for alpha channel of :attr:`scrim_color`. For internal usage only. """ scrim_alpha_transition = StringProperty("linear") """ The name of the animation transition type to use for changing :attr:`scrim_alpha`. :attr:`scrim_alpha_transition` is a :class:`~kivy.properties.StringProperty` and defaults to `'linear'`. """ def _get_scrim_alpha_transition(self): return getattr(AnimationTransition, self.scrim_alpha_transition) _scrim_alpha_transition = AliasProperty( _get_scrim_alpha_transition, None, bind=("scrim_alpha_transition", ), cache=True, ) opening_transition = StringProperty("out_cubic") opening_time = NumericProperty(0.2) closing_transition = StringProperty("out_sine") closing_time = NumericProperty(0.2) def __init__(self, **kwargs): super().__init__(**kwargs) self.bind( open_progress=self.update_status, status=self.update_status, state=self.update_status, ) Window.bind(on_keyboard=self._handle_keyboard) def set_state(self, new_state="toggle", animation=True): """Change state of the side panel. New_state can be one of `"toggle"`, `"open"` or `"close"`. """ if new_state == "toggle": new_state = "close" if self.state == "open" else "open" if new_state == "open": Animation.cancel_all(self, "open_progress") self.status = "opening_with_animation" if animation: Animation( open_progress=1.0, d=self.opening_time * (1 - self.open_progress), t=self.opening_transition, ).start(self) else: self.open_progress = 1 else: # "close" Animation.cancel_all(self, "open_progress") self.status = "closing_with_animation" if animation: Animation( open_progress=0.0, d=self.closing_time * self.open_progress, t=self.closing_transition, ).start(self) else: self.open_progress = 0 def toggle_nav_drawer(self): Logger.warning("KivyMD: The 'toggle_nav_drawer' method is deprecated, " "use 'set_state' instead.") self.set_state("toggle", animation=True) def update_status(self, *_): status = self.status if status == "closed": self.state = "close" elif status == "opened": self.state = "open" elif self.open_progress == 1 and status == "opening_with_animation": self.status = "opened" self.state = "open" elif self.open_progress == 0 and status == "closing_with_animation": self.status = "closed" self.state = "close" elif status in ( "opening_with_swipe", "opening_with_animation", "closing_with_swipe", "closing_with_animation", ): pass def get_dist_from_side(self, x): if self.anchor == "left": return 0 if x < 0 else x return 0 if x > Window.width else Window.width - x def on_touch_down(self, touch): if self.status == "closed": return False elif self.status == "opened": for child in self.children[:]: if child.dispatch("on_touch_down", touch): return True return True def on_touch_move(self, touch): if self.status == "closed": if (self.get_dist_from_side(touch.ox) <= self.swipe_edge_width and abs(touch.x - touch.ox) > self.swipe_distance): self.status = "opening_with_swipe" elif self.status == "opened": self.status = "closing_with_swipe" if self.status in ("opening_with_swipe", "closing_with_swipe"): self.open_progress = max( min(self.open_progress + touch.dx / self.width, 1), 0) return True return super().on_touch_move(touch) def on_touch_up(self, touch): if self.status == "opening_with_swipe": if self.open_progress > 0.5: self.set_state("open", animation=True) else: self.set_state("close", animation=True) elif self.status == "closing_with_swipe": if self.open_progress < 0.5: self.set_state("close", animation=True) else: self.set_state("open", animation=True) elif self.status == "opened": if (self.close_on_click and self.get_dist_from_side(touch.ox) > self.width): self.set_state("close", animation=True) elif self.status == "closed": return False return True def _handle_keyboard(self, window, key, *largs): if key == 27 and self.status == "opened" and self.close_on_click: self.set_state("close") return True