]> git.neil.brown.name Git - freerunner.git/blob - contacts/contacts.py
contacts app
[freerunner.git] / contacts / contacts.py
1 #!/usr/bin/env python
2
3 #
4 # Contacts manager
5 #  Currently a 'contact' is a name, a number, and a speed-dial letter
6 #
7 # We have a content pane and a row of buttons at the bottom.
8 # The content is either:
9 #   - list of contact names - highlighted minipane at bottom with number
10 #   - detailed contact info that can be editted (name, number, speed-pos)
11 #
12 # When list of contacts are displayed, typing a char adds that char to
13 # a  substring and only contacts containing that substring are listed.
14 # An extra entry at the start is given which matches exactly the substring
15 # and can be used to create a new entry.
16 # Alternately, the list can show only 'deleted' entries, still with substring
17 # matching.  Colour is different in this case.
18 #
19 # Buttons for contact list are:
20 #  When nothing selected (list_none):
21 #     New ALL Undelete
22 #  When contact selected (list_sel):
23 #     Call SMS Open ALL
24 #  When new-entry selected (list_new):
25 #    Create ALL
26 #  When viewing deleted entries and nothing or first is selected (del_none):
27 #    Return
28 #  When viewing deleted entries and one is selected (del_sel):
29 #    Undelete Open Return
30 #
31 # When the detailed contact info is displayed all fields can be
32 # edited and change background colour if they differ from stored value
33 # Fields are: Name Number
34 # Button for details are:
35 #  When nothing is changed (edit_nil):
36 #     Call SMS Close Delete
37 #  When something is changed on new entry (edit_new)
38 #     Discard  Create
39 #  When something is changed on old entry (edit old)
40 #     Restore Save Create
41 #
42 # 'delete' doesn't actually delete, but adds '!delete$DATE-' to the name which
43 # causes most lookups to ignore the entry.
44 #
45 # TODO
46 # - find way to properly reset 'current' pos after edit
47 # - have a global 'state' object which other things refer to
48 #   It has an 'updated' state which other objects can connect to
49 # - save file after an update
50
51 import gtk, pango, time, gobject, os
52 from scrawl import Scrawl
53 from listselect import ListSelect
54
55
56 class Contacts(gtk.Window):
57     def __init__(self):
58         gtk.Window.__init__(self)
59         self.set_default_size(480,640)
60         self.set_title("Contacts")
61         self.connect('destroy', self.close_win)
62
63         self.current = None
64         self.timer = None
65         self.make_ui()
66         self.load_book("/media/card/address-book")
67         self.show()
68
69
70     def make_ui(self):
71         # UI consists of:
72         #   list of contacts -or-
73         #   editable field
74         #   -and-
75         #   variable list of buttons.
76         #
77         ctx = self.get_pango_context()
78         fd = ctx.get_font_description()
79         fd.set_absolute_size(35*pango.SCALE)
80         self.fd = fd
81
82         v = gtk.VBox(); v.show()
83         self.add(v)
84
85         s = self.listui()
86         self.lst = s
87         v.pack_start(s, expand=True)
88         s.show()
89         self.show()
90         self.sc.set_colour('red')
91
92         s = self.editui()
93         v.pack_start(s, expand = True)
94         s.hide()
95         self.ed = s
96
97         bv = gtk.VBox(); bv.show(); v.pack_start(bv, expand=False)
98         def hide_some(w):
99             for c in w.get_children():
100                 c.hide()
101         bv.hide_some = lambda : hide_some(bv)
102         self.buttons = bv
103
104         b = self.buttonlist(bv)
105         self.button(b, 'New', self.open)
106         self.button(b, 'Undelete', self.undelete)
107         self.button(b, 'ALL', self.all)
108         self.list_none = b
109
110         b = self.buttonlist(bv)
111         self.button(b, 'Call', self.call)
112         self.button(b, 'SMS', self.sms)
113         self.button(b, 'Open', self.open)
114         self.button(b, 'Delete', self.delete)
115         self.list_sel = b
116
117         b = self.buttonlist(bv)
118         self.button(b, 'Create', self.open)
119         self.button(b, 'ALL', self.all)
120         self.list_new = b
121
122         b = self.buttonlist(bv)
123         self.button(b, 'Return', self.all)
124         self.del_none = b
125
126         b = self.buttonlist(bv)
127         self.button(b, 'Undelete', self.delete)
128         self.button(b, 'Open', self.open)
129         self.button(b, 'Return', self.all)
130         self.del_sel = b
131
132         b = self.buttonlist(bv)
133         self.button(b, 'Call', self.call)
134         self.button(b, 'SMS', self.sms)
135         self.button(b, 'Close', self.close)
136         self.button(b, 'Delete', self.delete)
137         self.edit_nil = b
138
139         b = self.buttonlist(bv)
140         self.button(b, 'Discard', self.close)
141         self.button(b, 'Create', self.create)
142         self.edit_new = b
143
144         b = self.buttonlist(bv)
145         self.button(b, 'Restore', self.open)
146         self.button(b, 'Save', self.save)
147         self.button(b, 'Create', self.create)
148         self.edit_old = b
149
150         self.list_none.show()
151
152     def listui(self):
153         s = ListSelect(); s.show()
154         s.set_format("normal","black", background="grey", selected="white")
155         s.set_format("deleted","red", background="grey", selected="white")
156         s.set_format("virtual","blue", background="grey", selected="white")
157         s.connect('selected', self.selected)
158         s.set_zoom(37)
159         self.clist = contact_list()
160         s.list = self.clist
161         s.list_changed()
162         self.sel = s
163         def gottap(p):
164             x,y = p
165             s.tap(x,y)
166         self.sc = Scrawl(s, self.gotsym, gottap, None, None)
167
168         s.set_events(s.get_events() | gtk.gdk.KEY_PRESS_MASK)
169         def key(list, ev):
170             if len(ev.string) == 1:
171                 self.gotsym(ev.string)
172             elif ev.keyval == 65288:
173                 self.gotsym('<BS>')
174             else:
175                 #print ev.keyval, len(ev.string), ev.string
176                 pass
177         s.connect('key_press_event', key)
178         s.set_flags(gtk.CAN_FOCUS)
179         s.grab_focus()
180
181         v = gtk.VBox(); v.show()
182         v.pack_start(s, expand=True)
183         l = gtk.Label('')
184         l.modify_font(self.fd)
185         self.number_label = l
186         v.pack_start(l, expand=False)
187         return v
188
189     def editui(self):
190         ui = gtk.VBox()
191
192         self.fields = {}
193         self.field(ui, 'Name')
194         self.field(ui, 'Number')
195         self.field(ui, 'Speed Number')
196         return ui
197
198     def field(self, v, lbl):
199         h = gtk.HBox(); h.show()
200         l = gtk.Label(lbl); l.show()
201         l.modify_font(self.fd)
202         h.pack_start(l, expand=False)
203         e = gtk.Entry(); e.show()
204         e.modify_font(self.fd)
205         h.pack_start(e, expand=True)
206         e.connect('changed', self.field_update, lbl)
207         v.pack_start(h, expand=True, fill=True)
208         self.fields[lbl] = e
209         return e
210
211     def buttonlist(self, v):
212         b = gtk.HBox()
213         b.set_homogeneous(True)
214         b.hide()
215         v.pack_end(b, expand=False)
216         return b
217
218     def button(self, bl, label, func):
219         b = gtk.Button()
220         if label[0:3] == "gtk" :
221             img = gtk.image_new_from_stock(label, self.isize)
222             img.show()
223             b.add(img)
224         else:
225             b.set_label(label)
226             b.child.modify_font(self.fd)
227         b.show()
228         b.connect('clicked', func)
229         b.set_focus_on_click(False)
230         bl.pack_start(b, expand = True)
231
232     def close_win(self, widget):
233         gtk.main_quit()
234
235     def selected(self, list, item):
236         self.buttons.hide_some()
237
238         if item == None:
239             item = -1
240         if self.clist.show_deleted:
241             if item < 0 or (item == 0 and self.clist.str != ''):
242                 self.del_none.show()
243             else:
244                 self.del_sel.show()
245                 i = self.clist.getitem(item)
246                 self.current = i
247         elif item < 0:
248             self.list_none.show()
249             self.number_label.hide()
250             self.current = None
251         elif item == 0 and self.clist.str != '':
252             self.list_new.show()
253             self.number_label.hide()
254             self.current = None
255         else:
256             self.list_sel.show()
257             i = self.clist.getitem(item)
258             self.current = i
259             if i == None:
260                 self.number_label.hide()
261             else:
262                 self.number_label.set_text(self.list[i][1])
263                 self.number_label.show()
264
265     def field_update(self, ev, fld):
266         if self.flds[fld] == self.fields[fld].get_text():
267             self.fields[fld].modify_base(gtk.STATE_NORMAL,None)
268         else:
269             self.fields[fld].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('yellow'))
270
271         same = True
272         for i in ['Name', 'Number', 'Speed Number']:
273             if self.fields[i].get_text() != self.flds[i]:
274                 same = False
275         self.buttons.hide_some()
276         if self.current == None:
277             self.edit_new.show()
278         elif same:
279             self.edit_nil.show()
280         else:
281             self.edit_old.show()
282         pass
283
284     def load_book(self, file):
285         try:
286             f = open(file)
287             self.fname = file
288         except:
289             f = open('/home/neilb/home/mobile-numbers-jan-08')
290             self.fname = '/home/neilb/home/mobile-numbers-jan-08'
291         rv = []
292         speed = {}
293         for l in f:
294             x = l.split(';')
295             if len(x[0]) == 1:
296                 speed[x[1]] = x[0]
297             else:
298                 rv.append([x[0],x[1], ''])
299         if speed:
300             for i in range(len(rv)):
301                 if rv[i][0] in speed:
302                     rv[i][2] = speed[rv[i][0]]
303         rv.sort(lambda x,y: cmp(x[0].lower(),y[0].lower()))
304         self.list = rv
305         self.clist.set_list(rv)
306
307     def save_book(self):
308         try:
309             f = open(self.fname + '.new', 'w')
310         except:
311             return
312         for e in self.list:
313             name,num,speed = e
314             f.write(name+';'+num+';\n')
315             if speed:
316                 f.write(speed+';'+name+';\n')
317         f.close()
318         os.rename(self.fname+'.new', self.fname)
319
320     def queue_save(self):
321         if self.timer:
322             gobject.source_remove(self.timer)
323         self.timer = gobject.timeout_add(15*1000, self.do_save)
324     def do_save(self):
325         if self.timer:
326             gobject.source_remove(self.timer)
327         self.timer = None
328         self.save_book()
329
330     def gotsym(self,sym):
331         if sym == '<BS>':
332             s = self.clist.str[:-1]
333         elif len(sym) > 1:
334             s = self.clist.str
335         elif ord(sym) >= 32:
336             s = self.clist.str + sym
337         else:
338             return
339         self.clist.set_str(s)
340         self.sel.list_changed()
341         self.sel.select(None)
342
343     def open(self, ev):
344         self.lst.hide()
345         self.ed.show()
346
347         self.buttons.hide_some()
348         if self.current == None:
349             self.edit_new.show()
350         else:
351             self.edit_nil.show()
352         self.flds = {}
353         self.flds['Name'] = ''
354         self.flds['Number'] = ''
355         self.flds['Speed Number'] = ''
356         if self.current != None:
357             self.flds['Name'] = self.list[self.current][0]
358             self.flds['Number'] = self.list[self.current][1]
359             self.flds['Speed Number'] = self.list[self.current][2]
360         elif self.clist.str:
361             self.flds['Name'] = self.clist.str
362             
363         for f in ['Name', 'Number', 'Speed Number'] :
364             self.fields[f].set_text(self.flds[f])
365             self.fields[f].modify_base(gtk.STATE_NORMAL,None)
366
367     def all(self, ev):
368         self.clist.set_str('')
369         self.clist.show_deleted = False
370         self.sel.select(None)
371         self.sel.list_changed()
372         pass
373     def create(self, ev):
374         self.save(None)
375     def save(self,ev):
376         name = self.fields['Name'].get_text()
377         num = self.fields['Number'].get_text()
378         speed = self.fields['Speed Number'].get_text()
379         if name == '' or name.find(';') >= 0:
380             self.fields['Name'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink'))
381             return
382         if num == '' or num.find(';') >= 0:
383             self.fields['Number'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink'))
384             return
385         if len(speed) > 1 or speed.find(';') >= 0:
386             self.fields['Speed Number'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink'))
387             return
388         if self.current == None or ev == None:
389             self.list.append([name, num, speed])
390         else:
391             self.list[self.current] = [name, num, speed]
392         self.flds['Name'] = name
393         self.flds['Number'] = num
394         self.flds['Speed Number'] = speed
395         self.list.sort(lambda x,y: cmp(x[0].lower(),y[0].lower()))
396         self.clist.set_list(self.list)
397         self.sel.list_changed()
398         self.close(ev)
399         self.queue_save()
400         pass
401
402     def delete(self, ev):
403         if self.current != None:
404             n = self.list[self.current][0]
405             if n[:8] == '!deleted':
406                 p = n.find('-')
407                 if p <= 0:
408                     p = 7
409                 n = n[p+1:]
410             else:
411                 n = time.strftime('!deleted.%Y.%m.%d-') + n
412             self.list[self.current][0] = n
413             self.list.sort(lambda x,y: cmp(x[0].lower(),y[0].lower()))
414             self.clist.set_list(self.list)
415             self.sel.list_changed()
416             self.close(ev)
417             self.queue_save()
418         pass
419
420     def undelete(self, ev):
421         self.clist.show_deleted = True
422         self.clist.set_str('')
423         self.sel.list_changed()
424         self.sel.select(None)
425         pass
426     def call(self, ev):
427         pass
428     def sms(self, ev):
429         pass
430     def close(self, ev):
431         self.lst.show()
432         self.sel.grab_focus()
433         self.ed.hide()
434         if self.current != None and self.current >= len(self.clist):
435             self.current = None
436         self.selected(None, self.sel.selected)
437
438 class contact_list:
439     def __init__(self):
440         self.list = []
441         self.match_start = []
442         self.match_word = []
443         self.match_any = []
444         self.deleted = []
445         self.valid = []
446         self.match_str = ''
447         self.match_deleted = False
448         self.str = ''
449         self.show_deleted = False
450     def set_list(self, list):
451         self.list = list
452         self.match_str = ''
453         self.deleted = []
454         self.valid = []
455         for i in range(len(self.list)):
456             name,number,speed = self.list[i]
457             name = name.lower()
458             if name[:8] == '!deleted':
459                 self.deleted.append(i)
460             else:
461                 self.valid.append(i)
462
463     def set_str(self,str):
464         self.str = str
465
466     def recalc(self):
467         if self.str == self.match_str and self.show_deleted == self.match_deleted:
468             return
469         if self.match_str != '' and self.str[:len(self.match_str)] == self.match_str and self.show_deleted == self.match_deleted:
470             # str is a bit longer
471             self.recalc_quick()
472             return
473         self.match_start = []
474         self.match_word = []
475         self.match_any = []
476         self.match_str = self.str
477         self.match_deleted = self.show_deleted
478         s = self.str.lower()
479         l = len(self.str)
480         for i in self.deleted if self.match_deleted else self.valid:
481             name,number,speed = self.list[i]
482             name = name.lower()
483             if name[0:l] == s:
484                 self.match_start.append(i)
485             elif name.find(' '+s) >= 0:
486                 self.match_word.append(i)
487             elif name.find(s) >= 0:
488                 self.match_any.append(i)
489         self.total = len(self.match_start) + len(self.match_word) + len(self.match_any)
490
491     def recalc_quick(self):
492         # The string has been extended so we only look through what we already have
493         self.match_str = self.str
494         s = self.str.lower()
495         l = len(self.str)
496         
497         lst = self.match_start
498         self.match_start = []
499         for i in lst:
500             name,number,speed = self.list[i]
501             name = name.lower()
502             if name[0:l] == s:
503                 self.match_start.append(i)
504             else:
505                 self.match_word.append(i)
506         self.match_word.sort()
507         lst = self.match_word
508         self.match_word = []
509         for i in lst:
510             name, number, speed = self.list[i]
511             name = name.lower()
512             if name.find(' '+s) >= 0:
513                 self.match_word.append(i)
514             else:
515                 self.match_any.append(i)
516         self.match_any.sort()
517         lst = self.match_any
518         self.match_any = []
519         for i in lst:
520             name, number, speed = self.list[i]
521             name = name.lower()
522             if name.find(s) >= 0:
523                 self.match_any.append(i)
524         self.total = len(self.match_start) + len(self.match_word) + len(self.match_any)
525                     
526     def __len__(self):
527         self.recalc()
528         if self.str == '':
529             if self.match_deleted:
530                 return len(self.deleted)
531             else:
532                 return len(self.valid)
533         else:
534             return self.total + 1
535     def getitem(self, ind):
536         if ind < 0:
537             print '!!!!', ind
538         if self.str == '':
539             if self.match_deleted:
540                 return self.deleted[ind]
541             else:
542                 return self.valid[ind]
543
544         if ind == 0:
545             return -1
546         ind -= 1
547         if ind < len(self.match_start):
548             return self.match_start[ind]
549         ind -= len(self.match_start)
550         if ind < len(self.match_word):
551             return self.match_word[ind]
552         ind -= len(self.match_word)
553         if ind < len(self.match_any):
554             return self.match_any[ind]
555         return None
556         
557     def __getitem__(self, ind):
558         i = self.getitem(ind)
559         if i < 0:
560             return (self.str, 'virtual')
561         if i == None:
562             return None
563         s = self.list[i][0]
564         if s[:8] == '!deleted':
565             p = s.find('-')
566             if p <= 0:
567                 p = 7
568             return (s[p+1:],'deleted')
569         else:
570             return (s, 'normal')
571
572 if __name__ == "__main__":
573
574     c = Contacts()
575     gtk.main()
576