예제 #1
0
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)
예제 #2
0
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)
예제 #3
0
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)
예제 #4
0
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)
예제 #5
0
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)
예제 #6
0
파일: vkeyboard.py 프로젝트: hansent/kivy
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)
예제 #7
0
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
예제 #8
0
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
예제 #9
0
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
예제 #10
0
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
예제 #11
0
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
예제 #12
0
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)
예제 #13
0
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''