--- /dev/null
+#!/usr/bin/env python
+
+
+# scribble - scribble pad designed for Neo Freerunner
+#
+# Copyright (C) 2008 Neil Brown <neil@brown.name>
+#
+#
+# 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.
+#
+# The GNU General Public License, version 2, is available at
+# http://www.fsf.org/licensing/licenses/info/GPLv2.html
+# Or you can write to the Free Software Foundation, Inc., 51
+# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# Author: Neil Brown
+# Email: <neil@brown.name>
+
+
+# TODO
+# - index page
+# list of pages
+# Buttons: select, delete, new
+# - bigger buttons - fewer
+# Need: undo, redo, colour, text/line
+# When text is selected, these change to:
+# align, join, make-title
+
+import pygtk
+import gtk
+import os
+import pango
+import gobject
+import time
+import listselect
+
+###########################################################
+# Writing recognistion code
+import math
+
+
+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")
+ # double the stem for F
+ dict.add('F', "L(4)2,6.S(3)7,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)1,6.S(7)3,5")
+
+ dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8")
+
+ # Capital L, like J, doubles the foot
+ dict.add('L', "L(4)0,8.S(7)4,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")
+ dict.add('V', "R(4)2,0")
+
+ 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)1,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('l', "L(4)0,8", top='L')
+ dict.add('l', "S(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', "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.R(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('s', "L(5)1,8.R(7)2,3", 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', "L(4)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(4)2,8.R(4)4,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("<newline>", "S(4)2,6")
+
+
+class DictSegment:
+ # Each segment has four 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 required 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
+ # the 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 printers 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 hold all the pattern for symbols and
+ # performs lookup
+ # Each pattern in the directionary can be associated
+ # with 3 symbols. One when drawing in middle of screen,
+ # one for top of screen, one for bottom.
+ # Often these will all be the same.
+ # This allows e.g. s and S to have the same pattern in different
+ # location 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))
+
+ def _match(self, p):
+ max = -1
+ val = None
+ for (ptn, sym, top, bot) in self.dict:
+ cnt = ptn.match(p)
+ if cnt > max:
+ max = cnt
+ val = (sym, top, bot)
+ elif cnt == max:
+ 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
+
+
+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
+ def __init__(self,x,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
+ 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
+
+ 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
+ (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)
+
+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
+ (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy)
+ if (maxx - minx) * 3 < (maxy - miny) * 2:
+ # too narrow
+ 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
+ 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 9
+ 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
+
+
+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
+
+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.
+
+ 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 ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or
+ (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) 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]
+ curcurve = B.curve(A)
+ for C in self.list[2:]:
+ cabc = C.curve(A)
+ cab = B.curve(A)
+ cbc = C.curve(B)
+ 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):
+ # all happy
+ 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.
+ 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:]:
+ l = p.sqlen(n[-1])
+ if l > leng:
+ 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...
+ 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 some row or column, then
+ # line is S
+ rwdiff = False; cldiff = False
+ rw = bb.row(s[0]); cl=bb.col(s[0])
+ for p in s:
+ if bb.row(p) != rw: rwdiff = True
+ if bb.col(p) != cl: cldiff = True
+ if not rwdiff or not cldiff: 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]
+
+
+
+def page_cmp(a,b):
+ if a.lower() < b.lower():
+ return -1
+ if a.lower() > b.lower():
+ return 1
+ if a < b:
+ return -1
+ if a > b:
+ return 1
+ return 0
+
+def inc_name(a):
+ l = len(a)
+ while l > 0 and a[l-1] >= '0' and a[l-1] <= '9':
+ l -= 1
+ # a[l:] is the last number
+ if l == len(a):
+ # there is no number
+ return a + ".1"
+ num = 0 + int(a[l:])
+ return a[0:l] + ("%d" % (num+1))
+
+class ScribblePad:
+
+ def __init__(self):
+ window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ window.connect("destroy", self.close_application)
+ window.set_title("ScribblePad")
+ #window.set_size_request(480,640)
+ self.window = window
+
+ vb = gtk.VBox()
+ vb.show()
+ self.draw_box = vb
+
+ bar = gtk.HBox(True)
+ bar.set_size_request(-1, 60)
+ vb.pack_end(bar, expand=False)
+ bar.show()
+
+ l = gtk.Label('Page Name')
+ vb.pack_start(l, expand=False)
+ l.show()
+ self.name = l
+
+ page = gtk.DrawingArea()
+ page.set_size_request(480,540)
+ vb.add(page)
+ page.show()
+ ctx = page.get_pango_context()
+ fd = ctx.get_font_description()
+ fd.set_absolute_size(25*pango.SCALE)
+ page.modify_font(fd)
+
+ l.modify_font(fd)
+
+ dflt = gtk.widget_get_default_style()
+ fd = dflt.font_desc
+ fd.set_absolute_size(25*pango.SCALE)
+
+ self.pixbuf = None
+ self.width = 0
+ self.height = 0
+ self.need_redraw = True
+ self.zoomx = None; self.zoomy = None
+
+ # Now the widgets:
+
+ done = gtk.Button("Done"); done.show()
+ colbtn = gtk.Button("colour"); colbtn.show()
+ undo = gtk.Button('Undo') ; undo.show()
+ redo = gtk.Button('Redo') ; redo.show()
+
+ done.child.modify_font(fd)
+ colbtn.child.modify_font(fd)
+ undo.child.modify_font(fd)
+ redo.child.modify_font(fd)
+
+ bar.add(done)
+ bar.add(colbtn)
+ bar.add(undo)
+ bar.add(redo)
+
+ colbtn.connect("clicked", self.colour_change)
+ undo.connect("clicked", self.undo)
+ redo.connect("clicked", self.redo)
+ done.connect("clicked", self.done)
+
+ self.col_align = colbtn
+ self.undo_join = undo
+ self.redo_rename = redo
+
+ self.page = page
+ self.colbtn = colbtn
+ self.line = None
+ self.lines = []
+ self.hist = [] # undo history
+ self.selecting = False
+ self.textcurs = 0
+ self.textstr = None
+ self.textpos = None
+
+
+ page.connect("button_press_event", self.press)
+ page.connect("button_release_event", self.release)
+ page.connect("motion_notify_event", self.motion)
+ page.connect("expose-event", self.refresh)
+ page.connect("configure-event", self.reconfigure)
+ page.connect("key_press_event", self.type)
+ page.set_events(gtk.gdk.EXPOSURE_MASK
+ | gtk.gdk.STRUCTURE_MASK
+ | gtk.gdk.BUTTON_PRESS_MASK
+ | gtk.gdk.BUTTON_RELEASE_MASK
+ | gtk.gdk.KEY_PRESS_MASK
+ | gtk.gdk.KEY_RELEASE_MASK
+ | gtk.gdk.POINTER_MOTION_MASK
+ | gtk.gdk.POINTER_MOTION_HINT_MASK)
+ page.set_property('can-focus', True)
+
+
+ # Now create the index window
+ # A listselect of all the pages, with a row of buttons:
+ # open delete new undelete
+ ls = listselect.ListSelect(center = False)
+ self.listsel = ls
+
+ ls.connect('selected', self.page_select)
+ ls.set_colour('page','blue')
+ ls.set_zoom(38)
+ ls.show()
+
+ vb = gtk.VBox()
+ vb.show()
+ self.list_box = vb
+
+ vb.add(ls)
+ bar = gtk.HBox(True); bar.show()
+ bar.set_size_request(-1, 60)
+ b= gtk.Button("Open"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.open_page)
+ b= gtk.Button("Del"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.del_page)
+ b= gtk.Button("New"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.new_page)
+ b= gtk.Button("Undelete"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.undelete_pages)
+
+ vb.pack_end(bar, expand=False)
+
+ window.add(self.draw_box)
+
+ window.set_default_size(480,640)
+
+ window.show()
+
+
+ if 'HOME' in os.environ:
+ home = os.environ['HOME']
+ else:
+ home = ""
+ if home == "" or home == "/":
+ home = "/home/root"
+ self.page_dir = home + '/Pages'
+ self.load_pages()
+
+ colourmap = page.get_colormap()
+ self.colourmap = {}
+ self.colnames = [ 'black', 'red', 'blue', 'green', 'purple', 'pink', 'yellow' ]
+ for col in self.colnames:
+ c = gtk.gdk.color_parse(col)
+ gc = page.window.new_gc()
+ gc.line_width = 2
+ gc.set_foreground(colourmap.alloc_color(c))
+ self.colourmap[col] = gc
+ self.reset_colour = False
+
+ self.colour_textmode = self.colourmap['blue']
+
+ self.colourname = "black"
+ self.colour = self.colourmap['black']
+ self.colbtn.child.set_text('black')
+ self.bg = page.get_style().bg_gc[gtk.STATE_NORMAL]
+
+ self.dict = Dictionary()
+ LoadDict(self.dict)
+ self.textstr = None
+
+
+ ctx = page.get_pango_context()
+ fd = ctx.get_font_description()
+ met = ctx.get_metrics(fd)
+ self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
+ self.lineascent = met.get_ascent() / pango.SCALE
+
+ self.timeout = None
+
+ window.remove(self.draw_box)
+ window.add(self.list_box)
+
+
+ def close_application(self, widget):
+ self.save_page()
+ gtk.main_quit()
+
+ def load_pages(self):
+ try:
+ os.mkdir(self.page_dir)
+ except:
+ pass
+ self.names = os.listdir(self.page_dir)
+ if len(self.names) == 0:
+ self.names.append("1")
+ self.names.sort(page_cmp)
+ self.pages = {}
+ self.pagenum = 0
+ self.load_page()
+ self.update_list()
+
+ def update_list(self):
+ l = []
+ for p in self.names:
+ l.append([p,'page'])
+ self.listsel.list = l
+ self.listsel.list_changed()
+ self.listsel.select(self.pagenum)
+ return
+
+ def page_select(self, list, item):
+ if item == None:
+ return
+ self.pagenum = item
+
+ def open_page(self, b):
+ self.load_page()
+ self.window.remove(self.list_box)
+ self.window.add(self.draw_box)
+
+ def done(self, b):
+ self.flush_text()
+ self.save_page()
+ self.window.remove(self.draw_box)
+ self.window.add(self.list_box)
+
+ def del_page(self, b):
+ pass
+
+ def new_page(self, b):
+ newname = self.choose_unique(self.names[self.pagenum])
+ self.names = self.names[0:self.pagenum+1] + [ newname ] + \
+ self.names[self.pagenum+1:]
+ self.pagenum += 1;
+ self.update_list()
+ self.listsel.select(self.pagenum)
+
+ return
+
+ def undelete_pages(self, b):
+ pass
+
+
+ def type(self, c, ev):
+ if ev.keyval == 65288:
+ self.add_sym('<BS>')
+ elif ev.string == '\r':
+ self.add_sym('<newline>')
+ else:
+ self.add_sym(ev.string)
+
+ def press(self, c, ev):
+ # Start a new line
+ if self.timeout:
+ gobject.source_remove(self.timeout)
+ self.timeout = None
+ self.taptime = time.time()
+ self.movetext = None
+ c.grab_focus()
+ self.movetimeout = None
+ self.selecting = False
+ if self.selection:
+ self.selection = None
+ self.need_redraw = True
+ self.name_buttons()
+ self.redraw()
+
+ self.line = [ self.colourname, [int(ev.x), int(ev.y)] ]
+ return
+ def release(self, c, ev):
+ if self.movetimeout:
+ gobject.source_remove(self.movetimeout)
+ self.movetimeout = None
+
+ if self.movetext:
+ self.movetext = None
+ self.line = None
+ self.need_redraw = True
+ self.redraw()
+ return
+ if self.line == None:
+ return
+
+ if self.selecting:
+ self.selecting = False
+ self.line = None
+ return
+ if self.timeout == None:
+ self.timeout = gobject.timeout_add(20*1000, self.tick)
+
+ if len(self.line) == 2:
+ # just set a cursor
+ need_redraw = ( self.textstr != None)
+ oldpos = None
+ if not self.textstr:
+ oldpos = self.textpos
+ self.flush_text()
+ if need_redraw:
+ self.redraw()
+ (lineno,index) = self.find_text(self.line[1])
+
+ if lineno == None:
+ # new text,
+ pos = self.align_text(self.line[1])
+ if oldpos and abs(pos[0]-oldpos[0]) < 40 and \
+ abs(pos[1] - oldpos[1]) < 40:
+ # turn of text mode
+ self.flush_text()
+ self.line = None
+ self.need_redraw = True
+ self.setlabel()
+ return
+ self.textpos = pos
+ self.textstr = ""
+ self.textcurs = 0
+ # draw the cursor
+ self.draw_text(pos, self.colour, "", 0)
+ self.line = None
+ else:
+ # clicked inside an old text.
+ # shuffle it to the top, open it, edit.
+ ln = self.lines[lineno]
+ self.lines = self.lines[:lineno] + self.lines[lineno+1:]
+ self.textpos = ln[1]
+ self.textstr = ln[2]
+ if ln[0] in self.colourmap:
+ self.colourname = ln[0]
+ else:
+ self.colourname = "black"
+ self.colbtn.child.set_text(self.colourname)
+ self.colour = self.colourmap[self.colourname]
+ self.textcurs = index + 1
+ self.need_redraw = True
+ self.redraw()
+ self.setlabel()
+ self.line = None
+ return
+ if self.textstr != None:
+ sym = self.getsym()
+ if sym:
+ self.add_sym(sym)
+ else:
+ self.redraw()
+ self.line = None
+ self.reset_colour = True
+ return
+
+ self.lines.append(self.line)
+ self.line = None
+ self.reset_colour = True
+ self.need_redraw = True
+ return
+ 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)
+ prev = self.line[-1]
+ if not self.movetext and abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10:
+ return
+ if not self.movetext and len(self.line) == 2 and time.time() - self.taptime > 0.5:
+ self.flush_text()
+ (lineno, index) = self.find_text(prev)
+ if lineno != None:
+ self.movetext = self.lines[lineno]
+ self.moveoffset = [prev[0]-self.movetext[1][0], prev[1]-self.movetext[1][1]]
+ if self.movetext:
+ self.movetext[1] = [x-self.moveoffset[0],y-self.moveoffset[1]]
+ self.need_redraw = True
+ self.redraw()
+ return
+
+ if self.movetimeout:
+ gobject.source_remove(self.movetimeout)
+ self.movetimeout = gobject.timeout_add(650, self.movetick)
+
+ if self.textstr != None:
+ c.window.draw_line(self.colour_textmode, prev[0],prev[1],x,y)
+ else:
+ c.window.draw_line(self.colour, prev[0],prev[1],x,y)
+ self.line.append([x,y])
+ return
+
+ def movetick(self):
+ # longish pause while drawing
+ self.flush_text()
+ self.selecting = True
+ self.need_redraw = True
+ self.redraw()
+
+ def tick(self):
+ # nothing for 20 seconds, flush the page
+ self.save_page()
+ gobject.source_remove(self.timeout)
+ self.timeout = None
+
+ def find_text(self, pos):
+ x = pos[0]; y = pos[1] + self.lineascent
+ for i in range(0, len(self.lines)):
+ p = self.lines[i]
+ if type(p[2]) != str:
+ continue
+ if x >= p[1][0] and y >= p[1][1] and y < p[1][1] + self.lineheight:
+ # could be this line - check more precisely
+ layout = self.page.create_pango_layout(p[2]+' ')
+ (ink, log) = layout.get_pixel_extents()
+ (ex,ey,ew,eh) = log
+ if x < p[1][0] + ex or x > p[1][0] + ex + ew or \
+ y < p[1][1] + ey or \
+ y > p[1][1] + ey + self.lineheight :
+ continue
+ # OK, it is in this one. Find out where.
+ (index, gr) = layout.xy_to_index((x - p[1][0] - ex) * pango.SCALE,
+ (y - p[1][1] - ey - self.lineheight) * pango.SCALE)
+ if index >= len(p[2]):
+ index = len(p[2])-1
+ return (i, index)
+ return (None, None)
+
+ def align_text(self, pos):
+ # align pos to existing text.
+ # if pos is near one-line-past a previous text, move the exactly
+ # one-line-past
+ x = pos[0]; y = pos[1] + self.lineascent
+ for l in self.lines:
+ if type(l[2]) != str:
+ continue
+ if abs(x - l[1][0]) > self.lineheight:
+ continue
+ if abs(y - (l[1][1] + self.lineheight)) > self.lineheight:
+ continue
+ return [ l[1][0], l[1][1] + self.lineheight ]
+ return pos
+
+ def flush_text(self):
+ if self.textstr == None:
+ self.textpos = None
+ return
+ self.setlabel()
+ if len(self.textstr) == 0:
+ self.textstr = None
+ self.textpos = None
+ return
+ l = [self.colourname, self.textpos, self.textstr]
+ self.lines.append(l)
+ self.need_redraw = True
+ self.textstr = None
+ self.textpos = None
+
+ def draw_text(self, pos, colour, str, cursor = None, drawable = None, selected=False):
+ if drawable == None:
+ drawable = self.page.window
+ layout = self.page.create_pango_layout(str)
+ if self.zoomx != None:
+ xs = 2; xo = -self.zoomx
+ ys = 2; yo = -self.zoomy
+ else:
+ xs = 1 ; xo = 0
+ ys = 1 ; yo = 0
+ (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+ if len(pos) == 2 or pos[2] != ew or pos[3] != eh:
+ while len(pos) > 2:
+ pos.pop()
+ pos.append(ew)
+ pos.append(eh)
+ if selected:
+ drawable.draw_rectangle(self.colourmap['yellow'], True,
+ ex+pos[0], ey+pos[1]-self.lineascent, ew, eh)
+ drawable.draw_layout(colour, pos[0]*xs+xo,
+ (pos[1] - self.lineascent)*ys + yo,
+ layout)
+ if cursor != None:
+ (strong,weak) = layout.get_cursor_pos(cursor)
+ (x,y,width,height) = strong
+ x = pos[0] + x / pango.SCALE
+ y = pos[1]
+ drawable.draw_line(self.colour_textmode,
+ x*xs+xo,y*ys+yo, (x-3)*xs+xo, (y+3)*ys+yo)
+ drawable.draw_line(self.colour_textmode,
+ x*xs+xo,y*ys+yo, (x+3)*xs+xo, (y+3)*ys+yo)
+
+ def add_sym(self, sym):
+ if self.textstr == None:
+ return
+ if sym == "<BS>":
+ if self.textcurs > 0:
+ self.textstr = self.textstr[0:self.textcurs-1]+ \
+ self.textstr[self.textcurs:]
+ self.textcurs -= 1
+ elif sym == "<left>":
+ if self.textcurs > 0:
+ self.textcurs -= 1
+ elif sym == "<right>":
+ if self.textcurs < len(self.textstr):
+ self.textcurs += 1
+ elif sym == "<newline>":
+ tail = self.textstr[self.textcurs:]
+ self.textstr = self.textstr[:self.textcurs]
+ oldpos = self.textpos
+ self.flush_text()
+ self.textcurs = len(tail)
+ self.textstr = tail
+ self.textpos = [ oldpos[0], oldpos[1] +
+ self.lineheight ]
+ else:
+ self.textstr = self.textstr[0:self.textcurs] + sym + \
+ self.textstr[self.textcurs:]
+ self.textcurs += 1
+ self.redraw()
+
+
+ def getsym(self):
+ alloc = self.page.get_allocation()
+ pagebb = BBox(Point(0,0))
+ pagebb.add(Point(alloc.width, alloc.height))
+ pagebb.finish(div = 2)
+
+ p = PPath(self.line[1][0], self.line[1][1])
+ for pp in self.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 refresh(self, area, ev):
+ self.redraw()
+
+ def setlabel(self):
+ if self.textstr == None:
+ self.name.set_label('Page: ' + self.names[self.pagenum] + ' (draw)')
+ else:
+ self.name.set_label('Page: ' + self.names[self.pagenum] + ' (text)')
+
+ def name_buttons(self):
+ if self.selection:
+ self.col_align.child.set_text('Align')
+ self.undo_join.child.set_text('Join')
+ self.redo_rename.child.set_text('Rename')
+ else:
+ self.col_align.child.set_text(self.colourname)
+ self.undo_join.child.set_text('Undo')
+ self.redo_rename.child.set_text('Redo')
+
+ def redraw(self):
+ self.setlabel()
+
+ if self.need_redraw:
+ self.draw_buf()
+ self.page.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0,
+ self.width, self.height)
+
+ if self.textstr != None:
+ self.draw_text(self.textpos, self.colour, self.textstr,
+ self.textcurs)
+
+ return
+ def draw_buf(self):
+ if self.pixbuf == None:
+ alloc = self.page.get_allocation()
+ self.pixbuf = gtk.gdk.Pixmap(self.page.window, alloc.width, alloc.height)
+ self.width = alloc.width
+ self.height = alloc.height
+
+ self.pixbuf.draw_rectangle(self.bg, True, 0, 0,
+ self.width, self.height)
+ if self.zoomx != None:
+ xs = 2; xo = -self.zoomx
+ ys = 2; yo = -self.zoomy
+ else:
+ xs = 1 ; xo = 0
+ ys = 1 ; yo = 0
+ self.selection = []
+ for l in self.lines:
+ if l[0] in self.colourmap:
+ col = self.colourmap[l[0]]
+ else:
+ col = self.colourmap['black']
+ st = l[1]
+ if type(l[2]) == list:
+ for p in l[2:]:
+ self.pixbuf.draw_line(col, st[0]*xs + xo, st[1]*ys + yo,
+ p[0]*xs + xo,p[1]* ys + yo)
+ st = p
+ if type(l[2]) == str:
+ # if this text is 'near' the current line, make it red
+ selected = False
+ if self.selecting and self.line and len(st) == 4:
+ for p in self.line[1:]:
+ if p[0] > st[0] and \
+ p[0] < st[0] + st[2] and \
+ p[1] > st[1] - self.lineascent and \
+ p[1] < st[1] - self.lineascent + st[3]:
+ selected = True
+ break
+ if selected:
+ self.selection.append(l)
+ self.draw_text(st, col, l[2], drawable = self.pixbuf,
+ selected = selected)
+ self.need_redraw = False
+ self.name_buttons()
+
+
+ def reconfigure(self, w, ev):
+ alloc = w.get_allocation()
+ if self.pixbuf == None:
+ return
+ if alloc.width != self.width or alloc.height != self.height:
+ self.pixbuf = None
+ self.need_redraw = True
+
+
+ def colour_change(self,t):
+ if self.selection:
+ # button is 'join' not 'colour'
+ return self.realign(t)
+
+ if self.reset_colour and self.colourname != 'black':
+ next = 'black'
+ else:
+ next = 'black'
+ prev = ''
+ for c in self.colnames:
+ if self.colourname == prev:
+ next = c
+ prev = c
+ self.reset_colour = False
+ self.colourname = next
+ self.colour = self.colourmap[next]
+ t.child.set_text(next)
+ if self.textstr:
+ self.draw_text(self.textpos, self.colour, self.textstr,
+ self.textcurs)
+
+ return
+ def text_change(self,t):
+ self.flush_text()
+ return
+ def undo(self,b):
+ if self.selection:
+ return self.join(b)
+
+ if len(self.lines) == 0:
+ return
+ self.hist.append(self.lines.pop())
+ self.need_redraw = True
+ self.redraw()
+ return
+ def redo(self,b):
+ if self.selection:
+ self.rename(self.selection[0][2])
+ return
+ if len(self.hist) == 0:
+ return
+ self.lines.append(self.hist.pop())
+ self.need_redraw = True
+ self.redraw()
+ return
+ def choose_unique(self, newname):
+ while newname in self.names:
+ new2 = inc_name(newname)
+ if new2 not in self.names:
+ newname = new2
+ elif (newname + ".1") not in self.names:
+ newname = newname + ".1"
+ else:
+ newname = newname + ".0.1"
+
+ return newname
+ def delete(self,b):
+ # hack
+ if self.selection:
+ return self.join(b)
+ self.flush_text()
+ if len(self.names) <= 1:
+ return
+ if len(self.lines) > 0:
+ return
+ self.save_page()
+ nm = self.names[self.pagenum]
+ if nm in self.pages:
+ del self.pages[nm]
+ self.names = self.names[0:self.pagenum] + self.names[self.pagenum+1:]
+ if self.pagenum >= len(self.names):
+ self.pagenum -= 1
+ self.load_page()
+ self.need_redraw = True
+ self.redraw()
+
+ return
+
+ def cmplines(self, a,b):
+ pa = a[1]
+ pb = b[1]
+ if pa[1] != pb[1]:
+ return pa[1] - pb[1]
+ return pa[0] - pb[0]
+
+ def realign(self, b):
+ self.selection.sort(self.cmplines)
+ x = self.selection[0][1][0]
+ y = self.selection[0][1][1]
+ for i in range(len(self.selection)):
+ self.selection[i][1][0] = x
+ self.selection[i][1][1] = y
+ y += self.lineheight
+ self.need_redraw = True
+ self.redraw()
+
+ def join(self, b):
+ self.selection.sort(self.cmplines)
+ txt = ""
+ for i in range(len(self.selection)):
+ if txt:
+ txt = txt + ' ' + self.selection[i][2]
+ else:
+ txt = self.selection[i][2]
+ self.selection[i][2] = None
+ self.selection[0][2] = txt
+ i = 0;
+ while i < len(self.lines):
+ if len(self.lines[i]) > 2 and self.lines[i][2] == None:
+ self.lines = self.lines[:i] + self.lines[i+1:]
+ else:
+ i += 1
+ self.need_redraw = True
+ self.redraw()
+
+
+ def rename(self, newname):
+ # Rename current page and rename the file
+ if self.names[self.pagenum] == newname:
+ return
+ self.save_page()
+ newname = self.choose_unique(newname)
+ oldpath = self.page_dir + "/" + self.names[self.pagenum]
+ newpath = self.page_dir + "/" + newname
+ try :
+ os.rename(oldpath, newpath)
+ self.names[self.pagenum] = newname
+ self.names.sort(page_cmp)
+ self.pagenum = self.names.index(newname)
+ self.setlabel()
+ except:
+ pass
+ self.update_list()
+
+ def setname(self,b):
+ if self.textstr:
+ if len(self.textstr) > 0:
+ self.rename(self.textstr)
+
+ def clear(self,b):
+ while len(self.lines) > 0:
+ self.hist.append(self.lines.pop())
+ self.need_redraw = True
+ self.redraw()
+ return
+
+ def parseline(self, l):
+ # string in "", or num,num. ':' separates words
+ words = l.strip().split(':')
+ line = []
+ for w in words:
+ if w[0] == '"':
+ w = w[1:-1]
+ elif w.find(',') >= 0:
+ n = w.find(',')
+ x = int(w[:n])
+ y = int(w[n+1:])
+ w = [x,y]
+ line.append(w)
+ return line
+
+ def load_page(self):
+ self.need_redraw = True
+ nm = self.names[self.pagenum]
+ if nm in self.pages:
+ self.lines = self.pages[nm]
+ return
+ self.lines = [];
+ try:
+ f = open(self.page_dir + "/" + self.names[self.pagenum], "r")
+ except:
+ f = None
+ if f:
+ l = f.readline()
+ while len(l) > 0:
+ self.lines.append(self.parseline(l))
+ l = f.readline()
+ f.close()
+ return
+
+ def save_page(self):
+ t = self.textstr; tc = self.textcurs
+ self.flush_text()
+ self.pages[self.names[self.pagenum]] = self.lines
+ tosave = self.lines
+ if t and len(t):
+ # restore the text
+ ln = self.lines[-1]
+ self.lines = self.lines[:-1]
+ self.textpos = ln[1]
+ self.textstr = ln[2]
+ self.textcurs = tc
+
+ fn = self.page_dir + "/" + self.names[self.pagenum]
+ if len(tosave) == 0:
+ try:
+ os.unlink(fn)
+ except:
+ pass
+ return
+ f = open(fn, "w")
+ for l in tosave:
+ start = True
+ if not l:
+ continue
+ for w in l:
+ if not start:
+ f.write(":")
+ start = False
+ if isinstance(w, str):
+ f.write('"%s"' % w)
+ elif isinstance(w, list):
+ f.write("%d,%d" %( w[0],w[1]))
+ f.write("\n")
+ f.close()
+
+def main():
+ gtk.main()
+ return 0
+if __name__ == "__main__":
+ ScribblePad()
+ main()