Version

Quick search

Multistroke Recognition Database Demonstration

pic22

This application records gestures and attempts to match them. You should see a black drawing surface with some buttons across the bottom. As you make a gesture on the drawing surface, the gesture will be added to the history and a match will be attempted. If you go to the history tab, name the gesture, and add it to the database, then similar gestures in the future will be recognized. You can load and save databases of gestures in .kg files.

This demonstration code spans many files, with this being the primary file. The information pop-up (‘No match’) comes from the file helpers.py. The history pane is managed in the file historymanager.py and described in the file historymanager.kv. The database pane and storage is managed in the file gesturedatabase.py and the described in the file gesturedatabase.kv. The general logic of the sliders and buttons are in the file settings.py and described in settings.kv. but the actual settings pane is described in the file multistroke.kv and managed from this file.

File demo/multistroke/main.py

'''
Multistroke Recognition Database Demonstration
==============================================

This application records gestures and attempts to match them. You should
see a black drawing surface with some buttons across the bottom. As you
make a gesture on the drawing surface, the gesture will be added to
the history and a match will be attempted. If you go to the history tab,
name the gesture, and add it to the database, then similar gestures in the
future will be recognized. You can load and save databases of gestures
in .kg files.

This demonstration code spans many files, with this being the primary file.
The information pop-up ('No match') comes from the file helpers.py.
The history pane is managed in the file historymanager.py and described
in the file historymanager.kv. The database pane and storage is managed in
the file gesturedatabase.py and the described in the file gesturedatabase.kv.
The general logic of the sliders and buttons are in the file
settings.py and described in settings.kv. but the actual settings pane is
described in the file multistroke.kv and managed from this file.

'''
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.gesturesurface import GestureSurface
from kivy.uix.screenmanager import ScreenManager, Screen, SlideTransition
from kivy.uix.label import Label
from kivy.multistroke import Recognizer

# Local libraries
from historymanager import GestureHistoryManager
from gesturedatabase import GestureDatabase
from settings import MultistrokeSettingsContainer


class MainMenu(GridLayout):
    pass


class MultistrokeAppSettings(MultistrokeSettingsContainer):
    pass


class MultistrokeApp(App):

    def goto_database_screen(self, *l):
        self.database.import_gdb()
        self.manager.current = 'database'

    def handle_gesture_cleanup(self, surface, g, *l):
        if hasattr(g, '_result_label'):
            surface.remove_widget(g._result_label)

    def handle_gesture_discard(self, surface, g, *l):
        # Don't bother creating Label if it's not going to be drawn
        if surface.draw_timeout == 0:
            return

        text = '[b]Discarded:[/b] Not enough input'
        g._result_label = Label(text=text, markup=True, size_hint=(None, None),
                                center=(g.bbox['minx'], g.bbox['miny']))
        self.surface.add_widget(g._result_label)

    def handle_gesture_complete(self, surface, g, *l):
        result = self.recognizer.recognize(g.get_vectors())
        result._gesture_obj = g
        result.bind(on_complete=self.handle_recognize_complete)

    def handle_recognize_complete(self, result, *l):
        self.history.add_recognizer_result(result)

        # Don't bother creating Label if it's not going to be drawn
        if self.surface.draw_timeout == 0:
            return

        best = result.best
        if best['name'] is None:
            text = '[b]No match[/b]'
        else:
            text = 'Name: [b]%s[/b]\nScore: [b]%f[/b]\nDistance: [b]%f[/b]' % (
                   best['name'], best['score'], best['dist'])

        g = result._gesture_obj
        g._result_label = Label(text=text, markup=True, size_hint=(None, None),
                                center=(g.bbox['minx'], g.bbox['miny']))
        self.surface.add_widget(g._result_label)

    def build(self):
        # Setting NoTransition breaks the "history" screen! Possibly related
        # to some inexplicable rendering bugs on my particular system
        self.manager = ScreenManager(transition=SlideTransition(
                                     duration=.15))
        self.recognizer = Recognizer()

        # Setup the GestureSurface and bindings to our Recognizer
        surface = GestureSurface(line_width=2, draw_bbox=True,
                                 use_random_color=True)
        surface_screen = Screen(name='surface')
        surface_screen.add_widget(surface)
        self.manager.add_widget(surface_screen)

        surface.bind(on_gesture_discard=self.handle_gesture_discard)
        surface.bind(on_gesture_complete=self.handle_gesture_complete)
        surface.bind(on_gesture_cleanup=self.handle_gesture_cleanup)
        self.surface = surface

        # History is the list of gestures drawn on the surface
        history = GestureHistoryManager()
        history_screen = Screen(name='history')
        history_screen.add_widget(history)
        self.history = history
        self.manager.add_widget(history_screen)

        # Database is the list of gesture templates in Recognizer
        database = GestureDatabase(recognizer=self.recognizer)
        database_screen = Screen(name='database')
        database_screen.add_widget(database)
        self.database = database
        self.manager.add_widget(database_screen)

        # Settings screen
        app_settings = MultistrokeAppSettings()
        ids = app_settings.ids

        ids.max_strokes.bind(value=surface.setter('max_strokes'))
        ids.temporal_win.bind(value=surface.setter('temporal_window'))
        ids.timeout.bind(value=surface.setter('draw_timeout'))
        ids.line_width.bind(value=surface.setter('line_width'))
        ids.draw_bbox.bind(value=surface.setter('draw_bbox'))
        ids.use_random_color.bind(value=surface.setter('use_random_color'))

        settings_screen = Screen(name='settings')
        settings_screen.add_widget(app_settings)
        self.manager.add_widget(settings_screen)

        # Wrap in a gridlayout so the main menu is always visible
        layout = GridLayout(cols=1)
        layout.add_widget(self.manager)
        layout.add_widget(MainMenu())
        return layout


