Table Of Contents
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.
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