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
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
<GestureHistoryManager>:
    rows: 1
    spacing: 10
    GridLayout:
        cols: 1
        size_hint_x: None
        width: 150
        canvas:
            Color:
                rgba: 1, 1, 1, .1
            Rectangle:
                size: self.size
                pos: self.pos

        Button:
            text: 'Clear History'
            size_hint_y: None
            height: 50
            on_press: root.clear_history()

        ScrollView:
            id: scrollview
            scroll_type: ['bars', 'content']
            bar_width: 4
            GridLayout:
                id: history
                cols: 1
                size_hint: 1, None
                height: self.minimum_height

<GestureSettingsForm>:
    orientation: 'vertical'
    spacing: 10
    GridLayout:
        id: settings
        cols: 1
        top: root.top
        Label:
            text: '[b]Results (scroll for more)[/b]'
            markup: True
            size_hint_y: None
            height: 30
            halign: 'left'
            valign: 'middle'
            text_size: self.size
            canvas:
                Color:
                    rgba: 47 / 255., 167 / 255., 212 / 255., .4
                Rectangle:
                    pos: self.x, self.y + 1
                    size: self.size
                Color:
                    rgb: .5, .5, .5
                Rectangle:
                    pos: self.x, self.y - 2
                    size: self.width, 1

        GridLayout:
            id: analysis
            top: root.top
            rows: 1

<GestureVisualizer>:
    canvas:
        Color:
            rgba: 1, 1, 1, self.selected and .3 or .1
        Rectangle:
            pos: self.pos
            size: self.size


<RecognizerResultDetails>:
    canvas:
        Color:
            rgba: 1, 0, 0, .1
        Rectangle:
            size: self.size
            pos: self.pos

    ScrollView:
        id: result_scrollview
        scroll_type: ['bars', 'content']
        bar_width: 4
        GridLayout:
            id: result_list
            cols: 1
            size_hint: 1, None
            height: self.minimum_height

    Button:
        size_hint: None, None
        width: 150
        height: 70
        text: 'Re-analyze'
        on_press: root.dispatch('on_reanalyze_selected')


<RecognizerResultLabel>:
    size_hint_y: None
    height: 70
    markup: True
    halign: 'left'
    valign: 'top'
    text_size: self.size


<AddGestureSettings>:
    MultistrokeSettingTitle:
        title: 'New gesture settings'
        desc: 'Affects how to future input is matched against new gesture'

    MultistrokeSettingBoolean:
        id: permute
        title: 'Use Heap Permute algorithm?'
        desc:
            ('This will generate all possible stroke orders from the ' +
            'input. Only suitable for gestures with 1-3 strokes (or ' +
            'the number of templates will be huge)')
        button_text: 'Heap Permute?'
        value: True

    MultistrokeSettingBoolean:
        id: stroke_sens
        title: 'Require same number of strokes?'
        desc:
            ('When enabled, the new gesture will only match candidates ' +
            'with exactly the same stroke count. Enable if possible.')
        button_text: 'Stroke sensitive?'
        value: True

    MultistrokeSettingBoolean:
        id: orientation_sens
        title: 'Is gesture orientation sensitive?'
        desc:
            ('Enable to differentiate gestures that differ only by ' +
            'orientation (d/p, b/q, w/m), disable for gestures that ' +
            'look the same in any orientation (like a circle)')
        button_text: 'Orientation\nsensitive?'
        value: True

    MultistrokeSettingSlider:
        id: angle_sim
        title: 'Angle similarity threshold'
        type: 'float'
        desc:
            ('Use a low number to distinguish similar gestures, higher ' +
            'number to match similar gestures (with differing angle)')
        value: 30.
        min: 1.0
        max: 179.0

    MultistrokeSettingString:
        id: name
        title: 'Gesture name'
        type: 'float'
        desc:
            ('Name of new gesture (including all generated templates). ' +
            'You can have as many gestures with the same name as you need')

    Button:
        size_hint_y: None
        height: 40
        text: 'Add to database'
        on_press: root.parent.parent.parent.add_selected_to_database()