if __name__ in ('__main__', '__android__'):
    MultistrokeApp().run()

File demo/multistroke/helpers.py

__all__ = ('InformationPopup', )

from kivy.uix.popup import Popup
from kivy.properties import StringProperty
from kivy.factory import Factory
from kivy.lang import Builder
from kivy.clock import Clock

Builder.load_string('''
<InformationPopup>:
    auto_dismiss: True
    size_hint: None, None
    size: 400, 200
    on_open: root.dismiss_trigger()
    title: root.title
    Label:
        text: root.text
''')


class InformationPopup(Popup):
    title = StringProperty('Information')
    text = StringProperty('')

    def __init__(self, time=1.5, **kwargs):
        super(InformationPopup, self).__init__(**kwargs)
        self.dismiss_trigger = Clock.create_trigger(self.dismiss, time)


Factory.register('InformationPopup', cls=InformationPopup)

File demo/multistroke/historymanager.py

__all__ = ('GestureHistoryManager', 'GestureVisualizer')

from kivy.app import App
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.uix.widget import Widget
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.graphics import Color, Line
from kivy.properties import ObjectProperty, BooleanProperty
from kivy.compat import PY2

# local libraries
from helpers import InformationPopup
from settings import MultistrokeSettingsContainer


# refuse heap permute for gestures with more strokes than 3
# (you can increase it, but 4 strokes = 384 templates, 5 = 3840)
MAX_PERMUTE_STROKES = 3

Builder.load_file('historymanager.kv')


