class SpineAsset(Widget): filename = StringProperty() animations = ListProperty([]) debug = BooleanProperty(False) pause = BooleanProperty(False) valign = OptionProperty("middle", options=("bottom", "middle")) def __init__(self, **kwargs): self.canvas = Canvas() super(SpineAsset, self).__init__(**kwargs) Clock.schedule_interval(self.update, 0) def on_filename(self, *args): self.load_spine_asset(self.filename) self.canvas.clear() self.animations = [ animation.name for animation in self.skeletonData.animations ] self.animate(self.animations[0]) def on_debug(self, *args): self.skeleton.debug = self.debug def on_pause(self, *args): self.skeleton.pause = self.pause def load_spine_asset(self, basefn): atlas = Atlas(filename="{}.atlas".format(basefn)) loader = AtlasAttachmentLoader(atlas) skeletonJson = SkeletonJson(loader) self.skeletonData = skeletonJson.readSkeletonDataFile( "{}.json".format(basefn)) self.skeleton = Skeleton(skeletonData=self.skeletonData) self.skeleton.debug = False self.animation = None def animate(self, name): self.animation = self.skeletonData.findAnimation(name) skeleton = self.skeleton skeleton.setToBindPose() skeleton.flipX = False skeleton.flipY = False skeleton.updateWorldTransform() def update(self, dt): # print("update") if not self.pause: if self.animation: self.animation.apply(skeleton=self.skeleton, time=Clock.get_time() / 2., loop=True) self.skeleton.setPosisiton(skeletonX, skeletonY) self.skeleton.updateWorldTransform() self.skeleton.draw(self.canvas)
class SpineAsset(Widget): filename = StringProperty() animations = ListProperty([]) debug = BooleanProperty(False) valign = OptionProperty("middle", options=("bottom", "middle")) def __init__(self, **kwargs): self.canvas = Canvas() super(SpineAsset, self).__init__(**kwargs) Clock.schedule_interval(self.update, 0) def on_filename(self, *args): self.load_spine_asset(self.filename) self.canvas.clear() self.animations = [animation.name for animation in self.skeletonData.animations] self.animate(self.animations[0]) def on_debug(self, *args): self.skeleton.debug = self.debug def load_spine_asset(self, basefn): atlas = Atlas(filename="{}.atlas".format(basefn)) loader = AtlasAttachmentLoader(atlas) skeletonJson = SkeletonJson(loader) self.skeletonData = skeletonJson.readSkeletonDataFile( "{}.json".format(basefn)) self.skeleton = Skeleton(skeletonData=self.skeletonData) self.skeleton.debug = False self.animation = None def animate(self, name): self.animation = self.skeletonData.findAnimation(name) skeleton = self.skeleton skeleton.setToBindPose() skeleton.flipX = False skeleton.flipY = False skeleton.updateWorldTransform() def update(self, dt): if self.animation: self.animation.apply(skeleton=self.skeleton, time=Clock.get_time() / 2., loop=True) self.skeleton.updateWorldTransform() self.skeleton.draw(self.canvas)
class MapView(Widget): """MapView is the widget that control the map displaying, navigation, and layers management. """ lon = NumericProperty() """Longitude at the center of the widget """ lat = NumericProperty() """Latitude at the center of the widget """ zoom = NumericProperty(0) """Zoom of the widget. Must be between :meth:`MapSource.get_min_zoom` and :meth:`MapSource.get_max_zoom`. Default to 0. """ map_source = ObjectProperty(MapSource()) """Provider of the map, default to a empty :class:`MapSource`. """ double_tap_zoom = BooleanProperty(False) """If True, this will activate the double-tap to zoom. """ pause_on_action = BooleanProperty(True) """Pause any map loading / tiles loading when an action is done. This allow better performance on mobile, but can be safely deactivated on desktop. """ snap_to_zoom = BooleanProperty(True) """When the user initiate a zoom, it will snap to the closest zoom for better graphics. The map can be blur if the map is scaled between 2 zoom. Default to True, even if it doesn't fully working yet. """ animation_duration = NumericProperty(100) """Duration to animate Tiles alpha from 0 to 1 when it's ready to show. Default to 100 as 100ms. Use 0 to deactivate. """ delta_x = NumericProperty(0) delta_y = NumericProperty(0) background_color = ListProperty([181 / 255., 208 / 255., 208 / 255., 1]) _zoom = NumericProperty(0) _pause = BooleanProperty(False) _scale = 1. __events__ = ["on_map_relocated"] # Public API @property def viewport_pos(self): vx, vy = self._scatter.to_local(self.x, self.y) return vx - self.delta_x, vy - self.delta_y @property def scale(self): if self._invalid_scale: self._invalid_scale = False self._scale = self._scatter.scale return self._scale def get_bbox(self, margin=0): """Returns the bounding box from the bottom/left (lat1, lon1) to top/right (lat2, lon2). """ x1, y1 = self.to_local(0 - margin, 0 - margin) x2, y2 = self.to_local((self.width + margin), (self.height + margin)) c1 = self.get_latlon_at(x1, y1) c2 = self.get_latlon_at(x2, y2) return Bbox((c1.lat, c1.lon, c2.lat, c2.lon)) bbox = AliasProperty(get_bbox, None, bind=["lat", "lon", "_zoom"]) def unload(self): """Unload the view and all the layers. It also cancel all the remaining downloads. """ self.remove_all_tiles() def get_window_xy_from(self, lat, lon, zoom): """Returns the x/y position in the widget absolute coordinates from a lat/lon""" scale = self.scale vx, vy = self.viewport_pos ms = self.map_source x = ms.get_x(zoom, lon) - vx y = ms.get_y(zoom, lat) - vy x *= scale y *= scale return x, y def center_on(self, *args): """Center the map on the coordinate :class:`Coordinate`, or a (lat, lon) """ map_source = self.map_source zoom = self._zoom if len(args) == 1 and isinstance(args[0], Coordinate): coord = args[0] lat = coord.lat lon = coord.lon elif len(args) == 2: lat, lon = args else: raise Exception("Invalid argument for center_on") lon = clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) lat = clamp(lat, MIN_LATITUDE, MAX_LATITUDE) scale = self._scatter.scale x = map_source.get_x(zoom, lon) - self.center_x / scale y = map_source.get_y(zoom, lat) - self.center_y / scale self.delta_x = -x self.delta_y = -y self.lon = lon self.lat = lat self._scatter.pos = 0, 0 self.trigger_update(True) def set_zoom_at(self, zoom, x, y, scale=None): """Sets the zoom level, leaving the (x, y) at the exact same point in the view. """ zoom = clamp(zoom, self.map_source.get_min_zoom(), self.map_source.get_max_zoom()) if int(zoom) == int(self._zoom): if scale is None: return elif scale == self.scale: return scale = scale or 1. # first, rescale the scatter scatter = self._scatter scale = clamp(scale, scatter.scale_min, scatter.scale_max) rescale = scale * 1.0 / scatter.scale scatter.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=scatter.to_local(x, y)) # adjust position if the zoom changed c1 = self.map_source.get_col_count(self._zoom) c2 = self.map_source.get_col_count(zoom) if c1 != c2: f = float(c2) / float(c1) self.delta_x = scatter.x + self.delta_x * f self.delta_y = scatter.y + self.delta_y * f # back to 0 every time scatter.apply_transform(Matrix().translate( -scatter.x, -scatter.y, 0 ), post_multiply=True) # avoid triggering zoom changes. self._zoom = zoom self.zoom = self._zoom def on_zoom(self, instance, zoom): if zoom == self._zoom: return x = self.map_source.get_x(zoom, self.lon) - self.delta_x y = self.map_source.get_y(zoom, self.lat) - self.delta_y self.set_zoom_at(zoom, x, y) self.center_on(self.lat, self.lon) def get_latlon_at(self, x, y, zoom=None): """Return the current :class:`Coordinate` within the (x, y) widget coordinate. """ if zoom is None: zoom = self._zoom vx, vy = self.viewport_pos scale = self._scale return Coordinate( lat=self.map_source.get_lat(zoom, y / scale + vy), lon=self.map_source.get_lon(zoom, x / scale + vx)) def add_marker(self, marker, layer=None): """Add a marker into the layer. If layer is None, it will be added in the default marker layer. If there is no default marker layer, a new one will be automatically created """ if layer is None: if not self._default_marker_layer: layer = MarkerMapLayer() self.add_layer(layer) else: layer = self._default_marker_layer layer.add_widget(marker) layer.set_marker_position(self, marker) def remove_marker(self, marker): """Remove a marker from its layer """ marker.detach() def add_layer(self, layer, mode="window"): """Add a new layer to update at the same time the base tile layer. mode can be either "scatter" or "window". If "scatter", it means the layer will be within the scatter transformation. It's perfect if you want to display path / shape, but not for text. If "window", it will have no transformation. You need to position the widget yourself: think as Z-sprite / billboard. Defaults to "window". """ assert(mode in ("scatter", "window")) if self._default_marker_layer is None and \ isinstance(layer, MarkerMapLayer): self._default_marker_layer = layer self._layers.append(layer) c = self.canvas if mode == "scatter": self.canvas = self.canvas_layers else: self.canvas = self.canvas_layers_out layer.canvas_parent = self.canvas super(MapView, self).add_widget(layer) self.canvas = c def remove_layer(self, layer): """Remove the layer """ c = self.canvas self._layers.remove(layer) self.canvas = layer.canvas_parent super(MapView, self).remove_widget(layer) self.canvas = c def sync_to(self, other): """Reflect the lat/lon/zoom of the other MapView to the current one. """ if self._zoom != other._zoom: self.set_zoom_at(other._zoom, *self.center) self.center_on(other.get_latlon_at(*self.center)) # Private API def __init__(self, cache_dir='cache', **kwargs): from kivy.base import EventLoop EventLoop.ensure_window() CACHE['directory'] = cache_dir self._invalid_scale = True self._tiles = [] self._tiles_bg = [] self._tilemap = {} self._layers = [] self._default_marker_layer = None self._need_redraw_all = False self._transform_lock = False self.trigger_update(True) self.canvas = Canvas() self._scatter = MapViewScatter() self.add_widget(self._scatter) with self._scatter.canvas: self.canvas_map = Canvas() self.canvas_layers = Canvas() with self.canvas: self.canvas_layers_out = Canvas() self._scale_target_anim = False self._scale_target = 1. self._touch_count = 0 Clock.schedule_interval(self._animate_color, 1 / 60.) self.lat = kwargs.get("lat", self.lat) self.lon = kwargs.get("lon", self.lon) super(MapView, self).__init__(**kwargs) def _animate_color(self, dt): # fast path d = self.animation_duration if d == 0: for tile in self._tiles: if tile.state == "need-animation": tile.g_color.a = 1. tile.state = "animated" for tile in self._tiles_bg: if tile.state == "need-animation": tile.g_color.a = 1. tile.state = "animated" else: d = d / 1000. for tile in self._tiles: if tile.state != "need-animation": continue tile.g_color.a += dt / d if tile.g_color.a >= 1: tile.state = "animated" for tile in self._tiles_bg: if tile.state != "need-animation": continue tile.g_color.a += dt / d if tile.g_color.a >= 1: tile.state = "animated" def add_widget(self, widget): if isinstance(widget, MapMarker): self.add_marker(widget) elif isinstance(widget, MapLayer): self.add_layer(widget) else: super(MapView, self).add_widget(widget) def remove_widget(self, widget): if isinstance(widget, MapMarker): self.remove_marker(widget) elif isinstance(widget, MapLayer): self.remove_layer(widget) else: super(MapView, self).remove_widget(widget) def on_map_relocated(self, zoom, coord): pass def animated_diff_scale_at(self, d, x, y): self._scale_target_time = 1. self._scale_target_pos = x, y if self._scale_target_anim == False: self._scale_target_anim = True self._scale_target = d else: self._scale_target += d Clock.unschedule(self._animate_scale) Clock.schedule_interval(self._animate_scale, 1 / 60.) def _animate_scale(self, dt): diff = self._scale_target / 3. if abs(diff) < 0.01: diff = self._scale_target self._scale_target = 0 else: self._scale_target -= diff self._scale_target_time -= dt self.diff_scale_at(diff, *self._scale_target_pos) ret = self._scale_target != 0 if not ret: self._pause = False return ret def diff_scale_at(self, d, x, y): scatter = self._scatter scale = scatter.scale * (2 ** d) self.scale_at(scale, x, y) def scale_at(self, scale, x, y): scatter = self._scatter scale = clamp(scale, scatter.scale_min, scatter.scale_max) rescale = scale * 1.0 / scatter.scale scatter.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=scatter.to_local(x, y)) def on_touch_down(self, touch): if not self.collide_point(*touch.pos): return if self.pause_on_action: self._pause = True if "button" in touch.profile and touch.button in ("scrolldown", "scrollup"): d = 1 if touch.button == "scrollup" else -1 self.animated_diff_scale_at(d, *touch.pos) return True elif touch.is_double_tap and self.double_tap_zoom: self.animated_diff_scale_at(1, *touch.pos) return True touch.grab(self) self._touch_count += 1 if self._touch_count == 1: self._touch_zoom = (self.zoom, self._scale) return super(MapView, self).on_touch_down(touch) def on_touch_up(self, touch): if touch.grab_current == self: touch.ungrab(self) self._touch_count -= 1 if self._touch_count == 0: # animate to the closest zoom zoom, scale = self._touch_zoom cur_zoom = self.zoom cur_scale = self._scale if cur_zoom < zoom or cur_scale < scale: self.animated_diff_scale_at(1. - cur_scale, *touch.pos) elif cur_zoom > zoom or cur_scale > scale: self.animated_diff_scale_at(2. - cur_scale, *touch.pos) self._pause = False return True return super(MapView, self).on_touch_up(touch) def on_transform(self, *args): self._invalid_scale = True if self._transform_lock: return self._transform_lock = True # recalculate viewport map_source = self.map_source zoom = self._zoom scatter = self._scatter scale = scatter.scale if scale >= 2.: zoom += 1 scale /= 2. elif scale < 1: zoom -= 1 scale *= 2. zoom = clamp(zoom, map_source.min_zoom, map_source.max_zoom) if zoom != self._zoom: self.set_zoom_at(zoom, scatter.x, scatter.y, scale=scale) self.trigger_update(True) else: if zoom == map_source.min_zoom and scatter.scale < 1.: scatter.scale = 1. self.trigger_update(True) else: self.trigger_update(False) if map_source.bounds: self._apply_bounds() self._transform_lock = False self._scale = self._scatter.scale def _apply_bounds(self): # if the map_source have any constraints, apply them here. map_source = self.map_source zoom = self._zoom min_lon, min_lat, max_lon, max_lat = map_source.bounds xmin = map_source.get_x(zoom, min_lon) xmax = map_source.get_x(zoom, max_lon) ymin = map_source.get_y(zoom, min_lat) ymax = map_source.get_y(zoom, max_lat) dx = self.delta_x dy = self.delta_y oxmin, oymin = self._scatter.to_local(self.x, self.y) oxmax, oymax = self._scatter.to_local(self.right, self.top) s = self._scale cxmin = (oxmin - dx) if cxmin < xmin: self._scatter.x += (cxmin - xmin) * s cymin = (oymin - dy) if cymin < ymin: self._scatter.y += (cymin - ymin) * s cxmax = (oxmax - dx) if cxmax > xmax: self._scatter.x -= (xmax - cxmax) * s cymax = (oymax - dy) if cymax > ymax: self._scatter.y -= (ymax - cymax) * s def on__pause(self, instance, value): if not value: self.trigger_update(True) def trigger_update(self, full): self._need_redraw_full = full or self._need_redraw_full Clock.unschedule(self.do_update) Clock.schedule_once(self.do_update, -1) def do_update(self, dt): zoom = self._zoom scale = self._scale self.lon = self.map_source.get_lon(zoom, (self.center_x - self._scatter.x) / scale - self.delta_x) self.lat = self.map_source.get_lat(zoom, (self.center_y - self._scatter.y) / scale - self.delta_y) self.dispatch("on_map_relocated", zoom, Coordinate(self.lon, self.lat)) for layer in self._layers: layer.reposition() if self._need_redraw_full: self._need_redraw_full = False self.move_tiles_to_background() self.load_visible_tiles() else: self.load_visible_tiles() def bbox_for_zoom(self, vx, vy, w, h, zoom): # return a tile-bbox for the zoom map_source = self.map_source size = map_source.dp_tile_size scale = self._scale max_x_end = map_source.get_col_count(zoom) max_y_end = map_source.get_row_count(zoom) x_count = int(ceil(w / scale / float(size))) + 1 y_count = int(ceil(h / scale / float(size))) + 1 tile_x_first = int(clamp(vx / float(size), 0, max_x_end)) tile_y_first = int(clamp(vy / float(size), 0, max_y_end)) tile_x_last = tile_x_first + x_count tile_y_last = tile_y_first + y_count tile_x_last = int(clamp(tile_x_last, tile_x_first, max_x_end)) tile_y_last = int(clamp(tile_y_last, tile_y_first, max_y_end)) x_count = tile_x_last - tile_x_first y_count = tile_y_last - tile_y_first return (tile_x_first, tile_y_first, tile_x_last, tile_y_last, x_count, y_count) def load_visible_tiles(self): map_source = self.map_source vx, vy = self.viewport_pos zoom = self._zoom dirs = [0, 1, 0, -1, 0] bbox_for_zoom = self.bbox_for_zoom size = map_source.dp_tile_size tile_x_first, tile_y_first, tile_x_last, tile_y_last, \ x_count, y_count = bbox_for_zoom(vx, vy, self.width, self.height, zoom) #print "Range {},{} to {},{}".format( # tile_x_first, tile_y_first, # tile_x_last, tile_y_last) # Adjust tiles behind us for tile in self._tiles_bg[:]: tile_x = tile.tile_x tile_y = tile.tile_y f = 2 ** (zoom - tile.zoom) w = self.width / f h = self.height / f btile_x_first, btile_y_first, btile_x_last, btile_y_last, \ _, _ = bbox_for_zoom(vx / f, vy / f, w, h, tile.zoom) if tile_x < btile_x_first or tile_x >= btile_x_last or \ tile_y < btile_y_first or tile_y >= btile_y_last: tile.state = "done" self._tiles_bg.remove(tile) self.canvas_map.before.remove(tile.g_color) self.canvas_map.before.remove(tile) continue tsize = size * f tile.size = tsize, tsize tile.pos = ( tile_x * tsize + self.delta_x, tile_y * tsize + self.delta_y) # Get rid of old tiles first for tile in self._tiles[:]: tile_x = tile.tile_x tile_y = tile.tile_y if tile_x < tile_x_first or tile_x >= tile_x_last or \ tile_y < tile_y_first or tile_y >= tile_y_last: tile.state = "done" self.tile_map_set(tile_x, tile_y, False) self._tiles.remove(tile) self.canvas_map.remove(tile) self.canvas_map.remove(tile.g_color) else: tile.size = (size, size) tile.pos = (tile_x * size + self.delta_x, tile_y * size + self.delta_y) # Load new tiles if needed x = tile_x_first + x_count // 2 - 1 y = tile_y_first + y_count // 2 - 1 arm_max = max(x_count, y_count) + 2 arm_size = 1 turn = 0 while arm_size < arm_max: for i in range(arm_size): if not self.tile_in_tile_map(x, y) and \ y >= tile_y_first and y < tile_y_last and \ x >= tile_x_first and x < tile_x_last: self.load_tile(x, y, size, zoom) x += dirs[turn % 4 + 1] y += dirs[turn % 4] if turn % 2 == 1: arm_size += 1 turn += 1 def load_tile(self, x, y, size, zoom): if self.tile_in_tile_map(x, y) or zoom != self._zoom: return self.load_tile_for_source(self.map_source, 1., size, x, y, zoom) # XXX do overlay support self.tile_map_set(x, y, True) def load_tile_for_source(self, map_source, opacity, size, x, y, zoom): tile = Tile(size=(size, size)) tile.g_color = Color(1, 1, 1, 0) tile.tile_x = x tile.tile_y = y tile.zoom = zoom tile.pos = (x * size + self.delta_x, y * size + self.delta_y) tile.map_source = map_source tile.state = "loading" if not self._pause: map_source.fill_tile(tile) self.canvas_map.add(tile.g_color) self.canvas_map.add(tile) self._tiles.append(tile) def move_tiles_to_background(self): # remove all the tiles of the main map to the background map # retain only the one who are on the current zoom level # for all the tile in the background, stop the download if not yet started. zoom = self._zoom tiles = self._tiles btiles = self._tiles_bg canvas_map = self.canvas_map tile_size = self.map_source.tile_size # move all tiles to background while tiles: tile = tiles.pop() if tile.state == "loading": tile.state == "done" continue btiles.append(tile) # clear the canvas canvas_map.clear() canvas_map.before.clear() self._tilemap = {} # unsure if it's really needed, i personnally didn't get issues right now #btiles.sort(key=lambda z: -z.zoom) # add all the btiles into the back canvas. # except for the tiles that are owned by the current zoom level for tile in btiles[:]: if tile.zoom == zoom: btiles.remove(tile) tiles.append(tile) tile.size = tile_size, tile_size canvas_map.add(tile.g_color) canvas_map.add(tile) self.tile_map_set(tile.tile_x, tile.tile_y, True) continue canvas_map.before.add(tile.g_color) canvas_map.before.add(tile) def remove_all_tiles(self): # clear the map of all tiles. self.canvas_map.clear() self.canvas_map.before.clear() for tile in self._tiles: tile.state = "done" del self._tiles[:] del self._tiles_bg[:] self._tilemap = {} def tile_map_set(self, tile_x, tile_y, value): key = tile_y * self.map_source.get_col_count(self._zoom) + tile_x if value: self._tilemap[key] = value else: self._tilemap.pop(key, None) def tile_in_tile_map(self, tile_x, tile_y): key = tile_y * self.map_source.get_col_count(self._zoom) + tile_x return key in self._tilemap def on_size(self, instance, size): for layer in self._layers: layer.size = size self.center_on(self.lat, self.lon) self.trigger_update(True) def on_pos(self, instance, pos): self.center_on(self.lat, self.lon) self.trigger_update(True) def on_map_source(self, instance, source): if isinstance(source, string_types): self.map_source = MapSource.from_provider(source) elif isinstance(source, (tuple, list)): cache_key, min_zoom, max_zoom, url, attribution, options = source self.map_source = MapSource(url=url, cache_key=cache_key, min_zoom=min_zoom, max_zoom=max_zoom, attribution=attribution, **options) elif isinstance(source, MapSource): self.map_source = source else: raise Exception("Invalid map source provider") self.zoom = clamp(self.zoom, self.map_source.min_zoom, self.map_source.max_zoom) self.remove_all_tiles() self.trigger_update(True)
class MapView(Widget): """MapView is the widget that control the map displaying, navigation, and layers management. """ lon = NumericProperty() """Longitude at the center of the widget """ lat = NumericProperty() """Latitude at the center of the widget """ zoom = NumericProperty(0) """Zoom of the widget. Must be between :meth:`MapSource.get_min_zoom` and :meth:`MapSource.get_max_zoom`. Default to 0. """ map_source = ObjectProperty(MapSource()) """Provider of the map, default to a empty :class:`MapSource`. """ double_tap_zoom = BooleanProperty(False) """If True, this will activate the double-tap to zoom. """ pause_on_action = BooleanProperty(True) """Pause any map loading / tiles loading when an action is done. This allow better performance on mobile, but can be safely deactivated on desktop. """ snap_to_zoom = BooleanProperty(True) """When the user initiate a zoom, it will snap to the closest zoom for better graphics. The map can be blur if the map is scaled between 2 zoom. Default to True, even if it doesn't fully working yet. """ animation_duration = NumericProperty(100) """Duration to animate Tiles alpha from 0 to 1 when it's ready to show. Default to 100 as 100ms. Use 0 to deactivate. """ delta_x = NumericProperty(0) delta_y = NumericProperty(0) background_color = ListProperty([181 / 255., 208 / 255., 208 / 255., 1]) cache_dir = StringProperty(CACHE_DIR) _zoom = NumericProperty(0) _pause = BooleanProperty(False) _scale = 1. _disabled_count = 0 __events__ = ["on_map_relocated"] # Public API @property def viewport_pos(self): vx, vy = self._scatter.to_local(self.x, self.y) return vx - self.delta_x, vy - self.delta_y @property def scale(self): if self._invalid_scale: self._invalid_scale = False self._scale = self._scatter.scale return self._scale def get_bbox(self, margin=0): """Returns the bounding box from the bottom/left (lat1, lon1) to top/right (lat2, lon2). """ x1, y1 = self.to_local(0 - margin, 0 - margin) x2, y2 = self.to_local((self.width + margin), (self.height + margin)) c1 = self.get_latlon_at(x1, y1) c2 = self.get_latlon_at(x2, y2) return Bbox((c1.lat, c1.lon, c2.lat, c2.lon)) bbox = AliasProperty(get_bbox, None, bind=["lat", "lon", "_zoom"]) def unload(self): """Unload the view and all the layers. It also cancel all the remaining downloads. """ self.remove_all_tiles() def get_window_xy_from(self, lat, lon, zoom): """Returns the x/y position in the widget absolute coordinates from a lat/lon""" scale = self.scale vx, vy = self.viewport_pos ms = self.map_source x = ms.get_x(zoom, lon) - vx y = ms.get_y(zoom, lat) - vy x *= scale y *= scale x = x + self.pos[0] y = y + self.pos[1] return x, y def center_on(self, *args): """Center the map on the coordinate :class:`Coordinate`, or a (lat, lon) """ map_source = self.map_source zoom = self._zoom if len(args) == 1 and isinstance(args[0], Coordinate): coord = args[0] lat = coord.lat lon = coord.lon elif len(args) == 2: lat, lon = args else: raise Exception("Invalid argument for center_on") lon = clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) lat = clamp(lat, MIN_LATITUDE, MAX_LATITUDE) scale = self._scatter.scale x = map_source.get_x(zoom, lon) - self.center_x / scale y = map_source.get_y(zoom, lat) - self.center_y / scale self.delta_x = -x self.delta_y = -y self.lon = lon self.lat = lat self._scatter.pos = 0, 0 self.trigger_update(True) def set_zoom_at(self, zoom, x, y, scale=None): """Sets the zoom level, leaving the (x, y) at the exact same point in the view. """ zoom = clamp(zoom, self.map_source.get_min_zoom(), self.map_source.get_max_zoom()) if int(zoom) == int(self._zoom): if scale is None: return elif scale == self.scale: return scale = scale or 1. # first, rescale the scatter scatter = self._scatter scale = clamp(scale, scatter.scale_min, scatter.scale_max) rescale = scale * 1.0 / scatter.scale scatter.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=scatter.to_local(x, y)) # adjust position if the zoom changed c1 = self.map_source.get_col_count(self._zoom) c2 = self.map_source.get_col_count(zoom) if c1 != c2: f = float(c2) / float(c1) self.delta_x = scatter.x + self.delta_x * f self.delta_y = scatter.y + self.delta_y * f # back to 0 every time scatter.apply_transform(Matrix().translate(-scatter.x, -scatter.y, 0), post_multiply=True) # avoid triggering zoom changes. self._zoom = zoom self.zoom = self._zoom def on_zoom(self, instance, zoom): if zoom == self._zoom: return x = self.map_source.get_x(zoom, self.lon) - self.delta_x y = self.map_source.get_y(zoom, self.lat) - self.delta_y self.set_zoom_at(zoom, x, y) self.center_on(self.lat, self.lon) def get_latlon_at(self, x, y, zoom=None): """Return the current :class:`Coordinate` within the (x, y) widget coordinate. """ if zoom is None: zoom = self._zoom vx, vy = self.viewport_pos scale = self._scale return Coordinate(lat=self.map_source.get_lat(zoom, y / scale + vy), lon=self.map_source.get_lon(zoom, x / scale + vx)) def add_marker(self, marker, layer=None): """Add a marker into the layer. If layer is None, it will be added in the default marker layer. If there is no default marker layer, a new one will be automatically created """ if layer is None: if not self._default_marker_layer: layer = MarkerMapLayer() self.add_layer(layer) else: layer = self._default_marker_layer layer.add_widget(marker) layer.set_marker_position(self, marker) def remove_marker(self, marker): """Remove a marker from its layer """ marker.detach() def add_layer(self, layer, mode="window"): """Add a new layer to update at the same time the base tile layer. mode can be either "scatter" or "window". If "scatter", it means the layer will be within the scatter transformation. It's perfect if you want to display path / shape, but not for text. If "window", it will have no transformation. You need to position the widget yourself: think as Z-sprite / billboard. Defaults to "window". """ assert (mode in ("scatter", "window")) if self._default_marker_layer is None and \ isinstance(layer, MarkerMapLayer): self._default_marker_layer = layer self._layers.append(layer) c = self.canvas if mode == "scatter": self.canvas = self.canvas_layers else: self.canvas = self.canvas_layers_out layer.canvas_parent = self.canvas super(MapView, self).add_widget(layer) self.canvas = c def remove_layer(self, layer): """Remove the layer """ c = self.canvas self._layers.remove(layer) self.canvas = layer.canvas_parent super(MapView, self).remove_widget(layer) self.canvas = c def sync_to(self, other): """Reflect the lat/lon/zoom of the other MapView to the current one. """ if self._zoom != other._zoom: self.set_zoom_at(other._zoom, *self.center) self.center_on(other.get_latlon_at(*self.center)) # Private API def __init__(self, **kwargs): from kivy.base import EventLoop EventLoop.ensure_window() self._invalid_scale = True self._tiles = [] self._tiles_bg = [] self._tilemap = {} self._layers = [] self._default_marker_layer = None self._need_redraw_all = False self._transform_lock = False self.trigger_update(True) self.canvas = Canvas() self._scatter = MapViewScatter() self.add_widget(self._scatter) with self._scatter.canvas: self.canvas_map = Canvas() self.canvas_layers = Canvas() with self.canvas: self.canvas_layers_out = Canvas() self._scale_target_anim = False self._scale_target = 1. self._touch_count = 0 self.map_source.cache_dir = self.cache_dir Clock.schedule_interval(self._animate_color, 1 / 60.) self.lat = kwargs.get("lat", self.lat) self.lon = kwargs.get("lon", self.lon) super(MapView, self).__init__(**kwargs) def _animate_color(self, dt): # fast path d = self.animation_duration if d == 0: for tile in self._tiles: if tile.state == "need-animation": tile.g_color.a = 1. tile.state = "animated" for tile in self._tiles_bg: if tile.state == "need-animation": tile.g_color.a = 1. tile.state = "animated" else: d = d / 1000. for tile in self._tiles: if tile.state != "need-animation": continue tile.g_color.a += dt / d if tile.g_color.a >= 1: tile.state = "animated" for tile in self._tiles_bg: if tile.state != "need-animation": continue tile.g_color.a += dt / d if tile.g_color.a >= 1: tile.state = "animated" def add_widget(self, widget): if isinstance(widget, MapMarker): self.add_marker(widget) elif isinstance(widget, MapLayer): self.add_layer(widget) else: super(MapView, self).add_widget(widget) def remove_widget(self, widget): if isinstance(widget, MapMarker): self.remove_marker(widget) elif isinstance(widget, MapLayer): self.remove_layer(widget) else: super(MapView, self).remove_widget(widget) def on_map_relocated(self, zoom, coord): pass def animated_diff_scale_at(self, d, x, y): self._scale_target_time = 1. self._scale_target_pos = x, y if self._scale_target_anim is False: self._scale_target_anim = True self._scale_target = d else: self._scale_target += d Clock.unschedule(self._animate_scale) Clock.schedule_interval(self._animate_scale, 1 / 60.) def _animate_scale(self, dt): diff = self._scale_target / 3. if abs(diff) < 0.01: diff = self._scale_target self._scale_target = 0 else: self._scale_target -= diff self._scale_target_time -= dt self.diff_scale_at(diff, *self._scale_target_pos) ret = self._scale_target != 0 if not ret: self._pause = False return ret def diff_scale_at(self, d, x, y): scatter = self._scatter scale = scatter.scale * (2**d) self.scale_at(scale, x, y) def scale_at(self, scale, x, y): scatter = self._scatter scale = clamp(scale, scatter.scale_min, scatter.scale_max) rescale = scale * 1.0 / scatter.scale scatter.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=scatter.to_local(x, y)) def on_touch_down(self, touch): if not self.collide_point(*touch.pos): return if self.pause_on_action: self._pause = True if "button" in touch.profile and touch.button in ("scrolldown", "scrollup"): d = 1 if touch.button == "scrollup" else -1 self.animated_diff_scale_at(d, *touch.pos) return True elif touch.is_double_tap and self.double_tap_zoom: self.animated_diff_scale_at(1, *touch.pos) return True touch.grab(self) self._touch_count += 1 if self._touch_count == 1: self._touch_zoom = (self.zoom, self._scale) return super(MapView, self).on_touch_down(touch) def on_touch_up(self, touch): if touch.grab_current == self: touch.ungrab(self) self._touch_count -= 1 if self._touch_count == 0: # animate to the closest zoom zoom, scale = self._touch_zoom cur_zoom = self.zoom cur_scale = self._scale if cur_zoom < zoom or cur_scale < scale: self.animated_diff_scale_at(1. - cur_scale, *touch.pos) elif cur_zoom > zoom or cur_scale > scale: self.animated_diff_scale_at(2. - cur_scale, *touch.pos) self._pause = False return True return super(MapView, self).on_touch_up(touch) def on_transform(self, *args): self._invalid_scale = True if self._transform_lock: return self._transform_lock = True # recalculate viewport map_source = self.map_source zoom = self._zoom scatter = self._scatter scale = scatter.scale if scale >= 2.01: zoom += 1 scale /= 2. elif scale < 0.99: zoom -= 1 scale *= 2. zoom = clamp(zoom, map_source.min_zoom, map_source.max_zoom) if zoom != self._zoom: self.set_zoom_at(zoom, scatter.x, scatter.y, scale=scale) self.trigger_update(True) else: if zoom == map_source.min_zoom and scatter.scale < 1.: scatter.scale = 1. self.trigger_update(True) else: self.trigger_update(False) if map_source.bounds: self._apply_bounds() self._transform_lock = False self._scale = self._scatter.scale def _apply_bounds(self): # if the map_source have any constraints, apply them here. map_source = self.map_source zoom = self._zoom min_lon, min_lat, max_lon, max_lat = map_source.bounds xmin = map_source.get_x(zoom, min_lon) xmax = map_source.get_x(zoom, max_lon) ymin = map_source.get_y(zoom, min_lat) ymax = map_source.get_y(zoom, max_lat) dx = self.delta_x dy = self.delta_y oxmin, oymin = self._scatter.to_local(self.x, self.y) oxmax, oymax = self._scatter.to_local(self.right, self.top) s = self._scale cxmin = (oxmin - dx) if cxmin < xmin: self._scatter.x += (cxmin - xmin) * s cymin = (oymin - dy) if cymin < ymin: self._scatter.y += (cymin - ymin) * s cxmax = (oxmax - dx) if cxmax > xmax: self._scatter.x -= (xmax - cxmax) * s cymax = (oymax - dy) if cymax > ymax: self._scatter.y -= (ymax - cymax) * s def on__pause(self, instance, value): if not value: self.trigger_update(True) def trigger_update(self, full): self._need_redraw_full = full or self._need_redraw_full Clock.unschedule(self.do_update) Clock.schedule_once(self.do_update, -1) def do_update(self, dt): zoom = self._zoom scale = self._scale self.lon = self.map_source.get_lon( zoom, (self.center_x - self._scatter.x) / scale - self.delta_x) self.lat = self.map_source.get_lat( zoom, (self.center_y - self._scatter.y) / scale - self.delta_y) self.dispatch("on_map_relocated", zoom, Coordinate(self.lon, self.lat)) for layer in self._layers: layer.reposition() if self._need_redraw_full: self._need_redraw_full = False self.move_tiles_to_background() self.load_visible_tiles() else: self.load_visible_tiles() def bbox_for_zoom(self, vx, vy, w, h, zoom): # return a tile-bbox for the zoom map_source = self.map_source size = map_source.dp_tile_size scale = self._scale max_x_end = map_source.get_col_count(zoom) max_y_end = map_source.get_row_count(zoom) x_count = int(ceil(w / scale / float(size))) + 1 y_count = int(ceil(h / scale / float(size))) + 1 tile_x_first = int(clamp(vx / float(size), 0, max_x_end)) tile_y_first = int(clamp(vy / float(size), 0, max_y_end)) tile_x_last = tile_x_first + x_count tile_y_last = tile_y_first + y_count tile_x_last = int(clamp(tile_x_last, tile_x_first, max_x_end)) tile_y_last = int(clamp(tile_y_last, tile_y_first, max_y_end)) x_count = tile_x_last - tile_x_first y_count = tile_y_last - tile_y_first return (tile_x_first, tile_y_first, tile_x_last, tile_y_last, x_count, y_count) def load_visible_tiles(self): map_source = self.map_source vx, vy = self.viewport_pos zoom = self._zoom dirs = [0, 1, 0, -1, 0] bbox_for_zoom = self.bbox_for_zoom size = map_source.dp_tile_size tile_x_first, tile_y_first, tile_x_last, tile_y_last, \ x_count, y_count = bbox_for_zoom(vx, vy, self.width, self.height, zoom) # print "Range {},{} to {},{}".format( # tile_x_first, tile_y_first, # tile_x_last, tile_y_last) # Adjust tiles behind us for tile in self._tiles_bg[:]: tile_x = tile.tile_x tile_y = tile.tile_y f = 2**(zoom - tile.zoom) w = self.width / f h = self.height / f btile_x_first, btile_y_first, btile_x_last, btile_y_last, \ _, _ = bbox_for_zoom(vx / f, vy / f, w, h, tile.zoom) if tile_x < btile_x_first or tile_x >= btile_x_last or \ tile_y < btile_y_first or tile_y >= btile_y_last: tile.state = "done" self._tiles_bg.remove(tile) self.canvas_map.before.remove(tile.g_color) self.canvas_map.before.remove(tile) continue tsize = size * f tile.size = tsize, tsize tile.pos = (tile_x * tsize + self.delta_x, tile_y * tsize + self.delta_y) # Get rid of old tiles first for tile in self._tiles[:]: tile_x = tile.tile_x tile_y = tile.tile_y if tile_x < tile_x_first or tile_x >= tile_x_last or \ tile_y < tile_y_first or tile_y >= tile_y_last: tile.state = "done" self.tile_map_set(tile_x, tile_y, False) self._tiles.remove(tile) self.canvas_map.remove(tile) self.canvas_map.remove(tile.g_color) else: tile.size = (size, size) tile.pos = (tile_x * size + self.delta_x, tile_y * size + self.delta_y) # Load new tiles if needed x = tile_x_first + x_count // 2 - 1 y = tile_y_first + y_count // 2 - 1 arm_max = max(x_count, y_count) + 2 arm_size = 1 turn = 0 while arm_size < arm_max: for i in range(arm_size): if not self.tile_in_tile_map(x, y) and \ y >= tile_y_first and y < tile_y_last and \ x >= tile_x_first and x < tile_x_last: self.load_tile(x, y, size, zoom) x += dirs[turn % 4 + 1] y += dirs[turn % 4] if turn % 2 == 1: arm_size += 1 turn += 1 def load_tile(self, x, y, size, zoom): if self.tile_in_tile_map(x, y) or zoom != self._zoom: return self.load_tile_for_source(self.map_source, 1., size, x, y, zoom) # XXX do overlay support self.tile_map_set(x, y, True) def load_tile_for_source(self, map_source, opacity, size, x, y, zoom): tile = Tile(size=(size, size), cache_dir=self.cache_dir) tile.g_color = Color(1, 1, 1, 0) tile.tile_x = x tile.tile_y = y tile.zoom = zoom tile.pos = (x * size + self.delta_x, y * size + self.delta_y) tile.map_source = map_source tile.state = "loading" if not self._pause: map_source.fill_tile(tile) self.canvas_map.add(tile.g_color) self.canvas_map.add(tile) self._tiles.append(tile) def move_tiles_to_background(self): # remove all the tiles of the main map to the background map # retain only the one who are on the current zoom level # for all the tile in the background, stop the download if not yet started. zoom = self._zoom tiles = self._tiles btiles = self._tiles_bg canvas_map = self.canvas_map tile_size = self.map_source.tile_size # move all tiles to background while tiles: tile = tiles.pop() if tile.state == "loading": tile.state = "done" continue btiles.append(tile) # clear the canvas canvas_map.clear() canvas_map.before.clear() self._tilemap = {} # unsure if it's really needed, i personnally didn't get issues right now # btiles.sort(key=lambda z: -z.zoom) # add all the btiles into the back canvas. # except for the tiles that are owned by the current zoom level for tile in btiles[:]: if tile.zoom == zoom: btiles.remove(tile) tiles.append(tile) tile.size = tile_size, tile_size canvas_map.add(tile.g_color) canvas_map.add(tile) self.tile_map_set(tile.tile_x, tile.tile_y, True) continue canvas_map.before.add(tile.g_color) canvas_map.before.add(tile) def remove_all_tiles(self): # clear the map of all tiles. self.canvas_map.clear() self.canvas_map.before.clear() for tile in self._tiles: tile.state = "done" del self._tiles[:] del self._tiles_bg[:] self._tilemap = {} def tile_map_set(self, tile_x, tile_y, value): key = tile_y * self.map_source.get_col_count(self._zoom) + tile_x if value: self._tilemap[key] = value else: self._tilemap.pop(key, None) def tile_in_tile_map(self, tile_x, tile_y): key = tile_y * self.map_source.get_col_count(self._zoom) + tile_x return key in self._tilemap def on_size(self, instance, size): for layer in self._layers: layer.size = size self.center_on(self.lat, self.lon) self.trigger_update(True) def on_pos(self, instance, pos): self.center_on(self.lat, self.lon) self.trigger_update(True) def on_map_source(self, instance, source): if isinstance(source, string_types): self.map_source = MapSource.from_provider(source) elif isinstance(source, (tuple, list)): cache_key, min_zoom, max_zoom, url, attribution, options = source self.map_source = MapSource(url=url, cache_key=cache_key, min_zoom=min_zoom, max_zoom=max_zoom, attribution=attribution, cache_dir=self.cache_dir, **options) elif isinstance(source, MapSource): self.map_source = source else: raise Exception("Invalid map source provider") self.zoom = clamp(self.zoom, self.map_source.min_zoom, self.map_source.max_zoom) self.remove_all_tiles() self.trigger_update(True)
class VKeyboard(Scatter): ''' VKeyboard is an onscreen keyboard with multitouch support. Its layout is entirely customizable and you can switch between available layouts using a button in the bottom right of the widget. :Events: `on_key_down`: keycode, internal, modifiers Fired when the keyboard received a key down event (key press). `on_key_up`: keycode, internal, modifiers Fired when the keyboard received a key up event (key release). ''' target = ObjectProperty(None, allownone=True) '''Target widget associated with the VKeyboard. If set, it will be used to send keyboard events. If the VKeyboard mode is "free", it will also be used to set the initial position. :attr:`target` is an :class:`~kivy.properties.ObjectProperty` instance and defaults to None. ''' callback = ObjectProperty(None, allownone=True) '''Callback can be set to a function that will be called if the VKeyboard is closed by the user. :attr:`target` is an :class:`~kivy.properties.ObjectProperty` instance and defaults to None. ''' layout = StringProperty(None) '''Layout to use for the VKeyboard. By default, it will be the layout set in the configuration, according to the `keyboard_layout` in `[kivy]` section. .. versionchanged:: 1.8.0 If layout is a .json filename, it will loaded and added to the available_layouts. :attr:`layout` is a :class:`~kivy.properties.StringProperty` and defaults to None. ''' layout_path = StringProperty(default_layout_path) '''Path from which layouts are read. :attr:`layout` is a :class:`~kivy.properties.StringProperty` and defaults to :file:`<kivy_data_dir>/keyboards/` ''' available_layouts = DictProperty({}) '''Dictionary of all available layouts. Keys are the layout ID, and the value is the JSON (translated into a Python object). :attr:`available_layouts` is a :class:`~kivy.properties.DictProperty` and defaults to {}. ''' docked = BooleanProperty(False) '''Indicate whether the VKeyboard is docked on the screen or not. If you change it, you must manually call :meth:`setup_mode` otherwise it will have no impact. If the VKeyboard is created by the Window, the docked mode will be automatically set by the configuration, using the `keyboard_mode` token in `[kivy]` section. :attr:`docked` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' margin_hint = ListProperty([.05, .06, .05, .06]) '''Margin hint, used as spacing between keyboard background and keys content. The margin is composed of four values, between 0 and 1:: margin_hint = [top, right, bottom, left] The margin hints will be multiplied by width and height, according to their position. :attr:`margin_hint` is a :class:`~kivy.properties.ListProperty` and defaults to [.05, .06, .05, .06] ''' key_margin = ListProperty([2, 2, 2, 2]) '''Key margin, used to create space between keys. The margin is composed of four values, in pixels:: key_margin = [top, right, bottom, left] :attr:`key_margin` is a :class:`~kivy.properties.ListProperty` and defaults to [2, 2, 2, 2] ''' font_size = NumericProperty(20.) '''font_size, specifies the size of the text on the virtual keyboard keys. It should be kept within limits to ensure the text does not extend beyond the bounds of the key or become too small to read. .. versionadded:: 1.10.0 :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and defaults to 20. ''' background_color = ColorProperty([1, 1, 1, 1]) '''Background color, in the format (r, g, b, a). If a background is set, the color will be combined with the background texture. :attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and defaults to [1, 1, 1, 1]. .. versionchanged:: 2.0.0 Changed from :class:`~kivy.properties.ListProperty` to :class:`~kivy.properties.ColorProperty`. ''' background = StringProperty( 'atlas://data/images/defaulttheme/vkeyboard_background') '''Filename of the background image. :attr:`background` is a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://data/images/defaulttheme/vkeyboard_background`. ''' background_disabled = StringProperty( 'atlas://data/images/defaulttheme/vkeyboard_disabled_background') '''Filename of the background image when the vkeyboard is disabled. .. versionadded:: 1.8.0 :attr:`background_disabled` is a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://data/images/defaulttheme/vkeyboard__disabled_background`. ''' key_background_color = ColorProperty([1, 1, 1, 1]) '''Key background color, in the format (r, g, b, a). If a key background is set, the color will be combined with the key background texture. :attr:`key_background_color` is a :class:`~kivy.properties.ColorProperty` and defaults to [1, 1, 1, 1]. .. versionchanged:: 2.0.0 Changed from :class:`~kivy.properties.ListProperty` to :class:`~kivy.properties.ColorProperty`. ''' key_background_normal = StringProperty( 'atlas://data/images/defaulttheme/vkeyboard_key_normal') '''Filename of the key background image for use when no touches are active on the widget. :attr:`key_background_normal` is a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://data/images/defaulttheme/vkeyboard_key_normal`. ''' key_disabled_background_normal = StringProperty( 'atlas://data/images/defaulttheme/vkeyboard_key_normal') '''Filename of the key background image for use when no touches are active on the widget and vkeyboard is disabled. .. versionadded:: 1.8.0 :attr:`key_disabled_background_normal` is a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://data/images/defaulttheme/vkeyboard_disabled_key_normal`. ''' key_background_down = StringProperty( 'atlas://data/images/defaulttheme/vkeyboard_key_down') '''Filename of the key background image for use when a touch is active on the widget. :attr:`key_background_down` is a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://data/images/defaulttheme/vkeyboard_key_down`. ''' background_border = ListProperty([16, 16, 16, 16]) '''Background image border. Used for controlling the :attr:`~kivy.graphics.vertex_instructions.BorderImage.border` property of the background. :attr:`background_border` is a :class:`~kivy.properties.ListProperty` and defaults to [16, 16, 16, 16] ''' key_border = ListProperty([8, 8, 8, 8]) '''Key image border. Used for controlling the :attr:`~kivy.graphics.vertex_instructions.BorderImage.border` property of the key. :attr:`key_border` is a :class:`~kivy.properties.ListProperty` and defaults to [16, 16, 16, 16] ''' # XXX internal variables layout_mode = OptionProperty('normal', options=('normal', 'shift', 'special')) layout_geometry = DictProperty({}) have_capslock = BooleanProperty(False) have_shift = BooleanProperty(False) have_special = BooleanProperty(False) active_keys = DictProperty({}) font_name = StringProperty('data/fonts/DejaVuSans.ttf') repeat_touch = ObjectProperty(allownone=True) _start_repeat_key_ev = None _repeat_key_ev = None __events__ = ('on_key_down', 'on_key_up', 'on_textinput') def __init__(self, **kwargs): # XXX move to style.kv if 'size_hint' not in kwargs: if 'size_hint_x' not in kwargs: self.size_hint_x = None if 'size_hint_y' not in kwargs: self.size_hint_y = None if 'size' not in kwargs: if 'width' not in kwargs: self.width = 700 if 'height' not in kwargs: self.height = 200 if 'scale_min' not in kwargs: self.scale_min = .4 if 'scale_max' not in kwargs: self.scale_max = 1.6 if 'docked' not in kwargs: self.docked = False layout_mode = self._trigger_update_layout_mode = Clock.create_trigger( self._update_layout_mode) layouts = self._trigger_load_layouts = Clock.create_trigger( self._load_layouts) layout = self._trigger_load_layout = Clock.create_trigger( self._load_layout) fbind = self.fbind fbind('docked', self.setup_mode) fbind('have_shift', layout_mode) fbind('have_capslock', layout_mode) fbind('have_special', layout_mode) fbind('layout_path', layouts) fbind('layout', layout) super(VKeyboard, self).__init__(**kwargs) # load all the layouts found in the layout_path directory self._load_layouts() # ensure we have default layouts available_layouts = self.available_layouts if not available_layouts: Logger.critical('VKeyboard: unable to load default layouts') # load the default layout from configuration if self.layout is None: self.layout = Config.get('kivy', 'keyboard_layout') else: # ensure the current layout is found on the available layout self._trigger_load_layout() # update layout mode (shift or normal) self._trigger_update_layout_mode() # create a top layer to draw active keys on with self.canvas: self.background_key_layer = Canvas() self.active_keys_layer = Canvas() def on_disabled(self, instance, value): self.refresh_keys() def _update_layout_mode(self, *l): # update mode according to capslock and shift key mode = self.have_capslock != self.have_shift mode = 'shift' if mode else 'normal' if self.have_special: mode = "special" if mode != self.layout_mode: self.layout_mode = mode self.refresh(False) def _load_layout(self, *largs): # ensure new layouts are loaded first if self._trigger_load_layouts.is_triggered: self._load_layouts() self._trigger_load_layouts.cancel() value = self.layout available_layouts = self.available_layouts # it's a filename, try to load it directly if self.layout[-5:] == '.json': if value not in available_layouts: fn = resource_find(self.layout) self._load_layout_fn(fn, self.layout) if not available_layouts: return if value not in available_layouts and value != 'qwerty': Logger.error( 'Vkeyboard: <%s> keyboard layout mentioned in ' 'conf file was not found, fallback on qwerty' % value) self.layout = 'qwerty' self.refresh(True) def _load_layouts(self, *largs): # first load available layouts from json files # XXX fix to be able to reload layout when path is changing value = self.layout_path for fn in listdir(value): self._load_layout_fn(join(value, fn), basename(splitext(fn)[0])) def _load_layout_fn(self, fn, name): available_layouts = self.available_layouts if fn[-5:] != '.json': return with open(fn, 'r', encoding='utf-8') as fd: json_content = fd.read() layout = loads(json_content) available_layouts[name] = layout def setup_mode(self, *largs): '''Call this method when you want to readjust the keyboard according to options: :attr:`docked` or not, with attached :attr:`target` or not: * If :attr:`docked` is True, it will call :meth:`setup_mode_dock` * If :attr:`docked` is False, it will call :meth:`setup_mode_free` Feel free to overload these methods to create new positioning behavior. ''' if self.docked: self.setup_mode_dock() else: self.setup_mode_free() def setup_mode_dock(self, *largs): '''Setup the keyboard in docked mode. Dock mode will reset the rotation, disable translation, rotation and scale. Scale and position will be automatically adjusted to attach the keyboard to the bottom of the screen. .. note:: Don't call this method directly, use :meth:`setup_mode` instead. ''' self.do_translation = False self.do_rotation = False self.do_scale = False self.rotation = 0 win = self.get_parent_window() scale = win.width / float(self.width) self.scale = scale self.pos = 0, 0 win.bind(on_resize=self._update_dock_mode) def _update_dock_mode(self, win, *largs): scale = win.width / float(self.width) self.scale = scale self.pos = 0, 0 def setup_mode_free(self): '''Setup the keyboard in free mode. Free mode is designed to let the user control the position and orientation of the keyboard. The only real usage is for a multiuser environment, but you might found other ways to use it. If a :attr:`target` is set, it will place the vkeyboard under the target. .. note:: Don't call this method directly, use :meth:`setup_mode` instead. ''' self.do_translation = True self.do_rotation = True self.do_scale = True target = self.target if not target: return # NOTE all math will be done in window point of view # determine rotation of the target a = Vector(1, 0) b = Vector(target.to_window(0, 0)) c = Vector(target.to_window(1, 0)) - b self.rotation = -a.angle(c) # determine the position of center/top of the keyboard dpos = Vector(self.to_window(self.width / 2., self.height)) # determine the position of center/bottom of the target cpos = Vector(target.to_window(target.center_x, target.y)) # the goal now is to map both point, calculate the diff between them diff = dpos - cpos # we still have an issue, self.pos represent the bounding box, # not the 0,0 coordinate of the scatter. we need to apply also # the diff between them (inside and outside coordinate matrix). # It's hard to explain, but do a scheme on a paper, write all # the vector i'm calculating, and you'll understand. :) diff2 = Vector(self.x + self.width / 2., self.y + self.height) - \ Vector(self.to_parent(self.width / 2., self.height)) diff -= diff2 # now we have a good "diff", set it as a pos. self.pos = -diff def change_layout(self): # XXX implement popup with all available layouts pass def refresh(self, force=False): '''(internal) Recreate the entire widget and graphics according to the selected layout. ''' self.clear_widgets() if force: self.refresh_keys_hint() self.refresh_keys() self.refresh_active_keys_layer() def refresh_active_keys_layer(self): self.active_keys_layer.clear() active_keys = self.active_keys layout_geometry = self.layout_geometry background = resource_find(self.key_background_down) texture = Image(background, mipmap=True).texture with self.active_keys_layer: Color(*self.key_background_color) for line_nb, index in active_keys.values(): pos, size = layout_geometry['LINE_%d' % line_nb][index] BorderImage(texture=texture, pos=pos, size=size, border=self.key_border) def refresh_keys_hint(self): layout = self.available_layouts[self.layout] layout_cols = layout['cols'] layout_rows = layout['rows'] layout_geometry = self.layout_geometry mtop, mright, mbottom, mleft = self.margin_hint # get relative EFFICIENT surface of the layout without external margins el_hint = 1. - mleft - mright eh_hint = 1. - mtop - mbottom ex_hint = 0 + mleft ey_hint = 0 + mbottom # get relative unit surface uw_hint = (1. / layout_cols) * el_hint uh_hint = (1. / layout_rows) * eh_hint layout_geometry['U_HINT'] = (uw_hint, uh_hint) # calculate individual key RELATIVE surface and pos (without key # margin) current_y_hint = ey_hint + eh_hint for line_nb in range(1, layout_rows + 1): current_y_hint -= uh_hint # get line_name line_name = '%s_%d' % (self.layout_mode, line_nb) line_hint = 'LINE_HINT_%d' % line_nb layout_geometry[line_hint] = [] current_x_hint = ex_hint # go through the list of keys (tuples of 4) for key in layout[line_name]: # calculate relative pos, size layout_geometry[line_hint].append([ (current_x_hint, current_y_hint), (key[3] * uw_hint, uh_hint)]) current_x_hint += key[3] * uw_hint self.layout_geometry = layout_geometry def refresh_keys(self): layout = self.available_layouts[self.layout] layout_rows = layout['rows'] layout_geometry = self.layout_geometry w, h = self.size kmtop, kmright, kmbottom, kmleft = self.key_margin uw_hint, uh_hint = layout_geometry['U_HINT'] for line_nb in range(1, layout_rows + 1): llg = layout_geometry['LINE_%d' % line_nb] = [] llg_append = llg.append for key in layout_geometry['LINE_HINT_%d' % line_nb]: x_hint, y_hint = key[0] w_hint, h_hint = key[1] kx = x_hint * w ky = y_hint * h kw = w_hint * w kh = h_hint * h # now adjust, considering the key margin kx = int(kx + kmleft) ky = int(ky + kmbottom) kw = int(kw - kmleft - kmright) kh = int(kh - kmbottom - kmtop) pos = (kx, ky) size = (kw, kh) llg_append((pos, size)) self.layout_geometry = layout_geometry self.draw_keys() def draw_keys(self): layout = self.available_layouts[self.layout] layout_rows = layout['rows'] layout_geometry = self.layout_geometry layout_mode = self.layout_mode # draw background background = resource_find(self.background_disabled if self.disabled else self.background) texture = Image(background, mipmap=True).texture self.background_key_layer.clear() with self.background_key_layer: Color(*self.background_color) BorderImage(texture=texture, size=self.size, border=self.background_border) # XXX separate drawing the keys and the fonts to avoid # XXX reloading the texture each time # first draw keys without the font key_normal = resource_find(self.key_background_disabled_normal if self.disabled else self.key_background_normal) texture = Image(key_normal, mipmap=True).texture with self.background_key_layer: Color(*self.key_background_color) for line_nb in range(1, layout_rows + 1): for pos, size in layout_geometry['LINE_%d' % line_nb]: BorderImage(texture=texture, pos=pos, size=size, border=self.key_border) # then draw the text for line_nb in range(1, layout_rows + 1): key_nb = 0 for pos, size in layout_geometry['LINE_%d' % line_nb]: # retrieve the relative text text = layout[layout_mode + '_' + str(line_nb)][key_nb][0] z = Label(text=text, font_size=self.font_size, pos=pos, size=size, font_name=self.font_name) self.add_widget(z) key_nb += 1 def on_key_down(self, *largs): pass def on_key_up(self, *largs): pass def on_textinput(self, *largs): pass def get_key_at_pos(self, x, y): w, h = self.size x_hint = x / w # focus on the surface without margins layout_geometry = self.layout_geometry layout = self.available_layouts[self.layout] layout_rows = layout['rows'] mtop, mright, mbottom, mleft = self.margin_hint # get the line of the layout e_height = h - (mbottom + mtop) * h # efficient height in pixels line_height = e_height / layout_rows # line height in px y = y - mbottom * h line_nb = layout_rows - int(y / line_height) if line_nb > layout_rows: line_nb = layout_rows if line_nb < 1: line_nb = 1 # get the key within the line key_index = '' current_key_index = 0 for key in layout_geometry['LINE_HINT_%d' % line_nb]: if x_hint >= key[0][0] and x_hint < key[0][0] + key[1][0]: key_index = current_key_index break else: current_key_index += 1 if key_index == '': return None # get the full character key = layout['%s_%d' % (self.layout_mode, line_nb)][key_index] return [key, (line_nb, key_index)] def collide_margin(self, x, y): '''Do a collision test, and return True if the (x, y) is inside the vkeyboard margin. ''' mtop, mright, mbottom, mleft = self.margin_hint x_hint = x / self.width y_hint = y / self.height if x_hint > mleft and x_hint < 1. - mright \ and y_hint > mbottom and y_hint < 1. - mtop: return False return True def process_key_on(self, touch): if not touch: return x, y = self.to_local(*touch.pos) key = self.get_key_at_pos(x, y) if not key: return key_data = key[0] displayed_char, internal, special_char, size = key_data line_nb, key_index = key[1] # save pressed key on the touch ud = touch.ud[self.uid] = {} ud['key'] = key # for caps lock or shift only: uid = touch.uid if special_char is not None: # Do not repeat special keys if special_char in ('capslock', 'shift', 'layout', 'special'): if self._start_repeat_key_ev is not None: self._start_repeat_key_ev.cancel() self._start_repeat_key_ev = None self.repeat_touch = None if special_char == 'capslock': self.have_capslock = not self.have_capslock uid = -1 elif special_char == 'shift': self.have_shift = True elif special_char == 'special': self.have_special = True elif special_char == 'layout': self.change_layout() # send info to the bus b_keycode = special_char b_modifiers = self._get_modifiers() if self.get_parent_window().__class__.__module__ == \ 'kivy.core.window.window_sdl2' and internal: self.dispatch('on_textinput', internal) else: self.dispatch('on_key_down', b_keycode, internal, b_modifiers) # save key as an active key for drawing self.active_keys[uid] = key[1] self.refresh_active_keys_layer() def process_key_up(self, touch): uid = touch.uid if self.uid not in touch.ud: return # save pressed key on the touch key_data, key = touch.ud[self.uid]['key'] displayed_char, internal, special_char, size = key_data # send info to the bus b_keycode = special_char b_modifiers = self._get_modifiers() self.dispatch('on_key_up', b_keycode, internal, b_modifiers) if special_char == 'capslock': uid = -1 if uid in self.active_keys: self.active_keys.pop(uid, None) if special_char == 'shift': self.have_shift = False elif special_char == 'special': self.have_special = False if special_char == 'capslock' and self.have_capslock: self.active_keys[-1] = key self.refresh_active_keys_layer() def _get_modifiers(self): ret = [] if self.have_shift: ret.append('shift') if self.have_capslock: ret.append('capslock') return ret def _start_repeat_key(self, *kwargs): self._repeat_key_ev = Clock.schedule_interval(self._repeat_key, 0.05) def _repeat_key(self, *kwargs): self.process_key_on(self.repeat_touch) def on_touch_down(self, touch): x, y = touch.pos if not self.collide_point(x, y): return if self.disabled: return True x, y = self.to_local(x, y) if not self.collide_margin(x, y): if self.repeat_touch is None: self._start_repeat_key_ev = Clock.schedule_once( self._start_repeat_key, 0.5) self.repeat_touch = touch self.process_key_on(touch) touch.grab(self, exclusive=True) else: super(VKeyboard, self).on_touch_down(touch) return True def on_touch_up(self, touch): if touch.grab_current is self: self.process_key_up(touch) if self._start_repeat_key_ev is not None: self._start_repeat_key_ev.cancel() self._start_repeat_key_ev = None if touch == self.repeat_touch: if self._repeat_key_ev is not None: self._repeat_key_ev.cancel() self._repeat_key_ev = None self.repeat_touch = None return super(VKeyboard, self).on_touch_up(touch)
class VKeyboard(Scatter): """ VKeyboard is an onscreen keyboard with multitouch support. Its layout is entirely customizable and you can switch between available layouts using a button in the bottom right of the widget. :Events: `on_key_down`: keycode, internal, modifiers Fired when the keyboard received a key down event (key press). `on_key_up`: keycode, internal, modifiers Fired when the keyboard received a key up event (key release). """ target = ObjectProperty(None, allownone=True) """Target widget associated with the VKeyboard. If set, it will be used to send keyboard events. If the VKeyboard mode is "free", it will also be used to set the initial position. :data:`target` is an :class:`~kivy.properties.ObjectProperty` instance and defaults to None. """ callback = ObjectProperty(None, allownone=True) """Callback can be set to a function that will be called if the VKeyboard is closed by the user. :data:`target` is an :class:`~kivy.properties.ObjectProperty` instance and defaults to None. """ layout = StringProperty(None) """Layout to use for the VKeyboard. By default, it will be the layout set in the configuration, according to the `keyboard_layout` in `[kivy]` section. .. versionchanged:: 1.8.0 If layout is a .json filename, it will loaded and added to the available_layouts. :data:`layout` is a :class:`~kivy.properties.StringProperty` and defaults to None. """ layout_path = StringProperty(default_layout_path) """Path from which layouts are read. :data:`layout` is a :class:`~kivy.properties.StringProperty` and defaults to :file:`<kivy_data_dir>/keyboards/` """ available_layouts = DictProperty({}) """Dictionary of all available layouts. Keys are the layout ID, and the value is the JSON (translated into a Python object). :data:`available_layouts` is a :class:`~kivy.properties.DictProperty` and defaults to {}. """ docked = BooleanProperty(False) """Indicate whether the VKeyboard is docked on the screen or not. If you change it, you must manually call :meth:`setup_mode` otherwise it will have no impact. If the VKeyboard is created by the Window, the docked mode will be automatically set by the configuration, using the `keyboard_mode` token in `[kivy]` section. :data:`docked` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. """ margin_hint = ListProperty([0.05, 0.06, 0.05, 0.06]) """Margin hint, used as spacing between keyboard background and keys content. The margin is composed of four values, between 0 and 1:: margin_hint = [top, right, bottom, left] The margin hints will be multiplied by width and height, according to their position. :data:`margin_hint` is a :class:`~kivy.properties.ListProperty` and defaults to [.05, .06, .05, .06] """ key_margin = ListProperty([2, 2, 2, 2]) """Key margin, used to create space between keys. The margin is composed of four values, in pixels:: key_margin = [top, right, bottom, left] :data:`key_margin` is a :class:`~kivy.properties.ListProperty` and defaults to [2, 2, 2, 2] """ background_color = ListProperty([1, 1, 1, 1]) """Background color, in the format (r, g, b, a). If a background is set, the color will be combined with the background texture. :data:`background_color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1, 1]. """ background = StringProperty("atlas://data/images/defaulttheme/vkeyboard_background") """Filename of the background image. :data:`background` a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://data/images/defaulttheme/vkeyboard_background`. """ background_disabled = StringProperty("atlas://data/images/defaulttheme/vkeyboard_disabled_background") """Filename of the background image when vkeyboard is disabled. .. versionadded:: 1.8.0 :data:`background_disabled` is a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://data/images/defaulttheme/vkeyboard__disabled_background`. """ key_background_color = ListProperty([1, 1, 1, 1]) """Key background color, in the format (r, g, b, a). If a key background is set, the color will be combined with the key background texture. :data:`key_background_color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1, 1]. """ key_background_normal = StringProperty("atlas://data/images/defaulttheme/vkeyboard_key_normal") """Filename of the key background image for use when no touches are active on the widget. :data:`key_background_normal` a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://data/images/defaulttheme/vkeyboard_key_normal`. """ key_disabled_background_normal = StringProperty("atlas://data/images/defaulttheme/vkeyboard_key_normal") """Filename of the key background image for use when no touches are active on the widget and vkeyboard is disabled. ..versionadded:: 1.8.0 :data:`key_disabled_background_normal` a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://data/images/defaulttheme/vkeyboard_disabled_key_normal`. """ key_background_down = StringProperty("atlas://data/images/defaulttheme/vkeyboard_key_down") """Filename of the key background image for use when a touch is active on the widget. :data:`key_background_down` a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://data/images/defaulttheme/vkeyboard_key_down`. """ background_border = ListProperty([16, 16, 16, 16]) """Background image border. Used for controlling the :data:`~kivy.graphics.vertex_instructions.BorderImage.border` property of the background. :data:`background_border` is a :class:`~kivy.properties.ListProperty` and defaults to [16, 16, 16, 16] """ key_border = ListProperty([8, 8, 8, 8]) """Key image border. Used for controlling the :data:`~kivy.graphics.vertex_instructions.BorderImage.border` property of the key. :data:`key_border` is a :class:`~kivy.properties.ListProperty` and defaults to [16, 16, 16, 16] """ # XXX internal variables layout_mode = OptionProperty("normal", options=("normal", "shift")) layout_geometry = DictProperty({}) have_capslock = BooleanProperty(False) have_shift = BooleanProperty(False) active_keys = DictProperty({}) font_size = NumericProperty("20dp") font_name = StringProperty("data/fonts/DejaVuSans.ttf") __events__ = ("on_key_down", "on_key_up") def __init__(self, **kwargs): # XXX move to style.kv kwargs.setdefault("size_hint", (None, None)) kwargs.setdefault("scale_min", 0.4) kwargs.setdefault("scale_max", 1.6) kwargs.setdefault("size", (700, 200)) kwargs.setdefault("docked", False) self._trigger_update_layout_mode = Clock.create_trigger(self._update_layout_mode) self._trigger_load_layouts = Clock.create_trigger(self._load_layouts) self._trigger_load_layout = Clock.create_trigger(self._load_layout) self.bind( docked=self.setup_mode, have_shift=self._trigger_update_layout_mode, have_capslock=self._trigger_update_layout_mode, layout_path=self._trigger_load_layouts, layout=self._trigger_load_layout, ) super(VKeyboard, self).__init__(**kwargs) # load all the layouts found in the layout_path directory self._load_layouts() # ensure we have default layouts available_layouts = self.available_layouts if not available_layouts: Logger.critical("VKeyboard: unable to load default layouts") # load the default layout from configuration if self.layout is None: self.layout = Config.get("kivy", "keyboard_layout") else: # ensure the current layout is found on the available layout self._trigger_load_layout() # update layout mode (shift or normal) self._trigger_update_layout_mode() # create a top layer to draw active keys on with self.canvas: self.background_key_layer = Canvas() self.active_keys_layer = Canvas() # prepare layout widget self.refresh_keys_hint() self.refresh_keys() def on_disabled(self, intance, value): self.refresh_keys() def _update_layout_mode(self, *l): # update mode according to capslock and shift key mode = self.have_capslock != self.have_shift mode = "shift" if mode else "normal" if mode != self.layout_mode: self.layout_mode = mode self.refresh(False) def _load_layout(self, *largs): # ensure new layouts are loaded first if self._trigger_load_layouts.is_triggered: self._load_layouts() self._trigger_load_layouts.cancel() value = self.layout available_layouts = self.available_layouts # it's a filename, try to load it directly if self.layout[-5:] == ".json": if value not in available_layouts: fn = resource_find(self.layout) self._load_layout_fn(fn, self.layout) if not available_layouts: return if value not in available_layouts and value != "qwerty": Logger.error( "Vkeyboard: <%s> keyboard layout mentioned in " "conf file was not found, fallback on qwerty" % value ) self.layout = "qwerty" self.refresh(True) def _load_layouts(self, *largs): # first load available layouts from json files # XXX fix to be able to reload layout when path is changing value = self.layout_path for fn in listdir(value): self._load_layout_fn(join(value, fn), basename(splitext(fn)[0])) def _load_layout_fn(self, fn, name): available_layouts = self.available_layouts if fn[-5:] != ".json": return with open(fn, "r") as fd: json_content = fd.read() layout = loads(json_content) available_layouts[name] = layout def setup_mode(self, *largs): """Call this method when you want to readjust the keyboard according to options: :data:`docked` or not, with attached :data:`target` or not: * If :data:`docked` is True, it will call :meth:`setup_mode_dock` * If :data:`docked` is False, it will call :meth:`setup_mode_free` Feel free to overload these methods to create new positioning behavior. """ if self.docked: self.setup_mode_dock() else: self.setup_mode_free() def setup_mode_dock(self, *largs): """Setup the keyboard in docked mode. Dock mode will reset the rotation, disable translation, rotation and scale. Scale and position will be automatically adjusted to attach the keyboard to the bottom of the screen. .. note:: Don't call this method directly, use :meth:`setup_mode` instead. """ self.do_translation = False self.do_rotation = False self.do_scale = False self.rotation = 0 win = self.get_parent_window() scale = win.width / float(self.width) self.scale = scale self.pos = 0, 0 win.bind(on_resize=self._update_dock_mode) def _update_dock_mode(self, win, *largs): scale = win.width / float(self.width) self.scale = scale self.pos = 0, 0 def setup_mode_free(self): """Setup the keyboard in free mode. Free mode is designed to let the user control the position and orientation of the keyboard. The only real usage is for a multiuser environment, but you might found other ways to use it. If a :data:`target` is set, it will place the vkeyboard under the target. .. note:: Don't call this method directly, use :meth:`setup_mode` instead. """ self.do_translation = True self.do_rotation = True self.do_scale = True target = self.target if not target: return # NOTE all math will be done in window point of view # determine rotation of the target a = Vector(1, 0) b = Vector(target.to_window(0, 0)) c = Vector(target.to_window(1, 0)) - b self.rotation = -a.angle(c) # determine the position of center/top of the keyboard dpos = Vector(self.to_window(self.width / 2.0, self.height)) # determine the position of center/bottom of the target cpos = Vector(target.to_window(target.center_x, target.y)) # the goal now is to map both point, calculate the diff between them diff = dpos - cpos # we still have an issue, self.pos represent the bounding box, not the # 0,0 coordinate of the scatter. we need to apply also the diff between # them (inside and outside coordinate matrix). It's hard to explain, but # do a scheme on a paper, wrote all the vector i'm calculating, and # you'll understand. :) diff2 = Vector(self.x + self.width / 2.0, self.y + self.height) - Vector( self.to_parent(self.width / 2.0, self.height) ) diff -= diff2 # now we have a good "diff", set it as a pos. self.pos = -diff def change_layout(self): # XXX implement popup with all available layouts pass def refresh(self, force=False): """(internal) Recreate the entire widget and graphics according to the selected layout. """ self.clear_widgets() if force: self.refresh_keys_hint() self.refresh_keys() self.refresh_active_keys_layer() def refresh_active_keys_layer(self): self.active_keys_layer.clear() active_keys = self.active_keys layout_geometry = self.layout_geometry background = resource_find(self.key_background_down) texture = Image(background, mipmap=True).texture with self.active_keys_layer: Color(1, 1, 1) for line_nb, index in active_keys.values(): pos, size = layout_geometry["LINE_%d" % line_nb][index] BorderImage(texture=texture, pos=pos, size=size, border=self.key_border) def refresh_keys_hint(self): layout = self.available_layouts[self.layout] layout_cols = layout["cols"] layout_rows = layout["rows"] layout_geometry = self.layout_geometry mtop, mright, mbottom, mleft = self.margin_hint # get relative EFFICIENT surface of the layout without external margins el_hint = 1.0 - mleft - mright eh_hint = 1.0 - mtop - mbottom ex_hint = 0 + mleft ey_hint = 0 + mbottom # get relative unit surface uw_hint = (1.0 / layout_cols) * el_hint uh_hint = (1.0 / layout_rows) * eh_hint layout_geometry["U_HINT"] = (uw_hint, uh_hint) # calculate individual key RELATIVE surface and pos (without key margin) current_y_hint = ey_hint + eh_hint for line_nb in range(1, layout_rows + 1): current_y_hint -= uh_hint # get line_name line_name = "%s_%d" % (self.layout_mode, line_nb) line_hint = "LINE_HINT_%d" % line_nb layout_geometry[line_hint] = [] current_x_hint = ex_hint # go through the list of keys (tuples of 4) for key in layout[line_name]: # calculate relative pos, size layout_geometry[line_hint].append([(current_x_hint, current_y_hint), (key[3] * uw_hint, uh_hint)]) current_x_hint += key[3] * uw_hint self.layout_geometry = layout_geometry def refresh_keys(self): layout = self.available_layouts[self.layout] layout_rows = layout["rows"] layout_geometry = self.layout_geometry w, h = self.size kmtop, kmright, kmbottom, kmleft = self.key_margin uw_hint, uh_hint = layout_geometry["U_HINT"] for line_nb in range(1, layout_rows + 1): llg = layout_geometry["LINE_%d" % line_nb] = [] llg_append = llg.append for key in layout_geometry["LINE_HINT_%d" % line_nb]: x_hint, y_hint = key[0] w_hint, h_hint = key[1] kx = x_hint * w ky = y_hint * h kw = w_hint * w kh = h_hint * h # now adjust, considering the key margin kx = int(kx + kmleft) ky = int(ky + kmbottom) kw = int(kw - kmleft - kmright) kh = int(kh - kmbottom - kmtop) pos = (kx, ky) size = (kw, kh) llg_append((pos, size)) self.layout_geometry = layout_geometry self.draw_keys() def draw_keys(self): layout = self.available_layouts[self.layout] layout_rows = layout["rows"] layout_geometry = self.layout_geometry layout_mode = self.layout_mode # draw background w, h = self.size background = resource_find(self.background_disabled if self.disabled else self.background) texture = Image(background, mipmap=True).texture self.background_key_layer.clear() with self.background_key_layer: Color(*self.background_color) BorderImage(texture=texture, size=self.size, border=self.background_border) # XXX seperate drawing the keys and the fonts to avoid # XXX reloading the texture each time # first draw keys without the font key_normal = resource_find(self.key_background_disabled_normal if self.disabled else self.key_background_normal) texture = Image(key_normal, mipmap=True).texture with self.background_key_layer: for line_nb in range(1, layout_rows + 1): for pos, size in layout_geometry["LINE_%d" % line_nb]: BorderImage(texture=texture, pos=pos, size=size, border=self.key_border) # then draw the text # calculate font_size font_size = int(w) / 46 # draw for line_nb in range(1, layout_rows + 1): key_nb = 0 for pos, size in layout_geometry["LINE_%d" % line_nb]: # retrieve the relative text text = layout[layout_mode + "_" + str(line_nb)][key_nb][0] l = Label(text=text, font_size=font_size, pos=pos, size=size, font_name=self.font_name) self.add_widget(l) key_nb += 1 def on_key_down(self, *largs): pass def on_key_up(self, *largs): pass def get_key_at_pos(self, x, y): w, h = self.size x_hint = x / w # focus on the surface without margins layout_geometry = self.layout_geometry layout = self.available_layouts[self.layout] layout_rows = layout["rows"] mtop, mright, mbottom, mleft = self.margin_hint # get the line of the layout e_height = h - (mbottom + mtop) * h # efficient height in pixels line_height = e_height / layout_rows # line height in px y = y - mbottom * h line_nb = layout_rows - int(y / line_height) if line_nb > layout_rows: line_nb = layout_rows if line_nb < 1: line_nb = 1 # get the key within the line key_index = "" current_key_index = 0 for key in layout_geometry["LINE_HINT_%d" % line_nb]: if x_hint >= key[0][0] and x_hint < key[0][0] + key[1][0]: key_index = current_key_index break else: current_key_index += 1 if key_index == "": return None # get the full character key = layout["%s_%d" % (self.layout_mode, line_nb)][key_index] return [key, (line_nb, key_index)] def collide_margin(self, x, y): """Do a collision test, and return True if the (x, y) is inside the vkeyboard margin. """ mtop, mright, mbottom, mleft = self.margin_hint x_hint = x / self.width y_hint = y / self.height if x_hint > mleft and x_hint < 1.0 - mright and y_hint > mbottom and y_hint < 1.0 - mtop: return False return True def process_key_on(self, touch): x, y = self.to_local(*touch.pos) key = self.get_key_at_pos(x, y) if not key: return key_data = key[0] displayed_char, internal, special_char, size = key_data line_nb, key_index = key[1] # save pressed key on the touch ud = touch.ud[self.uid] = {} ud["key"] = key # for caps lock or shift only: uid = touch.uid if special_char is not None: if special_char == "capslock": self.have_capslock = not self.have_capslock uid = -1 elif special_char == "shift": self.have_shift = True elif special_char == "layout": self.change_layout() # send info to the bus b_keycode = special_char b_modifiers = self._get_modifiers() self.dispatch("on_key_down", b_keycode, internal, b_modifiers) # save key as an active key for drawing self.active_keys[uid] = key[1] self.refresh_active_keys_layer() def process_key_up(self, touch): uid = touch.uid if self.uid not in touch.ud: return # save pressed key on the touch key_data, key = touch.ud[self.uid]["key"] displayed_char, internal, special_char, size = key_data # send info to the bus b_keycode = special_char b_modifiers = self._get_modifiers() self.dispatch("on_key_up", b_keycode, internal, b_modifiers) if special_char == "capslock": uid = -1 if uid in self.active_keys: self.active_keys.pop(uid, None) if special_char == "shift": self.have_shift = False if special_char == "capslock" and self.have_capslock: self.active_keys[-1] = key self.refresh_active_keys_layer() def _get_modifiers(self): ret = [] if self.have_shift: ret.append("shift") if self.have_capslock: ret.append("capslock") return ret def on_touch_down(self, touch): x, y = touch.pos if not self.collide_point(x, y): return if self.disabled: return True x, y = self.to_local(x, y) if not self.collide_margin(x, y): self.process_key_on(touch) touch.grab(self, exclusive=True) else: super(VKeyboard, self).on_touch_down(touch) return True def on_touch_up(self, touch): if touch.grab_current is self: self.process_key_up(touch) return super(VKeyboard, self).on_touch_up(touch)
class GeoJsonMapLayer(MapLayer): source = StringProperty() geojson = ObjectProperty() initial_zoom = None first_time = True def __init__(self, **kwargs): super(GeoJsonMapLayer, self).__init__(**kwargs) with self.canvas: self.canvas_polygon = Canvas() self.canvas_line = Canvas() with self.canvas_polygon.before: PushMatrix() self.g_matrix = MatrixInstruction() self.g_scale = Scale() self.g_translate = Translate() with self.canvas_polygon: self.g_canvas_polygon = Canvas() with self.canvas_polygon.after: PopMatrix() def reposition(self): vx, vy = self.parent.delta_x, self.parent.delta_y pzoom = self.parent.zoom zoom = self.initial_zoom if zoom is None: self.initial_zoom = zoom = pzoom if zoom != pzoom: diff = 2**(pzoom - zoom) vx /= diff vy /= diff self.g_scale.x = self.g_scale.y = diff else: self.g_scale.x = self.g_scale.y = 1. self.g_translate.xy = vx, vy self.g_matrix.matrix = self.parent._scatter.transform if self.geojson: update = not self.first_time self.on_geojson(self, self.geojson, update=update) self.first_time = False def on_geojson(self, instance, geojson, update=False): if self.parent is None: return if not update: # print "Reload geojson (polygon)" self.g_canvas_polygon.clear() self._geojson_part(geojson, geotype="Polygon") # print "Reload geojson (LineString)" self.canvas_line.clear() self._geojson_part(geojson, geotype="LineString") def on_source(self, instance, value): if value.startswith("http://") or value.startswith("https://"): Downloader.instance().download(value, self._load_geojson_url) else: with open(value, "rb") as fd: geojson = json.load(fd) self.geojson = geojson def _load_geojson_url(self, url, r): self.geojson = r.json() def _geojson_part(self, part, geotype=None): tp = part["type"] if tp == "FeatureCollection": for feature in part["features"]: if geotype and feature["geometry"]["type"] != geotype: continue self._geojson_part_f(feature) elif tp == "Feature": if geotype and part["geometry"]["type"] == geotype: self._geojson_part_f(part) else: # unhandled geojson part pass def _geojson_part_f(self, feature): properties = feature["properties"] geometry = feature["geometry"] graphics = self._geojson_part_geometry(geometry, properties) for g in graphics: tp = geometry["type"] if tp == "Polygon": self.g_canvas_polygon.add(g) else: self.canvas_line.add(g) def _geojson_part_geometry(self, geometry, properties): from kivy.graphics import Mesh, Line, Color from kivy.graphics.tesselator import Tesselator, WINDING_ODD, TYPE_POLYGONS from kivy.utils import get_color_from_hex from kivy.metrics import dp tp = geometry["type"] graphics = [] if tp == "Polygon": tess = Tesselator() for c in geometry["coordinates"]: xy = list(self._lonlat_to_xy(c)) xy = flatten(xy) tess.add_contour(xy) tess.tesselate(WINDING_ODD, TYPE_POLYGONS) color = self._get_color_from(properties.get("color", "FF000088")) graphics.append(Color(*color)) for vertices, indices in tess.meshes: graphics.append( Mesh(vertices=vertices, indices=indices, mode="triangle_fan")) elif tp == "LineString": stroke = get_color_from_hex(properties.get("stroke", "#ffffff")) stroke_width = dp(properties.get("stroke-width")) xy = list(self._lonlat_to_xy(geometry["coordinates"])) xy = flatten(xy) graphics.append(Color(*stroke)) graphics.append(Line(points=xy, width=stroke_width)) return graphics def _lonlat_to_xy(self, lonlats): view = self.parent zoom = view.zoom for lon, lat in lonlats: p = view.get_window_xy_from(lat, lon, zoom) p = p[0] - self.parent.delta_x, p[1] - self.parent.delta_y p = self.parent._scatter.to_local(*p) yield p def _get_color_from(self, value): color = COLORS.get(value.lower(), value) color = get_color_from_hex(color) return color
class GeoJsonMapLayer(MapLayer): source = StringProperty() geojson = ObjectProperty() initial_zoom = None first_time = True def __init__(self, **kwargs): super(GeoJsonMapLayer, self).__init__(**kwargs) with self.canvas: self.scissor = ScissorPush(x=0, y=0, width=100, height=100) self.canvas_polygon = Canvas() self.canvas_line = Canvas() ScissorPop() with self.canvas_polygon.before: PushMatrix() self.g_matrix = MatrixInstruction() self.g_scale = Scale() self.g_translate = Translate() with self.canvas_polygon: self.g_canvas_polygon = Canvas() with self.canvas_polygon.after: PopMatrix() def reposition(self): self.scissor.x = self.parent.parent.x self.scissor.width = self.parent.parent.width self.scissor.y = self.parent.parent.y self.scissor.height = self.parent.parent.height vx, vy = self.parent.delta_x, self.parent.delta_y pzoom = self.parent.zoom zoom = self.initial_zoom if zoom is None: self.initial_zoom = zoom = pzoom if zoom != pzoom: diff = 2 ** (pzoom - zoom) vx /= diff vy /= diff self.g_scale.x = self.g_scale.y = diff else: self.g_scale.x = self.g_scale.y = 1. self.g_translate.xy = vx, vy self.g_matrix.matrix = self.parent._scatter.transform if self.geojson: update = not self.first_time self.on_geojson(self, self.geojson, update=update) self.first_time = False def on_geojson(self, instance, geojson, update=False): if self.parent is None: return if not update: # print "Reload geojson (polygon)" self.g_canvas_polygon.clear() self._geojson_part(geojson, geotype="Polygon") # print "Reload geojson (LineString)" self.canvas_line.clear() self._geojson_part(geojson, geotype="LineString") def on_source(self, instance, value): if value.startswith("http://") or value.startswith("https://"): Downloader.instance().download(value, self._load_geojson_url) else: # "rb" was replaced by "r" to avoid this error: # TypeError: the JSON object must be str, not 'bytes' with open(value, "r") as fd: geojson = json.load(fd) self.geojson = geojson def _load_geojson_url(self, url, r): self.geojson = r.json() def _geojson_part(self, part, geotype=None): tp = part["type"] if tp == "FeatureCollection": for feature in part["features"]: if geotype and feature["geometry"]["type"] != geotype: continue self._geojson_part_f(feature) elif tp == "Feature": if geotype and part["geometry"]["type"] == geotype: self._geojson_part_f(part) else: # unhandled geojson part pass def _geojson_part_f(self, feature): properties = feature["properties"] geometry = feature["geometry"] graphics = self._geojson_part_geometry(geometry, properties) for g in graphics: tp = geometry["type"] if tp == "Polygon": self.g_canvas_polygon.add(g) else: self.canvas_line.add(g) def _geojson_part_geometry(self, geometry, properties): from kivy.graphics import Mesh, Line, Color from kivy.graphics.tesselator import Tesselator, WINDING_ODD, TYPE_POLYGONS from kivy.utils import get_color_from_hex from kivy.metrics import dp tp = geometry["type"] graphics = [] if tp == "Polygon": tess = Tesselator() for c in geometry["coordinates"]: xy = list(self._lonlat_to_xy(c)) xy = flatten(xy) tess.add_contour(xy) tess.tesselate(WINDING_ODD, TYPE_POLYGONS) color = self._get_color_from(properties.get("color", "FF000088")) graphics.append(Color(*color)) for vertices, indices in tess.meshes: graphics.append(Mesh(vertices=vertices, indices=indices, mode="triangle_fan")) elif tp == "LineString": stroke = get_color_from_hex(properties.get("stroke", "#ff000088")) stroke_width = dp(properties.get("stroke-width", 2)) xy = list(self._lonlat_to_xy(geometry["coordinates"])) xy = flatten(xy) graphics.append(Color(*stroke)) graphics.append(Line(points=xy, width=stroke_width)) return graphics def _lonlat_to_xy(self, lonlats): view = self.parent zoom = view.zoom for lon, lat in lonlats: yield view.get_window_xy_from(lat, lon, zoom) # p = view.get_window_xy_from(lat, lon, zoom) # p = p[0] - self.parent.delta_x, p[1] - self.parent.delta_y # p = self.parent._scatter.to_local(*p) # yield p def _get_color_from(self, value): color = COLORS.get(value.lower(), value) color = get_color_from_hex(color) return color
class GeoJsonMapLayer(MapLayer): source = StringProperty() geojson = ObjectProperty() cache_dir = StringProperty(CACHE_DIR) def __init__(self, **kwargs): self.first_time = True self.initial_zoom = None super(GeoJsonMapLayer, self).__init__(**kwargs) with self.canvas: self.canvas_polygon = Canvas() with self.canvas_polygon.before: PushMatrix() self.g_matrix = MatrixInstruction() self.g_scale = Scale() self.g_translate = Translate() with self.canvas_polygon: self.g_canvas_polygon = Canvas() with self.canvas_polygon.after: PopMatrix() def reposition(self): vx, vy = self.parent.delta_x, self.parent.delta_y pzoom = self.parent.zoom zoom = self.initial_zoom if zoom is None: self.initial_zoom = zoom = pzoom if zoom != pzoom: diff = 2**(pzoom - zoom) vx /= diff vy /= diff self.g_scale.x = self.g_scale.y = diff else: self.g_scale.x = self.g_scale.y = 1. self.g_translate.xy = vx, vy self.g_matrix.matrix = self.parent._scatter.transform if self.geojson: update = not self.first_time self.on_geojson(self, self.geojson, update=update) self.first_time = False def traverse_feature(self, func, part=None): """Traverse the whole geojson and call the func with every element found. """ if part is None: part = self.geojson if not part: return tp = part["type"] if tp == "FeatureCollection": for feature in part["features"]: func(feature) elif tp == "Feature": func(part) @property def bounds(self): # return the min lon, max lon, min lat, max lat bounds = [float("inf"), float("-inf"), float("inf"), float("-inf")] def _submit_coordinate(coord): lon, lat = coord bounds[0] = min(bounds[0], lon) bounds[1] = max(bounds[1], lon) bounds[2] = min(bounds[2], lat) bounds[3] = max(bounds[3], lat) def _get_bounds(feature): geometry = feature["geometry"] tp = geometry["type"] if tp == "Point": _submit_coordinate(geometry["coordinates"]) elif tp == "Polygon": for coordinate in geometry["coordinates"][0]: _submit_coordinate(coordinate) elif tp == "MultiPolygon": for polygon in geometry["coordinates"]: for coordinate in polygon[0]: _submit_coordinate(coordinate) self.traverse_feature(_get_bounds) return bounds @property def center(self): min_lon, max_lon, min_lat, max_lat = self.bounds cx = (max_lon - min_lon) / 2. cy = (max_lat - min_lat) / 2. return min_lon + cx, min_lat + cy def on_geojson(self, instance, geojson, update=False): if self.parent is None: return if not update: # print "Reload geojson (polygon)" self.g_canvas_polygon.clear() self._geojson_part(geojson, geotype="Polygon") # print "Reload geojson (LineString)" self._geojson_part(geojson, geotype="LineString") def on_source(self, instance, value): if value.startswith("http://") or value.startswith("https://"): Downloader.instance(cache_dir=self.cache_dir).download( value, self._load_geojson_url) else: with open(value, "rb") as fd: geojson = json.load(fd) self.geojson = geojson def _load_geojson_url(self, url, r): self.geojson = r.json() def _geojson_part(self, part, geotype=None): tp = part["type"] if tp == "FeatureCollection": for feature in part["features"]: if geotype and feature["geometry"]["type"] != geotype: continue self._geojson_part_f(feature) elif tp == "Feature": if geotype and part["geometry"]["type"] == geotype: self._geojson_part_f(part) else: # unhandled geojson part pass def _geojson_part_f(self, feature): properties = feature["properties"] geometry = feature["geometry"] graphics = self._geojson_part_geometry(geometry, properties) for g in graphics: self.g_canvas_polygon.add(g) def _geojson_part_geometry(self, geometry, properties): tp = geometry["type"] graphics = [] if tp == "Polygon": tess = Tesselator() for c in geometry["coordinates"]: xy = list(self._lonlat_to_xy(c)) xy = flatten(xy) tess.add_contour(xy) tess.tesselate(WINDING_ODD, TYPE_POLYGONS) color = self._get_color_from( properties.get("style").get("stroke", "FF000088")) graphics.append(Color(*color)) for vertices, indices in tess.meshes: graphics.append( Mesh(vertices=vertices, indices=indices, mode="triangle_fan")) elif tp == "LineString": stroke = get_color_from_hex( properties.get("style").get("stroke", "#ffffff")) print "stroke: " + ` stroke ` #storke = [0.0, 0.0, 0.0, 1] print 'properties.get("width") :' + ` properties.get("style").get( "width") ` stroke_width = dp(properties.get("style").get("width")) print "stroke_width: " + ` stroke_width ` xy = list(self._lonlat_to_xy(geometry["coordinates"])) xy = flatten(xy) graphics.append(Color(*stroke)) graphics.append(Line(points=xy, width=stroke_width)) return graphics def _lonlat_to_xy(self, lonlats): view = self.parent zoom = view.zoom for lon, lat in lonlats: p = view.get_window_xy_from(lat, lon, zoom) p = p[0] - self.parent.delta_x, p[1] - self.parent.delta_y p = self.parent._scatter.to_local(*p) yield p def _get_color_from(self, value): color = COLORS.get(value.lower(), value) color = get_color_from_hex(color) return color
class SkeletonViewer(Widget): animations = ListProperty([]) skins = ListProperty([]) skinCurrent = StringProperty("") animationCurrent = StringProperty("") pause = BooleanProperty(False) alphaP = BooleanProperty(False) loop = BooleanProperty(False) speed = NumericProperty(1.0) scale = NumericProperty(0.0) mix = NumericProperty(0.0) flipX = BooleanProperty(False) flipY = BooleanProperty(False) debugBones = BooleanProperty(False) debugRegions = BooleanProperty(False) debugBounds = BooleanProperty(False) debugMeshHull = BooleanProperty(False) debugMeshTriangles = BooleanProperty(False) skeletonX = NumericProperty(50) skeletonY = NumericProperty(50) percent = NumericProperty(0) def __init__(self, **kwargs): self.register_event_type('on_touch_hover') self.chooser = None self.popup = None self.skeletons = [] self.states = [] # self.batch = PolygonSpriteBatch() self.renderer = SkeletonRenderer() self.debugRenderer = SkeletonRendererDebug() self.draw_canvas = Canvas() super().__init__(**kwargs) self.canvas.add(self.draw_canvas) def on_touch_hover(self, touch): return False def on_skinCurrent(self, instance, value): if self.skinCurrent != "Skin:": for skeleton in self.skeletons: skeleton.setSkin(self.skinCurrent) skeleton.setSlotsToSetupPose() def on_animationCurrent(self, instance, value): if self.animationCurrent != "animation:": for state in self.states: state.setAnimation(0, self.animationCurrent, self.loop) def on_setup_pose_both(self, *args): if self.skeleton is not None: self.skeleton.setToSetupPose() def on_setup_pose_bones(self, *args): if self.skeleton is not None: self.skeleton.setBonesToSetupPose() def on_setup_pose_slots(self, *args): if self.skeleton is not None: self.skeleton.setSlotsToSetupPose() def on_scale(self, *args): Clock.unschedule(self.set_scale) Clock.schedule_once(self.set_scale) def set_scale(self, *args): if len(self.skeletons) > 0: self.load_skeleton() def on_mix(self, *args): for state in self.states: state.getData().setDefaultMix(self.mix) def open_skeleton(self, *args): print("open new skeleton") self.chooser = FileChooserListViewBase( path= "C:/Users/ethan/OneDrive/Desktop/danmachi assets/danmachi chars/gac_1011002023", filters=['*.skel']) # self.popup = PopupBase(title='Test popup', content=self.chooser, auto_dismiss=False) self.chooser.add_widget( Button(text="Cancel", on_release=self.cancel_skeleton, size_hint=(None, None))) self.chooser.add_widget( Button(text="Select", pos=(self.chooser.width, 0), size_hint=(None, None), on_release=self.load_skeleton)) self.popup.open() def cancel_skeleton(self, *args): self.popup.dismiss() def load_skeleton(self, *args): if self.popup is not None: self.popup.dismiss() if self.chooser is None: return if len(self.chooser.selection) != 1: return name = str(self.chooser.selection[0].rsplit("\\", 1)[1][:-5]) print(name) filepath = self.chooser.selection[0].replace('\\', '/') skele_loader = SkeletonLoader() self.skeletons = [] self.skeletons.append( skele_loader.load_skeleton(filepath, False, self.scale)) self.skeletons.append( skele_loader.load_skeleton( "C:/Users/ethan/OneDrive/Desktop/danmachi assets/danmachi chars/gac_1011004010/1041004010.skel", False, self.scale)) self.skeletons.append( skele_loader.load_skeleton( "C:/Users/ethan/OneDrive/Desktop/danmachi assets/danmachi chars/gac_1011006009/1041006009.skel", False, self.scale)) self.skeletons.append( skele_loader.load_skeleton( "C:/Users/ethan/OneDrive/Desktop/danmachi assets/danmachi chars/gac_1011018009/1041018009.skel", False, self.scale)) self.skeletons.append( skele_loader.load_skeleton( "C:/Users/ethan/OneDrive/Desktop/danmachi assets/danmachi chars/gac_1011041004/1041041004.skel", False, self.scale)) self.skeletons.append( skele_loader.load_skeleton( "C:/Users/ethan/OneDrive/Desktop/danmachi assets/danmachi chars/gac_1011043003/1041043003.skel", False, self.scale)) self.skeletons.append( skele_loader.load_skeleton( "C:/Users/ethan/OneDrive/Desktop/danmachi assets/danmachi chars/gac_1011091003/1041091003.skel", False, self.scale)) self.skeletons.append( skele_loader.load_skeleton( "C:/Users/ethan/OneDrive/Desktop/danmachi assets/danmachi chars/gac_1011092003/1041092003.skel", False, self.scale)) self.states = [] self.states.append( AnimationState(AnimationStateData(self.skeletons[0].getData()))) self.states.append( AnimationState(AnimationStateData(self.skeletons[1].getData()))) self.states.append( AnimationState(AnimationStateData(self.skeletons[2].getData()))) self.states.append( AnimationState(AnimationStateData(self.skeletons[3].getData()))) self.states.append( AnimationState(AnimationStateData(self.skeletons[4].getData()))) self.states.append( AnimationState(AnimationStateData(self.skeletons[5].getData()))) self.states.append( AnimationState(AnimationStateData(self.skeletons[6].getData()))) self.states.append( AnimationState(AnimationStateData(self.skeletons[7].getData()))) items = [] for skin in self.skeletons[0].getData().getSkins(): items.append(skin.getName()) self.ids.skins.values = items self.ids.skins.text = items[0] items = [] for animation in self.skeletons[0].getData().getAnimations(): items.append(animation.getName()) self.ids.animations.values = items self.ids.animations.text = items[0] self.skeletons[0].setSkin(self.ids.skins.text) for skeleton in self.skeletons[1:]: skeleton.setSkin(skeleton.getData().getSkins()[0].getName()) self.states[0].setAnimation(0, self.ids.animations.text, True) for state, skeleton in zip(self.states[1:], self.skeletons[1:]): state.setAnimation(0, skeleton.getData().getAnimations()[0].getName(), True) print("Finished loading skeleton!") Clock.schedule_interval(self.render, 1 / 144) def render(self, delta): if len(self.skeletons) > 0: self.draw_canvas.clear() for state in self.states: state.getData().setDefaultMix(self.mix) self.renderer.setPremultipliedAlpha(self.alphaP) delta = min(delta, 0.032) * self.speed for skeleton in self.skeletons: skeleton.update(delta) skeleton.setFlip(self.flipX, self.flipY) if not self.pause: for state, skeleton in zip(self.states, self.skeletons): state.update(delta) state.apply(skeleton) self.skeletons[0].setPosition(self.skeletonX, self.skeletonY) self.skeletons[1].setPosition(self.skeletonX + 300, self.skeletonY) self.skeletons[2].setPosition(self.skeletonX + 600, self.skeletonY) self.skeletons[3].setPosition(self.skeletonX + 150, self.skeletonY - 75) self.skeletons[4].setPosition(self.skeletonX + 450, self.skeletonY - 75) self.skeletons[5].setPosition(self.skeletonX + 75, self.skeletonY - 150) self.skeletons[6].setPosition(self.skeletonX + 375, self.skeletonY - 150) self.skeletons[7].setPosition(self.skeletonX + 675, self.skeletonY - 150) for skeleton in self.skeletons: skeleton.updateWorldTransform() self.renderer.draw(self.draw_canvas, skeleton) entry = self.states[0].getCurrent(0) if entry is not None: self.percent = entry.getTime() / entry.getEndTime() if entry.getLoop(): self.percent %= 1
class TeleniumClient(pyjsonrpc.HttpRequestHandler): @pyjsonrpc.rpcmethod def ping(self): return True @pyjsonrpc.rpcmethod def get_token(self): return os.environ.get("TELENIUM_TOKEN") def selectAll(self, selector, root=None): if root is None: root = App.get_running_app().root.parent parser = XpathParser() matches = parser.parse(selector).execute(root) return matches or [] def selectFirst(self, selector, root=None): matches = self.selectAll(selector, root=root) if matches: return matches[0] def getattr(self, selector, key): widget = self.selectFirst(selector) if widget: return getattr(widget, key) def path_to(self, widget): from kivy.core.window import Window root = Window if widget.parent is root or widget.parent == widget or not widget.parent: return "/{}".format(widget.__class__.__name__) return "{}/{}[{}]".format(self.path_to(widget.parent), widget.__class__.__name__, widget.parent.children.index(widget)) @pyjsonrpc.rpcmethod @kivythread def app_quit(self): App.get_running_app().stop() return True @pyjsonrpc.rpcmethod def app_ready(self): app = App.get_running_app() if app is None: return False if app.root is None: return False return True @pyjsonrpc.rpcmethod def select(self, selector, with_bounds=False): if not with_bounds: return map(self.path_to, self.selectAll(selector)) results = [] for widget in self.selectAll(selector): left, bottom = widget.to_window(widget.x, widget.y) right, top = widget.to_window(widget.x + widget.width, widget.y + widget.height) bounds = (left, bottom, right, top) path = self.path_to(widget) results.append((path, bounds)) return results @pyjsonrpc.rpcmethod def highlight(self, selector): if not selector: results = [] else: try: results = self.select(selector, with_bounds=True) except: self._highlight([]) raise self._highlight(results) return results @kivythread def _highlight(self, results): from kivy.graphics import Color, Rectangle, Canvas from kivy.core.window import Window if not hasattr(self, "_canvas"): self._canvas = Canvas() Window.canvas.remove(self._canvas) Window.canvas.add(self._canvas) self._canvas.clear() with self._canvas: Color(1, 0, 0, 0.5) for widget, bounds in results: left, bottom, right, top = bounds Rectangle(pos=(left, bottom), size=(right - left, top - bottom)) @pyjsonrpc.rpcmethod def getattr(self, selector, key): widget = self.selectFirst(selector) try: return getattr(widget, key) except: return @pyjsonrpc.rpcmethod @kivythread def setattr(self, selector, key, value): ret = False for widget in self.selectAll(selector): setattr(widget, key, value) ret = True return ret @pyjsonrpc.rpcmethod @kivythread def element(self, selector): if self.selectFirst(selector): return True @pyjsonrpc.rpcmethod @kivythread def execute(self, cmd): app = App.get_running_app() idmap = {"app": app} try: exec cmd in idmap, idmap except: traceback.print_exc() return False return True @pyjsonrpc.rpcmethod def pick(self, all=False): from kivy.core.window import Window widgets = [] ev = threading.Event() def on_touch_down(touch): root = App.get_running_app().root for widget in Window.children: if all: widgets.extend(list(collide_at(root, touch.x, touch.y))) else: widget = pick(root, touch.x, touch.y) widgets.append(widget) ev.set() return True orig_on_touch_down = Window.on_touch_down Window.on_touch_down = on_touch_down ev.wait() Window.on_touch_down = orig_on_touch_down if widgets: if all: ret = map(self.path_to, widgets) else: ret = self.path_to(widgets[0]) return ret @pyjsonrpc.rpcmethod @kivythread def click_on(self, selector): w = self.selectFirst(selector) if w: from kivy.core.window import Window cx, cy = w.to_window(w.center_x, w.center_y) sx = cx / float(Window.width) sy = cy / float(Window.height) me = TeleniumMotionEvent("telenium", id=next(nextid), args=[sx, sy]) telenium_input.events.append(("begin", me)) telenium_input.events.append(("end", me)) return True @pyjsonrpc.rpcmethod def send_keycode(self, keycodes): # very hard to get it right, not fully tested and fail proof. # just the basics. from kivy.core.window import Keyboard keys = keycodes.split("+") scancode = 0 key = None sym = "" modifiers = [] for el in keys: if re.match("^[A-Z]", el): lower_el = el.lower() # modifier detected ? add it if lower_el in ("ctrl", "meta", "alt", "shift"): modifiers.append(lower_el) continue # not a modifier, convert to scancode sym = lower_el key = Keyboard.keycodes.get(lower_el, 0) else: # may fail, so nothing would be done. try: key = int(el) sym = unichr(key) except: traceback.print_exc() return False self._send_keycode(key, scancode, sym, modifiers) return True @kivythread def _send_keycode(self, key, scancode, sym, modifiers): from kivy.core.window import Window print("Telenium: send key key={!r} scancode={} sym={!r} modifiers={}". format(key, scancode, sym, modifiers)) if not Window.dispatch("on_key_down", key, scancode, sym, modifiers): Window.dispatch("on_keyboard", key, scancode, sym, modifiers) Window.dispatch("on_key_up", key, scancode) return True
class VKeyboard(Scatter): ''' VKeyboard is an onscreen keyboard with multitouch support. Its layout is entirely customizable and you can switch between available layouts using a button in the bottom right of the widget. ''' target = ObjectProperty(None, allownone=True) '''Target widget associated to VKeyboard. If set, it will be used to send keys events, and if the VKeyboard mode is "free", it will be also used to set the initial position. :data:`target` is a :class:`~kivy.properties.ObjectProperty` instance, default to None. ''' callback = ObjectProperty(None, allownone=True) '''Callback can be set to a function that will be called if the VKeyboard is closed by user. :data:`target` is a :class:`~kivy.properties.ObjectProperty` instance, default to None. ''' layout = StringProperty(None) '''Layout to use for the VKeyboard. By default, it will be the layout set in the configuration, according to the `keyboard_layout` in `[kivy]` section. :data:`layout` is a :class:`~kivy.properties.StringProperty`, default to None. ''' layout_path = StringProperty(default_layout_path) '''Path to read layouts from. :data:`layout` is a :class:`~kivy.properties.StringProperty`, default to :file:`<kivy_data_dir>/keyboards/` ''' available_layouts = DictProperty({}) '''Dictionnary of all available layouts. Keys is the layout ID, and the value is the JSON (translated in Python object). :data:`available_layouts` is a :class:`~kivy.properties.DictProperty`, default to {} ''' docked = BooleanProperty(False) '''Indicate if the VKeyboard is docked on the screen or not. If you change it, you must call manually :meth:`setup_mode`. Otherwise, it will have no impact. If the VKeyboard is created by the Window, the dock mode will be automatically set by the configuration, with `keyboard_mode` token in `[kivy]` section. :data:`docked` is a :class:`~kivy.properties.BooleanProperty`, default to False. ''' margin_hint = ListProperty([.05, .06, .05, .06]) '''Margin hint, used as spacing between keyboard background and keys content. The margin is composed of 4 values, between 0 and 1:: margin_hint = [top, right, bottom, left] The margin hints will be multiplied by width and height, according to their position. :data:`margin_hint` is a :class:`~kivy.properties.ListProperty`, default to [.05, .06, .05, .06] ''' key_margin = ListProperty([2, 2, 2, 2]) '''Key margin, used to create space between keys. The margin is composed of 4 values, in pixels:: key_margin = [top, right, bottom, left] :data:`key_margin` is a :class:`~kivy.properties.ListProperty`, default to [2, 2, 2, 2] ''' background_color = ListProperty([1, 1, 1, 1]) '''Background color, in the format (r, g, b, a). If a background is set, the color will be multiply with the background texture. :data:`background_color` is a :class:`~kivy.properties.ListProperty`, default to [1, 1, 1, 1]. ''' background = StringProperty('data/images/vkeyboard_background.png') '''Filename of the background image. :data:`background` a :class:`~kivy.properties.StringProperty`, default to :file:`data/images/vkeyboard_background.png`. ''' key_background_color = ListProperty([1, 1, 1, 1]) '''Key background color, in the format (r, g, b, a). If a key background is set, the color will be multiply with the key background texture. :data:`key_background_color` is a :class:`~kivy.properties.ListProperty`, default to [1, 1, 1, 1]. ''' key_background_normal = StringProperty( 'data/images/vkeyboard_key_normal.png') '''Filename of the key background image when no touch are on it. :data:`key_background_normal` a :class:`~kivy.properties.StringProperty`, default to :file:`data/images/vkeyboard_key_normal.png`. ''' key_background_down = StringProperty('data/images/vkeyboard_key_down.png') '''Filename of the key background image one touch is on it. :data:`key_background_down` a :class:`~kivy.properties.StringProperty`, default to :file:`data/images/vkeyboard_key_down.png`. ''' background_border = ListProperty([16, 16, 16, 16]) '''Background image border. It's used for controling the :data:`~kivy.graphics.vertex_instructions.BorderImage.border` property of the background. :data:`background_border` is a :class:`~kivy.properties.ListProperty`, default to [16, 16, 16, 16] ''' key_border = ListProperty([8, 8, 8, 8]) '''Key image border. It's used for controling the :data:`~kivy.graphics.vertex_instructions.BorderImage.border` property of the key. :data:`key_border` is a :class:`~kivy.properties.ListProperty`, default to [16, 16, 16, 16] ''' # XXX internal variables layout_mode = OptionProperty('normal', options=('normal', 'shift')) layout_geometry = DictProperty({}) have_capslock = BooleanProperty(False) have_shift = BooleanProperty(False) active_keys = DictProperty({}) font_size = NumericProperty(15) font_name = StringProperty('data/fonts/DejaVuSans.ttf') def __init__(self, **kwargs): # XXX move to style.kv kwargs.setdefault('size_hint', (None, None)) kwargs.setdefault('scale_min', .4) kwargs.setdefault('scale_max', 1.6) kwargs.setdefault('size', (700, 200)) kwargs.setdefault('docked', False) self._trigger_update_layout_mode = Clock.create_trigger( self._update_layout_mode) self._trigger_load_layouts = Clock.create_trigger( self._load_layouts) self._trigger_load_layout = Clock.create_trigger( self._load_layout) self.bind( docked=self.setup_mode, have_shift=self._trigger_update_layout_mode, have_capslock=self._trigger_update_layout_mode, layout_path=self._trigger_load_layouts, layout=self._trigger_load_layout) self.register_event_type('on_key_down') self.register_event_type('on_key_up') super(VKeyboard, self).__init__(**kwargs) # load all the layouts found in the layout_path directory self._load_layouts() # ensure we have default layouts available_layouts = self.available_layouts if not available_layouts: Logger.critical('VKeyboard: unable to load defaults layouts') # load the default layout from configuration if self.layout is None: self.layout = Config.get('kivy', 'keyboard_layout') else: # ensure the current layout is found on the available layout self._trigger_load_layout() # update layout mode (shift or normal) self._trigger_update_layout_mode() # create a top layer to draw active keys on with self.canvas: self.background_key_layer = Canvas() self.active_keys_layer = Canvas() # prepare layout widget self.refresh_keys_hint() self.refresh_keys() def _update_layout_mode(self, *l): # update mode according to capslock and shift key mode = self.have_capslock != self.have_shift mode = 'shift' if mode else 'normal' if mode != self.layout_mode: self.layout_mode = mode self.refresh(False) def _load_layout(self, *largs): available_layouts = self.available_layouts if not available_layouts: return value = self.layout if value not in available_layouts and value != 'qwerty': Logger.error( 'Vkeyboard: <%s> keyboard layout mentionned in ' 'conf file was not found, fallback on qwerty' % value) self.layout = 'qwerty' self.refresh(True) def _load_layouts(self, *largs): # first load available layouts from json files # XXX fix to be able to reload layout when path is changing available_layouts = self.available_layouts value = self.layout_path for fname in listdir(value): basename, extension = splitext(fname) if extension != '.json': continue filename = join(value, fname) with open(filename, 'r') as fd: json_content = fd.read() layout = loads(json_content) available_layouts[basename] = layout def setup_mode(self, *largs): '''Call this method when you want to reajust the keyboard according to his options: :data:`docked` or not, with attached :data:`target` or not: * If :data:`docked` is True, it will call :meth:`setup_mode_dock` * If :data:`docked` is False, it will call :meth:`setup_mode_free` Fell free to overload theses methods in order to create a new positioning behavior. ''' if self.docked: self.setup_mode_dock() else: self.setup_mode_free() def setup_mode_dock(self, *largs): '''Setup the keyboard in dock mode. Dock mode will reset the rotation, disable translation, rotation and scale. Scale and position will be automatically adjusted to attach the keyboard in the bottom of the screen. .. note:: Don't call this method directly, use :meth:`setup_mode` instead. ''' self.do_translation = False self.do_rotation = False self.do_scale = False self.rotation = 0 win = self.get_parent_window() scale = win.width / float(self.width) self.scale = scale self.pos = 0, 0 win.bind(on_resize=self._update_dock_mode) def _update_dock_mode(self, win, *largs): scale = win.width / float(self.width) self.scale = scale self.pos = 0, 0 def setup_mode_free(self): '''Setup the keyboard in free mode. Free mode is designed to let the user control the position and orientation of the keyboard. The only real usage is for multi users environment, but you might found others ways to use it. If a :data:`target` is set, it will place the vkeyboard under the target. .. note:: Don't call this method directly, use :meth:`setup_mode` instead. ''' self.do_translation = True self.do_rotation = True self.do_scale = True target = self.target if not target: return # NOTE all math will be done in window point of view # determine rotation of the target a = Vector(1, 0) b = Vector(target.to_window(0, 0)) c = Vector(target.to_window(1, 0)) - b self.rotation = -a.angle(c) # determine the position of center/top of the keyboard dpos = Vector(self.to_window(self.width / 2., self.height)) # determine the position of center/bottom of the target cpos = Vector(target.to_window(target.center_x, target.y)) # the goal now is to map both point, calculate the diff between them diff = dpos - cpos # we still have an issue, self.pos represent the bounding box, not the # 0,0 coordinate of the scatter. we need to apply also the diff between # them (inside and outside coordinate matrix). It's hard to explain, but # do a scheme on a paper, wrote all the vector i'm calculating, and # you'll understand. :) diff2 = Vector(self.x + self.width / 2., self.y + self.height) - \ Vector(self.to_parent(self.width / 2., self.height)) diff -= diff2 # now we have a good "diff", set it as a pos. self.pos = -diff def change_layout(self): # XXX implement popup with all available layouts pass def refresh(self, force=False): '''(internal) Recreate the whole widgets and graphics according to the selected layout. ''' self.clear_widgets() if force: self.refresh_keys_hint() self.refresh_keys() self.refresh_active_keys_layer() def refresh_active_keys_layer(self): self.active_keys_layer.clear() active_keys = self.active_keys layout_geometry = self.layout_geometry background = resource_find(self.key_background_down) texture = Image(background, mipmap=True).texture with self.active_keys_layer: Color(1, 1, 1) for line_nb, index in active_keys.itervalues(): pos, size = layout_geometry['LINE_%d' % line_nb][index] BorderImage(texture=texture, pos=pos, size=size, border=self.key_border) def refresh_keys_hint(self): layout = self.available_layouts[self.layout] layout_cols = layout['cols'] layout_rows = layout['rows'] layout_geometry = self.layout_geometry mtop, mright, mbottom, mleft = self.margin_hint # get relative EFFICIENT surface of the layout without external margins el_hint = 1. - mleft - mright eh_hint = 1. - mtop - mbottom ex_hint = 0 + mleft ey_hint = 0 + mbottom # get relative unit surface uw_hint = (1. / layout_cols) * el_hint uh_hint = (1. / layout_rows) * eh_hint layout_geometry['U_HINT'] = (uw_hint, uh_hint) # calculate individual key RELATIVE surface and pos (without key margin) current_y_hint = ey_hint + eh_hint for line_nb in xrange(1, layout_rows + 1): current_y_hint -= uh_hint # get line_name line_name = '%s_%d' % (self.layout_mode, line_nb) line_hint = 'LINE_HINT_%d' % line_nb layout_geometry[line_hint] = [] current_x_hint = ex_hint # go through the list of keys (tuples of 4) for key in layout[line_name]: # calculate relative pos, size layout_geometry[line_hint].append([ (current_x_hint, current_y_hint), (key[3] * uw_hint, uh_hint)]) current_x_hint += key[3] * uw_hint self.layout_geometry = layout_geometry def refresh_keys(self): layout = self.available_layouts[self.layout] layout_rows = layout['rows'] layout_geometry = self.layout_geometry w, h = self.size kmtop, kmright, kmbottom, kmleft = self.key_margin uw_hint, uh_hint = layout_geometry['U_HINT'] for line_nb in xrange(1, layout_rows + 1): llg = layout_geometry['LINE_%d' % line_nb] = [] llg_append = llg.append for key in layout_geometry['LINE_HINT_%d' % line_nb]: x_hint, y_hint = key[0] w_hint, h_hint = key[1] kx = x_hint * w ky = y_hint * h kw = w_hint * w kh = h_hint * h # now adjust, considering the key margin kx = int(kx + kmleft) ky = int(ky + kmbottom) kw = int(kw - kmleft - kmright) kh = int(kh - kmbottom - kmtop) pos = (kx, ky) size = (kw, kh) llg_append((pos, size)) self.layout_geometry = layout_geometry self.draw_keys() def draw_keys(self): layout = self.available_layouts[self.layout] layout_rows = layout['rows'] layout_geometry = self.layout_geometry layout_mode = self.layout_mode # draw background w, h = self.size background = resource_find(self.background) texture = Image(background, mipmap=True).texture with self.background_key_layer: Color(*self.background_color) BorderImage(texture=texture, size=self.size, border=self.background_border) # XXX seperate drawing the keys and the fonts to avoid # XXX reloading the texture each time # first draw keys without the font key_normal = resource_find(self.key_background_normal) texture = Image(key_normal, mipmap=True).texture for line_nb in xrange(1, layout_rows + 1): for pos, size in layout_geometry['LINE_%d' % line_nb]: with self.background_key_layer: Color(self.key_background_color) BorderImage(texture=texture, pos=pos, size=size, border=self.key_border) # then draw the text # calculate font_size font_size = int(w) / 46 # draw for line_nb in xrange(1, layout_rows + 1): key_nb = 0 for pos, size in layout_geometry['LINE_%d' % line_nb]: # retrieve the relative text text = layout[layout_mode +'_'+ str(line_nb)][key_nb][0] l = Label(text=text, font_size=font_size, pos=pos, size=size, font_name=self.font_name) self.add_widget(l) key_nb += 1 def on_key_down(self, *largs): pass def on_key_up(self, *largs): pass def get_key_at_pos(self, x, y): w, h = self.size x_hint = x / w # focus on the surface without margins layout_geometry = self.layout_geometry layout = self.available_layouts[self.layout] layout_rows = layout['rows'] mtop, mright, mbottom, mleft = self.margin_hint # get the line of the layout e_height = h - (mbottom + mtop) * h # efficient height in pixels line_height = e_height / layout_rows # line height in px y = y - mbottom * h line_nb = layout_rows - int(y / line_height) if line_nb > layout_rows: line_nb = layout_rows if line_nb < 1: line_nb = 1 # get the key within the line key_index = '' current_key_index =0 for key in layout_geometry['LINE_HINT_%d' % line_nb]: if x_hint >= key[0][0] and x_hint < key[0][0] + key[1][0]: key_index = current_key_index break else: current_key_index +=1 if key_index == '': return None # get the full character key = layout['%s_%d' % (self.layout_mode, line_nb)][key_index] return [key, (line_nb, key_index)] def collide_margin(self, x, y): '''Do a collision test, and return True if the (x, y) is inside the vkeyboard margin. ''' mtop, mright, mbottom, mleft = self.margin_hint x_hint = x / self.width y_hint = y / self.height if x_hint > mleft and x_hint < 1. - mright \ and y_hint > mbottom and y_hint < 1. - mtop: return False return True def process_key_on(self, touch): x, y = self.to_local(*touch.pos) key = self.get_key_at_pos(x, y) if not key: return key_data = key[0] displayed_char, internal, special_char, size = key_data line_nb, key_index = key[1] # save pressed key on the touch ud = touch.ud[self.uid] = {} ud['key'] = key # for caps lock or shift only: uid = touch.uid if special_char is not None: if special_char == 'capslock': self.have_capslock = not self.have_capslock uid = -1 elif special_char == 'shift': self.have_shift = True elif special_char == 'layout': self.change_layout() # send info to the bus b_keycode = special_char b_modifiers = self._get_modifiers() self.dispatch('on_key_down', b_keycode, internal, b_modifiers) # save key as an active key for drawing self.active_keys[uid] = key[1] self.refresh_active_keys_layer() def process_key_up(self, touch): uid = touch.uid if self.uid not in touch.ud: return # save pressed key on the touch key_data, key = touch.ud[self.uid]['key'] displayed_char, internal, special_char, size = key_data # send info to the bus b_keycode = special_char b_modifiers = self._get_modifiers() self.dispatch('on_key_up', b_keycode, internal, b_modifiers) if special_char == 'capslock': uid = -1 if uid in self.active_keys: self.active_keys.pop(uid, None) if special_char == 'shift': self.have_shift = False if special_char == 'capslock' and self.have_capslock: self.active_keys[-1] = key self.refresh_active_keys_layer() def _get_modifiers(self): ret = [] if self.have_shift: ret.append('shift') if self.have_capslock: ret.append('capslock') return ret def on_touch_down(self, touch): x, y = touch.pos if not self.collide_point(x, y): return x, y = self.to_local(x, y) if not self.collide_margin(x, y): self.process_key_on(touch) touch.grab(self) else: super(VKeyboard, self).on_touch_down(touch) return True def on_touch_up(self, touch): if touch.grab_current is self: self.process_key_up(touch) return super(VKeyboard, self).on_touch_up(touch)
class VKeyboard(Scatter): ''' VKeyboard is an onscreen keyboard with multitouch support. Its layout is entirely customizable and you can switch between available layouts using a button in the bottom right of the widget. :Events: `on_key_down`: keycode, internal, modifiers Fired when the keyboard received a key down event (key press). `on_key_up`: keycode, internal, modifiers Fired when the keyboard received a key up event (key release). ''' target = ObjectProperty(None, allownone=True) '''Target widget associated with the VKeyboard. If set, it will be used to send keyboard events. If the VKeyboard mode is "free", it will also be used to set the initial position. :data:`target` is an :class:`~kivy.properties.ObjectProperty` instance and defaults to None. ''' callback = ObjectProperty(None, allownone=True) '''Callback can be set to a function that will be called if the VKeyboard is closed by the user. :data:`target` is an :class:`~kivy.properties.ObjectProperty` instance and defaults to None. ''' layout = StringProperty(None) '''Layout to use for the VKeyboard. By default, it will be the layout set in the configuration, according to the `keyboard_layout` in `[kivy]` section. .. versionchanged:: 1.8.0 If layout is a .json filename, it will loaded and added to the available_layouts. :data:`layout` is a :class:`~kivy.properties.StringProperty` and defaults to None. ''' layout_path = StringProperty(default_layout_path) '''Path from which layouts are read. :data:`layout` is a :class:`~kivy.properties.StringProperty` and defaults to :file:`<kivy_data_dir>/keyboards/` ''' available_layouts = DictProperty({}) '''Dictionary of all available layouts. Keys are the layout ID, and the value is the JSON (translated into a Python object). :data:`available_layouts` is a :class:`~kivy.properties.DictProperty` and defaults to {}. ''' docked = BooleanProperty(False) '''Indicate whether the VKeyboard is docked on the screen or not. If you change it, you must manually call :meth:`setup_mode` otherwise it will have no impact. If the VKeyboard is created by the Window, the docked mode will be automatically set by the configuration, using the `keyboard_mode` token in `[kivy]` section. :data:`docked` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' margin_hint = ListProperty([.05, .06, .05, .06]) '''Margin hint, used as spacing between keyboard background and keys content. The margin is composed of four values, between 0 and 1:: margin_hint = [top, right, bottom, left] The margin hints will be multiplied by width and height, according to their position. :data:`margin_hint` is a :class:`~kivy.properties.ListProperty` and defaults to [.05, .06, .05, .06] ''' key_margin = ListProperty([2, 2, 2, 2]) '''Key margin, used to create space between keys. The margin is composed of four values, in pixels:: key_margin = [top, right, bottom, left] :data:`key_margin` is a :class:`~kivy.properties.ListProperty` and defaults to [2, 2, 2, 2] ''' background_color = ListProperty([1, 1, 1, 1]) '''Background color, in the format (r, g, b, a). If a background is set, the color will be combined with the background texture. :data:`background_color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1, 1]. ''' background = StringProperty( 'atlas://./defaulttheme/vkeyboard_background') '''Filename of the background image. :data:`background` a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://./defaulttheme/vkeyboard_background`. ''' background_disabled = StringProperty( 'atlas://./defaulttheme/vkeyboard_disabled_background') '''Filename of the background image when vkeyboard is disabled. .. versionadded:: 1.8.0 :data:`background_disabled` is a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://./defaulttheme/vkeyboard__disabled_background`. ''' key_background_color = ListProperty([1, 1, 1, 1]) '''Key background color, in the format (r, g, b, a). If a key background is set, the color will be combined with the key background texture. :data:`key_background_color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1, 1]. ''' key_background_normal = StringProperty( 'atlas://./defaulttheme/vkeyboard_key_normal') '''Filename of the key background image for use when no touches are active on the widget. :data:`key_background_normal` a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://./defaulttheme/vkeyboard_key_normal`. ''' key_disabled_background_normal = StringProperty( 'atlas://./defaulttheme/vkeyboard_key_normal') '''Filename of the key background image for use when no touches are active on the widget and vkeyboard is disabled. ..versionadded:: 1.8.0 :data:`key_disabled_background_normal` a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://./defaulttheme/vkeyboard_disabled_key_normal`. ''' key_background_down = StringProperty( 'atlas://./defaulttheme/vkeyboard_key_down') '''Filename of the key background image for use when a touch is active on the widget. :data:`key_background_down` a :class:`~kivy.properties.StringProperty` and defaults to :file:`atlas://./defaulttheme/vkeyboard_key_down`. ''' background_border = ListProperty([16, 16, 16, 16]) '''Background image border. Used for controlling the :data:`~kivy.graphics.vertex_instructions.BorderImage.border` property of the background. :data:`background_border` is a :class:`~kivy.properties.ListProperty` and defaults to [16, 16, 16, 16] ''' key_border = ListProperty([8, 8, 8, 8]) '''Key image border. Used for controlling the :data:`~kivy.graphics.vertex_instructions.BorderImage.border` property of the key. :data:`key_border` is a :class:`~kivy.properties.ListProperty` and defaults to [16, 16, 16, 16] ''' # XXX internal variables layout_mode = OptionProperty('normal', options=('normal', 'shift', 'capslock')) layout_geometry = DictProperty({}) have_capslock = BooleanProperty(False) have_shift = BooleanProperty(False) active_keys = DictProperty({}) font_size = NumericProperty('20dp') font_name = StringProperty('./DejaVuSans.ttf') __events__ = ('on_key_down', 'on_key_up') def __init__(self, **kwargs): # XXX move to style.kv kwargs.setdefault('size_hint', (None, None)) kwargs.setdefault('scale_min', .4) kwargs.setdefault('scale_max', 1.6) kwargs.setdefault('size', (700, 200)) kwargs.setdefault('docked', False) self._trigger_update_layout_mode = Clock.create_trigger( self._update_layout_mode) self._trigger_load_layouts = Clock.create_trigger( self._load_layouts) self._trigger_load_layout = Clock.create_trigger( self._load_layout) self.bind( docked=self.setup_mode, have_shift=self._trigger_update_layout_mode, have_capslock=self._trigger_update_layout_mode, layout_path=self._trigger_load_layouts, layout=self._trigger_load_layout) super(VKeyboard, self).__init__(**kwargs) # load all the layouts found in the layout_path directory self._load_layouts() # ensure we have default layouts available_layouts = self.available_layouts if not available_layouts: Logger.critical('VKeyboard: unable to load default layouts') # load the default layout from configuration if self.layout is None: self.layout = Config.get('kivy', 'keyboard_layout') else: # ensure the current layout is found on the available layout self._trigger_load_layout() # update layout mode (shift or normal) self._trigger_update_layout_mode() # create a top layer to draw active keys on with self.canvas: self.background_key_layer = Canvas() self.active_keys_layer = Canvas() # prepare layout widget self.refresh_keys_hint() self.refresh_keys() self.key_width, self.key_height = self.layout_geometry['LINE_3'][1][1] layout = self.available_layouts[self.layout] self.key_centers = {} for r in xrange(1, layout['rows'] + 1): row = layout['%s_%d' % (self.layout_mode, r)] row_geom = self.layout_geometry['LINE_%d' % r] for c, ((x, y), (w, h)) in zip(row, row_geom): if c[0].isalpha(): self.key_centers[c[0]] = (x + w * 0.5, y + h * 0.5) self.words = trie.Trie() with open('0grams') as nograms: total = float(nograms.read()) with open('1grams') as unigrams: for line in unigrams: w, c = line[:-1].split('\t', 1) if w.isalpha(): self.words[w.lower()] = self.val_dist(tuple(map(self.key_centers.__getitem__, w.lower()))) + (float(c) / total,) self.labels = [] #self.config = ConfigParser() #self.config.read('settings.ini') self.user_nograms = 1 self.user_unigrams = {'the':1} self.user_bigrams = {} self.user_paths = {} '''import random for word in random.sample(self.words.keys(), 10): print word print '======' count = 0 ranks = [] candidates = self.candidate_matches(self.word_sample_n(word, 50)) if candidates[0][0] != word: print word count += 1 for i, e in enumerate(candidates): if e[0] == word: ranks += [i] else: ranks += [0] print len([r for r in ranks if r <= 5]) / float(len(ranks)) print sum(ranks) / float(len(ranks)) print 1 - count / 10000.''' def reload_layout(self): layout = self.available_layouts[self.layout] self.key_centers = {} for r in xrange(1, layout['rows'] + 1): row = layout['%s_%d' % (self.layout_mode, r)] row_geom = self.layout_geometry['LINE_%d' % r] for c, ((x, y), (w, h)) in zip(row, row_geom): if c[0].isalpha(): self.key_centers[c[0]] = (x + w * 0.5, y + h * 0.5) words = trie.Trie() for word in self.words: words[word] = self.val_dist(tuple(map(self.key_centers.__getitem__, word))) + (self.words[word][-1],) self.words = words def get_text_area(self): return self.get_parent_window().children[1].children[0] def val_dist(self, path): tot = 0.0 for i in xrange(1, len(path)): tot += ((path[i][0]-path[i-1][0])**2 + (path[i][1]-path[i-1][1])**2)**0.5 return (path, tot) def get_ngram_probability(self, word, prev_word): nogram = self.user_nograms bigram = self.user_bigrams.get((prev_word, word), 0) unigram1 = self.user_unigrams.get(prev_word, 0) unigram2 = self.user_unigrams.get(word, 0) p = 0.4 * (bigram + 1) / (unigram1 + len(self.user_unigrams)) + 0.1 * (unigram2 + 1) / (nogram + len(self.user_unigrams)) p = p + 0.5 * self.words[word][2] return p def candidate_matches(self, gesture): candidates = [] #g0 = self.get_key_at_pos(*gesture[0])[0][2] #g1 = self.get_key_at_pos(*gesture[-1])[0][2] gest_length = self.val_dist(gesture)[1] prev_word = self.get_previous_word() for word in self.words: #if word[0] != g0 or word[-1] != g1: # continue path = self.words[word] if abs(path[0][0][0] - gesture[0][0]) > self.key_width or abs(path[0][0][1] - gesture[0][1]) > self.key_height: continue if abs(path[0][-1][0] - gesture[-1][0]) > self.key_width or abs(path[0][-1][1] - gesture[-1][1]) > self.key_height: continue if not 0.8*path[1] <= gest_length <= 1.4*path[1]: continue p = exp(-self.gesture_distance(gesture, word)/2) * self.get_ngram_probability(word, prev_word) candidates.append((word, p)) candidates.sort(key=lambda x: x[1], reverse=True) return candidates def candidate_predictions(self, word): prev_word = self.get_previous_word() candidates = [(w, 0.001**d * self.get_ngram_probability(w, prev_word)) for (w, d) in self.words.search_prediction(word, 2)] candidates.sort(key=lambda x: x[1], reverse=True) return candidates def candidate_corrections(self, word): prev_word = self.get_previous_word() candidates = [(w, 0.001**d * self.get_ngram_probability(w, prev_word)) for (w, d) in self.words.search_correction(word, 2)] candidates.sort(key=lambda x: x[1], reverse=True) return candidates def candidate_guesses(self): prev_word = self.get_previous_word() candidates = [(w, self.get_ngram_probability(w, prev_word)) for w in self.words] candidates.sort(key=lambda x: x[1], reverse=True) return candidates def word_sample_n(self, word, n): path = self.words[word][0] cum_length = [0.0] for i in xrange(1, len(path)): cum_length.append(cum_length[-1] + ((path[i][0]-path[i-1][0])**2 + (path[i][1]-path[i-1][1])**2)**0.5) import bisect points = [] for k in range(n): L = min(k * cum_length[-1] / (n - 1), cum_length[-1]) i = bisect.bisect_left(cum_length[1:], L) if i >= len(path) - 1: i -= 1 p = 0 if cum_length[i+1] == cum_length[i] else (L - cum_length[i]) / (cum_length[i+1] - cum_length[i]) x, y = path[i][0] + p * (path[i+1][0] - path[i][0]), path[i][1] + p * (path[i+1][1] - path[i][1]) points.append((x, y)) return points def gesture_distance(self, gesture, word): n = len(gesture) template = self.word_sample_n(word, n) return sum(((x1-x2)**2 + (y1-y2)**2)**0.5 for ((x1, y1), (x2, y2)) in zip(gesture, template)) / n def on_disabled(self, intance, value): self.refresh_keys() def _update_layout_mode(self, *l): # update mode according to capslock and shift key mode = self.have_capslock != self.have_shift if not mode: mode = 'normal' else: mode = 'shift' if self.have_shift else 'capslock' if mode != self.layout_mode: self.layout_mode = mode self.refresh(False) def _load_layout(self, *largs): # ensure new layouts are loaded first if self._trigger_load_layouts.is_triggered: self._load_layouts() self._trigger_load_layouts.cancel() value = self.layout available_layouts = self.available_layouts # it's a filename, try to load it directly if self.layout[-5:] == '.json': if value not in available_layouts: fn = resource_find(self.layout) self._load_layout_fn(fn, self.layout) if not available_layouts: return if value not in available_layouts and value != 'qwerty': Logger.error( 'Vkeyboard: <%s> keyboard layout mentioned in ' 'conf file was not found, fallback on qwerty' % value) self.layout = 'qwerty' self.refresh(True) def _load_layouts(self, *largs): # first load available layouts from json files # XXX fix to be able to reload layout when path is changing value = self.layout_path for fn in listdir(value): self._load_layout_fn(join(value, fn), basename(splitext(fn)[0])) def _load_layout_fn(self, fn, name): available_layouts = self.available_layouts if fn[-5:] != '.json': return with open(fn, 'r') as fd: json_content = fd.read() layout = loads(json_content) available_layouts[name] = layout def setup_mode(self, *largs): '''Call this method when you want to readjust the keyboard according to options: :data:`docked` or not, with attached :data:`target` or not: * If :data:`docked` is True, it will call :meth:`setup_mode_dock` * If :data:`docked` is False, it will call :meth:`setup_mode_free` Feel free to overload these methods to create new positioning behavior. ''' if self.docked: self.setup_mode_dock() else: self.setup_mode_free() def setup_mode_dock(self, *largs): '''Setup the keyboard in docked mode. Dock mode will reset the rotation, disable translation, rotation and scale. Scale and position will be automatically adjusted to attach the keyboard to the bottom of the screen. .. note:: Don't call this method directly, use :meth:`setup_mode` instead. ''' self.do_translation = False self.do_rotation = False self.do_scale = False self.rotation = 0 win = self.get_parent_window() scale = win.width / float(self.width) self.scale = scale self.pos = 0, 0 win.bind(on_resize=self._update_dock_mode) def _update_dock_mode(self, win, *largs): scale = win.width / float(self.width) self.scale = scale self.pos = 0, 0 def setup_mode_free(self): '''Setup the keyboard in free mode. Free mode is designed to let the user control the position and orientation of the keyboard. The only real usage is for a multiuser environment, but you might found other ways to use it. If a :data:`target` is set, it will place the vkeyboard under the target. .. note:: Don't call this method directly, use :meth:`setup_mode` instead. ''' self.do_translation = True self.do_rotation = True self.do_scale = True target = self.target if not target: return # NOTE all math will be done in window point of view # determine rotation of the target a = Vector(1, 0) b = Vector(target.to_window(0, 0)) c = Vector(target.to_window(1, 0)) - b self.rotation = -a.angle(c) # determine the position of center/top of the keyboard dpos = Vector(self.to_window(self.width / 2., self.height)) # determine the position of center/bottom of the target cpos = Vector(target.to_window(target.center_x, target.y)) # the goal now is to map both point, calculate the diff between them diff = dpos - cpos # we still have an issue, self.pos represent the bounding box, not the # 0,0 coordinate of the scatter. we need to apply also the diff between # them (inside and outside coordinate matrix). It's hard to explain, but # do a scheme on a paper, wrote all the vector i'm calculating, and # you'll understand. :) diff2 = Vector(self.x + self.width / 2., self.y + self.height) - \ Vector(self.to_parent(self.width / 2., self.height)) diff -= diff2 # now we have a good "diff", set it as a pos. self.pos = -diff def refresh(self, force=False): '''(internal) Recreate the entire widget and graphics according to the selected layout. ''' self.clear_widgets() if force: self.refresh_keys_hint() self.refresh_keys() self.refresh_active_keys_layer() def refresh_active_keys_layer(self): self.active_keys_layer.clear() active_keys = self.active_keys layout_geometry = self.layout_geometry background = resource_find(self.key_background_down) texture = Image(background, mipmap=True).texture with self.active_keys_layer: Color(1, 1, 1) for line_nb, index in active_keys.values(): pos, size = layout_geometry['LINE_%d' % line_nb][index] BorderImage(texture=texture, pos=pos, size=size, border=self.key_border) def refresh_keys_hint(self): layout = self.available_layouts[self.layout] layout_cols = layout['cols'] layout_rows = layout['rows'] layout_geometry = self.layout_geometry mtop, mright, mbottom, mleft = self.margin_hint # get relative EFFICIENT surface of the layout without external margins el_hint = 1. - mleft - mright eh_hint = 1. - mtop - mbottom ex_hint = 0 + mleft ey_hint = 0 + mbottom # get relative unit surface uw_hint = (1. / layout_cols) * el_hint uh_hint = (1. / layout_rows) * eh_hint layout_geometry['U_HINT'] = (uw_hint, uh_hint) # calculate individual key RELATIVE surface and pos (without key margin) current_y_hint = ey_hint + eh_hint for line_nb in range(1, layout_rows + 1): current_y_hint -= uh_hint # get line_name line_name = '%s_%d' % (self.layout_mode, line_nb) line_hint = 'LINE_HINT_%d' % line_nb layout_geometry[line_hint] = [] current_x_hint = ex_hint # go through the list of keys (tuples of 4) for key in layout[line_name]: # calculate relative pos, size layout_geometry[line_hint].append([ (current_x_hint, current_y_hint), (key[3] * uw_hint, uh_hint)]) current_x_hint += key[3] * uw_hint self.layout_geometry = layout_geometry def refresh_keys(self): layout = self.available_layouts[self.layout] layout_rows = layout['rows'] layout_geometry = self.layout_geometry w, h = self.size kmtop, kmright, kmbottom, kmleft = self.key_margin uw_hint, uh_hint = layout_geometry['U_HINT'] for line_nb in range(1, layout_rows + 1): llg = layout_geometry['LINE_%d' % line_nb] = [] llg_append = llg.append for key in layout_geometry['LINE_HINT_%d' % line_nb]: x_hint, y_hint = key[0] w_hint, h_hint = key[1] kx = x_hint * w ky = y_hint * h kw = w_hint * w kh = h_hint * h # now adjust, considering the key margin kx = int(kx + kmleft) ky = int(ky + kmbottom) kw = int(kw - kmleft - kmright) kh = int(kh - kmbottom - kmtop) pos = (kx, ky) size = (kw, kh) llg_append((pos, size)) self.layout_geometry = layout_geometry self.draw_keys() def draw_keys(self): layout = self.available_layouts[self.layout] layout_rows = layout['rows'] layout_geometry = self.layout_geometry layout_mode = self.layout_mode # draw background w, h = self.size background = resource_find(self.background) texture = Image(background, mipmap=True).texture self.background_key_layer.clear() with self.background_key_layer: Color(*self.background_color) BorderImage(texture=texture, size=self.size, border=self.background_border) # XXX seperate drawing the keys and the fonts to avoid # XXX reloading the texture each time # first draw keys without the font key_normal = resource_find(self.key_background_normal) texture = Image(key_normal, mipmap=True).texture with self.background_key_layer: for line_nb in range(1, layout_rows + 1): for pos, size in layout_geometry['LINE_%d' % line_nb]: BorderImage(texture=texture, pos=pos, size=size, border=self.key_border) self.labels = [] font_size = int(w) / 60 key_nb = 0 for pos, size in layout_geometry['LINE_1']: # retrieve the relative text text = layout[layout_mode + '_1'][key_nb][0] l = Label(text=text, font_size=font_size, pos=pos, size=size, font_name=self.font_name) self.add_widget(l) self.labels.append(l) key_nb += 1 # then draw the text # calculate font_size font_size = int(w) / 46 # draw for line_nb in range(2, layout_rows + 1): key_nb = 0 for pos, size in layout_geometry['LINE_%d' % line_nb]: # retrieve the relative text text = layout[layout_mode + '_' + str(line_nb)][key_nb][0] l = Label(text=text, font_size=font_size, pos=pos, size=size, font_name=self.font_name) self.add_widget(l) key_nb += 1 def on_key_down(self, *largs): pass def on_key_up(self, *largs): pass def get_key_at_pos(self, x, y): w, h = self.size x_hint = x / w # focus on the surface without margins layout_geometry = self.layout_geometry layout = self.available_layouts[self.layout] layout_rows = layout['rows'] mtop, mright, mbottom, mleft = self.margin_hint # get the line of the layout e_height = h - (mbottom + mtop) * h # efficient height in pixels line_height = e_height / layout_rows # line height in px y = y - mbottom * h line_nb = layout_rows - int(y / line_height) if line_nb > layout_rows: line_nb = layout_rows if line_nb < 1: line_nb = 1 # get the key within the line key_index = '' current_key_index = 0 for key in layout_geometry['LINE_HINT_%d' % line_nb]: if x_hint >= key[0][0] and x_hint < key[0][0] + key[1][0]: key_index = current_key_index break else: current_key_index += 1 if key_index == '': return None # get the full character key = layout['%s_%d' % (self.layout_mode, line_nb)][key_index] return [key, (line_nb, key_index)] def collide_margin(self, x, y): '''Do a collision test, and return True if the (x, y) is inside the vkeyboard margin. ''' mtop, mright, mbottom, mleft = self.margin_hint x_hint = x / self.width y_hint = y / self.height if x_hint > mleft and x_hint < 1. - mright \ and y_hint > mbottom and y_hint < 1. - mtop: return False return True def process_key_on(self, touch): x, y = self.to_local(*touch.pos) key = self.get_key_at_pos(x, y) if not key: return key_data = key[0] displayed_char, internal, special_char, size = key_data line_nb, key_index = key[1] # save pressed key on the touch ud = touch.ud[self.uid] = {} ud['key'] = key # for caps lock or shift only: uid = touch.uid if special_char is not None: if special_char == 'capslock': self.have_capslock = not self.have_capslock uid = -1 elif special_char == 'shift': self.have_shift = True # save key as an active key for drawing self.active_keys[uid] = key[1] self.refresh_active_keys_layer() def process_key_up(self, touch): uid = touch.uid if self.uid not in touch.ud: return # save pressed key on the touch key_data, key = touch.ud[self.uid]['key'] displayed_char, internal, special_char, size = key_data # send info to the bus b_keycode = special_char b_modifiers = self._get_modifiers() self.dispatch('on_key_up', b_keycode, internal, b_modifiers) if special_char == 'capslock': uid = -1 if uid in self.active_keys: self.active_keys.pop(uid, None) if special_char == 'shift': self.have_shift = False if special_char == 'capslock' and self.have_capslock: self.active_keys[-1] = key self.refresh_active_keys_layer() def _get_modifiers(self): ret = [] if self.have_shift: ret.append('shift') if self.have_capslock: ret.append('capslock') return ret def on_touch_down(self, touch): if touch.ud is None: return x, y = touch.pos if not self.collide_point(x, y): return x, y = self.to_local(x, y) touch.ud['key'] = self.get_key_at_pos(x, y) if touch.ud['key'] is None: del touch.ud['key'] return if len(touch.ud['key'][0][2]) == 1 and touch.ud['key'][0][2].isalpha(): with self.canvas: Color(0.5, 0.6, 1) touch.ud['line'] = Line(points=[x, y], width=2) elif touch.ud['key'][0][2] == u'ctrl': touch.ud['ctrl'] = None with self.canvas: Color(0.5, 0.6, 1) touch.ud['line'] = Line(points=[x, y], width=2) if not self.collide_margin(x, y): self.process_key_on(touch) touch.grab(self, exclusive=True) else: super(VKeyboard, self).on_touch_down(touch) return True def get_current_word(self): textarea = self.get_text_area() i = textarea.cursor_index() T = textarea.text[:i] for j in xrange(i-1, -1, -1): if not T[j].isalpha(): break else: return T return T[j + 1:i] def get_previous_word(self): textarea = self.get_text_area() len_cur = len(self.get_current_word()) i = max(textarea.cursor_index() - len_cur - 1, 0) T = textarea.text[:i] while len(T) > 0 and not T[-1].isalpha(): T = T[:-1] i = len(T) for j in xrange(i-1, -1, -1): if not T[j].isalpha(): break else: return T return T[j + 1:i] def on_touch_move(self, touch): if touch.ud is None: return x, y = self.to_local(*touch.pos) if 'line' in touch.ud: touch.ud['line'].points += [x, y] if 'key' in touch.ud and touch.ud['key'] != self.get_key_at_pos(x, y): touch.ud['key'] = None def on_touch_up(self, touch): if touch.ud is None: return x, y = self.to_local(*touch.pos) if 'key' in touch.ud and touch.ud['key'] == self.get_key_at_pos(x, y) and self.get_key_at_pos(x, y) is not None: displayed_char, internal, special_char, size = touch.ud['key'][0] b_keycode = special_char b_modifiers = self._get_modifiers() if special_char.startswith('sug'): if internal is not None and internal != '': word = self.get_current_word() textarea = self.get_text_area() textarea.select_text(textarea.cursor_index() - len(word), textarea.cursor_index()) textarea.delete_selection() if internal is not None and len(internal) == 1 and not (len(special_char) == 1 and special_char.isalpha()): prev_word = str(self.get_previous_word()) cur_word = str(self.get_current_word()) if cur_word != '': self.user_nograms += 1 if cur_word not in self.words: self.words[cur_word.lower()] = self.val_dist(tuple(map(self.key_centers.__getitem__, cur_word.lower()))) + (0.0,) self.user_unigrams[cur_word] = self.user_unigrams.get(cur_word, 0) + 1 if prev_word != '': self.user_bigrams[(prev_word, cur_word)] = self.user_bigrams.get((prev_word, cur_word), 0) + 1 self.dispatch('on_key_down', b_keycode, internal, b_modifiers) matches = self.candidate_guesses()[:6] self.update_candidates(matches) else: self.dispatch('on_key_down', b_keycode, internal, b_modifiers) else: self.dispatch('on_key_down', b_keycode, internal, b_modifiers) if (len(special_char) == 1 and special_char.isalpha()) or special_char == 'backspace': word = self.get_current_word() if len(word) >= 4: matches = self.candidate_predictions(word)[:6] else: matches = [] self.update_candidates(matches) elif 'ctrl' in touch.ud and self.get_key_at_pos(x, y) is not None: displayed_char, internal, special_char, size = self.get_key_at_pos(x, y)[0] k = special_char if k == 'c': textarea = self.get_text_area() Clipboard.put(textarea.selection_text, 'STRING') textarea.cancel_selection() elif k == 'v': textarea = self.get_text_area() textarea.delete_selection() textarea.insert_text(Clipboard.get('STRING')) elif k == 'a': textarea = self.get_text_area() textarea.select_all() elif k == 'x': textarea = self.get_text_area() Clipboard.put(textarea.selection_text, 'STRING') textarea.delete_selection() elif k == 'z': textarea = self.get_text_area() textarea.do_undo() elif k == 'y': textarea = self.get_text_area() textarea.do_redo() elif k == 'l': available_layouts = self.available_layouts.keys() self.layout = available_layouts[(available_layouts.index(self.layout) + 1) % len(self.available_layouts)] self.reload_layout() #elif k == 's': # window = self.get_parent_window() # textarea = self.get_text_area() # def _on_close(self): # window.children[0].remove_widget(settings) # textarea.focus = True # settings = Settings(on_close=_on_close) # settings.add_json_panel('VKeyboard Settings', self.config, 'settings.json') # window.children[1].add_widget(settings) # window.release_keyboard(self) elif 'line' in touch.ud: gesture = touch.ud['line'].points gesture = [(gesture[i], gesture[i+1]) for i in xrange(0, len(gesture), 2)] matches = self.candidate_matches(gesture)[:6] self.update_candidates(matches) b_modifiers = self._get_modifiers() if 'shift' in b_modifiers and 'capslock' not in b_modifiers: matches = [(w[0].upper() + w[1:], p) for w, p in matches] elif 'capslock' in b_modifiers and 'shift' not in b_modifiers: matches = [(w.upper(), p) for w, p in matches] if len(matches) > 0: textarea = self.get_text_area() textarea.delete_selection() textarea.insert_text(matches[0][0]) if touch.grab_current is self: self.process_key_up(touch) if 'line' in touch.ud: self.canvas.remove(touch.ud['line']) return super(VKeyboard, self).on_touch_up(touch) def update_candidates(self, matches): layout = self.available_layouts[self.layout] i = -1 for i, (w, p) in enumerate(matches): layout['normal_1'][i] = [unicode(w), unicode(w), u'sug%d' % i, 2.5] layout['shift_1'][i] = [unicode(w[0].upper() + w[1:]), unicode(w[0].upper() + w[1:]), u'sug%d' % i, 2.5] layout['capslock_1'][i] = [unicode(w.upper()), unicode(w.upper()), u'sug%d' % i, 2.5] b_modifiers = self._get_modifiers() if ('shift' in b_modifiers) == ('capslock' in b_modifiers): self.labels[i].text = unicode(w) elif 'shift' in b_modifiers: self.labels[i].text = unicode(w[0].upper() + w[1:]) else: self.labels[i].text = unicode(w.upper()) for j in xrange(i + 1, 6): layout['normal_1'][j] = [u'', u'', u'sug%d' % j, 2.5] layout['shift_1'][j] = [u'', u'', u'sug%d' % j, 2.5] layout['capslock_1'][j] = [u'', u'', u'sug%d' % j, 2.5] self.labels[j].text = u''