first commit

This commit is contained in:
Yura 2024-09-15 15:12:16 +03:00
commit 417e54da96
5696 changed files with 900003 additions and 0 deletions

View file

@ -0,0 +1,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()

View file

@ -0,0 +1,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()

View file

@ -0,0 +1,30 @@
__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)

View file

@ -0,0 +1,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()

View file

@ -0,0 +1,276 @@
__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

View file

@ -0,0 +1,144 @@
'''
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()

View file

@ -0,0 +1,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

View file

@ -0,0 +1,110 @@
<MultistrokeSettingsContainer>:
cols: 1
spacing: 5
padding: 5
size_hint_y: None
height: self.minimum_height
<MultistrokeSettingItem>:
size_hint_y: None
height: 70
rows: 1
canvas:
Color:
rgba: 47 / 255., 167 / 255., 212 / 255., .1
Rectangle:
pos: self.x, self.y + 1
size: self.size
Color:
rgb: .2, .2, .2
Rectangle:
pos: self.x, self.y - 2
size: self.width, 1
Label:
size_hint_x: .6
id: labellayout
markup: True
text: u'{0}\n[size=13sp][color=999999]{1}[/color][/size]'.format(root.title or '', root.desc or '')
font_size: '15sp'
text_size: self.size
valign: 'top'
<MultistrokeSettingTitle>:
size_hint_y: None
height: 40
markup: True
text: u'{0}\n[size=13sp][color=999999]{1}[/color][/size]'.format(root.title or '', root.desc or '')
font_size: '15sp'
text_size: self.size
valign: 'top'
halign: 'right'
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
<MultistrokeSettingBoolean>:
value: button.state == 'down' and True or False
ToggleButton:
id: button
text: root.button_text
size_hint_x: None
width: 150
pos: root.pos
state: root.value and 'down' or 'normal'
<MultistrokeSettingString>:
value: input.text
AnchorLayout:
size_hint_x: None
width: 150
TextInput:
id: input
size_hint_y: None
height: 30
pos: root.pos
multiline: True
text: root.value
<MultistrokeSettingSlider>:
value: slider.value
Label:
id: sliderlabel
size_hint_x: None
width: 50
text: str(root._to_numtype(slider.value))
Slider:
id: slider
min: root.min
max: root.max
pos: root.pos
size_hint_x: None
width: 150
value: root.value
<EditSettingPopup>:
size_hint: None, None
size: 300, 150
title: 'Edit setting'
BoxLayout:
orientation: 'vertical'
TextInput:
id: input
markup: False
multiline: False
on_text_validate: root.dispatch('on_validate', self.text)
BoxLayout:
orientation: 'horizontal'
Button:
text: 'OK'
on_press: root.dispatch('on_validate', input.text)
Button:
text: 'Cancel'
on_press: root.dismiss()

View file

@ -0,0 +1,97 @@
__all__ = ('MultistrokeSettingsContainer', 'MultistrokeSettingItem',
'MultistrokeSettingBoolean', 'MultistrokeSettingSlider',
'MultistrokeSettingString', 'MultistrokeSettingTitle')
from kivy.factory import Factory
from kivy.lang import Builder
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.properties import (StringProperty, NumericProperty, OptionProperty,
BooleanProperty)
from kivy.uix.popup import Popup
Builder.load_file('settings.kv')
class MultistrokeSettingsContainer(GridLayout):
pass
class MultistrokeSettingItem(GridLayout):
title = StringProperty('<No title set>')
desc = StringProperty('')
class MultistrokeSettingTitle(Label):
title = StringProperty('<No title set>')
desc = StringProperty('')
class MultistrokeSettingBoolean(MultistrokeSettingItem):
button_text = StringProperty('')
value = BooleanProperty(False)
class MultistrokeSettingString(MultistrokeSettingItem):
value = StringProperty('')
class EditSettingPopup(Popup):
def __init__(self, **kwargs):
super(EditSettingPopup, self).__init__(**kwargs)
self.register_event_type('on_validate')
def on_validate(self, *l):
pass
class MultistrokeSettingSlider(MultistrokeSettingItem):
min = NumericProperty(0)
max = NumericProperty(100)
type = OptionProperty('int', options=['float', 'int'])
value = NumericProperty(0)
def __init__(self, **kwargs):
super(MultistrokeSettingSlider, self).__init__(**kwargs)
self._popup = EditSettingPopup()
self._popup.bind(on_validate=self._validate)
self._popup.bind(on_dismiss=self._dismiss)
def _to_numtype(self, v):
try:
if self.type == 'float':
return round(float(v), 1)
else:
return int(v)
except ValueError:
return self.min
def _dismiss(self, *l):
self._popup.ids.input.focus = False
def _validate(self, instance, value):
self._popup.dismiss()
val = self._to_numtype(self._popup.ids.input.text)
if val < self.min:
val = self.min
elif val > self.max:
val = self.max
self.value = val
def on_touch_down(self, touch):
if not self.ids.sliderlabel.collide_point(*touch.pos):
return super(MultistrokeSettingSlider, self).on_touch_down(touch)
ids = self._popup.ids
ids.value = str(self.value)
ids.input.text = str(self._to_numtype(self.value))
self._popup.open()
ids.input.focus = True
ids.input.select_all()
Factory.register('MultistrokeSettingsContainer',
cls=MultistrokeSettingsContainer)
Factory.register('MultistrokeSettingTitle', cls=MultistrokeSettingTitle)
Factory.register('MultistrokeSettingBoolean', cls=MultistrokeSettingBoolean)
Factory.register('MultistrokeSettingSlider', cls=MultistrokeSettingSlider)
Factory.register('MultistrokeSettingString', cls=MultistrokeSettingString)