class GestureHistoryManager(GridLayout):
    selected = ObjectProperty(None, allownone=True)

    def __init__(self, **kwargs):
        super(GestureHistoryManager, self).__init__(**kwargs)
        self.gesturesettingsform = GestureSettingsForm()
        rr = self.gesturesettingsform.rrdetails
        rr.bind(on_reanalyze_selected=self.reanalyze_selected)
        self.infopopup = InformationPopup()
        self.recognizer = App.get_running_app().recognizer

    def reanalyze_selected(self, *l):
        # recognize() can block the UI with max_gpf=100, show a message
        self.infopopup.text = 'Please wait, analyzing ..'
        self.infopopup.auto_dismiss = False
        self.infopopup.open()

        # Get a reference to the original GestureContainer object
        gesture_obj = self.selected._result_obj._gesture_obj

        # Reanalyze the candidate strokes using current database
        res = self.recognizer.recognize(gesture_obj.get_vectors(),
                                        max_gpf=100)

        # Tag the result with the gesture object (it didn't change)
        res._gesture_obj = gesture_obj

        # Tag the selected item with the updated ProgressTracker
        self.selected._result_obj = res
        res.bind(on_complete=self._reanalyze_complete)

    def _reanalyze_complete(self, *l):
        self.gesturesettingsform.load_visualizer(self.selected)
        self.infopopup.dismiss()

    def add_selected_to_database(self, *l):
        if self.selected is None:
            raise Exception('add_gesture_to_database before load_visualizer?')

        if self.gesturesettingsform.addsettings is None:
            raise Exception('add_gesture_to_database missing addsetings?')

        ids = self.gesturesettingsform.addsettings.ids

        name = ids.name.value.strip()
        if name == '':
            self.infopopup.auto_dismiss = True
            self.infopopup.text = 'You must specify a name for the gesture'
            self.infopopup.open()
            return

        permute = ids.permute.value
        sensitive = ids.orientation_sens.value
        strokelen = ids.stroke_sens.value
        angle_sim = ids.angle_sim.value

        cand = self.selected._result_obj._gesture_obj.get_vectors()

        if permute and len(cand) > MAX_PERMUTE_STROKES:
            t = "Can't heap permute %d-stroke gesture " % (len(cand))
            self.infopopup.text = t
            self.infopopup.auto_dismiss = True
            self.infopopup.open()
            return

        self.recognizer.add_gesture(
            name,
            cand,
            use_strokelen=strokelen,
            orientation_sensitive=sensitive,
            angle_similarity=angle_sim,
            permute=permute)

        self.infopopup.text = 'Gesture added to database'
        self.infopopup.auto_dismiss = True
        self.infopopup.open()

    def clear_history(self, *l):
        if self.selected:
            self.visualizer_deselect()
        self.ids.history.clear_widgets()

    def visualizer_select(self, visualizer, *l):
        if self.selected is not None:
            self.selected.selected = False
        else:
            self.add_widget(self.gesturesettingsform)

        self.gesturesettingsform.load_visualizer(visualizer)
        self.selected = visualizer

    def visualizer_deselect(self, *l):
        self.selected = None
        self.remove_widget(self.gesturesettingsform)

    def add_recognizer_result(self, result, *l):
        '''The result object is a ProgressTracker with additional
        data; in main.py it is tagged with the original GestureContainer
        that was analyzed (._gesture_obj)'''

        # Create a GestureVisualizer that draws the gesture on canvas
        visualizer = GestureVisualizer(result._gesture_obj,
                                       size_hint=(None, None), size=(150, 150))

        # Tag it with the result object so AddGestureForm.load_visualizer
        # has the results to build labels in the scrollview
        visualizer._result_obj = result

        visualizer.bind(on_select=self.visualizer_select)
        visualizer.bind(on_deselect=self.visualizer_deselect)

        # Add the visualizer to the list of gestures in 'history' screen
        self.ids.history.add_widget(visualizer)
        self._trigger_layout()
        self.ids.scrollview.update_from_scroll()


class RecognizerResultLabel(Label):
    '''This Label subclass is used to show a single result from the
    gesture matching process (is a child of GestureHistoryManager)'''
    pass


class RecognizerResultDetails(BoxLayout):
    '''Contains a ScrollView of RecognizerResultLabels, ie the list of
    matched gestures and their score/distance (is a child of
    GestureHistoryManager)'''
    def __init__(self, **kwargs):
        super(RecognizerResultDetails, self).__init__(**kwargs)
        self.register_event_type('on_reanalyze_selected')

    def on_reanalyze_selected(self, *l):
        pass


class AddGestureSettings(MultistrokeSettingsContainer):
    pass


