#
# 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
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):
# -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('<BS>')
+ 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 == '<BS>':
+ 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__":