]> git.neil.brown.name Git - plato.git/commitdiff
Add scrawl.py
authorNeilBrown <neilb@suse.de>
Sat, 21 Apr 2012 10:48:01 +0000 (20:48 +1000)
committerNeilBrown <neilb@suse.de>
Sat, 21 Apr 2012 10:48:01 +0000 (20:48 +1000)
'scrawl' converts mouse/finger movements into letters and numbers.

Signed-off-by: NeilBrown <neilb@suse.de>
lib/scrawl.py [new file with mode: 0644]

diff --git a/lib/scrawl.py b/lib/scrawl.py
new file mode 100644 (file)
index 0000000..098a6e1
--- /dev/null
@@ -0,0 +1,1111 @@
+#! /usr/bin/env python
+
+# Copyright (C) 2011-2012 Neil Brown <neilb@suse.de>
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License along
+#    with this program; if not, write to the Free Software Foundation, Inc.,
+#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+#TODO
+# - Check combinations of not-enabled
+
+# "Scrawl" is a module for processing mouse movements and converting
+# them to ascii characters.
+#
+# Given a widget-window it collects mouse events and reports them
+# as some of the following:
+#  'tap' - press/release with minimal movement is a tap
+#  'sym' - press-draw-release is interpreted as a sym where possible
+#  'drag' - press-hold-move is interpretted as a drag.  A callout provides
+#         start and current points, and a 'finish' signal
+#  'select' press-draw-hold is interpretted as a selection.  On the final
+#         release, a list of points is passed to the callout
+#
+# Each of these can be disabled by clearing the relevant '*call' handler
+# If 'sym' is None, it is passed to 'select'
+# If 'drag' is none, an initial hold is ignored
+#
+
+import gtk, gobject, time, math
+
+class Scrawl:
+    def __init__(self, win, sym = None, tap=None, drag=None, select=None):
+        self.window = win
+        self.symcall = sym
+        self.tapcall = tap
+        self.dragcall = drag
+        self.selectcall = select
+        self.collectcall = None
+
+        self.dragging = None
+        self.selecting = None
+        self.dragtime = 500
+        self.selecttime = 500
+        self.timer = None
+
+        self.line = None
+        self.colour = None
+
+        self.dict = Dictionary()
+        LoadDict(self.dict)
+
+        if win:
+            win.add_events(gtk.gdk.POINTER_MOTION_MASK
+                           | gtk.gdk.BUTTON_PRESS_MASK
+                           | gtk.gdk.BUTTON_RELEASE_MASK
+                           )
+
+            self.presshan = win.connect("button_press_event", self.press)
+            self.releasehan = win.connect("button_release_event", self.release)
+            self.motionhan = win.connect("motion_notify_event", self.motion)
+
+    def press(self, c, ev):
+        # Start new line
+        c.stop_emission("button_press_event")
+        self.first_point(int(ev.x), int(ev.y))
+        self.dragging = False
+        self.selecting = False
+        if self.timer:
+            gobject.source_remove(self.timer)
+        if self.dragcall:
+            self.timer = gobject.timeout_add(self.dragtime, self.set_drag)
+        if self.collectcall:
+            self.collectcall(None, None, None)
+    def first_point(self, x, y):
+        self.line = [ [x, y] ]
+        self.bbox = BBox(Point(x,y))
+        
+    def set_drag(self):
+        self.dragging = True
+        self.dragcall(self.line[0], self.line[-1], False)
+    def set_select(self):
+        if self.selectcall:
+            self.selecting = True
+            self.selectcall(self.line, False)
+
+    def release(self, c, ev):
+        c.stop_emission("button_release_event")
+        if self.timer:
+            gobject.source_remove(self.timer)
+            self.timer = None
+
+        line = self.line
+        self.line = None
+        if line == None:
+            return
+        if self.dragging:
+            if len(line) == 1:
+                # this must be treated like a select, but close the drag
+                self.dragcall(line[0], line[0], True)
+                if self.selectcall:
+                    self.selectcall(line, False)
+                    self.selectcall(line, True)
+                elif self.tapcall:
+                    c.handler_block(self.presshan)
+                    c.handler_block(self.releasehan)
+                    self.tapcall(line[0])
+                    c.handler_unblock(self.presshan)
+                    c.handler_unblock(self.releasehan)
+                return
+            self.dragcall(line[0], line[-1], True)
+            return
+
+        # send an expose event to clean up the line
+        bb = self.bbox
+        if self.window:
+            self.window.window.invalidate_rect(
+                gtk.gdk.Rectangle(bb.minx, bb.miny,
+                                  bb.width()+1, bb.height()+1),
+                True)
+        
+        if len(line) == 1 and self.tapcall:
+            c.handler_block(self.presshan)
+            c.handler_block(self.releasehan)
+            self.tapcall(line[0])
+            c.handler_unblock(self.presshan)
+            c.handler_unblock(self.releasehan)
+            return
+        if self.selectcall and self.selecting:
+            self.selectcall(line, True)
+            return
+
+        if not self.symcall:
+            if self.selectcall:
+                self.selectcall(line)
+            return
+
+        alloc = self.window.get_allocation()
+        self.line = line
+        sym = self.last_point(int(ev.x), int(ev.y),
+                              alloc.width, alloc.height)
+
+        if self.collectcall:
+            self.collectcall(line, p, self.bbox)
+        if sym:
+            self.symcall(sym)
+
+    def last_point(self, x, y, width, height):
+        # look for a symbol match
+        pagebb = BBox(Point(0,0))
+        pagebb.add(Point(width, height))
+        pagebb.finish(div = 2)
+
+        line = self.line
+        self.line = None
+        if len(line) < 2:
+            return None
+
+        p = PPath(line[0][0], line[0][1])
+        for pp in line[1:]:
+            p.add(pp[0], pp[1])
+        p.close()
+        patn = p.text()
+        pos = pagebb.relpos(p.bbox)
+        tpos = "mid"
+        if pos < 3:
+            tpos = "top"
+        if pos >= 6:
+            tpos = "bot"
+        sym = self.dict.match(patn, tpos)
+        if sym == None:
+            print "Failed to match pattern:", patn
+        return sym
+        
+    def motion(self, c, ev):
+        if self.line:
+            if ev.is_hint:
+                x, y, state = ev.window.get_pointer()
+            else:
+                x = ev.x
+                y = ev.y
+            x = int(x)
+            y = int(y)
+
+            if not self.next_point(x,y):
+                return
+
+            if self.timer:
+                gobject.source_remove(self.timer)
+            self.timer = None
+            if self.colour:
+                prev = self.line[-2]
+                c.window.draw_line(self.colour, prev[0], prev[1], x, y)
+            if self.dragging:
+                self.dragcall(self.line[0], self.line[-1], False)
+            else:
+                if not self.symcall:
+                    if self.selectcall:
+                        self.selecting = True
+                if self.selecting:
+                    self.selectcall(self.line, False)
+                else:
+                    self.timer = gobject.timeout_add(self.selecttime, self.set_select)
+
+    def next_point(self, x, y):
+        if self.line:
+            self.bbox.add(Point(x,y))
+            prev = self.line[-1]
+            if abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10:
+                return False
+            self.line.append([x,y])
+            return True
+        return False
+
+    def set_colour(self, col):
+        if type(col) == str:
+            c = gtk.gdk.color_parse(col)
+            gc = self.window.window.new_gc()
+            gc.set_foreground(self.window.get_colormap().alloc_color(c))
+            self.colour = gc
+        else:
+            self.colour = col
+
+    def set_collect(self, collect):
+        self.collectcall = collect
+
+def LoadDict(dict):
+    # Upper case.
+    # Where they are like lowercase, we either double
+    # the last stroke (L, J, I) or draw backwards (S, Z, X)
+    # U V are a special case
+
+    dict.add('A', "R(4)6,8")
+    dict.add('B', "R(4)6,4.R(7)1,6")
+    dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6")
+    dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6")
+    dict.add('C', "R(4)8,2")
+    dict.add('D', "R(4)6,6")
+    dict.add('E', "L(1)2,8.L(7)2,8")
+    dict.add('E', "L(1)2,8.R(4)0,6.L(7)2,8")
+    # double the stem for F
+    dict.add('F', "L(4)2,6.S(3)7,1")
+    dict.add('F', "L(4)2,6.S(3)1,1")
+    dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1")
+
+    dict.add('G', "L(4)2,5.S(8)1,7")
+    dict.add('G', "L(4)2,5.R(8)6,8")
+    # FIXME I need better straight-curve alignment
+    dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1")
+    dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1")
+    # capital I is down/up
+    dict.add('I', "S(4)1,7.S(4)7,1")
+
+    # Capital J has a left/right tail
+    dict.add('J', "R(4)2,6.S(7)3,5")
+
+    dict.add('K', "L(3)0,2.L(7)2,8")
+    dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8")
+    dict.add('K', "S(3)1,7.S(4)6,2.L(5)1,8")
+
+    # Capital L, like J, doubles the foot
+    dict.add('L', "L(4)0,8.S(7)5,3")
+
+    dict.add('M', "R(3)6,5.R(5)3,8")
+    dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8")
+
+    dict.add('N', "R(3)6,8.L(5)0,2")
+
+    # Capital O is CW, but can be CCW in special dict
+    dict.add('O', "R(4)1,1", bot='0')
+
+    dict.add('P', "R(4)6,3")
+    dict.add('Q', "R(4)7,7.S(8)0,8")
+
+    dict.add('R', "R(4)6,4.S(8)0,8")
+
+    # S is drawn bottom to top.
+    dict.add('S', "L(7)6,1.R(1)7,2")
+
+    # Double the stem for capital T
+    dict.add('T', "R(4)0,8.S(5)7,1")
+
+    # U is L to R, V is R to L for now
+    dict.add('U', "L(4)0,2")
+    # backwards U with extra stroke
+    dict.add('V', "R(4)2,0.S(3)0,8")
+    # If extra stroke look more like a loop
+    dict.add('V', "R(4)2,0.S(3)8,8")
+
+    dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0")
+    dict.add('W', "R(5)2,3.R(3)5,0")
+
+    dict.add('X', "R(4)6,0")
+
+    dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2")
+    dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2")
+
+    dict.add('Z', "R(4)8,2.L(4)6,0")
+
+    # Lower case
+    dict.add('a', "L(4)2,2.L(5)1,7")
+    dict.add('a', "L(4)2,2.L(5)0,8")
+    dict.add('a', "L(4)2,2.S(5)0,8")
+    dict.add('b', "S(3)1,7.R(7)6,3")
+    dict.add('c', "L(4)2,8", top='C')
+    dict.add('d', "L(4)5,2.S(5)1,7")
+    dict.add('d', "L(4)5,2.L(5)0,8")
+    dict.add('e', "S(4)3,5.L(4)5,8")
+    dict.add('e', "L(4)3,8")
+    dict.add('f', "L(4)2,6", top='F')
+    dict.add('f', "S(1)5,3.S(3)1,7", top='F')
+    dict.add('g', "L(1)2,2.R(4)1,6")
+    dict.add('h', "S(3)1,7.R(7)6,8")
+    dict.add('h', "L(3)0,5.R(7)6,8")
+    dict.add('i', "S(4)1,7", top='I', bot='1')
+    dict.add('j', "R(4)2,6", top='J')
+    dict.add('k', "L(3)0,5.L(7)2,8")
+    dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8")
+    dict.add('k', "S(3)1,7.S(6)6,2.L(7)1,8")
+    dict.add('l', "L(4)0,8", top='L')
+    dict.add('l', "S(3)1,7.S(7)3,5", top='L')
+    dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8")
+    dict.add('m', "S(3)1,7.R(4)6,5.L(4)0,2.R(5)6,8")
+    dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8")
+    dict.add('n', "S(3)1,7.R(4)6,8")
+    dict.add('o', "L(4)1,1", top='O', bot='0')
+    dict.add('p', "S(3)1,7.R(4)6,3")
+    dict.add('q', "L(1)2,2.L(5)1,5")
+    dict.add('q', "L(1)2,2.S(5)1,7.S(8)6,2")
+    dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7")
+    # FIXME this double 1,7 is due to a gentle where the
+    # second looks like a line because it is narrow.??
+    dict.add('r', "S(3)1,7.R(4)6,2")
+    dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5')
+    dict.add('t', "R(4)0,8", top='T', bot='7')
+    dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7')
+    dict.add('u', "L(4)0,2.S(5)1,7")
+    dict.add('v', "R(4)2,0")
+    dict.add('v', "L(3)0,2.S(2)3,5")
+    dict.add('v', "L(3)0,2.R(5)6,2")
+    dict.add('v', "L(3)0,2.L(2)0,2")
+    dict.add('w', "L(3)0,2.L(5)0,2", top='W')
+    dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W')
+    dict.add('w', "L(3)0,5.L(5)3,2", top='W')
+    dict.add('x', "L(4)0,6", top='X')
+    dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved
+    dict.add('y', "L(1)0,2.S(5)2,7", top='Y')
+    dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2')
+
+    # Digits
+    dict.add('0', "L(4)7,7")
+    dict.add('0', "R(4)7,7")
+    dict.add('1', "S(4)7,1")
+    dict.add('2', "R(4)0,6.S(7)3,5")
+    dict.add('2', "R(4)3,6.L(4)2,8")
+    dict.add('3', "R(1)0,6.R(7)1,6")
+    dict.add('4', "L(4)7,5")
+    dict.add('5', "L(1)2,6.R(7)0,3")
+    dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3")
+    dict.add('6', "L(4)2,3")
+    dict.add('7', "S(1)3,5.R(4)1,6")
+    dict.add('7', "R(4)0,6")
+    dict.add('7', "R(4)0,7")
+    dict.add('8', "L(1)2,7.R(4)4,2")
+    dict.add('8', "L(1)2,7.R(4)0,2")
+    dict.add('8', "L(5)2,8.R(4)1,2")
+    dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1")
+    dict.add('8', "L(4)2,8.R(4)0,2.L(3)6,1")
+    dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1")
+    dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0")
+    dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1")
+    dict.add('9', "L(1)2,2.S(5)1,7")
+
+    dict.add(' ', "S(4)3,5")
+    dict.add('<BS>', "S(4)5,3")
+    dict.add('-', "S(4)3,5.S(4)5,3")
+    dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5")
+    dict.add("<left>", "S(4)5,3.S(3)3,5")
+    dict.add("<right>","S(4)3,5.S(5)5,3")
+    dict.add("<up>", "S(4)7,1.S(1)1,7") # "<up>"
+    dict.add("<down>","S(4)1,7.S(7)7,1") # "<down>"
+    dict.add("<newline>", "S(4)2,6")
+    dict.add("<escape>", "S(4)8,0")
+
+    # cw loop from BR is period
+    dict.add(".", "R(4)8,8")
+    # reverse is ":"
+    dict.add(":", "L(4)8,8")
+    # ccw from LR is , - reverse is ';'
+    dict.add(",", "L(4)2,2")
+    dict.add(";", "R(4)2,2")
+
+    # slash - stoke down and back up
+    dict.add("/", "S(4)2,6.S(4)6,2")
+    dict.add("\\", "S(4)8,0.S(4)0,8")
+    dict.add("@", "L(4)2,2.L(4)5,5") # needs work
+    dict.add("@", "L(4)2,5.S(5)0,8.L(4)5,8")
+    ##dict.add("#", "L(3)0,2.R(3)6,8.L(6)8,0.S(7)6,2.S(5)5,3")
+    # $ is S with up/down stroke
+    dict.add("$", "L(1)2,8.R(4)3,1.S(4)1,7")
+    dict.add("$", "S(1)2,6.R(4)0,1.S(4)1,7")
+    dict.add("$", "L(4)2,6.R(4)0,7")
+    # ^ is A and reverse
+    dict.add("^", "R(4)6,8.L(4)8,6")
+    dict.add("^", "R(4)6,8.L(4)1,6")
+    dict.add("&", "R(4)8,6.L(4)1,5")
+    # * is a 5-point star, start at top - no different from 'o'!!
+    # Maybe 4 points with concave joins.
+    dict.add("*", "R(0)2,6.R(6)0,8.R(8)6,2.S(2)8,0")
+    # %  ccw, down, cw
+    dict.add("%", "L(1)2,5.S(4)2,6.R(7)6,6")
+    dict.add("%", "L(1)2,5.R(4)3,6.R(7)6,6")
+    # < - concave curves
+    dict.add("<", "R(1)2,6.R(7)0,8")
+    dict.add(">", "L(1)0,8.L(7)2,6")
+    # = - dash above dash ??   - too many options...
+    dict.add("=", "S(1)3,5.S(1)5,3.R(4)0,6.S(7)4,5")
+    dict.add("=", "S(1)3,4.L(4)2,8.R(4)0,6.S(7)5,5")
+    dict.add("=", "S(1)3,4.L(4)2,8.R(4)0,6.S(7)3,5")
+    dict.add("=", "S(1)3,4.L(4)2,8.L(4)8,3.S(7)3,5")
+    # + - backwards 4 ... too similar to 0 or D
+    # try a concave join down, up-left, across
+    dict.add("+", "S(4)1,7.L(6)8,0.S(4)3,5")
+    # ( [ { } ] )  .... too hard
+    # "!" is like L but the foot extends both sides.
+    dict.add("!", "L(5)0,8.S(7)5,3")
+    # .. or in other directions
+    dict.add("!", "R(3)2,6.S(7)3,5")
+    # "?" like "!" starting like '7'
+    dict.add("?", "R(4)0,7.L(8)1,8.S(7)5,3")
+
+    # ' is a 'j' and back up again
+    dict.add("'", "R(4)2,6.L(4)6,2")
+    dict.add("'", "R(4)2,6.L(4)2,1")
+    dict.add('"', "R(4)2,6.L(4)6,2.R(4)2,6")
+
+    # '|'  -- not i or I or ! but '|'
+    # up/down
+    dict.add("|", "S(4)7,1.S(4)1,7")
+
+class DictSegment:
+    # Each segment has for elements:
+    #   direction: Right Straight Left (R=cw, L=ccw)
+    #   location: 0-8.
+    #   start: 0-8
+    #   finish: 0-8
+    # Segments match if the difference at each element
+    # is 0, 1, or 3 (RSL coded as 012)
+    # A difference of 1 requires both to be same / 3
+    # On a match, return number of 0s
+    # On non-match, return -1
+    def __init__(self, str):
+        # D(L)S,R
+        # 0123456
+        self.e = [0,0,0,0]
+        if len(str) != 7:
+            raise ValueError
+        if str[1] != '(' or str[3] != ')' or str[5] != ',':
+            raise ValueError
+        if str[0] == 'R':
+            self.e[0] = 0
+        elif str[0] == 'L':
+            self.e[0] = 2
+        elif str[0] == 'S':
+            self.e[0] = 1
+        else:
+            raise ValueError
+
+        self.e[1] = int(str[2])
+        self.e[2] = int(str[4])
+        self.e[3] = int(str[6])
+
+    def match(self, other):
+        cnt = 0
+        for i in range(0,4):
+            diff = abs(self.e[i] - other.e[i])
+            if diff == 0:
+                cnt += 1
+            elif diff == 3:
+                pass
+            elif diff == 1 and (self.e[i]/3 == other.e[i]/3):
+                pass
+            else:
+                return -1
+        return cnt
+
+class DictPattern:
+    # A Dict Pattern is a list of segments.
+    # A parsed pattern matches a dict pattern if
+    # there are the same nubmer of segments and they
+    # all match.  The value of the match is the sum
+    # of the individual matches.
+    # A DictPattern is printed as segments joined by periods.
+    #
+    def __init__(self, str):
+        self.segs = map(DictSegment, str.split("."))
+    def match(self,other):
+        if len(self.segs) != len(other.segs):
+            return -1
+        cnt = 0
+        for i in range(0,len(self.segs)):
+            m = self.segs[i].match(other.segs[i])
+            if m < 0:
+                return m
+            cnt += m
+        return cnt
+
+
+class Dictionary:
+    # The dictionary holds all the patterns for symbols and
+    # performs lookup
+    # Each pattern in the directionary can be associated
+    # with 3 symbols.  One when drawn in middle of screen,
+    # one for top of screen, one for bottom.
+    # Often these will all be the same.
+    # This allows e.g. s, S and 5 to have the same pattern in different
+    # locations on the touchscreen.
+    # A match requires a unique entry with a match that is better
+    # than any other entry.
+    #
+    def __init__(self):
+        self.dict = []
+    def add(self, sym, pat, top = None, bot = None):
+        if top == None: top = sym
+        if bot == None: bot = sym
+        self.dict.append((DictPattern(pat), sym, top, bot, pat))
+
+    def _match(self, p):
+        max = -1
+        val = None
+        for (ptn, sym, top, bot, s) in self.dict:
+            cnt = ptn.match(p)
+            if cnt > max:
+                max = cnt
+                val = (sym, top, bot)
+            elif cnt == max and val != (sym, top, bot):
+                val = None
+        return val
+
+    def match(self, str, pos = "mid"):
+        p = DictPattern(str)
+        m = self._match(p)
+        if m == None:
+            return m
+        (mid, top, bot) = self._match(p)
+        if pos == "top": return top
+        if pos == "bot": return bot
+        return mid
+
+    def matches(self, str):
+        p = DictPattern(str)
+        max = -1
+        m = []
+        for (ptn, sym, top, bot, s) in self.dict:
+            cnt = ptn.match(p)
+            if cnt > max:
+                max = cnt
+                m = []
+            if cnt >= 0 and cnt == max:
+                m.append((s,sym))
+        return max, len(p.segs)*4, m
+
+class Point:
+    # This represents a point in the path and all the points leading
+    # up to it.  It allows us to find the direction and curvature from
+    # one point to another
+    # We store x,y, and sum/cnt of points so far
+    # This allows us to find the mean of a set of points. In particular,
+    # given 2 points we can find the mean of all the points between which
+    # will be on the same side of the joining line as most of the points.
+    # This can tell us if the curve is clockwise or counterclockwise, or
+    # (nearly) straight.
+    def __init__(self,x,y):
+        x = int(x); y = int(y)
+        self.xsum = x
+        self.ysum = y
+        self.x = x
+        self.y = y
+        self.cnt = 1
+
+    def copy(self):
+        n = Point(0,0)
+        n.xsum = self.xsum
+        n.ysum = self.ysum
+        n.x = self.x
+        n.y = self.y
+        n.cnt = self.cnt
+        return n
+
+    def add(self,x,y):
+        if self.x == x and self.y == y:
+            return
+        self.x = x
+        self.y = y
+        self.xsum += x
+        self.ysum += y
+        self.cnt += 1
+
+    def xlen(self,p):
+        return abs(self.x - p.x)
+    def ylen(self,p):
+        return abs(self.y - p.y)
+    def sqlen(self,p):
+        x = self.x - p.x
+        y = self.y - p.y
+        return x*x + y*y
+
+    def xdir(self,p):
+        if self.x > p.x:
+            return 1
+        if self.x < p.x:
+            return -1
+        return 0
+    def ydir(self,p):
+        if self.y > p.y:
+            return 1
+        if self.y < p.y:
+            return -1
+        return 0
+
+    # We calculate the 'curve' by measuring the
+    # distance of the mean point from the line between
+    # start and end, and dividing by the length of the
+    # line.  We don't bother taking sqroot so result is
+    # a square and is multiplied by 100 as we are using
+    # ints.
+    # We then desire if cw or ccw or s by comparing with
+    # arbitrary number '+/-6'
+    def curve(self,p):
+        if self.cnt == p.cnt:
+            return 0
+        x1 = p.x ; y1 = p.y
+        (x2,y2) = self.meanpoint(p)
+        x3 = self.x; y3 = self.y
+
+        curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+        curve = curve * 100 / ((y3-y1)*(y3-y1)
+                               + (x3-x1)*(x3-x1))
+        if curve > 6:
+            return 1
+        if curve < -6:
+            return -1
+        return 0
+
+    # Vcurve is the raw curve number and is only used
+    # to see if a curve is getting tigher..... WHY?
+    def Vcurve(self,p):
+        if self.cnt == p.cnt:
+            return 0
+        x1 = p.x ; y1 = p.y
+        (x2,y2) = self.meanpoint(p)
+        x3 = self.x; y3 = self.y
+
+        curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+        curve = curve * 100 / ((y3-y1)*(y3-y1)
+                               + (x3-x1)*(x3-x1))
+        return curve
+
+    def meanpoint(self,p):
+        x = (self.xsum - p.xsum) / (self.cnt - p.cnt)
+        y = (self.ysum - p.ysum) / (self.cnt - p.cnt)
+        return (x,y)
+
+    def is_sharp(self,A,C):
+        # Measure the cosine at self between A and C
+        # as A and C could be curve, we take the mean point on
+        # self.A and self.C as the points to find cosine between
+        # If the cosine is fairly close to 1 we assume there is
+        # a sharp turn at 'self'
+        (ax,ay) = self.meanpoint(A)
+        (cx,cy) = self.meanpoint(C)
+        a = ax-self.x; b=ay-self.y
+        c = cx-self.x; d=cy-self.y
+        x = a*c + b*d
+        y = a*d - b*c
+        h = math.sqrt(x*x+y*y)
+        if h > 0:
+            cs = x*1000/h
+        else:
+            cs = 0
+        return (cs > 900)
+
+    def slope(self, p):
+        # cannot divide safely, so return an x,y pair
+        return (self.x-p.x, self.y-p.y)
+
+    def is_reversal(self,start, mid):
+        # self comes after start->mid.  Is it a 'reversal' of direction.
+        # This means a change in direction of more than 45deg.
+        # The slope to mid is y/x
+        # The slopes at 45deg to this are -(x-y)/(x+y) and (x+y)/(x-y)
+        # we check if the slop from mid to self is not between those.
+        # The slope isn't complete info though.  If the new direction is
+        # more than 135 degrees we get a compatible slope but a changed
+        # direction. In that case relative direction is reversed.
+        # We can check that if ax+by < 0 where slope at self is b/a
+        #
+        # return 0 if not a reversal +/- 45deg
+        # return 1 if single reversal: +-135deg
+        # return 2 if double reversal: >135deg
+        cnt = 0
+        x,y = mid.slope(start)
+        new = self.slope(mid)
+        min = (x+y, -(x-y))
+        max = (x-y, x+y)
+        if slope_lt(min, max):
+            if slope_lt(new, min) or slope_lt(max, new):
+                cnt = 1
+        else:
+            if slope_lt(new, min) and slope_lt(max, new):
+                cnt = 1
+        if new[0]*x + new[1]*y < 0:
+            # complete reversal
+            cnt = 2 - cnt
+        return cnt
+
+def slope_lt(a,b):
+    ax,ay = a
+    bx,by = b
+    # is ay/ax < by/bx - without dividing as they could be zero
+    want_lt = True
+    if ax < 0:
+        want_lt = not want_lt
+    if bx < 0:
+        want_lt = not want_lt
+    if ay * bx < by * ax:
+        return want_lt
+    else:
+        return not want_lt
+
+class BBox:
+    # a BBox records min/max x/y of some Points and
+    # can subsequently report row, column, pos of each point
+    # can also locate one bbox in another
+
+    def __init__(self, p):
+        self.minx = p.x
+        self.maxx = p.x
+        self.miny = p.y
+        self.maxy = p.y
+
+    def width(self):
+        return self.maxx - self.minx
+    def height(self):
+        return self.maxy - self.miny
+
+    def add(self, p):
+        if p.x > self.maxx:
+            self.maxx = p.x
+        if p.x < self.minx:
+            self.minx = p.x
+
+        if p.y > self.maxy:
+            self.maxy = p.y
+        if p.y < self.miny:
+            self.miny = p.y
+    def finish(self, div = 3):
+        # if aspect ratio is bad, we adjust max/min accordingly
+        # before setting [xy][12].  We don't change self.min/max
+        # as they are used to place stroke in bigger bbox.
+        # Normally divisions are at 1/3 and 2/3. They can be moved
+        # by setting div e.g. 2 = 1/2 and 1/2
+        # This allows us to adjust to shape of drawing, but if very
+        # narrow we treat it a genuinely narrow.
+        (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy)
+        if (maxx - minx) * 3 < (maxy - miny) * 2:
+            # too narrow - make it a square
+            mid = int((maxx + minx)/2)
+            halfwidth = int ((maxy - miny)/3)
+            minx = mid - halfwidth
+            maxx = mid + halfwidth
+        if (maxy - miny) * 3 < (maxx - minx) * 2:
+            # too wide - increase hight to make a square
+            mid = int((maxy + miny)/2)
+            halfheight = int ((maxx - minx)/3)
+            miny = mid - halfheight
+            maxy = mid + halfheight
+
+        div1 = div - 1
+        self.x1 = int((div1*minx + maxx)/div)
+        self.x2 = int((minx + div1*maxx)/div)
+        self.y1 = int((div1*miny + maxy)/div)
+        self.y2 = int((miny + div1*maxy)/div)
+
+    def row(self, p):
+        # 0, 1, 2 - top to bottom
+        if p.y <= self.y1:
+            return 0
+        if p.y < self.y2:
+            return 1
+        return 2
+    def col(self, p):
+        if p.x <= self.x1:
+            return 0
+        if p.x < self.x2:
+            return 1
+        return 2
+    def box(self, p):
+        # 0 to 8
+        return self.row(p) * 3 + self.col(p)
+
+    def relpos(self,b):
+        # b is a box within self.  find location 0-8
+        if b.maxx < self.x2 and b.minx < self.x1:
+            x = 0
+        elif b.minx > self.x1 and b.maxx > self.x2:
+            x = 2
+        else:
+            x = 1
+        if b.maxy < self.y2 and b.miny < self.y1:
+            y = 0
+        elif b.miny > self.y1 and b.maxy > self.y2:
+            y = 2
+        else:
+            y = 1
+        return y*3 + x
+
+
+# check if a list of curvatures are compatible.
+# i.e. if those that aren't straight are either
+# all cw or all ccw
+def different(*args):
+    cur = 0
+    for i in args:
+        if cur != 0 and i != 0 and cur != i:
+            return True
+        if cur == 0:
+            cur = i
+    return False
+
+# Given a set of non-different curvatures, find the
+# overall curvature
+def maxcurve(*args):
+    for i in args:
+        if i != 0:
+            return i
+    return 0
+
+class PPath:
+    # a PPath refines a list of x,y points into a list of Points
+    # The Points mark out segments which end at significant Points
+    # such as inflections and reversals.
+    # In the first stage we try to find points where the path
+    # reverses direction or where curvature starts to decrease implying
+    # an inflection.  Once we get 4 pixels beyond such a point and it appears
+    # stable, we confirm it and start looking for the next point.
+    # This gives a smaller list of Points that are reversals or inflections.
+    # We then analyse these short segments for curvature and gather groups
+    # with the same curvature an create a sectlist where each entry holds
+    # a curvature number, and a list of Points.  It is possible that two
+    # adjacent entries share a Point if they have a gentle inflection
+    # between them.
+    def __init__(self, x,y):
+
+        self.start = Point(x,y)
+        self.mid = Point(x,y)
+        self.curr = Point(x,y)
+        self.list = [ self.start ]
+
+    def add(self, x, y):
+        self.curr.add(x,y)
+
+        if ( self.curr.is_reversal(self.start, self.mid) or
+             (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))):
+            pass
+        else:
+            self.mid = self.curr.copy()
+
+        if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4:
+            self.start = self.mid.copy()
+            self.list.append(self.start)
+            self.mid = self.curr.copy()
+
+    def close(self):
+        self.list.append(self.curr)
+
+    def get_sectlist(self):
+        if len(self.list) < 2:
+            return [[0,self.list]]
+        l = []
+        A = self.list[0]
+        B = self.list[1]
+        s = [A,B]
+        reversals = 0
+        curcurve = B.curve(A)
+        for C in self.list[2:]:
+            cabc = C.curve(A)
+            cab = B.curve(A)
+            cbc = C.curve(B)
+            r = reversals + C.is_reversal(A,B)
+            reversals = 0
+            if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve):
+                # B is too pointy, must break here
+                l.append([curcurve, s])
+                s = [B, C]
+                curcurve = cbc
+            elif not different(cabc, cab, cbc, curcurve) and r <= 4:
+                # all happy
+                reversals = r
+                s.append(C)
+                if curcurve == 0:
+                    curcurve = maxcurve(cab, cbc, cabc)
+            elif not different(cabc, cab, cbc)  :
+                # gentle inflection along AB
+                # was: AB goes in old and new section
+                # now: AB only in old section, but curcurve
+                #      preseved.
+                l.append([curcurve,s])
+                s = [A, B, C]
+                curcurve =maxcurve(cab, cbc, cabc)
+            else:
+                # Change of direction at B
+                l.append([curcurve,s])
+                s = [B, C]
+                curcurve = cbc
+
+            A = B
+            B = C
+        l.append([curcurve,s])
+
+        return l
+
+    def remove_shorts(self, bbox):
+        # in self.list, if a point is close to the previous point,
+        # remove it.
+        # "close" is relative to overall size of path.  If the square
+        # on the distance is less that a 1/4 of the bounding box, it is close.
+        # If the last point is close to it's previous point, we remove
+        # the second last point.
+        if len(self.list) <= 2:
+            return
+        w = bbox.width()/10
+        h = bbox.height()/10
+        n = [self.list[0]]
+        leng = w*h*2*2
+        for p in self.list[1:-1]:
+            l = p.sqlen(n[-1])
+            if l > leng:
+                n.append(p)
+        # and check the last one separately
+        p = self.list[-1]
+        l = p.sqlen(n[-1])
+        if l < leng:
+            # too short - remove the second last
+            n.pop()
+        n.append(p)
+        self.list = n
+
+    def text(self):
+        # OK, we have a list of points with curvature between.
+        # want to divide this into sections.
+        # for each 3 consectutive points ABC curve of ABC and AB and BC
+        # If all the same, they are all in a section.
+        # If not B starts a new section and the old ends on B or C...
+        # This produces the summary of the path - a number of segments joine
+        # with periods.
+        # Each segment has a curvature R L S and a location in the whole.
+        # It also has a start and end position relative to itself.
+        BB = BBox(self.list[0])
+        for p in self.list:
+            BB.add(p)
+        BB.finish()
+        self.bbox = BB
+        self.remove_shorts(BB)
+        sectlist = self.get_sectlist()
+        t = ""
+        for c, s in sectlist:
+            if c > 0:
+                dr = "R"  # clockwise is to the Right
+            elif c < 0:
+                dr = "L"  # counterclockwise to the Left
+            else:
+                dr = "S"  # straight
+            bb = BBox(s[0])
+            for p in s:
+                bb.add(p)
+            bb.finish()
+            # If  all points are in same row or column, then
+            # line is S - unless other changes direction
+            rwinc = False; rwdec = False
+            clinc = False; cldec = False
+
+            rw = bb.row(s[0]); cl=bb.col(s[0])
+            for p in s:
+                if bb.row(p) < rw: rwdec = True
+                if bb.row(p) > rw: rwinc = True
+                if bb.col(p) < cl: cldec = True
+                if bb.col(p) > cl: clinc = True
+                rw = bb.row(p)
+                cl = bb.col(p)
+            if (rwdec and rwinc) or (cldec and clinc):
+                # reversal so cannot be straight
+                pass
+            else:
+                if not (rwinc or rwdec) or not (clinc or cldec):
+                    # one didn't change, other monotonic, so 'S'
+                    dr = "S"
+            t1 = dr
+            t1 += "(%d)" % BB.relpos(bb)
+            t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1]))
+            t += t1 + '.'
+        return t[:-1]
+
+
+if __name__ == "__main__":
+    # test app for Scrawl.
+    # Create a window with a ListSelect and when a symbol is
+    # entered, select the first element with that letter
+    from listselect import ListSelect
+
+    w = gtk.Window(gtk.WINDOW_TOPLEVEL)
+    w.connect("destroy", lambda w: gtk.main_quit())
+    w.set_title("Scrawl Test")
+    w.show()
+
+    v = gtk.VBox(); v.show()
+    w.add(v)
+
+    s = ListSelect()
+    list = [ "The", "Quick", "Brown", "Fox", "jumps", "over", 
+             "the", "lazy", "Dog"]
+    el = []
+    for a in list:
+        el.append((a, ["blue", False, False, None, "white"]))
+    el[4] = (el[4][0], ["red",True,True,"black","white"])
+    s.list = el
+    s.selected = 2
+    v.pack_end(s, expand = True)
+    s.show()
+
+    def sel(s, n):
+        print n, s.list[n], "selected"
+    s.connect('selected', sel)
+
+    global sc
+
+    def gotsym(sym):
+        global sc
+        print "got sym:", sym
+        if sym == '-':
+            s.set_zoom(s.zoom-1)
+        elif sym == '+':
+            s.set_zoom(s.zoom+1)
+        elif sym == '1':
+            sc.symcall = None
+        else:
+            for i in range(len(list)):
+                if list[i].lower().find(sym.lower()) >= 0:
+                    print 'sel', i, list[i].lower(), sym.lower()
+                    s.select(i)
+                    break
+
+    def gottap(p):
+        x,y = p
+        s.tap(x,y)
+
+    global dragsource
+    dragsource = None
+    def gotdrag(start, here, done):
+        global dragsource
+        if dragsource == None:
+            dragsource = s.map_pos(start[0], start[1])
+            if dragsource != None:
+                s.list[dragsource][1][0] = 'black'
+                s.item_changed(dragsource)
+        dragdest = s.map_pos(here[0], here[1])
+        if dragsource != None and dragdest != None and dragsource != dragdest:
+            # swap and update dragsource
+            s.list[dragsource], s.list[dragdest] = \
+                                s.list[dragdest], s.list[dragsource]
+            list[dragsource], list[dragdest] = \
+                              list[dragdest], list[dragsource]
+            dragsource = dragdest
+            s.list_changed()
+        if done and dragsource != None:
+            s.list[dragsource][1][0] = 'blue'
+            s.item_changed(dragsource)
+            dragsource = None
+                                            
+
+    def gotsel(line, done):
+        if not done:
+            # set background to yellow
+            for p in line:
+                ind = s.map_pos(p[0],p[1])
+                if ind != None and s.list[ind][1][3] != 'yellow':
+                    s.list[ind][1][3] = "yellow"
+                    s.item_changed(ind)
+        else:
+            for e in s.list:
+                if e[1][3] == 'yellow':
+                    e[1][3] = None
+                    e[1][2] = not e[1][2]
+            s.list_changed()
+        
+    sc = Scrawl(s, gotsym, gottap, gotdrag, gotsel)
+    sc.set_colour('red')
+    print "Write letters to select word containing the letter"
+    print "click to select directly"
+    print "click-hold to draw words around"
+    print "drag-hold to select a range"
+    gtk.main()