class GestureSettingsForm(BoxLayout):
    '''This is the main content of the GestureHistoryManager, the form for
    adding a new gesture to the recognizer. It is added to the widget tree
    when a GestureVisualizer is selected.'''

    def __init__(self, **kwargs):
        super(GestureSettingsForm, self).__init__(**kwargs)
        self.infopopup = InformationPopup()
        self.rrdetails = RecognizerResultDetails()
        self.addsettings = None
        self.app = App.get_running_app()

    def load_visualizer(self, visualizer):
        if self.addsettings is None:
            self.addsettings = AddGestureSettings()
            self.ids.settings.add_widget(self.addsettings)

        self.visualizer = visualizer
        analysis = self.ids.analysis
        analysis.clear_widgets()
        analysis.add_widget(self.rrdetails)

        scrollv = self.rrdetails.ids.result_scrollview
        resultlist = self.rrdetails.ids.result_list
        resultlist.clear_widgets()

        r = visualizer._result_obj.results

        if not len(r):
            lbl = RecognizerResultLabel(text='[b]No match[/b]')
            resultlist.add_widget(lbl)
            scrollv.scroll_y = 1
            return

        if PY2:
            d = r.iteritems
        else:
            d = r.items

        for one in sorted(d(), key=lambda x: x[1]['score'],
                          reverse=True):
            data = one[1]
            lbl = RecognizerResultLabel(
                text='Name: [b]' + data['name'] + '[/b]' +
                     '\n      Score: ' + str(data['score']) +
                     '\n      Distance: ' + str(data['dist']))
            resultlist.add_widget(lbl)

        # Make sure the top is visible
        scrollv.scroll_y = 1


class GestureVisualizer(Widget):
    selected = BooleanProperty(False)

    def __init__(self, gesturecontainer, **kwargs):
        super(GestureVisualizer, self).__init__(**kwargs)

        self._gesture_container = gesturecontainer

        self._trigger_draw = Clock.create_trigger(self._draw_item, 0)
        self.bind(pos=self._trigger_draw, size=self._trigger_draw)
        self._trigger_draw()

        self.register_event_type('on_select')
        self.register_event_type('on_deselect')

    def on_touch_down(self, touch):
        if not self.collide_point(touch.x, touch.y):
            return
        self.selected = not self.selected
        self.dispatch(self.selected and 'on_select' or 'on_deselect')

    # FIXME: This seems inefficient, is there a better way??
    def _draw_item(self, dt):
        g = self._gesture_container
        bb = g.bbox
        minx, miny, maxx, maxy = bb['minx'], bb['miny'], bb['maxx'], bb['maxy']
        width, height = self.size
        xpos, ypos = self.pos

        if g.height > g.width:
            to_self = (height * 0.85) / g.height
        else:
            to_self = (width * 0.85) / g.width

        self.canvas.remove_group('gesture')

        cand = g.get_vectors()
        col = g.color
        for stroke in cand:
            out = []
            append = out.append
            for vec in stroke:
                x, y = vec
                x = (x - minx) * to_self
                w = (maxx - minx) * to_self
                append(x + xpos + (width - w) * .85 / 2)

                y = (y - miny) * to_self
                h = (maxy - miny) * to_self
                append(y + ypos + (height - h) * .85 / 2)

            with self.canvas:
                Color(col[0], col[1], col[2], mode='rgb')
                Line(points=out, group='gesture', width=2)

    def on_select(self, *l):
        pass

    def on_deselect(self, *l):
        pass

File demo/multistroke/historymanager.kv

  1<GestureHistoryManager>:
  2    rows: 1
  3    spacing: 10
  4    GridLayout:
  5        cols: 1
  6        size_hint_x: None
  7        width: 150
  8        canvas:
  9            Color:
 10                rgba: 1, 1, 1, .1
 11            Rectangle:
 12                size: self.size
 13                pos: self.pos
 14
 15        Button:
 16            text: 'Clear History'
 17            size_hint_y: None
 18            height: 50
 19            on_press: root.clear_history()
 20
 21        ScrollView:
 22            id: scrollview
 23            scroll_type: ['bars', 'content']
 24            bar_width: 4
 25            GridLayout:
 26                id: history
 27                cols: 1
 28                size_hint: 1, None
 29                height: self.minimum_height
 30
 31<GestureSettingsForm>:
 32    orientation: 'vertical'
 33    spacing: 10
 34    GridLayout:
 35        id: settings
 36        cols: 1
 37        top: root.top
 38        Label:
 39            text: '[b]Results (scroll for more)[/b]'
 40            markup: True
 41            size_hint_y: None
 42            height: 30
 43            halign: 'left'
 44            valign: 'middle'
 45            text_size: self.size
 46            canvas:
 47                Color:
 48                    rgba: 47 / 255., 167 / 255., 212 / 255., .4
 49                Rectangle:
 50                    pos: self.x, self.y + 1
 51                    size: self.size
 52                Color:
 53                    rgb: .5, .5, .5
 54                Rectangle:
 55                    pos: self.x, self.y - 2
 56                    size: self.width, 1
 57
 58        GridLayout:
 59            id: analysis
 60            top: root.top
 61            rows: 1
 62
 63<GestureVisualizer>:
 64    canvas:
 65        Color:
 66            rgba: 1, 1, 1, self.selected and .3 or .1
 67        Rectangle:
 68            pos: self.pos
 69            size: self.size
 70
 71
 72<RecognizerResultDetails>:
 73    canvas:
 74        Color:
 75            rgba: 1, 0, 0, .1
 76        Rectangle:
 77            size: self.size
 78            pos: self.pos
 79
 80    ScrollView:
 81        id: result_scrollview
 82        scroll_type: ['bars', 'content']
 83        bar_width: 4
 84        GridLayout:
 85            id: result_list
 86            cols: 1
 87            size_hint: 1, None
 88            height: self.minimum_height
 89
 90    Button:
 91        size_hint: None, None
 92        width: 150
 93        height: 70
 94        text: 'Re-analyze'
 95        on_press: root.dispatch('on_reanalyze_selected')
 96
 97
 98<RecognizerResultLabel>:
 99    size_hint_y: None