File demo/multistroke/gesturedatabase.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
__all__ = ('GestureDatabase', 'GestureDatabaseItem')

from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import NumericProperty, StringProperty
from kivy.properties import ListProperty, ObjectProperty
from kivy.uix.gridlayout import GridLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.popup import Popup
from kivy.graphics import Rectangle, Color
from kivy.multistroke import Recognizer

# local libraries
from helpers import InformationPopup


Builder.load_file('gesturedatabase.kv')


class GestureExportPopup(Popup):
    pass


class GestureImportPopup(Popup):
    pass


class GestureDatabaseItem(FloatLayout):
    name = StringProperty('(no name)')
    template_count = NumericProperty(0)
    gesture_list = ListProperty([])

    def __init__(self, **kwargs):
        super(GestureDatabaseItem, self).__init__(**kwargs)
        self.rect = None
        self._draw_trigger = Clock.create_trigger(self.draw_item, 0)
        self.update_template_count()
        self.bind(gesture_list=self.update_template_count)
        self.register_event_type('on_select')
        self.register_event_type('on_deselect')

    def toggle_selected(self, *l):
        self._draw_rect(clear=True)
        if self.ids.select.state == 'down':
            self.dispatch('on_select')
            self.ids.select.text = 'Deselect'
        else:
            self.dispatch('on_deselect')
            self.ids.select.text = 'Select'

    def update_template_count(self, *l):
        tpl_count = 0
        for g in self.gesture_list:
            tpl_count += len(g.templates)
        self.template_count = tpl_count

    def draw_item(self, *l):
        self.ids.namelbl.pos = self.pos
        self.ids.namelbl.y += 90
        self.ids.stats.pos = self.pos
        self.ids.stats.y += 40
        self.ids.select.pos = self.pos
        self._draw_rect()

    def _draw_rect(self, clear=False, *l):
        col = self.ids.select.state == 'down' and 1 or .2
        with self.canvas:
            Color(col, 0, 0, .15)
            if self.rect or clear:
                self.canvas.remove(self.rect)
            self.rect = Rectangle(size=self.size, pos=self.pos)

    def on_select(*l):
        pass

    def on_deselect(*l):
        pass


