277 lines
9.5 KiB
Python
277 lines
9.5 KiB
Python
|
__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
|