100    height: 70
101    markup: True
102    halign: 'left'
103    valign: 'top'
104    text_size: self.size
105
106
107<AddGestureSettings>:
108    MultistrokeSettingTitle:
109        title: 'New gesture settings'
110        desc: 'Affects how to future input is matched against new gesture'
111
112    MultistrokeSettingBoolean:
113        id: permute
114        title: 'Use Heap Permute algorithm?'
115        desc:
116            ('This will generate all possible stroke orders from the ' +
117            'input. Only suitable for gestures with 1-3 strokes (or ' +
118            'the number of templates will be huge)')
119        button_text: 'Heap Permute?'
120        value: True
121
122    MultistrokeSettingBoolean:
123        id: stroke_sens
124        title: 'Require same number of strokes?'
125        desc:
126            ('When enabled, the new gesture will only match candidates ' +
127            'with exactly the same stroke count. Enable if possible.')
128        button_text: 'Stroke sensitive?'
129        value: True
130
131    MultistrokeSettingBoolean:
132        id: orientation_sens
133        title: 'Is gesture orientation sensitive?'
134        desc:
135            ('Enable to differentiate gestures that differ only by ' +
136            'orientation (d/p, b/q, w/m), disable for gestures that ' +
137            'look the same in any orientation (like a circle)')
138        button_text: 'Orientation\nsensitive?'
139        value: True
140
141    MultistrokeSettingSlider:
142        id: angle_sim
143        title: 'Angle similarity threshold'
144        type: 'float'
145        desc:
146            ('Use a low number to distinguish similar gestures, higher ' +
147            'number to match similar gestures (with differing angle)')
148        value: 30.
149        min: 1.0
150        max: 179.0
151
152    MultistrokeSettingString:
153        id: name
154        title: 'Gesture name'
155        type: 'float'
156        desc:
157            ('Name of new gesture (including all generated templates). ' +
158            'You can have as many gestures with the same name as you need')
159
160    Button:
161        size_hint_y: None
162        height: 40
163        text: 'Add to database'
164        on_press: root.parent.parent.parent.add_selected_to_database()

