]> git.neil.brown.name Git - freerunner.git/commitdiff
contacts app master
authorNeilBrown <neilb@suse.de>
Sat, 12 Feb 2011 05:54:26 +0000 (16:54 +1100)
committerNeilBrown <neilb@suse.de>
Sat, 12 Feb 2011 05:54:26 +0000 (16:54 +1100)
Write most of contacts application.
Currently has buttons for 'call' and 'sms' which do nothing.

Signed-off-by: NeilBrown <neilb@suse.de>
contacts/contacts.py
lib/listselect.py

index a222d4cda1a4e1b624944bc3541533740244f205..5bfbf06ea1fcfb5df60b4c062701b167e20e3d96 100644 (file)
@@ -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('<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__":
 
index fdc398a45e1eadfa6ddeca14f8d1fc0cd392eb5e..6257ad554b9ebd6746d8efec62a4f2b25ff3e137 100644 (file)
@@ -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