]> git.neil.brown.name Git - scribble.git/blob - scribble.py
Autosave
[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                 c.window.draw_rectangle(self.colour_textmode, True, int(ev.x),int(ev.y),
819                                         2,2)
820                 self.line = None
821             else:
822                 # clicked inside an old text.
823                 # shuffle it to the top, open it, edit.
824                 self.texttoggle.set_sensitive(True)
825                 self.texttoggle.set_active(True)
826                 ln = self.lines[lineno]
827                 self.lines = self.lines[:lineno] + self.lines[lineno+1:]
828                 self.textpos = ln[1]
829                 self.textstr = ln[2]
830                 if ln[0] == "red":
831                     self.colour = self.colour_red
832                     self.redbutton.set_active(True)
833                 else:
834                     self.colour = self.colour_black
835                     self.redbutton.set_active(False)
836                 self.textcurs = index + 1
837                 self.redraw()
838             self.line = None
839             return
840         if self.texttoggle.get_active():
841             sym = self.getsym()
842             if sym:
843                 self.add_sym(sym)
844             else:
845                 self.redraw()
846             self.line = None
847             return
848
849         self.lines.append(self.line)
850         self.texttoggle.set_sensitive(False)
851         self.line = None
852         return
853     def motion(self, c, ev):
854         if self.line:
855             if ev.is_hint:
856                 x, y, state = ev.window.get_pointer()
857             else:
858                 x = ev.x
859                 y = ev.y
860             x = int(x)
861             y = int(y)
862             prev = self.line[-1]
863             if abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10:
864                 return
865             if self.texttoggle.get_active():
866                 c.window.draw_line(self.colour_textmode, prev[0],prev[1],x,y)
867             else:
868                 c.window.draw_line(self.colour, prev[0],prev[1],x,y)
869             self.line.append([x,y])
870         return
871
872     def tick(self):
873         # nothing for 20 seconds, flush the page
874         self.save_page()
875         gobject.source_remove(self.timeout)
876         self.timeout = None
877
878     def find_text(self, pos):
879         x = pos[0]; y = pos[1]
880         for i in range(0, len(self.lines)):
881             p = self.lines[i]
882             if type(p[2]) != str:
883                 continue
884             y = pos[1] + self.lineascent
885             if x >= p[1][0] and y >= p[1][1] and y < p[1][1] + self.lineheight:
886                 # could be this line - check more precisely
887                 layout = self.page.create_pango_layout(p[2])
888                 (ink, log) = layout.get_pixel_extents()
889                 (ex,ey,ew,eh) = log
890                 if x < p[1][0] + ex or x > p[1][0] + ex + ew  or \
891                    y < p[1][1] + ey or \
892                    y > p[1][1] + ey + self.lineheight :
893                     continue
894                 # OK, it is in this one.  Find out where.
895                 (index, gr) = layout.xy_to_index((x - p[1][0] - ex) * pango.SCALE,
896                                                  (y - p[1][1] - ey - self.lineheight) * pango.SCALE)
897                 return (i, index)
898         return (None, None)
899     def flush_text(self):
900         if self.textstr == None:
901             return
902         if len(self.textstr) == 0:
903             self.textstr = None
904             return
905         l = [self.colourname, self.textpos, self.textstr]
906         self.lines.append(l)
907         self.textstr = None
908
909     def draw_text(self, pos, colour, str, cursor = None):
910         layout = self.page.create_pango_layout(str)
911         self.page.window.draw_layout(colour, pos[0], pos[1] - self.lineascent,
912                                      layout)
913         if cursor != None:
914             (strong,weak) = layout.get_cursor_pos(cursor)
915             (x,y,width,height) = strong
916             self.page.window.draw_rectangle(self.colour_textmode, True,
917                                             pos[0] + x/pango.SCALE,
918                                             pos[1], 2,2)
919     def add_sym(self, sym):
920         if self.textstr == None:
921             self.textstr = ""
922             self.textcurs = 0
923         if sym == "<BS>":
924             if self.textcurs > 0:
925                 self.textstr = self.textstr[0:self.textcurs-1]+ \
926                                self.textstr[self.textcurs:]
927                 self.textcurs -= 1
928         elif sym == "<left>":
929             if self.textcurs > 0:
930                 self.textcurs -= 1
931         elif sym == "<right>":
932             if self.textcurs < len(self.textstr):
933                 self.textcurs += 1
934         elif sym == "<newline>":
935             tail = self.textstr[self.textcurs:]
936             self.textstr = self.textstr[:self.textcurs]
937             self.flush_text()
938             self.textcurs = len(tail)
939             self.textstr = tail
940             self.textpos = [ self.textpos[0], self.textpos[1] +
941                              self.lineheight ]
942         else:
943             self.textstr = self.textstr[0:self.textcurs] + sym + \
944                            self.textstr[self.textcurs:]
945             self.textcurs += 1
946         self.redraw()
947
948
949     def getsym(self):
950         alloc = self.page.get_allocation()
951         pagebb = BBox(Point(0,0))
952         pagebb.add(Point(alloc.width, alloc.height))
953         pagebb.finish(div = 2)
954
955         p = PPath(self.line[1][0], self.line[1][1])
956         for pp in self.line[1:]:
957             p.add(pp[0], pp[1])
958         p.close()
959         patn = p.text()
960         pos = pagebb.relpos(p.bbox)
961         tpos = "mid"
962         if pos < 3:
963             tpos = "top"
964         if pos >= 6:
965             tpos = "bot"
966         sym = self.dict.match(patn, tpos)
967         if sym == None:
968             print "Failed to match pattern:", patn
969         return sym
970
971     def refresh(self, area, ev):
972         self.redraw()
973     def redraw(self):
974         self.name.set_label(self.names[self.pagenum])
975         self.page.window.draw_rectangle(self.bg, True, 0, 0,
976                                         480,640)
977         for l in self.lines:
978             if l[0] == "red":
979                 col = self.colour_red
980             else:
981                 col = self.colour_black
982             st = l[1]
983             if type(l[2]) == list:
984                 for p in l[2:]:
985                     self.page.window.draw_line(col, st[0], st[1],
986                                                p[0],p[1])
987                     st = p
988             if type(l[2]) == str:
989                 self.draw_text(st, col, l[2])
990
991         if self.textstr != None:
992             self.draw_text(self.textpos, self.colour, self.textstr,
993                            self.textcurs)
994
995         return
996
997     def back(self,b):
998         self.save_page()
999         if self.pagenum <= 0:
1000             return
1001         self.pagenum -= 1
1002         self.load_page()
1003         self.redraw()
1004         return
1005     def fore(self,b):
1006         if self.pagenum >= len(self.names)-1:
1007             return self.add(b)
1008         self.save_page()
1009         self.pagenum += 1
1010         self.load_page()
1011         self.redraw()
1012
1013         return
1014     def colour_change(self,t):
1015         if t.get_active():
1016             self.colour = self.colour_red
1017             self.colourname = "red"
1018         else:
1019             self.colour = self.colour_black
1020             self.colourname = "black"
1021         if self.textstr:
1022             self.draw_text(self.textpos, self.colour, self.textstr,
1023                            self.textcurs)
1024             
1025         return
1026     def text_change(self,t):
1027         self.flush_text()
1028         return
1029     def undo(self,b):
1030         if len(self.lines) == 0:
1031             return
1032         self.hist.append(self.lines.pop())
1033         self.redraw()
1034         return
1035     def redo(self,b):
1036         if len(self.hist) == 0:
1037             return
1038         self.lines.append(self.hist.pop())
1039         self.redraw()
1040         return
1041     def add(self,b):
1042         # New name is either
1043         #  - take last number and increment it
1044         #  - add .1
1045         self.flush_text()
1046         if len(self.lines) == 0:
1047             # don't add after a blank page
1048             return
1049         self.save_page()
1050         newname = self.choose_unique(self.names[self.pagenum])
1051
1052         self.names = self.names[0:self.pagenum+1] + [ newname ] + \
1053                      self.names[self.pagenum+1:]
1054         self.pagenum += 1;
1055         self.lines = []
1056         self.redraw()
1057         return
1058     def choose_unique(self, newname):
1059         while newname in self.pages:
1060             new2 = inc_name(newname)
1061             if new2 not in self.pages:
1062                 newname = new2
1063             elif (newname + ".1") not in self.pages:
1064                 newname = newname + ".1"
1065             else:
1066                 newname = newname + ".0.1"
1067         return newname
1068     def delete(self,b):
1069         self.flush_text()
1070         if len(self.names) <= 1:
1071             return
1072         if len(self.lines) > 0:
1073             return
1074         self.save_page()
1075         nm = self.names[self.pagenum]
1076         if nm in self.pages:
1077             del self.pages[nm]
1078         self.names = self.names[0:self.pagenum] + self.names[self.pagenum+1:]
1079         if self.pagenum >= len(self.names):
1080             self.pagenum -= 1
1081         self.load_page()
1082         self.redraw()
1083
1084         return
1085     def rename(self, newname):
1086         # Rename current page and rename the file
1087         # Maybe we should resort the name list, but then we need to update
1088         # pagenum which is awkward.
1089         if self.names[self.pagenum] == newname:
1090             return
1091         newname = self.choose_unique(newname)
1092         oldpath = self.page_dir + "/" + self.names[self.pagenum]
1093         newpath = self.page_dir + "/" + newname
1094         try :
1095             os.rename(oldpath, newpath)
1096             self.names[self.pagenum] = newname
1097             self.name.set_label(self.names[self.pagenum])
1098         except:
1099             pass
1100
1101     def setname(self,b):
1102         if self.textstr:
1103             if len(self.textstr) > 0:
1104                 self.rename(self.textstr)
1105                 
1106     def clear(self,b):
1107         while len(self.lines) > 0:
1108             self.hist.append(self.lines.pop())
1109         self.redraw()
1110         return
1111
1112     def parseline(self, l):
1113         # string in "", or num,num.  ':' separates words
1114         words = l.strip().split(':')
1115         line = []
1116         for w in words:
1117             if w[0] == '"':
1118                 w = w[1:-1]
1119             elif w.find(',') >= 0:
1120                 n = w.find(',')
1121                 x = int(w[:n])
1122                 y = int(w[n+1:])
1123                 w = [x,y]
1124             line.append(w)
1125         return line
1126
1127     def load_page(self):
1128         nm = self.names[self.pagenum]
1129         if nm in self.pages:
1130             if self.names[self.pagenum] in self.pages:
1131                 self.lines = self.pages[self.names[self.pagenum]]
1132             return
1133         self.lines = [];
1134         try:
1135             f = open(self.page_dir + "/" + self.names[self.pagenum], "r")
1136         except:
1137             f = None
1138         if f:
1139             l = f.readline()
1140             while len(l) > 0:
1141                 self.lines.append(self.parseline(l))
1142                 l = f.readline()
1143             f.close()
1144         return
1145
1146     def save_page(self):
1147         self.flush_text()
1148         self.pages[self.names[self.pagenum]] = self.lines
1149         fn = self.page_dir + "/" + self.names[self.pagenum]
1150         if len(self.lines) == 0:
1151             try:
1152                 os.unlink(fn)
1153             except:
1154                 pass
1155             return
1156         f = open(fn, "w")
1157         for l in self.lines:
1158             start = True
1159             if not l:
1160                 continue
1161             for w in l:
1162                 if not start:
1163                     f.write(":")
1164                 start = False
1165                 if isinstance(w, str):
1166                     f.write('"%s"' % w)
1167                 elif isinstance(w, list):
1168                     f.write("%d,%d" %( w[0],w[1]))
1169             f.write("\n")
1170         f.close()
1171
1172 def main():
1173     gtk.main()
1174     return 0
1175 if __name__ == "__main__":
1176     ScribblePad()
1177     main()