class GestureDatabase(GridLayout):
    selected_count = NumericProperty(0)
    recognizer = ObjectProperty(None)
    export_popup = ObjectProperty(GestureExportPopup())
    import_popup = ObjectProperty(GestureImportPopup())
    info_popup = ObjectProperty(InformationPopup())

    def __init__(self, **kwargs):
        super(GestureDatabase, self).__init__(**kwargs)
        self.redraw_all = Clock.create_trigger(self._redraw_gesture_list, 0)
        self.export_popup.ids.save_btn.bind(on_press=self.perform_export)
        self.import_popup.ids.filechooser.bind(on_submit=self.perform_import)

    def import_gdb(self):
        self.gdict = {}
        for gesture in self.recognizer.db:
            if gesture.name not in self.gdict:
                self.gdict[gesture.name] = []
            self.gdict[gesture.name].append(gesture)

        self.selected_count = 0
        self.ids.gesture_list.clear_widgets()
        for k in sorted(self.gdict, key=lambda n: n.lower()):
            gitem = GestureDatabaseItem(name=k, gesture_list=self.gdict[k])
            gitem.bind(on_select=self.select_item)
            gitem.bind(on_deselect=self.deselect_item)
            self.ids.gesture_list.add_widget(gitem)

    def select_item(self, *l):
        self.selected_count += 1

    def deselect_item(self, *l):
        self.selected_count -= 1

    def mass_select(self, *l):
        if self.selected_count:
            for i in self.ids.gesture_list.children:
                if i.ids.select.state == 'down':
                    i.ids.select.state = 'normal'
                    i.draw_item()
        else:
            for i in self.ids.gesture_list.children:
                if i.ids.select.state == 'normal':
                    i.ids.select.state = 'down'
                    i.draw_item()

    def unload_gestures(self, *l):
        if not self.selected_count:
            self.recognizer.db = []
            self.ids.gesture_list.clear_widgets()
            self.selected_count = 0
            return

        for i in self.ids.gesture_list.children[:]:
            if i.ids.select.state == 'down':
                self.selected_count -= 1
                for g in i.gesture_list:
                    # if g in self.recognizer.db:  # not needed, for testing
                    self.recognizer.db.remove(g)
                    self.ids.gesture_list.remove_widget(i)

    def perform_export(self, *l):
        path = self.export_popup.ids.filename.text
        if not path:
            self.export_popup.dismiss()
            self.info_popup.text = 'Missing filename'
            self.info_popup.open()
            return
        elif not path.lower().endswith('.kg'):
            path += '.kg'

        self.save_selection_to_file(path)

        self.export_popup.dismiss()
        self.info_popup.text = 'Gestures exported!'
        self.info_popup.open()

    def perform_import(self, filechooser, *l):
        count = len(self.recognizer.db)
        for f in filechooser.selection:
            self.recognizer.import_gesture(filename=f)
        self.import_gdb()
        self.info_popup.text = ("Imported %d gestures.\n" %
                                (len(self.recognizer.db) - count))
        self.import_popup.dismiss()
        self.info_popup.open()

    def save_selection_to_file(self, filename, *l):
        if not self.selected_count:
            self.recognizer.export_gesture(filename=filename)
        else:
            tmpgdb = Recognizer()
            for i in self.ids.gesture_list.children:
                if i.ids.select.state == 'down':
                    for g in i.gesture_list:
                        tmpgdb.db.append(g)
            tmpgdb.export_gesture(filename=filename)

    def _redraw_gesture_list(self, *l):
        for child in self.ids.gesture_list.children:
            child._draw_trigger()

File demo/multistroke/gesturedatabase.kv

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#:import os os

<GestureDatabaseItem>:
    size_hint: None, None
    size: 120, 130
    on_pos: self._draw_trigger()
    on_size: self._draw_trigger()
    Label:
        id: namelbl
        text: root.name
        size_hint: 1, None
        height: 40
        font_size: 14
        color: 1, 0, 0, 1
    Label:
        id: stats
        text:
            ( str(root.template_count) + " templates\nin " +
            str(len(root.gesture_list)) + ' gestures' )
        size_hint: 1, None
        height: 60
    ToggleButton:
        id: select
        text: 'Select'
        size_hint: None, None
        size: 120, 30
        on_state: root.toggle_selected()

<GestureDatabase>:
    rows: 1
    spacing: 10
    padding: 10
    cols_minimum: {0: 200}
    GridLayout:
        id: menu
        cols: 1
        spacing: 10
        padding: 10
        size_hint: None, 1
        width: 200
        Button:
            text: root.selected_count and 'Deselect all' or 'Select all'
            size_hint_y: None
            height: 100
            on_press: root.mass_select()
        Button:
            text:
                (root.selected_count
                and 'Save ' + str(root.selected_count) + ' gestures'
                or 'Save all')
            size_hint_y: None
            height: 100
            on_press: root.export_popup.open()
        Button:
            text:
                (root.selected_count
                and 'Unload ' + str(root.selected_count) + ' gestures'
                or 'Unload all')
            size_hint_y: None
            height: 100
            on_press: root.unload_gestures()
        Button:
            text: 'Load from file'
            size_hint_y: None
            height: 100
            on_press: root.import_popup.open()
    ScrollView:
        on_scroll_y: root.redraw_all()
        StackLayout:
            id: gesture_list
            spacing: 10
            padding: 10
            size_hint: 1, None
            height: self.minimum_height