File demo/multistroke/gesturedatabase.py

  1__all__ = ('GestureDatabase', 'GestureDatabaseItem')
  2
  3from kivy.clock import Clock
  4from kivy.lang import Builder
  5from kivy.properties import NumericProperty, StringProperty
  6from kivy.properties import ListProperty, ObjectProperty
  7from kivy.uix.gridlayout import GridLayout
  8from kivy.uix.floatlayout import FloatLayout
  9from kivy.uix.popup import Popup
 10from kivy.graphics import Rectangle, Color
 11from kivy.multistroke import Recognizer
 12
 13# local libraries
 14from helpers import InformationPopup
 15
 16
 17Builder.load_file('gesturedatabase.kv')
 18
 19
 20class GestureExportPopup(Popup):
 21    pass
 22
 23
 24class GestureImportPopup(Popup):
 25    pass
 26
 27
 28class GestureDatabaseItem(FloatLayout):
 29    name = StringProperty('(no name)')
 30    template_count = NumericProperty(0)
 31    gesture_list = ListProperty([])
 32
 33    def __init__(self, **kwargs):
 34        super(GestureDatabaseItem, self).__init__(**kwargs)
 35        self.rect = None
 36        self._draw_trigger = Clock.create_trigger(self.draw_item, 0)
 37        self.update_template_count()
 38        self.bind(gesture_list=self.update_template_count)
 39        self.register_event_type('on_select')
 40        self.register_event_type('on_deselect')
 41
 42    def toggle_selected(self, *l):
 43        self._draw_rect(clear=True)
 44        if self.ids.select.state == 'down':
 45            self.dispatch('on_select')
 46            self.ids.select.text = 'Deselect'
 47        else:
 48            self.dispatch('on_deselect')
 49            self.ids.select.text = 'Select'
 50
 51    def update_template_count(self, *l):
 52        tpl_count = 0
 53        for g in self.gesture_list:
 54            tpl_count += len(g.templates)
 55        self.template_count = tpl_count
 56
 57    def draw_item(self, *l):
 58        self.ids.namelbl.pos = self.pos
 59        self.ids.namelbl.y += 90
 60        self.ids.stats.pos = self.pos
 61        self.ids.stats.y += 40
 62        self.ids.select.pos = self.pos
 63        self._draw_rect()
 64
 65    def _draw_rect(self, clear=False, *l):
 66        col = self.ids.select.state == 'down' and 1 or .2
 67        with self.canvas:
 68            Color(col, 0, 0, .15)
 69            if self.rect or clear:
 70                self.canvas.remove(self.rect)
 71            self.rect = Rectangle(size=self.size, pos=self.pos)
 72
 73    def on_select(*l):
 74        pass
 75
 76    def on_deselect(*l):
 77        pass
 78
 79
 80class GestureDatabase(GridLayout):
 81    selected_count = NumericProperty(0)
 82    recognizer = ObjectProperty(None)
 83    export_popup = ObjectProperty(GestureExportPopup())
 84    import_popup = ObjectProperty(GestureImportPopup())
 85    info_popup = ObjectProperty(InformationPopup())
 86
 87    def __init__(self, **kwargs):
 88        super(GestureDatabase, self).__init__(**kwargs)
 89        self.redraw_all = Clock.create_trigger(self._redraw_gesture_list, 0)
 90        self.export_popup.ids.save_btn.bind(on_press=self.perform_export)
 91        self.import_popup.ids.filechooser.bind(on_submit=self.perform_import)
 92
 93    def import_gdb(self):
 94        self.gdict = {}
 95        for gesture in self.recognizer.db:
 96            if gesture.name not in self.gdict:
 97                self.gdict[gesture.name] = []
 98            self.gdict[gesture.name].append(gesture)
 99
