]> git.neil.brown.name Git - scribble.git/blob - scribble.py
Fix breakage of "newline" code.
[scribble.git] / scribble.py
1 #!/usr/bin/env python
2
3
4 # scribble - scribble pad designed for Neo Freerunner
5 #
6 # Copyright (C) 2008 Neil Brown <neil@brown.name>
7 #
8 #
9 #    This program is free software; you can redistribute it and/or modify
10 #    it under the terms of the GNU General Public License as published by
11 #    the Free Software Foundation; either version 2 of the License, or
12 #    (at your option) any later version.
13 #
14 #    This program is distributed in the hope that it will be useful,
15 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
16 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 #    GNU General Public License for more details.
18 #
19 #    The GNU General Public License, version 2, is available at
20 #    http://www.fsf.org/licensing/licenses/info/GPLv2.html
21 #    Or you can write to the Free Software Foundation, Inc., 51
22 #    Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23 #
24 #    Author: Neil Brown
25 #    Email: <neil@brown.name>
26
27
28 import pygtk
29 import gtk
30 import os
31 import pango
32 import gobject
33
34 ###########################################################
35 # Writing recognistion code
36 import math
37
38
39 def LoadDict(dict):
40     # Upper case.
41     # Where they are like lowercase, we either double
42     # the last stroke (L, J, I) or draw backwards (S, Z, X)
43     # U V are a special case
44
45     dict.add('A', "R(4)6,8")
46     dict.add('B', "R(4)6,4.R(7)1,6")
47     dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6")
48     dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6")
49     dict.add('C', "R(4)8,2")
50     dict.add('D', "R(4)6,6")
51     dict.add('E', "L(1)2,8.L(7)2,8")
52     # double the stem for F
53     dict.add('F', "L(4)2,6.S(3)7,1")
54     dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1")
55
56     dict.add('G', "L(4)2,5.S(8)1,7")
57     dict.add('G', "L(4)2,5.R(8)6,8")
58     # FIXME I need better straight-curve alignment
59     dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1")
60     dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1")
61     # capital I is down/up
62     dict.add('I', "S(4)1,7.S(4)7,1")
63
64     # Capital J has a left/right tail
65     dict.add('J', "R(4)1,6.S(7)3,5")
66
67     dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8")
68
69     # Capital L, like J, doubles the foot
70     dict.add('L', "L(4)0,8.S(7)4,3")
71
72     dict.add('M', "R(3)6,5.R(5)3,8")
73     dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8")
74
75     dict.add('N', "R(3)6,8.L(5)0,2")
76
77     # Capital O is CW, but can be CCW in special dict
78     dict.add('O', "R(4)1,1", bot='0')
79
80     dict.add('P', "R(4)6,3")
81     dict.add('Q', "R(4)7,7.S(8)0,8")
82
83     dict.add('R', "R(4)6,4.S(8)0,8")
84
85     # S is drawn bottom to top.
86     dict.add('S', "L(7)6,1.R(1)7,2")
87
88     # Double the stem for capital T
89     dict.add('T', "R(4)0,8.S(5)7,1")
90
91     # U is L to R, V is R to L for now
92     dict.add('U', "L(4)0,2")
93     dict.add('V', "R(4)2,0")
94
95     dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0")
96     dict.add('W', "R(5)2,3.R(3)5,0")
97
98     dict.add('X', "R(4)6,0")
99
100     dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2")
101     dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2")
102
103     dict.add('Z', "R(4)8,2.L(4)6,0")
104
105     # Lower case
106     dict.add('a', "L(4)2,2.L(5)1,7")
107     dict.add('a', "L(4)2,2.L(5)0,8")
108     dict.add('a', "L(4)2,2.S(5)0,8")
109     dict.add('b', "S(3)1,7.R(7)6,3")
110     dict.add('c', "L(4)2,8", top='C')
111     dict.add('d', "L(4)5,2.S(5)1,7")
112     dict.add('d', "L(4)5,2.L(5)0,8")
113     dict.add('e', "S(4)3,5.L(4)5,8")
114     dict.add('e', "L(4)3,8")
115     dict.add('f', "L(4)2,6", top='F')
116     dict.add('f', "S(1)5,3.S(3)1,7", top='F')
117     dict.add('g', "L(1)2,2.R(4)1,6")
118     dict.add('h', "S(3)1,7.R(7)6,8")
119     dict.add('h', "L(3)0,5.R(7)6,8")
120     dict.add('i', "S(4)1,7", top='I', bot='1')
121     dict.add('j', "R(4)1,6", top='J')
122     dict.add('k', "L(3)0,5.L(7)2,8")
123     dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8")
124     dict.add('l', "L(4)0,8", top='L')
125     dict.add('l', "S(3)1,7.S(7)3,5", top='L')
126     dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8")
127     dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8")
128     dict.add('n', "S(3)1,7.R(4)6,8")
129     dict.add('o', "L(4)1,1", top='O', bot='0')
130     dict.add('p', "S(3)1,7.R(4)6,3")
131     dict.add('q', "L(1)2,2.L(5)1,5")
132     dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2")
133     dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7")
134     # FIXME this double 1,7 is due to a gentle where the
135     # second looks like a line because it is narrow.??
136     dict.add('r', "S(3)1,7.R(4)6,2")
137     dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5')
138     dict.add('t', "R(4)0,8", top='T', bot='7')
139     dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7')
140     dict.add('u', "L(4)0,2.S(5)1,7")
141     dict.add('v', "L(4)0,2.L(2)0,2")
142     dict.add('w', "L(3)0,2.L(5)0,2", top='W')
143     dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W')
144     dict.add('w', "L(3)0,5.L(5)3,2", top='W')
145     dict.add('x', "L(4)0,6", top='X')
146     dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved
147     dict.add('y', "L(1)0,2.S(5)2,7", top='Y')
148     dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2')
149
150     # Digits
151     dict.add('0', "L(4)7,7")
152     dict.add('0', "R(4)7,7")
153     dict.add('1', "S(4)7,1")
154     dict.add('2', "R(4)0,6.S(7)3,5")
155     dict.add('2', "R(4)3,6.L(4)2,8")
156     dict.add('3', "R(1)0,6.R(7)1,6")
157     dict.add('4', "L(4)7,5")
158     dict.add('5', "L(1)2,6.R(7)0,3")
159     dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3")
160     dict.add('6', "L(4)2,3")
161     dict.add('7', "S(1)3,5.R(4)1,6")
162     dict.add('7', "R(4)0,6")
163     dict.add('7', "R(4)0,7")
164     dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1")
165     dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1")
166     dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0")
167     dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1")
168     dict.add('9', "L(1)2,2.S(5)1,7")
169
170     dict.add(' ', "S(4)3,5")
171     dict.add('<BS>', "S(4)5,3")
172     dict.add('-', "S(4)3,5.S(4)5,3")
173     dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5")
174     dict.add("<left>", "S(4)5,3.S(3)3,5")
175     dict.add("<right>","S(4)3,5.S(5)5,3")
176     dict.add("<newline>", "S(4)2,6")
177
178
179 class DictSegment:
180     # Each segment has for elements:
181     #   direction: Right Straight Left (R=cw, L=ccw)
182     #   location: 0-8.
183     #   start: 0-8
184     #   finish: 0-8
185     # Segments match if there difference at each element
186     # is 0, 1, or 3 (RSL coded as 012)
187     # A difference of 1 required both to be same / 3
188     # On a match, return number of 0s
189     # On non-match, return -1
190     def __init__(self, str):
191         # D(L)S,R
192         # 0123456
193         self.e = [0,0,0,0]
194         if len(str) != 7:
195             raise ValueError
196         if str[1] != '(' or str[3] != ')' or str[5] != ',':
197             raise ValueError
198         if str[0] == 'R':
199             self.e[0] = 0
200         elif str[0] == 'L':
201             self.e[0] = 2
202         elif str[0] == 'S':
203             self.e[0] = 1
204         else:
205             raise ValueError
206
207         self.e[1] = int(str[2])
208         self.e[2] = int(str[4])
209         self.e[3] = int(str[6])
210
211     def match(self, other):
212         cnt = 0
213         for i in range(0,4):
214             diff = abs(self.e[i] - other.e[i])
215             if diff == 0:
216                 cnt += 1
217             elif diff == 3:
218                 pass
219             elif diff == 1 and (self.e[i]/3 == other.e[i]/3):
220                 pass
221             else:
222                 return -1
223         return cnt
224
225 class DictPattern:
226     # A Dict Pattern is a list of segments.
227     # A parsed pattern matches a dict pattern if
228     # the are the same nubmer of segments and they
229     # all match.  The value of the match is the sum
230     # of the individual matches.
231     # A DictPattern is printers as segments joined by periods.
232     #
233     def __init__(self, str):
234         self.segs = map(DictSegment, str.split("."))
235     def match(self,other):
236         if len(self.segs) != len(other.segs):
237             return -1
238         cnt = 0
239         for i in range(0,len(self.segs)):
240             m = self.segs[i].match(other.segs[i])
241             if m < 0:
242                 return m
243             cnt += m
244         return cnt
245
246
247 class Dictionary:
248     # The dictionary hold all the pattern for symbols and
249     # performs lookup
250     # Each pattern in the directionary can be associated
251     # with  3 symbols.  One when drawing in middle of screen,
252     # one for top of screen, one for bottom.
253     # Often these will all be the same.
254     # This allows e.g. s and S to have the same pattern in different
255     # location on the touchscreen.
256     # A match requires a unique entry with a match that is better
257     # than any other entry.
258     #
259     def __init__(self):
260         self.dict = []
261     def add(self, sym, pat, top = None, bot = None):
262         if top == None: top = sym
263         if bot == None: bot = sym
264         self.dict.append((DictPattern(pat), sym, top, bot))
265
266     def _match(self, p):
267         max = -1
268         val = None
269         for (ptn, sym, top, bot) in self.dict:
270             cnt = ptn.match(p)
271             if cnt > max:
272                 max = cnt
273                 val = (sym, top, bot)
274             elif cnt == max:
275                 val = None
276         return val
277
278     def match(self, str, pos = "mid"):
279         p = DictPattern(str)
280         m = self._match(p)
281         if m == None:
282             return m
283         (mid, top, bot) = self._match(p)
284         if pos == "top": return top
285         if pos == "bot": return bot
286         return mid
287
288
289 class Point:
290     # This represents a point in the path and all the points leading
291     # up to it.  It allows us to find the direction and curvature from
292     # one point to another
293     # We store x,y, and sum/cnt of points so far
294     def __init__(self,x,y) :
295         self.xsum = x
296         self.ysum = y
297         self.x = x
298         self.y = y
299         self.cnt = 1
300
301     def copy(self):
302         n = Point(0,0)
303         n.xsum = self.xsum
304         n.ysum = self.ysum
305         n.x = self.x
306         n.y = self.y
307         n.cnt = self.cnt
308         return n
309
310     def add(self,x,y):
311         if self.x == x and self.y == y:
312             return
313         self.x = x
314         self.y = y
315         self.xsum += x
316         self.ysum += y
317         self.cnt += 1
318
319     def xlen(self,p):
320         return abs(self.x - p.x)
321     def ylen(self,p):
322         return abs(self.y - p.y)
323     def sqlen(self,p):
324         x = self.x - p.x
325         y = self.y - p.y
326         return x*x + y*y
327
328     def xdir(self,p):
329         if self.x > p.x:
330             return 1
331         if self.x < p.x:
332             return -1
333         return 0
334     def ydir(self,p):
335         if self.y > p.y:
336             return 1
337         if self.y < p.y:
338             return -1
339         return 0
340     def curve(self,p):
341         if self.cnt == p.cnt:
342             return 0
343         x1 = p.x ; y1 = p.y
344         (x2,y2) = self.meanpoint(p)
345         x3 = self.x; y3 = self.y
346
347         curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
348         curve = curve * 100 / ((y3-y1)*(y3-y1)
349                                + (x3-x1)*(x3-x1))
350         if curve > 6:
351             return 1
352         if curve < -6:
353             return -1
354         return 0
355
356     def Vcurve(self,p):
357         if self.cnt == p.cnt:
358             return 0
359         x1 = p.x ; y1 = p.y
360         (x2,y2) = self.meanpoint(p)
361         x3 = self.x; y3 = self.y
362
363         curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
364         curve = curve * 100 / ((y3-y1)*(y3-y1)
365                                + (x3-x1)*(x3-x1))
366         return curve
367
368     def meanpoint(self,p):
369         x = (self.xsum - p.xsum) / (self.cnt - p.cnt)
370         y = (self.ysum - p.ysum) / (self.cnt - p.cnt)
371         return (x,y)
372
373     def is_sharp(self,A,C):
374         # Measure the cosine at self between A and C
375         # as A and C could be curve, we take the mean point on
376         # self.A and self.C as the points to find cosine between
377         (ax,ay) = self.meanpoint(A)
378         (cx,cy) = self.meanpoint(C)
379         a = ax-self.x; b=ay-self.y
380         c = cx-self.x; d=cy-self.y
381         x = a*c + b*d
382         y = a*d - b*c
383         h = math.sqrt(x*x+y*y)
384         if h > 0:
385             cs = x*1000/h
386         else:
387             cs = 0
388         return (cs > 900)
389
390 class BBox:
391     # a BBox records min/max x/y of some Points and
392     # can subsequently report row, column, pos of each point
393     # can also locate one bbox in another
394
395     def __init__(self, p):
396         self.minx = p.x
397         self.maxx = p.x
398         self.miny = p.y
399         self.maxy = p.y
400
401     def width(self):
402         return self.maxx - self.minx
403     def height(self):
404         return self.maxy - self.miny
405
406     def add(self, p):
407         if p.x > self.maxx:
408             self.maxx = p.x
409         if p.x < self.minx:
410             self.minx = p.x
411
412         if p.y > self.maxy:
413             self.maxy = p.y
414         if p.y < self.miny:
415             self.miny = p.y
416     def finish(self, div = 3):
417         # if aspect ratio is bad, we adjust max/min accordingly
418         # before setting [xy][12].  We don't change self.min/max
419         # as they are used to place stroke in bigger bbox.
420         # Normally divisions are at 1/3 and 2/3. They can be moved
421         # by setting div e.g. 2 = 1/2 and 1/2
422         (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy)
423         if (maxx - minx) * 3 < (maxy - miny) * 2:
424             # too narrow
425             mid = int((maxx + minx)/2)
426             halfwidth = int ((maxy - miny)/3)
427             minx = mid - halfwidth
428             maxx = mid + halfwidth
429         if (maxy - miny) * 3 < (maxx - minx) * 2:
430             # too wide
431             mid = int((maxy + miny)/2)
432             halfheight = int ((maxx - minx)/3)
433             miny = mid - halfheight
434             maxy = mid + halfheight
435
436         div1 = div - 1
437         self.x1 = int((div1*minx + maxx)/div)
438         self.x2 = int((minx + div1*maxx)/div)
439         self.y1 = int((div1*miny + maxy)/div)
440         self.y2 = int((miny + div1*maxy)/div)
441
442     def row(self, p):
443         # 0, 1, 2 - top to bottom
444         if p.y <= self.y1:
445             return 0
446         if p.y < self.y2:
447             return 1
448         return 2
449     def col(self, p):
450         if p.x <= self.x1:
451             return 0
452         if p.x < self.x2:
453             return 1
454         return 2
455     def box(self, p):
456         # 0 to 9
457         return self.row(p) * 3 + self.col(p)
458
459     def relpos(self,b):
460         # b is a box within self.  find location 0-8
461         if b.maxx < self.x2 and b.minx < self.x1:
462             x = 0
463         elif b.minx > self.x1 and b.maxx > self.x2:
464             x = 2
465         else:
466             x = 1
467         if b.maxy < self.y2 and b.miny < self.y1:
468             y = 0
469         elif b.miny > self.y1 and b.maxy > self.y2:
470             y = 2
471         else:
472             y = 1
473         return y*3 + x
474
475
476 def different(*args):
477     cur = 0
478     for i in args:
479         if cur != 0 and i != 0 and cur != i:
480             return True
481         if cur == 0:
482             cur = i
483     return False
484
485 def maxcurve(*args):
486     for i in args:
487         if i != 0:
488             return i
489     return 0
490
491 class PPath:
492     # a PPath refines a list of x,y points into a list of Points
493     # The Points mark out segments which end at significant Points
494     # such as inflections and reversals.
495
496     def __init__(self, x,y):
497
498         self.start = Point(x,y)
499         self.mid = Point(x,y)
500         self.curr = Point(x,y)
501         self.list = [ self.start ]
502
503     def add(self, x, y):
504         self.curr.add(x,y)
505
506         if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or
507              (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or
508              (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))):
509             pass
510         else:
511             self.mid = self.curr.copy()
512
513         if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4:
514             self.start = self.mid.copy()
515             self.list.append(self.start)
516             self.mid = self.curr.copy()
517
518     def close(self):
519         self.list.append(self.curr)
520
521     def get_sectlist(self):
522         if len(self.list) <= 2:
523             return [[0,self.list]]
524         l = []
525         A = self.list[0]
526         B = self.list[1]
527         s = [A,B]
528         curcurve = B.curve(A)
529         for C in self.list[2:]:
530             cabc = C.curve(A)
531             cab = B.curve(A)
532             cbc = C.curve(B)
533             if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve):
534                 # B is too pointy, must break here
535                 l.append([curcurve, s])
536                 s = [B, C]
537                 curcurve = cbc
538             elif not different(cabc, cab, cbc, curcurve):
539                 # all happy
540                 s.append(C)
541                 if curcurve == 0:
542                     curcurve = maxcurve(cab, cbc, cabc)
543             elif not different(cabc, cab, cbc)  :
544                 # gentle inflection along AB
545                 # was: AB goes in old and new section
546                 # now: AB only in old section, but curcurve
547                 #      preseved.
548                 l.append([curcurve,s])
549                 s = [A, B, C]
550                 curcurve =maxcurve(cab, cbc, cabc)
551             else:
552                 # Change of direction at B
553                 l.append([curcurve,s])
554                 s = [B, C]
555                 curcurve = cbc
556
557             A = B
558             B = C
559         l.append([curcurve,s])
560
561         return l
562
563     def remove_shorts(self, bbox):
564         # in self.list, if a point is close to the previous point,
565         # remove it.
566         if len(self.list) <= 2:
567             return
568         w = bbox.width()/10
569         h = bbox.height()/10
570         n = [self.list[0]]
571         leng = w*h*2*2
572         for p in self.list[1:]:
573             l = p.sqlen(n[-1])
574             if l > leng:
575                 n.append(p)
576         self.list = n
577
578     def text(self):
579         # OK, we have a list of points with curvature between.
580         # want to divide this into sections.
581         # for each 3 consectutive points ABC curve of ABC and AB and BC
582         # If all the same, they are all in a section.
583         # If not B starts a new section and the old ends on B or C...
584         BB = BBox(self.list[0])
585         for p in self.list:
586             BB.add(p)
587         BB.finish()
588         self.bbox = BB
589         self.remove_shorts(BB)
590         sectlist = self.get_sectlist()
591         t = ""
592         for c, s in sectlist:
593             if c > 0:
594                 dr = "R"  # clockwise is to the Right
595             elif c < 0:
596                 dr = "L"  # counterclockwise to the Left
597             else:
598                 dr = "S"  # straight
599             bb = BBox(s[0])
600             for p in s:
601                 bb.add(p)
602             bb.finish()
603             # If  all points are in some row or column, then
604             # line is S
605             rwdiff = False; cldiff = False
606             rw = bb.row(s[0]); cl=bb.col(s[0])
607             for p in s:
608                 if bb.row(p) != rw: rwdiff = True
609                 if bb.col(p) != cl: cldiff = True
610             if not rwdiff or not cldiff: dr = "S"
611
612             t1 = dr
613             t1 += "(%d)" % BB.relpos(bb)
614             t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1]))
615             t += t1 + '.'
616         return t[:-1]
617
618
619
620 def page_cmp(a,b):
621     if a < b:
622         return -1
623     if a > b:
624         return 1
625     return 0
626
627 def inc_name(a):
628     l = len(a)
629     while l > 0 and a[l-1] >= '0' and a[l-1] <= '9':
630         l -= 1
631     # a[l:] is the last number
632     if l == len(a):
633         # there is no number
634         return a + ".1"
635     num = 0 + int(a[l:])
636     return a[0:l] + ("%d" % (num+1))
637
638 class ScribblePad:
639
640     def __init__(self):
641         window = gtk.Window(gtk.WINDOW_TOPLEVEL)
642         window.connect("destroy", self.close_application)
643         window.set_title("ScribblePad")
644         #window.set_size_request(480,640)
645
646         vb = gtk.VBox()
647         window.add(vb)
648         vb.show()
649
650         bar = gtk.HBox()
651         bar.set_size_request(-1, 40)
652         vb.pack_start(bar, expand=False)
653         bar.show()
654
655         page = gtk.DrawingArea()
656         page.set_size_request(480,540)
657         vb.add(page)
658         page.show()
659         ctx = page.get_pango_context()
660         fd = ctx.get_font_description()
661         fd.set_absolute_size(25*pango.SCALE)
662         page.modify_font(fd)
663
664         dflt = gtk.widget_get_default_style()
665         fd = dflt.font_desc
666         fd.set_absolute_size(25*pango.SCALE)
667
668         # Now the widgets:
669         #  < > R u r A D C name
670         #back = gtk.Button(stock = gtk.STOCK_GO_BACK) ; back.show()
671         #fore = gtk.Button(stock = gtk.STOCK_GO_FORWARD) ; fore.show()
672         #red = gtk.ToggleButton("red"); red.show()
673         #undo = gtk.Button(stock = gtk.STOCK_UNDO) ; undo.show()
674         #redo = gtk.Button(stock = gtk.STOCK_REDO) ; redo.show()
675         #add = gtk.Button(stock = gtk.STOCK_ADD) ; add.show()
676         #delete = gtk.Button(stock = gtk.STOCK_REMOVE) ; delete.show()
677         #clear = gtk.Button(stock = gtk.STOCK_CLEAR) ; clear.show()
678         #name = gtk.Label("1.2.3.4.5") ; name.show()
679
680         back = gtk.Button("<") ; back.show()
681         fore = gtk.Button(">") ; fore.show()
682         red = gtk.ToggleButton("red"); red.show()
683         undo = gtk.Button("u") ; undo.show()
684         redo = gtk.Button("r") ; redo.show()
685         add = gtk.Button("+") ; add.show()
686         delete = gtk.Button("-") ; delete.show()
687         clear = gtk.Button("C") ; clear.show()
688         text = gtk.ToggleButton("T") ; text.show(); text.set_sensitive(False)
689         name = gtk.Button("1.2.3.4.5") ; name.show()
690
691         bar.add(back)
692         bar.add(fore)
693         bar.add(red)
694         bar.add(undo)
695         bar.add(redo)
696         bar.add(add)
697         bar.add(delete)
698         bar.add(clear)
699         bar.add(text)
700         bar.add(name)
701
702         back.connect("clicked", self.back)
703         fore.connect("clicked", self.fore)
704         red.connect("toggled", self.colour_change)
705         undo.connect("clicked", self.undo)
706         redo.connect("clicked", self.redo)
707         add.connect("clicked", self.add)
708         delete.connect("clicked", self.delete)
709         clear.connect("clicked", self.clear)
710         text.connect("toggled", self.text_change)
711         name.connect("clicked", self.setname)
712         self.name = name
713         self.page = page
714         self.redbutton = red
715         self.line = None
716         self.lines = []
717         self.hist = [] # undo history
718         self.texttoggle = text
719
720
721         page.connect("button_press_event", self.press)
722         page.connect("button_release_event", self.release)
723         page.connect("motion_notify_event", self.motion)
724         page.connect("expose-event", self.refresh)
725         page.set_events(gtk.gdk.EXPOSURE_MASK
726                         | gtk.gdk.BUTTON_PRESS_MASK
727                         | gtk.gdk.BUTTON_RELEASE_MASK
728                         | gtk.gdk.POINTER_MOTION_MASK
729                         | gtk.gdk.POINTER_MOTION_HINT_MASK)
730
731         window.show()
732         colourmap = page.get_colormap()
733         black = gtk.gdk.color_parse("black")
734         red = gtk.gdk.color_parse("red")
735         blue = gtk.gdk.color_parse("blue")
736         self.colour_black = page.window.new_gc()
737         self.colour_black.line_width = 2
738         self.colour_black.set_foreground(colourmap.alloc_color(black))
739
740         self.colour_red = page.window.new_gc()
741         self.colour_red.line_width = 2
742         self.colour_red.set_foreground(colourmap.alloc_color(red))
743
744         self.colour_textmode = page.window.new_gc()
745         self.colour_textmode.line_width = 2
746         self.colour_textmode.set_foreground(colourmap.alloc_color(blue))
747
748         self.colour = self.colour_black
749         self.colourname = "black"
750         self.bg = page.get_style().bg_gc[gtk.STATE_NORMAL]
751
752         if 'HOME' in os.environ:
753             home = os.environ['HOME']
754         else:
755             home = ""
756         if home == "" or home == "/":
757             home = "/home/root"
758         self.page_dir = home + '/Pages'
759         self.load_pages()
760
761         window.set_default_size(480,640)
762
763         window.show()
764
765         self.dict = Dictionary()
766         LoadDict(self.dict)
767         self.textstr = None
768
769
770         ctx = page.get_pango_context()
771         fd = ctx.get_font_description()
772         met = ctx.get_metrics(fd)
773         self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
774         self.lineascent = met.get_ascent() / pango.SCALE
775
776         self.timeout = None
777
778     def close_application(self, widget):
779         self.save_page()
780         gtk.main_quit()
781
782     def load_pages(self):
783         try:
784             os.mkdir(self.page_dir)
785         except:
786             pass
787         self.names = os.listdir(self.page_dir)
788         if len(self.names) == 0:
789             self.names.append("1")
790         self.names.sort(page_cmp)
791         self.pages = {}
792         self.pagenum = 0
793         self.load_page()
794         return
795
796     def press(self, c, ev):
797         # Start a new line
798         if self.timeout:
799             gobject.source_remove(self.timeout)
800             self.timeout = None
801
802         self.line = [ self.colourname, [int(ev.x), int(ev.y)] ]
803         return
804     def release(self, c, ev):
805         if self.line == None:
806             return
807         if self.timeout == None:
808             self.timeout = gobject.timeout_add(20*1000, self.tick)
809             
810         if len(self.line) == 2:
811             # just set a cursor
812             self.flush_text()
813             (lineno,index) = self.find_text(self.line[1])
814             if lineno == None:
815                 # new text, 
816                 self.textpos = self.line[1]
817                 self.texttoggle.set_sensitive(True)
818                 self.texttoggle.set_active(True)
819                 c.window.draw_rectangle(self.colour_textmode, True, int(ev.x),int(ev.y),
820                                         2,2)
821                 self.line = None
822             else:
823                 # clicked inside an old text.
824                 # shuffle it to the top, open it, edit.
825                 self.texttoggle.set_sensitive(True)
826                 self.texttoggle.set_active(True)
827                 ln = self.lines[lineno]
828                 self.lines = self.lines[:lineno] + self.lines[lineno+1:]
829                 self.textpos = ln[1]
830                 self.textstr = ln[2]
831                 if ln[0] == "red":
832                     self.colour = self.colour_red
833                     self.redbutton.set_active(True)
834                 else:
835                     self.colour = self.colour_black
836                     self.redbutton.set_active(False)
837                 self.textcurs = index + 1
838                 self.redraw()
839             self.line = None
840             return
841         if self.texttoggle.get_active():
842             sym = self.getsym()
843             if sym:
844                 self.add_sym(sym)
845             else:
846                 self.redraw()
847             self.line = None
848             return
849
850         self.lines.append(self.line)
851         self.texttoggle.set_sensitive(False)
852         self.line = None
853         return
854     def motion(self, c, ev):
855         if self.line:
856             if ev.is_hint:
857                 x, y, state = ev.window.get_pointer()
858             else:
859                 x = ev.x
860                 y = ev.y
861             x = int(x)
862             y = int(y)
863             prev = self.line[-1]
864             if abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10:
865                 return
866             if self.texttoggle.get_active():
867                 c.window.draw_line(self.colour_textmode, prev[0],prev[1],x,y)
868             else:
869                 c.window.draw_line(self.colour, prev[0],prev[1],x,y)
870             self.line.append([x,y])
871         return
872
873     def tick(self):
874         # nothing for 20 seconds, flush the page
875         self.save_page()
876         gobject.source_remove(self.timeout)
877         self.timeout = None
878
879     def find_text(self, pos):
880         x = pos[0]; y = pos[1]
881         for i in range(0, len(self.lines)):
882             p = self.lines[i]
883             if type(p[2]) != str:
884                 continue
885             y = pos[1] + self.lineascent
886             if x >= p[1][0] and y >= p[1][1] and y < p[1][1] + self.lineheight:
887                 # could be this line - check more precisely
888                 layout = self.page.create_pango_layout(p[2])
889                 (ink, log) = layout.get_pixel_extents()
890                 (ex,ey,ew,eh) = log
891                 if x < p[1][0] + ex or x > p[1][0] + ex + ew  or \
892                    y < p[1][1] + ey or \
893                    y > p[1][1] + ey + self.lineheight :
894                     continue
895                 # OK, it is in this one.  Find out where.
896                 (index, gr) = layout.xy_to_index((x - p[1][0] - ex) * pango.SCALE,
897                                                  (y - p[1][1] - ey - self.lineheight) * pango.SCALE)
898                 return (i, index)
899         return (None, None)
900     def flush_text(self):
901         if self.textstr == None:
902             return
903         if len(self.textstr) == 0:
904             self.textstr = None
905             return
906         l = [self.colourname, self.textpos, self.textstr]
907         self.lines.append(l)
908         self.textstr = None
909         self.texttoggle.set_active(False)
910
911     def draw_text(self, pos, colour, str, cursor = None):
912         layout = self.page.create_pango_layout(str)
913         self.page.window.draw_layout(colour, pos[0], pos[1] - self.lineascent,
914                                      layout)
915         if cursor != None:
916             (strong,weak) = layout.get_cursor_pos(cursor)
917             (x,y,width,height) = strong
918             self.page.window.draw_rectangle(self.colour_textmode, True,
919                                             pos[0] + x/pango.SCALE,
920                                             pos[1], 2,2)
921     def add_sym(self, sym):
922         if self.textstr == None:
923             self.textstr = ""
924             self.textcurs = 0
925         if sym == "<BS>":
926             if self.textcurs > 0:
927                 self.textstr = self.textstr[0:self.textcurs-1]+ \
928                                self.textstr[self.textcurs:]
929                 self.textcurs -= 1
930         elif sym == "<left>":
931             if self.textcurs > 0:
932                 self.textcurs -= 1
933         elif sym == "<right>":
934             if self.textcurs < len(self.textstr):
935                 self.textcurs += 1
936         elif sym == "<newline>":
937             tail = self.textstr[self.textcurs:]
938             self.textstr = self.textstr[:self.textcurs]
939             self.flush_text()
940             self.texttoggle.set_active(True)
941             self.textcurs = len(tail)
942             self.textstr = tail
943             self.textpos = [ self.textpos[0], self.textpos[1] +
944                              self.lineheight ]
945         else:
946             self.textstr = self.textstr[0:self.textcurs] + sym + \
947                            self.textstr[self.textcurs:]
948             self.textcurs += 1
949         self.redraw()
950
951
952     def getsym(self):
953         alloc = self.page.get_allocation()
954         pagebb = BBox(Point(0,0))
955         pagebb.add(Point(alloc.width, alloc.height))
956         pagebb.finish(div = 2)
957
958         p = PPath(self.line[1][0], self.line[1][1])
959         for pp in self.line[1:]:
960             p.add(pp[0], pp[1])
961         p.close()
962         patn = p.text()
963         pos = pagebb.relpos(p.bbox)
964         tpos = "mid"
965         if pos < 3:
966             tpos = "top"
967         if pos >= 6:
968             tpos = "bot"
969         sym = self.dict.match(patn, tpos)
970         if sym == None:
971             print "Failed to match pattern:", patn
972         return sym
973
974     def refresh(self, area, ev):
975         self.redraw()
976     def redraw(self):
977         self.name.set_label(self.names[self.pagenum])
978         self.page.window.draw_rectangle(self.bg, True, 0, 0,
979                                         480,640)
980         for l in self.lines:
981             if l[0] == "red":
982                 col = self.colour_red
983             else:
984                 col = self.colour_black
985             st = l[1]
986             if type(l[2]) == list:
987                 for p in l[2:]:
988                     self.page.window.draw_line(col, st[0], st[1],
989                                                p[0],p[1])
990                     st = p
991             if type(l[2]) == str:
992                 self.draw_text(st, col, l[2])
993
994         if self.textstr != None:
995             self.draw_text(self.textpos, self.colour, self.textstr,
996                            self.textcurs)
997
998         return
999
1000     def back(self,b):
1001         self.save_page()
1002         if self.pagenum <= 0:
1003             return
1004         self.pagenum -= 1
1005         self.load_page()
1006         self.redraw()
1007         return
1008     def fore(self,b):
1009         if self.pagenum >= len(self.names)-1:
1010             return self.add(b)
1011         self.save_page()
1012         self.pagenum += 1
1013         self.load_page()
1014         self.redraw()
1015
1016         return
1017     def colour_change(self,t):
1018         if t.get_active():
1019             self.colour = self.colour_red
1020             self.colourname = "red"
1021         else:
1022             self.colour = self.colour_black
1023             self.colourname = "black"
1024         if self.textstr:
1025             self.draw_text(self.textpos, self.colour, self.textstr,
1026                            self.textcurs)
1027             
1028         return
1029     def text_change(self,t):
1030         self.flush_text()
1031         return
1032     def undo(self,b):
1033         if len(self.lines) == 0:
1034             return
1035         self.hist.append(self.lines.pop())
1036         self.redraw()
1037         return
1038     def redo(self,b):
1039         if len(self.hist) == 0:
1040             return
1041         self.lines.append(self.hist.pop())
1042         self.redraw()
1043         return
1044     def add(self,b):
1045         # New name is either
1046         #  - take last number and increment it
1047         #  - add .1
1048         self.flush_text()
1049         if len(self.lines) == 0:
1050             # don't add after a blank page
1051             return
1052         self.save_page()
1053         newname = self.choose_unique(self.names[self.pagenum])
1054
1055         self.names = self.names[0:self.pagenum+1] + [ newname ] + \
1056                      self.names[self.pagenum+1:]
1057         self.pagenum += 1;
1058         self.lines = []
1059         self.redraw()
1060         return
1061     def choose_unique(self, newname):
1062         while newname in self.pages:
1063             new2 = inc_name(newname)
1064             if new2 not in self.pages:
1065                 newname = new2
1066             elif (newname + ".1") not in self.pages:
1067                 newname = newname + ".1"
1068             else:
1069                 newname = newname + ".0.1"
1070         return newname
1071     def delete(self,b):
1072         self.flush_text()
1073         if len(self.names) <= 1:
1074             return
1075         if len(self.lines) > 0:
1076             return
1077         self.save_page()
1078         nm = self.names[self.pagenum]
1079         if nm in self.pages:
1080             del self.pages[nm]
1081         self.names = self.names[0:self.pagenum] + self.names[self.pagenum+1:]
1082         if self.pagenum >= len(self.names):
1083             self.pagenum -= 1
1084         self.load_page()
1085         self.redraw()
1086
1087         return
1088     def rename(self, newname):
1089         # Rename current page and rename the file
1090         # Maybe we should resort the name list, but then we need to update
1091         # pagenum which is awkward.
1092         if self.names[self.pagenum] == newname:
1093             return
1094         newname = self.choose_unique(newname)
1095         oldpath = self.page_dir + "/" + self.names[self.pagenum]
1096         newpath = self.page_dir + "/" + newname
1097         try :
1098             os.rename(oldpath, newpath)
1099             self.names[self.pagenum] = newname
1100             self.name.set_label(self.names[self.pagenum])
1101         except:
1102             pass
1103
1104     def setname(self,b):
1105         if self.textstr:
1106             if len(self.textstr) > 0:
1107                 self.rename(self.textstr)
1108                 
1109     def clear(self,b):
1110         while len(self.lines) > 0:
1111             self.hist.append(self.lines.pop())
1112         self.redraw()
1113         return
1114
1115     def parseline(self, l):
1116         # string in "", or num,num.  ':' separates words
1117         words = l.strip().split(':')
1118         line = []
1119         for w in words:
1120             if w[0] == '"':
1121                 w = w[1:-1]
1122             elif w.find(',') >= 0:
1123                 n = w.find(',')
1124                 x = int(w[:n])
1125                 y = int(w[n+1:])
1126                 w = [x,y]
1127             line.append(w)
1128         return line
1129
1130     def load_page(self):
1131         nm = self.names[self.pagenum]
1132         if nm in self.pages:
1133             if self.names[self.pagenum] in self.pages:
1134                 self.lines = self.pages[self.names[self.pagenum]]
1135             return
1136         self.lines = [];
1137         try:
1138             f = open(self.page_dir + "/" + self.names[self.pagenum], "r")
1139         except:
1140             f = None
1141         if f:
1142             l = f.readline()
1143             while len(l) > 0:
1144                 self.lines.append(self.parseline(l))
1145                 l = f.readline()
1146             f.close()
1147         return
1148
1149     def save_page(self):
1150         self.flush_text()
1151         self.pages[self.names[self.pagenum]] = self.lines
1152         fn = self.page_dir + "/" + self.names[self.pagenum]
1153         if len(self.lines) == 0:
1154             try:
1155                 os.unlink(fn)
1156             except:
1157                 pass
1158             return
1159         f = open(fn, "w")
1160         for l in self.lines:
1161             start = True
1162             if not l:
1163                 continue
1164             for w in l:
1165                 if not start:
1166                     f.write(":")
1167                 start = False
1168                 if isinstance(w, str):
1169                     f.write('"%s"' % w)
1170                 elif isinstance(w, list):
1171                     f.write("%d,%d" %( w[0],w[1]))
1172             f.write("\n")
1173         f.close()
1174
1175 def main():
1176     gtk.main()
1177     return 0
1178 if __name__ == "__main__":
1179     ScribblePad()
1180     main()