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