100        self.selected_count = 0
101        self.ids.gesture_list.clear_widgets()
102        for k in sorted(self.gdict, key=lambda n: n.lower()):
103            gitem = GestureDatabaseItem(name=k, gesture_list=self.gdict[k])
104            gitem.bind(on_select=self.select_item)
105            gitem.bind(on_deselect=self.deselect_item)
106            self.ids.gesture_list.add_widget(gitem)
107
108    def select_item(self, *l):
109        self.selected_count += 1
110
111    def deselect_item(self, *l):
112        self.selected_count -= 1
113
114    def mass_select(self, *l):
115        if self.selected_count:
116            for i in self.ids.gesture_list.children:
117                if i.ids.select.state == 'down':
118                    i.ids.select.state = 'normal'
119                    i.draw_item()
120        else:
121            for i in self.ids.gesture_list.children:
122                if i.ids.select.state == 'normal':
123                    i.ids.select.state = 'down'
124                    i.draw_item()
125
126    def unload_gestures(self, *l):
127        if not self.selected_count:
128            self.recognizer.db = []
129            self.ids.gesture_list.clear_widgets()
130            self.selected_count = 0
131            return
132
133        for i in self.ids.gesture_list.children[:]:
134            if i.ids.select.state == 'down':
135                self.selected_count -= 1
136                for g in i.gesture_list:
137                    # if g in self.recognizer.db:  # not needed, for testing
138                    self.recognizer.db.remove(g)
139                    self.ids.gesture_list.remove_widget(i)
140
141    def perform_export(self, *l):
142        path = self.export_popup.ids.filename.text
143        if not path:
144            self.export_popup.dismiss()
145            self.info_popup.text = 'Missing filename'
146            self.info_popup.open()
147            return
148        elif not path.lower().endswith('.kg'):
149            path += '.kg'
150
151        self.save_selection_to_file(path)
152
153        self.export_popup.dismiss()
154        self.info_popup.text = 'Gestures exported!'
155        self.info_popup.open()
156
157    def perform_import(self, filechooser, *l):
158        count = len(self.recognizer.db)
159        for f in filechooser.selection:
160            self.recognizer.import_gesture(filename=f)
161        self.import_gdb()
162        self.info_popup.text = ("Imported %d gestures.\n" %
163                                (len(self.recognizer.db) - count))
164        self.import_popup.dismiss()
165        self.info_popup.open()
166
167    def save_selection_to_file(self, filename, *l):
168        if not self.selected_count:
169            self.recognizer.export_gesture(filename=filename)
170        else:
171            tmpgdb = Recognizer()
172            for i in self.ids.gesture_list.children:
173                if i.ids.select.state == 'down':
174                    for g in i.gesture_list:
175                        tmpgdb.db.append(g)
176            tmpgdb.export_gesture(filename=filename)
177
178    def _redraw_gesture_list(self, *l):
179        for child in self.ids.gesture_list.children:
180            child._draw_trigger()

File demo/multistroke/gesturedatabase.kv

  1#:import os os
  2
  3<GestureDatabaseItem>:
  4    size_hint: None, None
  5    size: 120, 130
  6    on_pos: self._draw_trigger()
  7    on_size: self._draw_trigger()
  8    Label:
  9        id: namelbl
 10        text: root.name
 11        size_hint: 1, None
 12        height: 40
 13        font_size: 14
 14        color: 1, 0, 0, 1
 15    Label:
 16        id: stats
 17        text:
 18            ( str(root.template_count) + " templates\nin " +
 19            str(len(root.gesture_list)) + ' gestures' )
 20        size_hint: 1, None
 21        height: 60
 22    ToggleButton:
 23        id: select
 24        text: 'Select'
 25        size_hint: None, None
 26        size: 120, 30
 27        on_state: root.toggle_selected()
 28
 29<GestureDatabase>:
 30    rows: 1
 31    spacing: 10
 32    padding: 10
 33    cols_minimum: {0: 200}
 34    GridLayout:
 35        id: menu
 36        cols: 1
 37        spacing: 10
 38        padding: 10
 39        size_hint: None, 1
 40        width: 200
 41        Button:
 42            text: root.selected_count and 'Deselect all' or 'Select all'
 43            size_hint_y: None
 44            height: 100
 45            on_press: root.mass_select()
 46        Button:
 47            text:
 48                (root.selected_count
 49                and 'Save ' + str(root.selected_count) + ' gestures'
 50                or 'Save all')
 51            size_hint_y: None
 52            height: 100
 53            on_press: root.export_popup.open()
 54        Button:
 55            text:
 56                (root.selected_count
 57                and 'Unload ' + str(root.selected_count) + ' gestures'
 58                or 'Unload all')
 59            size_hint_y: None
 60            height: 100
 61            on_press: root.unload_gestures()
 62        Button:
 63            text: 'Load from file'
 64            size_hint_y: None
 65            height: 100
 66            on_press: root.import_popup.open()
 67    ScrollView:
 68        on_scroll_y: root.redraw_all()
 69        StackLayout:
 70            id: gesture_list
 71            spacing: 10
 72            padding: 10
 73            size_hint: 1, None
 74            height: self.minimum_height
 75
 76<GestureExportPopup>:
 77    title: 'Specify filename'
 78    auto_dismiss: True
 79    size_hint: None, None
 80    size: 400, 400
 81    GridLayout:
 82        cols: 1
 83        spacing: 10
 84        padding: 10
 85        rows_minimum: {0: 100}
 86        Label:
 87            text:
 88                ( 'The extension .kg will be appended automatically.\n' +
 89                'The file is saved to the current working directory, unless\n' +
 90                'you specify an absolute path')
 91        TextInput:
 92            id: filename
 93            multiline: False
 94            size_hint: 1, None
 95            height: 40
 96        Button:
 97            id: save_btn
 98            text: 'Save'
 99            size_hint: 1, None
