3 # Create/edit/send/display/search SMS messages.
4 # Two main displays: Create and display
6 # Allow entry of recipient and text of SMS message and allow basic editting
7 # When entering recipient, text box can show address matches for selection
8 # Bottom buttons are "Select"..
9 # When entering text, if there is no text, buttom buttons are:
11 # If these is some text, bottom buttons are:
15 # We usually display a list of messages which can be selected from
16 # There is a 'search' box to restrict message to those with a string
17 # Options for selected message are:
18 # Delete Reply View Open(for draft)/Forward(for non-draft)
19 # In View mode, the whole text is displayed and the 'View' button becomes "Index"
20 # or "Show List" or "ReadIt"
21 # General options are:
25 # Delete becomes Undelete and can undelete a whole stack.
26 # Delete can become undelete without deleting be press-and-hold
29 # Messages are sent using a separate program. e.g. sms-gsm
30 # Different recipients can use different programs based on flag in address book.
31 # Somehow senders can be configured.
32 # e.g. sms-exetel needs username, password, sender strings.
33 # press-and-hold on the send button allows a sender to be selected.
36 # Send an SMS message using some backend.
41 # 'del' to return to 'list' view
42 # top buttons: del, view/list, new/open/reply
43 # so can only reply when viewing whole message
50 # DONE handle newline chars in summary
51 # DONE cope properly when the month changes.
52 # switch-to-'new' on 'expose'
53 # 'draft' button becomes 'cancel' when all is empty
54 # DONE better display of name/number of destination
55 # jump to list mode when change 'list'
56 # 'open' becomes 'reply' when current message was received.
57 # new message becomes non-new when replied to
58 # '<list>' button doesn't select, but just makes choice.
59 # 'new' becomes 'select' when <list> has been pressed.
60 # DONE Start in 'read', preferrably 'new'
61 # DONE always report status from send
62 # DONE draft/new/recv/sent/all - 5 groups
63 # DONE allow scrolling through list
64 # DONE + prefix to work
65 # DONE compose as 'GSM' or 'EXE' send
66 # DONE somehow do addressbook lookup for compose
67 # DONE addressbook lookup for display
68 # On 'send' move to 'sent' (not draft) and display list
69 # When open 'draft', delete from drafts... or later..
70 # When 'reply' to new message, make it not 'new'
72 # get 'cut' to work from phone number entry.
73 # how to configure sender...
74 # need to select 'number only' mode for entry
75 # need drop-down of common numbers
78 # faster text input!!!
79 # DONE status message of transmission
80 # DONE maybe go to 'past messages' on send - need to go somewhere
81 # cut from other sources??
82 # DONE scroll if message is too long!
84 # DONE reread sms file when changing view
85 # Don't add drafts that have not been changed... or
86 # When opening a draft, delete it... or replace when re-add
87 # DONE when sending a message, store - as draft if send failed
88 # DONE show the 'send' status somewhere
89 # DONE add a 'new' button from 'list' to 'send'
90 # Need 'reply' button.. Make 'open' show 'reply' when 'to' me.
91 # Scroll when near top or bottom
92 # hide status line when not needed.
94 # 'folder' view - by month or day
95 # highlight 'new' and 'draft' messages in different colour
96 # support 'sent' and 'received' distinction
97 # when return from viewing a 'new' message, clear the 'new' status
98 # enable starting in 'listing/New' mode
101 import sys, time, os, re
103 from subprocess import Popen, PIPE
104 from storesms import SMSstore, SMSmesg
107 ###########################################################
108 # Writing recognistion code
114 # Where they are like lowercase, we either double
115 # the last stroke (L, J, I) or draw backwards (S, Z, X)
116 # U V are a special case
118 dict.add('A', "R(4)6,8")
119 dict.add('B', "R(4)6,4.R(7)1,6")
120 dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6")
121 dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6")
122 dict.add('C', "R(4)8,2")
123 dict.add('D', "R(4)6,6")
124 dict.add('E', "L(1)2,8.L(7)2,8")
125 # double the stem for F
126 dict.add('F', "L(4)2,6.S(3)7,1")
127 dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1")
129 dict.add('G', "L(4)2,5.S(8)1,7")
130 dict.add('G', "L(4)2,5.R(8)6,8")
131 # FIXME I need better straight-curve alignment
132 dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1")
133 dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1")
134 # capital I is down/up
135 dict.add('I', "S(4)1,7.S(4)7,1")
137 # Capital J has a left/right tail
138 dict.add('J', "R(4)1,6.S(7)3,5")
140 dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8")
142 # Capital L, like J, doubles the foot
143 dict.add('L', "L(4)0,8.S(7)4,3")
145 dict.add('M', "R(3)6,5.R(5)3,8")
146 dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8")
148 dict.add('N', "R(3)6,8.L(5)0,2")
150 # Capital O is CW, but can be CCW in special dict
151 dict.add('O', "R(4)1,1", bot='0')
153 dict.add('P', "R(4)6,3")
154 dict.add('Q', "R(4)7,7.S(8)0,8")
156 dict.add('R', "R(4)6,4.S(8)0,8")
158 # S is drawn bottom to top.
159 dict.add('S', "L(7)6,1.R(1)7,2")
161 # Double the stem for capital T
162 dict.add('T', "R(4)0,8.S(5)7,1")
164 # U is L to R, V is R to L for now
165 dict.add('U', "L(4)0,2")
166 dict.add('V', "R(4)2,0")
168 dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0")
169 dict.add('W', "R(5)2,3.R(3)5,0")
171 dict.add('X', "R(4)6,0")
173 dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2")
174 dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2")
176 dict.add('Z', "R(4)8,2.L(4)6,0")
179 dict.add('a', "L(4)2,2.L(5)1,7")
180 dict.add('a', "L(4)2,2.L(5)0,8")
181 dict.add('a', "L(4)2,2.S(5)0,8")
182 dict.add('b', "S(3)1,7.R(7)6,3")
183 dict.add('c', "L(4)2,8", top='C')
184 dict.add('d', "L(4)5,2.S(5)1,7")
185 dict.add('d', "L(4)5,2.L(5)0,8")
186 dict.add('e', "S(4)3,5.L(4)5,8")
187 dict.add('e', "L(4)3,8")
188 dict.add('f', "L(4)2,6", top='F')
189 dict.add('f', "S(1)5,3.S(3)1,7", top='F')
190 dict.add('g', "L(1)2,2.R(4)1,6")
191 dict.add('h', "S(3)1,7.R(7)6,8")
192 dict.add('h', "L(3)0,5.R(7)6,8")
193 dict.add('i', "S(4)1,7", top='I', bot='1')
194 dict.add('j', "R(4)1,6", top='J')
195 dict.add('k', "L(3)0,5.L(7)2,8")
196 dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8")
197 dict.add('l', "L(4)0,8", top='L')
198 dict.add('l', "S(3)1,7.S(7)3,5", top='L')
199 dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8")
200 dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8")
201 dict.add('n', "S(3)1,7.R(4)6,8")
202 dict.add('o', "L(4)1,1", top='O', bot='0')
203 dict.add('p', "S(3)1,7.R(4)6,3")
204 dict.add('q', "L(1)2,2.L(5)1,5")
205 dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2")
206 dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7")
207 # FIXME this double 1,7 is due to a gentle where the
208 # second looks like a line because it is narrow.??
209 dict.add('r', "S(3)1,7.R(4)6,2")
210 dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5')
211 dict.add('t', "R(4)0,8", top='T', bot='7')
212 dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7')
213 dict.add('u', "L(4)0,2.S(5)1,7")
214 dict.add('v', "L(4)0,2.L(2)0,2")
215 dict.add('w', "L(3)0,2.L(5)0,2", top='W')
216 dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W')
217 dict.add('w', "L(3)0,5.L(5)3,2", top='W')
218 dict.add('x', "L(4)0,6", top='X')
219 dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved
220 dict.add('y', "L(1)0,2.S(5)2,7", top='Y')
221 dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2')
224 dict.add('0', "L(4)7,7")
225 dict.add('0', "R(4)7,7")
226 dict.add('1', "S(4)7,1")
227 dict.add('2', "R(4)0,6.S(7)3,5")
228 dict.add('2', "R(4)3,6.L(4)2,8")
229 dict.add('3', "R(1)0,6.R(7)1,6")
230 dict.add('4', "L(4)7,5")
231 dict.add('5', "L(1)2,6.R(7)0,3")
232 dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3")
233 dict.add('6', "L(4)2,3")
234 dict.add('7', "S(1)3,5.R(4)1,6")
235 dict.add('7', "R(4)0,6")
236 dict.add('7', "R(4)0,7")
237 dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1")
238 dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1")
239 dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0")
240 dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1")
241 dict.add('9', "L(1)2,2.S(5)1,7")
243 dict.add(' ', "S(4)3,5")
244 dict.add('<BS>', "S(4)5,3")
245 dict.add('-', "S(4)3,5.S(4)5,3")
246 dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5")
247 dict.add("<left>", "S(4)5,3.S(3)3,5")
248 dict.add("<right>","S(4)3,5.S(5)5,3")
249 dict.add("<left>", "S(4)7,1.S(1)1,7") # "<up>"
250 dict.add("<right>","S(4)1,7.S(7)7,1") # "<down>"
251 dict.add("<newline>", "S(4)2,6")
255 # Each segment has for elements:
256 # direction: Right Straight Left (R=cw, L=ccw)
260 # Segments match if there difference at each element
261 # is 0, 1, or 3 (RSL coded as 012)
262 # A difference of 1 required both to be same / 3
263 # On a match, return number of 0s
264 # On non-match, return -1
265 def __init__(self, str):
271 if str[1] != '(' or str[3] != ')' or str[5] != ',':
282 self.e[1] = int(str[2])
283 self.e[2] = int(str[4])
284 self.e[3] = int(str[6])
286 def match(self, other):
289 diff = abs(self.e[i] - other.e[i])
294 elif diff == 1 and (self.e[i]/3 == other.e[i]/3):
301 # A Dict Pattern is a list of segments.
302 # A parsed pattern matches a dict pattern if
303 # the are the same nubmer of segments and they
304 # all match. The value of the match is the sum
305 # of the individual matches.
306 # A DictPattern is printers as segments joined by periods.
308 def __init__(self, str):
309 self.segs = map(DictSegment, str.split("."))
310 def match(self,other):
311 if len(self.segs) != len(other.segs):
314 for i in range(0,len(self.segs)):
315 m = self.segs[i].match(other.segs[i])
323 # The dictionary hold all the pattern for symbols and
325 # Each pattern in the directionary can be associated
326 # with 3 symbols. One when drawing in middle of screen,
327 # one for top of screen, one for bottom.
328 # Often these will all be the same.
329 # This allows e.g. s and S to have the same pattern in different
330 # location on the touchscreen.
331 # A match requires a unique entry with a match that is better
332 # than any other entry.
336 def add(self, sym, pat, top = None, bot = None):
337 if top == None: top = sym
338 if bot == None: bot = sym
339 self.dict.append((DictPattern(pat), sym, top, bot))
344 for (ptn, sym, top, bot) in self.dict:
348 val = (sym, top, bot)
353 def match(self, str, pos = "mid"):
358 (mid, top, bot) = self._match(p)
359 if pos == "top": return top
360 if pos == "bot": return bot
365 # This represents a point in the path and all the points leading
366 # up to it. It allows us to find the direction and curvature from
367 # one point to another
368 # We store x,y, and sum/cnt of points so far
369 def __init__(self,x,y) :
386 if self.x == x and self.y == y:
395 return abs(self.x - p.x)
397 return abs(self.y - p.y)
416 if self.cnt == p.cnt:
419 (x2,y2) = self.meanpoint(p)
420 x3 = self.x; y3 = self.y
422 curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
423 curve = curve * 100 / ((y3-y1)*(y3-y1)
432 if self.cnt == p.cnt:
435 (x2,y2) = self.meanpoint(p)
436 x3 = self.x; y3 = self.y
438 curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
439 curve = curve * 100 / ((y3-y1)*(y3-y1)
443 def meanpoint(self,p):
444 x = (self.xsum - p.xsum) / (self.cnt - p.cnt)
445 y = (self.ysum - p.ysum) / (self.cnt - p.cnt)
448 def is_sharp(self,A,C):
449 # Measure the cosine at self between A and C
450 # as A and C could be curve, we take the mean point on
451 # self.A and self.C as the points to find cosine between
452 (ax,ay) = self.meanpoint(A)
453 (cx,cy) = self.meanpoint(C)
454 a = ax-self.x; b=ay-self.y
455 c = cx-self.x; d=cy-self.y
458 h = math.sqrt(x*x+y*y)
466 # a BBox records min/max x/y of some Points and
467 # can subsequently report row, column, pos of each point
468 # can also locate one bbox in another
470 def __init__(self, p):
477 return self.maxx - self.minx
479 return self.maxy - self.miny
491 def finish(self, div = 3):
492 # if aspect ratio is bad, we adjust max/min accordingly
493 # before setting [xy][12]. We don't change self.min/max
494 # as they are used to place stroke in bigger bbox.
495 # Normally divisions are at 1/3 and 2/3. They can be moved
496 # by setting div e.g. 2 = 1/2 and 1/2
497 (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy)
498 if (maxx - minx) * 3 < (maxy - miny) * 2:
500 mid = int((maxx + minx)/2)
501 halfwidth = int ((maxy - miny)/3)
502 minx = mid - halfwidth
503 maxx = mid + halfwidth
504 if (maxy - miny) * 3 < (maxx - minx) * 2:
506 mid = int((maxy + miny)/2)
507 halfheight = int ((maxx - minx)/3)
508 miny = mid - halfheight
509 maxy = mid + halfheight
512 self.x1 = int((div1*minx + maxx)/div)
513 self.x2 = int((minx + div1*maxx)/div)
514 self.y1 = int((div1*miny + maxy)/div)
515 self.y2 = int((miny + div1*maxy)/div)
518 # 0, 1, 2 - top to bottom
532 return self.row(p) * 3 + self.col(p)
535 # b is a box within self. find location 0-8
536 if b.maxx < self.x2 and b.minx < self.x1:
538 elif b.minx > self.x1 and b.maxx > self.x2:
542 if b.maxy < self.y2 and b.miny < self.y1:
544 elif b.miny > self.y1 and b.maxy > self.y2:
551 def different(*args):
554 if cur != 0 and i != 0 and cur != i:
567 # a PPath refines a list of x,y points into a list of Points
568 # The Points mark out segments which end at significant Points
569 # such as inflections and reversals.
571 def __init__(self, x,y):
573 self.start = Point(x,y)
574 self.mid = Point(x,y)
575 self.curr = Point(x,y)
576 self.list = [ self.start ]
581 if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or
582 (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or
583 (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))):
586 self.mid = self.curr.copy()
588 if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4:
589 self.start = self.mid.copy()
590 self.list.append(self.start)
591 self.mid = self.curr.copy()
594 self.list.append(self.curr)
596 def get_sectlist(self):
597 if len(self.list) <= 2:
598 return [[0,self.list]]
603 curcurve = B.curve(A)
604 for C in self.list[2:]:
608 if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve):
609 # B is too pointy, must break here
610 l.append([curcurve, s])
613 elif not different(cabc, cab, cbc, curcurve):
617 curcurve = maxcurve(cab, cbc, cabc)
618 elif not different(cabc, cab, cbc) :
619 # gentle inflection along AB
620 # was: AB goes in old and new section
621 # now: AB only in old section, but curcurve
623 l.append([curcurve,s])
625 curcurve =maxcurve(cab, cbc, cabc)
627 # Change of direction at B
628 l.append([curcurve,s])
634 l.append([curcurve,s])
638 def remove_shorts(self, bbox):
639 # in self.list, if a point is close to the previous point,
641 if len(self.list) <= 2:
647 for p in self.list[1:]:
654 # OK, we have a list of points with curvature between.
655 # want to divide this into sections.
656 # for each 3 consectutive points ABC curve of ABC and AB and BC
657 # If all the same, they are all in a section.
658 # If not B starts a new section and the old ends on B or C...
659 BB = BBox(self.list[0])
664 self.remove_shorts(BB)
665 sectlist = self.get_sectlist()
667 for c, s in sectlist:
669 dr = "R" # clockwise is to the Right
671 dr = "L" # counterclockwise to the Left
678 # If all points are in some row or column, then
680 rwdiff = False; cldiff = False
681 rw = bb.row(s[0]); cl=bb.col(s[0])
683 if bb.row(p) != rw: rwdiff = True
684 if bb.col(p) != cl: cldiff = True
685 if not rwdiff or not cldiff: dr = "S"
688 t1 += "(%d)" % BB.relpos(bb)
689 t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1]))
695 def __init__(self, page, callout):
698 self.callout = callout
701 self.dict = Dictionary()
705 page.connect("button_press_event", self.press)
706 page.connect("button_release_event", self.release)
707 page.connect("motion_notify_event", self.motion)
708 page.set_events(page.get_events()
709 | gtk.gdk.BUTTON_PRESS_MASK
710 | gtk.gdk.BUTTON_RELEASE_MASK
711 | gtk.gdk.POINTER_MOTION_MASK
712 | gtk.gdk.POINTER_MOTION_HINT_MASK)
714 def set_colour(self, col):
717 def press(self, c, ev):
721 self.line = [ [int(ev.x), int(ev.y)] ]
722 if not ev.send_event:
723 self.page.stop_emission('button_press_event')
725 def release(self, c, ev):
726 if self.line == None:
728 if len(self.line) == 1:
729 self.callout('click', ev)
735 self.callout('sym', sym)
736 self.callout('redraw', None)
740 def motion(self, c, ev):
743 x, y, state = ev.window.get_pointer()
750 if abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10:
753 c.window.draw_line(self.colour, prev[0],prev[1],x,y)
754 self.line.append([x,y])
758 alloc = self.page.get_allocation()
759 pagebb = BBox(Point(0,0))
760 pagebb.add(Point(alloc.width, alloc.height))
761 pagebb.finish(div = 2)
763 p = PPath(self.line[1][0], self.line[1][1])
764 for pp in self.line[1:]:
768 pos = pagebb.relpos(p.bbox)
774 sym = self.dict.match(patn, tpos)
776 print "Failed to match pattern:", patn
783 ########################################################################
787 class FingerText(gtk.TextView):
789 gtk.TextView.__init__(self)
790 self.set_wrap_mode(gtk.WRAP_WORD_CHAR)
791 self.exphan = self.connect('expose-event', self.config)
792 self.input = text_input(self, self.stylus)
794 def config(self, *a):
795 self.disconnect(self.exphan)
796 c = gtk.gdk.color_parse('red')
797 gc = self.window.new_gc()
798 gc.set_foreground(self.get_colormap().alloc_color(c))
799 #gc.set_line_attributes(2, gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND)
800 gc.set_subwindow(gtk.gdk.INCLUDE_INFERIORS)
801 self.input.set_colour(gc)
803 def stylus(self, cmd, info):
805 tl = self.get_toplevel()
809 ev = gtk.gdk.Event(gtk.gdk.KEY_PRESS)
813 ev.hardware_keycode = 22
815 (ev.keyval,) = struct.unpack_from("b", info)
816 w.emit('key_press_event', ev)
817 #self.get_buffer().insert_at_cursor(info)
820 if not info.send_event:
821 info.send_event = True
822 ev2 = gtk.gdk.Event(gtk.gdk.BUTTON_PRESS)
823 ev2.send_event = True
824 ev2.window = info.window
828 ev2.button = info.button
829 self.emit('button_press_event', ev2)
830 self.emit('button_release_event', info)
834 def insert_at_cursor(self, text):
835 self.get_buffer().insert_at_cursor(text)
837 class FingerEntry(gtk.Entry):
839 gtk.Entry.__init__(self)
841 def insert_at_cursor(self, text):
842 c = self.get_property('cursor-position')
844 t = t[0:c]+text+t[c:]
847 class SMSlist(gtk.DrawingArea):
848 def __init__(self, getlist):
849 gtk.DrawingArea.__init__(self)
851 self.width = self.height = 0
852 self.need_redraw = True
855 self.get_list = getlist
857 self.connect("expose-event", self.redraw)
858 self.connect("configure-event", self.reconfig)
860 self.connect("button_release_event", self.release)
861 self.connect("button_press_event", self.press)
862 self.set_events(gtk.gdk.EXPOSURE_MASK
863 | gtk.gdk.BUTTON_PRESS_MASK
864 | gtk.gdk.BUTTON_RELEASE_MASK
865 | gtk.gdk.STRUCTURE_MASK)
868 fd = pango.FontDescription('sans 10')
869 fd.set_absolute_size(25 * pango.SCALE)
871 met = self.get_pango_context().get_metrics(fd)
872 self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
873 fd = pango.FontDescription('sans 5')
874 fd.set_absolute_size(15 * pango.SCALE)
885 def set_book(self, book):
889 alloc = self.get_allocation()
890 lines = alloc.height / self.lineheight
893 def reset_list(self):
896 self.size_requested = 0
900 self.need_redraw = True
903 def assign_colour(self, purpose, name):
904 self.collist[purpose] = name
906 def reconfig(self, w, ev):
907 alloc = w.get_allocation()
910 if alloc.width != self.width or alloc.height != self.height:
912 self.need_redraw = True
914 def add_col(self, sym, col):
915 c = gtk.gdk.color_parse(col)
916 gc = self.window.new_gc()
917 gc.set_foreground(self.get_colormap().alloc_color(c))
918 self.colours[sym] = gc
920 def redraw(self, w, ev):
921 if self.colours == None:
923 for p in self.collist:
924 self.add_col(p, self.collist[p])
925 self.bg = self.get_style().bg_gc[gtk.STATE_NORMAL]
930 self.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0,
931 self.width, self.height)
934 self.need_redraw = False
935 if self.pixbuf == None:
936 alloc = self.get_allocation()
937 self.pixbuf = gtk.gdk.Pixmap(self.window, alloc.width, alloc.height)
938 self.width = alloc.width
939 self.height = alloc.height
940 self.pixbuf.draw_rectangle(self.bg, True, 0, 0,
941 self.width, self.height)
943 if self.top > self.selected:
946 if self.smslist == None or \
947 (self.top + max > len(self.smslist) and self.size_requested < self.top + max):
948 self.size_requested = self.top + max
949 self.smslist = self.get_list(self.top + max)
950 for i in range(len(self.smslist)):
953 if i > self.top + max:
955 if i == self.selected:
956 col = self.colours['bg-selected']
958 col = self.colours['bg-%d'%(i%2)]
960 self.pixbuf.draw_rectangle(col,
961 True, 0, (i-self.top)*self.lineheight,
962 self.width, self.lineheight)
963 self.draw_sms(self.smslist[i], (i - self.top) * self.lineheight)
966 def draw_sms(self, sms, yoff):
968 self.modify_font(self.smallfont)
969 tm = time.strftime("%Y-%m-%d %H:%M:%S", sms.time[0:6]+(0,0,0))
970 then = time.mktime(sms.time[0:6]+(0,0,-1))
975 delta = "%02d sec ago" % diff
977 delta = "%02d min ago" % (diff/60)
978 elif diff < 48*60*60:
979 delta = "%02dh%02dm ago" % ((diff/60/60), (diff/60)%60)
984 l = self.create_pango_layout(tm)
985 self.pixbuf.draw_layout(self.colours['time'],
987 co = sms.correspondent
989 cor = book_name(self.book, co)
992 if sms.source == 'LOCAL':
993 col = self.colours['recipient']
996 col = self.colours['sender']
998 l = self.create_pango_layout(co)
999 self.pixbuf.draw_layout(col,
1000 0, yoff + self.lineheight/2, l)
1001 self.modify_font(self.font)
1002 t = sms.text.replace("\n", " ")
1003 t = t.replace("\n", " ")
1004 l = self.create_pango_layout(t)
1005 if sms.state in ['DRAFT', 'NEW']:
1006 col = self.colours['mesg-new']
1008 col = self.colours['mesg']
1009 self.pixbuf.draw_layout(col,
1012 def press(self,w,ev):
1013 row = int(ev.y / self.lineheight)
1014 self.selected = self.top + row
1015 if self.selected >= len(self.smslist):
1016 self.selected = len(self.smslist) - 1
1017 if self.selected < 0:
1021 self.top += row - l / 2
1022 if self.top >= len(self.smslist) - l:
1023 self.top = len(self.smslist) - l + 1
1029 def release(self,w,ev):
1032 def load_book(file):
1036 f = open('/home/neilb/home/mobile-numbers-jan-08')
1040 rv.append([x[0],x[1]])
1041 rv.sort(lambda x,y: cmp(x[0],y[0]))
1044 def book_lookup(book, name, num):
1047 if name.lower() == l[0][0:len(name)].lower():
1049 elif l[0].lower().find(name.lower()) >= 0:
1058 def book_parse(book, name):
1062 while len(name) and name[-1] == '.':
1065 return book_lookup(book, name, cnt)
1069 def book_name(book, num):
1073 if len(ad[1]) >= 8 and num[-8:] == ad[1][-8:]:
1077 def book_speed(book, sym):
1078 i = book_lookup(book, sym, 0)
1079 if i[0] == None or i[0] != sym:
1081 j = book_lookup(book, i[1], 0)
1086 def name_lookup(book, str):
1088 # - a number - to dial
1089 # - optionally a name that is associated with that number
1090 # - optionally a new name to save the number as
1091 # The name is normally alpha, but can be a single digit for
1093 # Dots following a name allow us to stop through multiple matches.
1096 # This is a speed dial. It maps to name, then number
1097 # A string of >1 digits
1098 # This is a literal number, we look up name if we can
1100 # This is a look up against recent incoming calls
1101 # We look up name in phone book
1102 # A string starting with alpha, possibly ending with dots
1103 # This is a regular lookup in the phone book
1104 # A number followed by a string
1105 # This provides the string as a new name for saving
1106 # A string of dots followed by a string
1107 # This also provides the string as a newname
1108 # An alpha string, with dots, followed by '+'then a single symbol
1109 # This saves the match as a speed dial
1111 # We return a triple of (number,oldname,newname)
1112 if re.match('^[A-Za-z0-9]$', str):
1114 s = book_speed(book, str)
1116 return (s[0], s[1], None)
1118 m = re.match('^(\+?\d+)([A-Za-z][A-Za-z0-9 ]*)?$', str)
1120 # Number and possible newname
1121 s = book_name(book, m.group(1))
1123 return (m.group(1), s[0], m.group(2))
1125 return (m.group(1), None, m.group(2))
1126 m = re.match('^([A-Za-z][A-Za-z0-9 ]*)(\.*)(\+[A-Za-z0-9])?$', str)
1131 speed = m.group(3)[1]
1132 i = book_lookup(book, m.group(1), len(m.group(2)))
1134 return (i[1], i[0], speed)
1137 class SendSMS(gtk.Window):
1138 def __init__(self, store):
1139 gtk.Window.__init__(self)
1140 self.set_default_size(480,640)
1141 self.set_title("SendSMS")
1143 self.connect('destroy', self.close_win)
1145 self.selecting = False
1146 self.viewing = False
1151 self.reload_book = True
1153 self.cutbuffer = None
1155 d = dnotify.dir(store.dirname)
1156 self.watcher = d.watch('newmesg', lambda f : self.got_new())
1158 self.watch_clip('sms-new')
1160 self.connect('property-notify-event', self.newprop)
1161 self.add_events(gtk.gdk.PROPERTY_CHANGE_MASK)
1162 def newprop(self, w, ev):
1163 if ev.atom == '_INPUT_TEXT':
1164 str = self.window.property_get('_INPUT_TEXT')
1165 self.numentry.set_text(str[2])
1167 def close_win(self, *a):
1171 def create_ui(self):
1173 fd = pango.FontDescription("sans 10")
1174 fd.set_absolute_size(25*pango.SCALE)
1175 self.button_font = fd
1176 v = gtk.VBox() ;v.show() ; self.add(v)
1178 self.sender = self.send_ui()
1182 self.listing = self.list_ui()
1186 self.book = load_book("/data/address-book")
1187 self.listview.set_book(self.book)
1189 self.rotate_list(self, target='All')
1197 v.pack_start(h, expand=False)
1198 l = gtk.Label('To:')
1199 l.modify_font(self.button_font)
1201 h.pack_start(l, expand=False)
1203 self.numentry = FingerEntry()
1204 h.pack_start(self.numentry)
1205 self.numentry.modify_font(self.button_font)
1206 self.numentry.show()
1207 self.numentry.connect('changed', self.update_to);
1211 l.modify_font(self.button_font)
1215 v.pack_start(h, expand=False)
1218 l = gtk.Label('0 chars')
1219 l.modify_font(self.button_font)
1224 v.pack_start(h, expand=False)
1227 h.set_size_request(-1,80)
1228 h.set_homogeneous(True)
1230 v.pack_start(h, expand=False)
1232 self.add_button(h, 'select', self.select)
1233 self.add_button(h, 'clear', self.clear)
1234 self.add_button(h, 'paste', self.paste)
1236 self.message = FingerText()
1238 self.message.modify_font(self.button_font)
1239 sw = gtk.ScrolledWindow() ; sw.show()
1240 sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
1241 #v.add(self.message)
1243 sw.add(self.message)
1244 self.message.get_buffer().connect('changed', self.buff_changed)
1247 h.set_size_request(-1,80)
1248 h.set_homogeneous(True)
1250 v.pack_end(h, expand=False)
1252 self.add_button(h, 'Send GSM', self.send, 'GSM')
1253 self.draft_button = self.add_button(h, 'Draft', self.draft)
1254 self.add_button(h, 'Send EXE', self.send, 'EXE')
1259 v = gtk.VBox() ; main = v
1261 h = gtk.HBox() ; h.show()
1262 h.set_size_request(-1,80)
1263 h.set_homogeneous(True)
1264 v.pack_start(h, expand = False)
1265 self.add_button(h, 'Del', self.delete)
1266 self.view_button = self.add_button(h, 'View', self.view)
1267 self.reply = self.add_button(h, 'New', self.open)
1269 h = gtk.HBox() ; h.show()
1270 v.pack_start(h, expand=False)
1271 b = gtk.Button("clr") ; b.show()
1272 b.connect('clicked', self.clear_search)
1273 h.pack_end(b, expand=False)
1274 l = gtk.Label('search:') ; l.show()
1275 h.pack_start(l, expand=False)
1277 e = gtk.Entry() ; e.show()
1278 self.search_entry = e
1281 self.listview = SMSlist(self.load_list)
1282 self.listview.show()
1283 self.listview.assign_colour('time', 'blue')
1284 self.listview.assign_colour('sender', 'red')
1285 self.listview.assign_colour('recipient', 'black')
1286 self.listview.assign_colour('mesg', 'black')
1287 self.listview.assign_colour('mesg-new', 'red')
1288 self.listview.assign_colour('bg-0', 'yellow')
1289 self.listview.assign_colour('bg-1', 'pink')
1290 self.listview.assign_colour('bg-selected', 'white')
1292 self.listbox = gtk.VBox()
1293 self.listbox.add(self.listview)
1298 bb = gtk.HBox() ; bb.show()
1299 bb.set_size_request(-1,80)
1300 bb.set_homogeneous(True)
1301 self.listbox.pack_end(bb, expand=False)
1302 self.buttonA = self.add_button(bb, 'Sent', self.rotate_list, 'A')
1303 self.buttonB = self.add_button(bb, 'Recv', self.rotate_list, 'B')
1306 self.last_response = gtk.Label('')
1307 self.listbox.pack_end(self.last_response, expand = False)
1309 self.singleview = gtk.TextView()
1310 self.singleview.modify_font(self.button_font)
1311 self.singleview.show()
1312 self.singleview.set_wrap_mode(gtk.WRAP_WORD_CHAR)
1313 sw = gtk.ScrolledWindow()
1314 sw.add(self.singleview)
1315 sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
1317 self.singlescroll = sw
1319 self.singlebox = gtk.VBox()
1320 self.singlebox.add(sw)
1321 self.singlebox.hide()
1322 v.add(self.singlebox)
1324 bb = gtk.HBox() ; bb.show()
1325 bb.set_size_request(-1,80)
1326 bb.set_homogeneous(True)
1327 self.singlebox.pack_end(bb, expand=False)
1328 self.add_button(bb, 'Call', self.docall)
1329 self.add_button(bb, 'Contacts', self.contacts)
1334 def add_button(self, parent, label, func, *args):
1335 b = gtk.Button(label)
1336 b.child.modify_font(self.button_font)
1337 b.connect('clicked', func, *args)
1338 b.set_property('can-focus', False)
1343 def update_to(self, w):
1346 self.reload_book = True
1347 self.to_label.set_text('')
1349 if self.reload_book:
1350 self.reload_book = False
1351 self.book = load_book("/data/address-book")
1352 self.listview.set_book(self.book)
1353 e = name_lookup(self.book, n)
1355 self.to_label.set_text(e[1] +
1360 self.to_label.set_text('??')
1362 self.buff_changed(None)
1364 def buff_changed(self, w):
1365 if self.numentry.get_text() == '' and self.message.get_buffer().get_property('text') == '':
1366 self.draft_button.child.set_text('Cancel')
1368 self.draft_button.child.set_text('SaveDraft')
1369 l = len(self.message.get_buffer().get_property('text'))
1374 self.cnt_label.set_text('%d chars / %d msgs' % (l, m))
1376 def select(self, w, *a):
1377 if not self.selecting:
1378 self.message.input.active = False
1379 w.child.set_text('Cut')
1380 self.selecting = True
1382 self.message.input.active = True
1383 w.child.set_text('Select')
1384 self.selecting = False
1385 b = self.message.get_buffer()
1386 bound = b.get_selection_bounds()
1391 b.delete_selection(True, True)
1393 def clear(self, *a):
1394 w = self.get_toplevel().get_focus()
1397 if w == self.message:
1398 self.cutbuffer = self.message.get_buffer().get_property('text')
1399 b = self.message.get_buffer()
1402 self.cutbuffer = w.get_text()
1405 def paste(self, *a):
1406 w = self.get_toplevel().get_focus()
1410 w.insert_at_cursor(self.cutbuffer)
1413 def watch_clip(self, board):
1414 self.cb = gtk.Clipboard(selection=board)
1415 self.targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ]
1416 self.cb.set_with_data(self.targets, self.get, self.got_clip, None)
1418 def got_clip(self, clipb, data):
1419 a = clipb.wait_for_text()
1420 print "sms got clip", a
1421 self.numentry.set_text(a)
1424 self.cb.set_with_data(self.targets, self.get, self.got_clip, None)
1427 def get(self, sel, info, data):
1428 sel.set_text("Number Please")
1430 def send(self, w, style):
1431 sender = '0403463349'
1432 recipient = self.number
1433 mesg = self.message.get_buffer().get_property('text')
1434 if not mesg or not recipient:
1438 p = Popen(['exesms', sender, recipient, mesg], stdout = PIPE)
1440 p = Popen(['gsm-sms', sender, recipient, mesg], stdout = PIPE)
1443 line = 'Fork Failed'
1445 line = 'no response'
1451 s = SMSmesg(to = recipient, text = mesg)
1453 if rv or line[0:2] != 'OK':
1459 self.last_response.set_text('Mess Send: '+ line.strip())
1460 self.last_response.show()
1464 self.rotate_list(target=target)
1466 def draft(self, *a):
1467 sender = '0403463349'
1468 recipient = self.numentry.get_text()
1473 mesg = self.message.get_buffer().get_property('text')
1475 s = SMSmesg(to = recipient, text = mesg, state = 'DRAFT')
1479 self.rotate_list(target='Draft')
1480 def config(self, *a):
1482 def delete(self, *a):
1483 if len(self.listview.smslist ) < 1:
1485 s = self.listview.smslist[self.listview.selected]
1486 self.store.delete(s)
1487 sel = self.listview.selected
1488 self.rotate_list(target=self.display_list)
1489 self.listview.selected = sel
1491 self.view(self.view_button)
1493 def view(self, w, *a):
1495 w.child.set_text('View')
1496 self.viewing = False
1497 self.singlebox.hide()
1499 if self.listview.smslist and len(self.listview.smslist ) >= 1:
1500 s = self.listview.smslist[self.listview.selected]
1501 if s.state == 'NEW':
1502 self.store.setstate(s, None)
1503 if self.display_list == 'New':
1504 self.rotate_list(target='New')
1505 self.reply.child.set_text('New')
1507 if not self.listview.smslist or len(self.listview.smslist ) < 1:
1509 s = self.listview.smslist[self.listview.selected]
1510 w.child.set_text('List')
1512 self.last_response.hide()
1515 n = book_name(self.book, s.correspondent)
1517 n = n[0] + ' ['+s.correspondent+']'
1522 if s.source == 'LOCAL':
1523 t = 'To: ' + n + '\n'
1525 t = 'From: %s (%s)\n' % (n, s.source)
1526 tm = time.strftime('%d%b%Y %H:%M:%S', s.time[0:6]+(0,0,0))
1527 t += 'Time: ' + tm + '\n'
1530 self.singleview.get_buffer().set_text(t)
1531 self.singlebox.show()
1533 if s.source == 'LOCAL':
1534 self.reply.child.set_text('Open')
1536 self.reply.child.set_text('Reply')
1540 if len(self.listview.smslist) < 1:
1542 s = self.listview.smslist[self.listview.selected]
1543 if s.state == 'NEW':
1544 self.store.setstate(s, None)
1546 self.numentry.set_text(s.correspondent)
1547 self.message.get_buffer().set_text(s.text)
1548 self.draft_button.child.set_text('SaveDraft')
1550 self.numentry.set_text('')
1551 self.message.get_buffer().set_text('')
1552 self.draft_button.child.set_text('Cancel')
1556 def load_list(self, lines):
1559 target = self.display_list
1560 patn = self.search_entry.get_text()
1561 #print 'pattern is', patn
1563 (now, l) = self.store.lookup(now, 'NEW')
1564 elif target == 'Draft':
1565 (now, l) = self.store.lookup(now, 'DRAFT')
1567 if lines == 0: lines = 20
1568 while now and len(l) < lines:
1569 (now, l2) = self.store.lookup(now)
1571 if patn and patn not in e.correspondent:
1575 elif target == 'Sent' and e.source == 'LOCAL':
1577 elif target == 'Recv' and e.source != 'LOCAL':
1581 def rotate_list(self, w=None, ev=None, which = None, target=None):
1583 # All, Recv, New, Sent, Draft
1584 # When one is current, two others can be selected
1588 target = self.display_list
1590 target = w.child.get_text()
1593 self.buttonA.child.set_text('Sent')
1594 self.buttonB.child.set_text('Recv')
1595 if target == 'Sent':
1596 self.buttonA.child.set_text('All')
1597 self.buttonB.child.set_text('Draft')
1598 if target == 'Draft':
1599 self.buttonA.child.set_text('All')
1600 self.buttonB.child.set_text('Sent')
1601 if target == 'Recv':
1602 self.buttonA.child.set_text('All')
1603 self.buttonB.child.set_text('New')
1605 self.buttonA.child.set_text('All')
1606 self.buttonB.child.set_text('Recv')
1608 self.display_list = target
1609 self.listview.reset_list()
1611 def clear_search(self, *a):
1615 self.rotate_list(self, target = 'New')
1617 def get_contact(self):
1618 if not self.listview.smslist or len(self.listview.smslist ) < 1:
1620 s = self.listview.smslist[self.listview.selected]
1621 return s.correspondent
1623 def docall(self, b):
1624 send_number('voice-dial', self.get_contact())
1626 def contacts(self, b):
1627 send_number('contact-find', self.get_contact())
1631 def clip_get_data(clip, sel, info, data):
1633 print 'get clip data', sel_number
1635 sel.set_text(sel_number)
1636 def clip_clear_data(clip, data):
1638 print 'clear clip data', clip.wait_for_text()
1641 def send_number(sel, num):
1642 global sel_number, clips
1646 if sel not in clips:
1647 clips[sel] = gtk.Clipboard(selection = sel)
1649 c.set_with_data([(gtk.gdk.SELECTION_TYPE_STRING, 0, 0)],
1650 clip_get_data, clip_clear_data, None)
1655 for p in ['/data','/media/card','/var/tmp']:
1656 if os.path.exists(p):
1659 w = SendSMS(SMSstore(pth+'/SMS'))
1660 gtk.settings_get_default().set_long_property("gtk-cursor-blink", 0, "main")
1664 if __name__ == '__main__':