<GestureExportPopup>:
    title: 'Specify filename'
    auto_dismiss: True
    size_hint: None, None
    size: 400, 400
    GridLayout:
        cols: 1
        spacing: 10
        padding: 10
        rows_minimum: {0: 100}
        Label:
            text:
                ( 'The extension .kg will be appended automatically.\n' +
                'The file is saved to the current working directory, unless\n' +
                'you specify an absolute path')
        TextInput:
            id: filename
            multiline: False
            size_hint: 1, None
            height: 40
        Button:
            id: save_btn
            text: 'Save'
            size_hint: 1, None
            height: 45
        Button:
            id: cancel_btn
            text: 'Cancel'
            size_hint: 1, None
            height: 45
            on_press: root.dismiss()

<GestureImportPopup>:
    auto_dismiss: True
    size_hint: None, None
    size: 450, 400
    FileChooserListView:
        id: filechooser
        size_hint: None, None
        size: 400, 380
        filters: ['*.kg']
        path: os.getcwd()

File demo/multistroke/multistroke.kv

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
<MainMenu>:
    rows: 1
    size_hint: (1, None)
    height: 50
    spacing: 5
    padding: 5
    ToggleButton:
        group: 'mainmenu'
        state: 'down'
        text: 'Gesture Surface'
        on_press:
            app.manager.current = 'surface'
            if self.state == 'normal': self.state = 'down'
    ToggleButton:
        group: 'mainmenu'
        text: 'History'
        on_press:
            app.manager.current = 'history'
            if self.state == 'normal': self.state = 'down'
    ToggleButton:
        group: 'mainmenu'
        text: 'Database'
        on_press:
            app.goto_database_screen()
            if self.state == 'normal': self.state = 'down'
    ToggleButton:
        group: 'mainmenu'
        text: 'Settings'
        on_press:
            app.manager.current = 'settings'
            if self.state == 'normal': self.state = 'down'

<MultistrokeAppSettings>:
    pos_hint: {'top': 1}

    MultistrokeSettingTitle:
        title: 'GestureSurface behavior'
        desc: 'Affects how gestures are detected and cleaned up'

    MultistrokeSettingSlider:
        id: max_strokes
        title: 'Max strokes'
        type: 'int'
        desc:
            ('Max number of strokes for a single gesture. If 0, the ' +
            'gesture will only be analyzed once the temporal window has ' +
            'expired since the last strokes touch up event')
        value: 4
        min: 0
        max: 15

    MultistrokeSettingSlider:
        id: temporal_win
        title: 'Temporal Window'
        type: 'float'
        desc:
            ('Time to wait from last touch up in a gesture before analyzing ' +
            'the input. If 0, only analyzed once Max Strokes is reached')
        value: 2.
        min: 0
        max: 60.

    MultistrokeSettingTitle:
        title: 'Drawing'
        desc: 'Affects how gestures are visualized on the GestureSurface'

    MultistrokeSettingSlider:
        id: timeout
        title: 'Draw Timeout'
        type: 'float'
        desc:
            ('How long to display the gesture (and result label) on the ' +
            'gesture surface once analysis has completed')
        value: 2.
        min: 0
        max: 60.

    MultistrokeSettingSlider:
        id: line_width
        title: 'Line width'
        type: 'int'
        desc:
            ('Width of lines on the gesture surface; 0 does not draw ' +
            'anything; 1 uses OpenGL line, >1 uses custom drawing method.')
        value: 2
        min: 0
        max: 10

    MultistrokeSettingBoolean:
        id: use_random_color
        title: 'Use random color?'
        desc: 'Use random color for each gesture? If disabled, white is used.'
        button_text: 'Random color?'
        value: True

    MultistrokeSettingBoolean:
        id: draw_bbox
        title: 'Draw gesture bounding box?'
        desc: 'Enable to draw a bounding box around the gesture'
        button_text: 'Draw bbox?'
        value: True