From: NeilBrown Date: Sat, 12 Feb 2011 05:54:26 +0000 (+1100) Subject: contacts app X-Git-Url: http://git.neil.brown.name/?a=commitdiff_plain;p=freerunner.git contacts app Write most of contacts application. Currently has buttons for 'call' and 'sms' which do nothing. Signed-off-by: NeilBrown --- diff --git a/contacts/contacts.py b/contacts/contacts.py index a222d4c..5bfbf06 100644 --- a/contacts/contacts.py +++ b/contacts/contacts.py @@ -6,36 +6,49 @@ # # We have a content pane and a row of buttons at the bottom. # The content is either: -# - list of contacts (name/number, one line each) -# - detailed contact info that can be editted. +# - list of contact names - highlighted minipane at bottom with number +# - detailed contact info that can be editted (name, number, speed-pos) # # When list of contacts are displayed, typing a char adds that char to -# a (hidden) substring and only contacts containing that substring are listed. -# The substring is highlighted. +# a substring and only contacts containing that substring are listed. # An extra entry at the start is given which matches exactly the substring # and can be used to create a new entry. +# Alternately, the list can show only 'deleted' entries, still with substring +# matching. Colour is different in this case. # # Buttons for contact list are: -# When nothing selected: +# When nothing selected (list_none): # New ALL Undelete -# When contact selected: +# When contact selected (list_sel): # Call SMS Open ALL -# When new-entry selected: +# When new-entry selected (list_new): # Create ALL +# When viewing deleted entries and nothing or first is selected (del_none): +# Return +# When viewing deleted entries and one is selected (del_sel): +# Undelete Open Return # # When the detailed contact info is displayed all fields can be # edited and change background colour if they differ from stored value +# Fields are: Name Number # Button for details are: -# When nothing is changed: +# When nothing is changed (edit_nil): # Call SMS Close Delete -# When something is changed on new entry -# Discard Create Duplicate -# When something is changed on old entry -# Restore Change Delete +# When something is changed on new entry (edit_new) +# Discard Create +# When something is changed on old entry (edit old) +# Restore Save Create # -# 'delete' doesn't actually delete, but adds ' - deleted' to the name which +# 'delete' doesn't actually delete, but adds '!delete$DATE-' to the name which # causes most lookups to ignore the entry. # +# TODO +# - find way to properly reset 'current' pos after edit +# - have a global 'state' object which other things refer to +# It has an 'updated' state which other objects can connect to +# - save file after an update + +import gtk, pango, time, gobject, os from scrawl import Scrawl from listselect import ListSelect @@ -47,7 +60,11 @@ class Contacts(gtk.Window): self.set_title("Contacts") self.connect('destroy', self.close_win) + self.current = None + self.timer = None self.make_ui() + self.load_book("/media/card/address-book") + self.show() def make_ui(self): @@ -57,16 +74,500 @@ class Contacts(gtk.Window): # -and- # variable list of buttons. # + ctx = self.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(35*pango.SCALE) + self.fd = fd + v = gtk.VBox(); v.show() self.add(v) - s = ListSelect() + s = self.listui() + self.lst = s + v.pack_start(s, expand=True) + s.show() + self.show() + self.sc.set_colour('red') + + s = self.editui() + v.pack_start(s, expand = True) + s.hide() + self.ed = s + + bv = gtk.VBox(); bv.show(); v.pack_start(bv, expand=False) + def hide_some(w): + for c in w.get_children(): + c.hide() + bv.hide_some = lambda : hide_some(bv) + self.buttons = bv + + b = self.buttonlist(bv) + self.button(b, 'New', self.open) + self.button(b, 'Undelete', self.undelete) + self.button(b, 'ALL', self.all) + self.list_none = b + + b = self.buttonlist(bv) + self.button(b, 'Call', self.call) + self.button(b, 'SMS', self.sms) + self.button(b, 'Open', self.open) + self.button(b, 'Delete', self.delete) + self.list_sel = b + + b = self.buttonlist(bv) + self.button(b, 'Create', self.open) + self.button(b, 'ALL', self.all) + self.list_new = b + + b = self.buttonlist(bv) + self.button(b, 'Return', self.all) + self.del_none = b + + b = self.buttonlist(bv) + self.button(b, 'Undelete', self.delete) + self.button(b, 'Open', self.open) + self.button(b, 'Return', self.all) + self.del_sel = b + + b = self.buttonlist(bv) + self.button(b, 'Call', self.call) + self.button(b, 'SMS', self.sms) + self.button(b, 'Close', self.close) + self.button(b, 'Delete', self.delete) + self.edit_nil = b + + b = self.buttonlist(bv) + self.button(b, 'Discard', self.close) + self.button(b, 'Create', self.create) + self.edit_new = b + + b = self.buttonlist(bv) + self.button(b, 'Restore', self.open) + self.button(b, 'Save', self.save) + self.button(b, 'Create', self.create) + self.edit_old = b + + self.list_none.show() + + def listui(self): + s = ListSelect(); s.show() + s.set_format("normal","black", background="grey", selected="white") + s.set_format("deleted","red", background="grey", selected="white") + s.set_format("virtual","blue", background="grey", selected="white") + s.connect('selected', self.selected) + s.set_zoom(37) + self.clist = contact_list() + s.list = self.clist + s.list_changed() self.sel = s - s.callout = self.selected + def gottap(p): + x,y = p + s.tap(x,y) + self.sc = Scrawl(s, self.gotsym, gottap, None, None) - v.pack_end(s, expand = True) - s.show() + s.set_events(s.get_events() | gtk.gdk.KEY_PRESS_MASK) + def key(list, ev): + if len(ev.string) == 1: + self.gotsym(ev.string) + elif ev.keyval == 65288: + self.gotsym('') + else: + #print ev.keyval, len(ev.string), ev.string + pass + s.connect('key_press_event', key) + s.set_flags(gtk.CAN_FOCUS) + s.grab_focus() + + v = gtk.VBox(); v.show() + v.pack_start(s, expand=True) + l = gtk.Label('') + l.modify_font(self.fd) + self.number_label = l + v.pack_start(l, expand=False) + return v + + def editui(self): + ui = gtk.VBox() + + self.fields = {} + self.field(ui, 'Name') + self.field(ui, 'Number') + self.field(ui, 'Speed Number') + return ui + + def field(self, v, lbl): + h = gtk.HBox(); h.show() + l = gtk.Label(lbl); l.show() + l.modify_font(self.fd) + h.pack_start(l, expand=False) + e = gtk.Entry(); e.show() + e.modify_font(self.fd) + h.pack_start(e, expand=True) + e.connect('changed', self.field_update, lbl) + v.pack_start(h, expand=True, fill=True) + self.fields[lbl] = e + return e + + def buttonlist(self, v): + b = gtk.HBox() + b.set_homogeneous(True) + b.hide() + v.pack_end(b, expand=False) + return b + + def button(self, bl, label, func): + b = gtk.Button() + if label[0:3] == "gtk" : + img = gtk.image_new_from_stock(label, self.isize) + img.show() + b.add(img) + else: + b.set_label(label) + b.child.modify_font(self.fd) + b.show() + b.connect('clicked', func) + b.set_focus_on_click(False) + bl.pack_start(b, expand = True) + + def close_win(self, widget): + gtk.main_quit() + + def selected(self, list, item): + self.buttons.hide_some() + + if item == None: + item = -1 + if self.clist.show_deleted: + if item < 0 or (item == 0 and self.clist.str != ''): + self.del_none.show() + else: + self.del_sel.show() + i = self.clist.getitem(item) + self.current = i + elif item < 0: + self.list_none.show() + self.number_label.hide() + self.current = None + elif item == 0 and self.clist.str != '': + self.list_new.show() + self.number_label.hide() + self.current = None + else: + self.list_sel.show() + i = self.clist.getitem(item) + self.current = i + if i == None: + self.number_label.hide() + else: + self.number_label.set_text(self.list[i][1]) + self.number_label.show() + + def field_update(self, ev, fld): + if self.flds[fld] == self.fields[fld].get_text(): + self.fields[fld].modify_base(gtk.STATE_NORMAL,None) + else: + self.fields[fld].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('yellow')) + + same = True + for i in ['Name', 'Number', 'Speed Number']: + if self.fields[i].get_text() != self.flds[i]: + same = False + self.buttons.hide_some() + if self.current == None: + self.edit_new.show() + elif same: + self.edit_nil.show() + else: + self.edit_old.show() + pass + + def load_book(self, file): + try: + f = open(file) + self.fname = file + except: + f = open('/home/neilb/home/mobile-numbers-jan-08') + self.fname = '/home/neilb/home/mobile-numbers-jan-08' + rv = [] + speed = {} + for l in f: + x = l.split(';') + if len(x[0]) == 1: + speed[x[1]] = x[0] + else: + rv.append([x[0],x[1], '']) + if speed: + for i in range(len(rv)): + if rv[i][0] in speed: + rv[i][2] = speed[rv[i][0]] + rv.sort(lambda x,y: cmp(x[0].lower(),y[0].lower())) + self.list = rv + self.clist.set_list(rv) + + def save_book(self): + try: + f = open(self.fname + '.new', 'w') + except: + return + for e in self.list: + name,num,speed = e + f.write(name+';'+num+';\n') + if speed: + f.write(speed+';'+name+';\n') + f.close() + os.rename(self.fname+'.new', self.fname) + + def queue_save(self): + if self.timer: + gobject.source_remove(self.timer) + self.timer = gobject.timeout_add(15*1000, self.do_save) + def do_save(self): + if self.timer: + gobject.source_remove(self.timer) + self.timer = None + self.save_book() + + def gotsym(self,sym): + if sym == '': + s = self.clist.str[:-1] + elif len(sym) > 1: + s = self.clist.str + elif ord(sym) >= 32: + s = self.clist.str + sym + else: + return + self.clist.set_str(s) + self.sel.list_changed() + self.sel.select(None) + + def open(self, ev): + self.lst.hide() + self.ed.show() + + self.buttons.hide_some() + if self.current == None: + self.edit_new.show() + else: + self.edit_nil.show() + self.flds = {} + self.flds['Name'] = '' + self.flds['Number'] = '' + self.flds['Speed Number'] = '' + if self.current != None: + self.flds['Name'] = self.list[self.current][0] + self.flds['Number'] = self.list[self.current][1] + self.flds['Speed Number'] = self.list[self.current][2] + elif self.clist.str: + self.flds['Name'] = self.clist.str + + for f in ['Name', 'Number', 'Speed Number'] : + self.fields[f].set_text(self.flds[f]) + self.fields[f].modify_base(gtk.STATE_NORMAL,None) + + def all(self, ev): + self.clist.set_str('') + self.clist.show_deleted = False + self.sel.select(None) + self.sel.list_changed() + pass + def create(self, ev): + self.save(None) + def save(self,ev): + name = self.fields['Name'].get_text() + num = self.fields['Number'].get_text() + speed = self.fields['Speed Number'].get_text() + if name == '' or name.find(';') >= 0: + self.fields['Name'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink')) + return + if num == '' or num.find(';') >= 0: + self.fields['Number'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink')) + return + if len(speed) > 1 or speed.find(';') >= 0: + self.fields['Speed Number'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink')) + return + if self.current == None or ev == None: + self.list.append([name, num, speed]) + else: + self.list[self.current] = [name, num, speed] + self.flds['Name'] = name + self.flds['Number'] = num + self.flds['Speed Number'] = speed + self.list.sort(lambda x,y: cmp(x[0].lower(),y[0].lower())) + self.clist.set_list(self.list) + self.sel.list_changed() + self.close(ev) + self.queue_save() + pass + + def delete(self, ev): + if self.current != None: + n = self.list[self.current][0] + if n[:8] == '!deleted': + p = n.find('-') + if p <= 0: + p = 7 + n = n[p+1:] + else: + n = time.strftime('!deleted.%Y.%m.%d-') + n + self.list[self.current][0] = n + self.list.sort(lambda x,y: cmp(x[0].lower(),y[0].lower())) + self.clist.set_list(self.list) + self.sel.list_changed() + self.close(ev) + self.queue_save() + pass + + def undelete(self, ev): + self.clist.show_deleted = True + self.clist.set_str('') + self.sel.list_changed() + self.sel.select(None) + pass + def call(self, ev): + pass + def sms(self, ev): + pass + def close(self, ev): + self.lst.show() + self.sel.grab_focus() + self.ed.hide() + if self.current != None and self.current >= len(self.clist): + self.current = None + self.selected(None, self.sel.selected) + +class contact_list: + def __init__(self): + self.list = [] + self.match_start = [] + self.match_word = [] + self.match_any = [] + self.deleted = [] + self.valid = [] + self.match_str = '' + self.match_deleted = False + self.str = '' + self.show_deleted = False + def set_list(self, list): + self.list = list + self.match_str = '' + self.deleted = [] + self.valid = [] + for i in range(len(self.list)): + name,number,speed = self.list[i] + name = name.lower() + if name[:8] == '!deleted': + self.deleted.append(i) + else: + self.valid.append(i) + + def set_str(self,str): + self.str = str + + def recalc(self): + if self.str == self.match_str and self.show_deleted == self.match_deleted: + return + if self.match_str != '' and self.str[:len(self.match_str)] == self.match_str and self.show_deleted == self.match_deleted: + # str is a bit longer + self.recalc_quick() + return + self.match_start = [] + self.match_word = [] + self.match_any = [] + self.match_str = self.str + self.match_deleted = self.show_deleted + s = self.str.lower() + l = len(self.str) + for i in self.deleted if self.match_deleted else self.valid: + name,number,speed = self.list[i] + name = name.lower() + if name[0:l] == s: + self.match_start.append(i) + elif name.find(' '+s) >= 0: + self.match_word.append(i) + elif name.find(s) >= 0: + self.match_any.append(i) + self.total = len(self.match_start) + len(self.match_word) + len(self.match_any) + + def recalc_quick(self): + # The string has been extended so we only look through what we already have + self.match_str = self.str + s = self.str.lower() + l = len(self.str) + + lst = self.match_start + self.match_start = [] + for i in lst: + name,number,speed = self.list[i] + name = name.lower() + if name[0:l] == s: + self.match_start.append(i) + else: + self.match_word.append(i) + self.match_word.sort() + lst = self.match_word + self.match_word = [] + for i in lst: + name, number, speed = self.list[i] + name = name.lower() + if name.find(' '+s) >= 0: + self.match_word.append(i) + else: + self.match_any.append(i) + self.match_any.sort() + lst = self.match_any + self.match_any = [] + for i in lst: + name, number, speed = self.list[i] + name = name.lower() + if name.find(s) >= 0: + self.match_any.append(i) + self.total = len(self.match_start) + len(self.match_word) + len(self.match_any) + + def __len__(self): + self.recalc() + if self.str == '': + if self.match_deleted: + return len(self.deleted) + else: + return len(self.valid) + else: + return self.total + 1 + def getitem(self, ind): + if ind < 0: + print '!!!!', ind + if self.str == '': + if self.match_deleted: + return self.deleted[ind] + else: + return self.valid[ind] + if ind == 0: + return -1 + ind -= 1 + if ind < len(self.match_start): + return self.match_start[ind] + ind -= len(self.match_start) + if ind < len(self.match_word): + return self.match_word[ind] + ind -= len(self.match_word) + if ind < len(self.match_any): + return self.match_any[ind] + return None + + def __getitem__(self, ind): + i = self.getitem(ind) + if i < 0: + return (self.str, 'virtual') + if i == None: + return None + s = self.list[i][0] + if s[:8] == '!deleted': + p = s.find('-') + if p <= 0: + p = 7 + return (s[p+1:],'deleted') + else: + return (s, 'normal') if __name__ == "__main__": diff --git a/lib/listselect.py b/lib/listselect.py index fdc398a..6257ad5 100644 --- a/lib/listselect.py +++ b/lib/listselect.py @@ -360,6 +360,7 @@ class ListSelect(gtk.DrawingArea): def select(self, ind): if self.selected == ind: + self.emit('selected', -1 if ind == None else ind) return if ind == None: self.selected = None