100            height: 45
101        Button:
102            id: cancel_btn
103            text: 'Cancel'
104            size_hint: 1, None
105            height: 45
106            on_press: root.dismiss()
107
108<GestureImportPopup>:
109    auto_dismiss: True
110    size_hint: None, None
111    size: 450, 400
112    FileChooserListView:
113        id: filechooser
114        size_hint: None, None
115        size: 400, 380
116        filters: ['*.kg']
117        path: os.getcwd()

File demo/multistroke/multistroke.kv

  1<MainMenu>:
  2    rows: 1
  3    size_hint: (1, None)
  4    height: 50
  5    spacing: 5
  6    padding: 5
  7    ToggleButton:
  8        group: 'mainmenu'
  9        state: 'down'
 10        text: 'Gesture Surface'
 11        on_press:
 12            app.manager.current = 'surface'
 13            if self.state == 'normal': self.state = 'down'
 14    ToggleButton:
 15        group: 'mainmenu'
 16        text: 'History'
 17        on_press:
 18            app.manager.current = 'history'
 19            if self.state == 'normal': self.state = 'down'
 20    ToggleButton:
 21        group: 'mainmenu'
 22        text: 'Database'
 23        on_press:
 24            app.goto_database_screen()
 25            if self.state == 'normal': self.state = 'down'
 26    ToggleButton:
 27        group: 'mainmenu'
 28        text: 'Settings'
 29        on_press:
 30            app.manager.current = 'settings'
 31            if self.state == 'normal': self.state = 'down'
 32
 33<MultistrokeAppSettings>:
 34    pos_hint: {'top': 1}
 35
 36    MultistrokeSettingTitle:
 37        title: 'GestureSurface behavior'
 38        desc: 'Affects how gestures are detected and cleaned up'
 39
 40    MultistrokeSettingSlider:
 41        id: max_strokes
 42        title: 'Max strokes'
 43        type: 'int'
 44        desc:
 45            ('Max number of strokes for a single gesture. If 0, the ' +
 46            'gesture will only be analyzed once the temporal window has ' +
 47            'expired since the last strokes touch up event')
 48        value: 4
 49        min: 0
 50        max: 15
 51
 52    MultistrokeSettingSlider:
 53        id: temporal_win
 54        title: 'Temporal Window'
 55        type: 'float'
 56        desc:
 57            ('Time to wait from last touch up in a gesture before analyzing ' +
 58            'the input. If 0, only analyzed once Max Strokes is reached')
 59        value: 2.
 60        min: 0
 61        max: 60.
 62
 63    MultistrokeSettingTitle:
 64        title: 'Drawing'
 65        desc: 'Affects how gestures are visualized on the GestureSurface'
 66
 67    MultistrokeSettingSlider:
 68        id: timeout
 69        title: 'Draw Timeout'
 70        type: 'float'
 71        desc:
 72            ('How long to display the gesture (and result label) on the ' +
 73            'gesture surface once analysis has completed')
 74        value: 2.
 75        min: 0
 76        max: 60.
 77
 78    MultistrokeSettingSlider:
 79        id: line_width
 80        title: 'Line width'
 81        type: 'int'
 82        desc:
 83            ('Width of lines on the gesture surface; 0 does not draw ' +
 84            'anything; 1 uses OpenGL line, >1 uses custom drawing method.')
 85        value: 2
 86        min: 0
 87        max: 10
 88
 89    MultistrokeSettingBoolean:
 90        id: use_random_color
 91        title: 'Use random color?'
 92        desc: 'Use random color for each gesture? If disabled, white is used.'
 93        button_text: 'Random color?'
 94        value: True
 95
 96    MultistrokeSettingBoolean:
 97        id: draw_bbox
 98        title: 'Draw gesture bounding box?'
 99        desc: 'Enable to draw a bounding box around the gesture'
100        button_text: 'Draw bbox?'
101        value: True