More gui cleanup
[gc-dialer] / src / gc_dialer.py
index cf7d4eb..bbbb1f6 100755 (executable)
@@ -1,11 +1,25 @@
-#!/usr/bin/python2.5
+#!/usr/bin/python
+
+# GC Dialer - Front end for Google's Grand Central service.
+# Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
+# 
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+# 
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+# 
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 
 
 """
 Grandcentral Dialer
-Python front-end to a wget script to use grandcentral.com to place outbound VOIP calls.
-(C) 2008 Mark Bergman
-bergman@merctech.com
 """
 
 
@@ -28,12 +42,6 @@ except ImportError:
 
 try:
        import osso
-       try:
-               import abook
-               import evolution.ebook as evobook
-       except ImportError:
-               abook = None
-               evobook = None
 except ImportError:
        osso = None
 
@@ -49,7 +57,8 @@ except ImportError:
        doctest = None
        optparse = None
 
-from gcbackend import GCDialer
+from gc_backend import GCDialer
+from evo_backend import EvolutionAddressBook
 
 import socket
 
@@ -117,6 +126,64 @@ def make_pretty(phonenumber):
        return prettynumber
 
 
+def make_idler(func):
+       """
+       Decorator that makes a generator-function into a function that will continue execution on next call
+       """
+       a = []
+
+       def decorated_func(*args, **kwds):
+               if not a:
+                       a.append(func(*args, **kwds))
+               try:
+                       a[0].next()
+                       return True
+               except StopIteration:
+                       del a[:]
+                       return False
+       
+       decorated_func.__name__ = func.__name__
+       decorated_func.__doc__ = func.__doc__
+       decorated_func.__dict__.update(func.__dict__)
+
+       return decorated_func
+
+
+class DummyAddressBook(object):
+       """
+       Minimal example of both an addressbook factory and an addressbook
+       """
+
+       def get_addressbooks(self):
+               """
+               @returns Iterable of (Address Book Factory, Book Id, Book Name)
+               """
+               yield self, "", "None"
+       
+       def open_addressbook(self, bookId):
+               return self
+
+       @staticmethod
+       def factory_short_name():
+               return ""
+
+       @staticmethod
+       def factory_name():
+               return ""
+
+       def get_contacts(self):
+               """
+               @returns Iterable of (contact id, contact name)
+               """
+               return []
+
+       def get_contact_details(self, contactId):
+               """
+               @returns Iterable of (Phone Type, Phone Number)
+               """
+               return []
+
+
 class PhoneTypeSelector(object):
 
        def __init__(self, widgetTree, gcBackend):
@@ -175,8 +242,9 @@ class PhoneTypeSelector(object):
 
 class Dialpad(object):
 
+       __pretty_app_name__ = "Dialer"
        __app_name__ = "gc_dialer"
-       __version__ = "0.7.0"
+       __version__ = "0.8.0"
        __app_magic__ = 0xdeadbeef
 
        _glade_files = [
@@ -193,6 +261,7 @@ class Dialpad(object):
                self._clipboard = gtk.clipboard_get()
 
                self._deviceIsOnline = True
+               self.callbacklist = None
                self._callbackNeedsSetup = True
 
                self._recenttime = 0.0
@@ -200,7 +269,7 @@ class Dialpad(object):
                self._recentviewselection = None
 
                self._contactstime = 0.0
-               self._contactsmodel = gtk.ListStore(gtk.gdk.Pixbuf, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
+               self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
                self._contactsviewselection = None
 
                for path in Dialpad._glade_files:
@@ -225,18 +294,17 @@ class Dialpad(object):
                global hildon
                self._app = None
                self._isFullScreen = False
-               if hildon is not None and isinstance(self._window, gtk.Window):
+               if hildon is not None and self._window is gtk.Window:
                        warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
                        hildon = None
                elif hildon is not None:
                        self._app = hildon.Program()
-                       self._window.set_title("Keypad")
                        self._app.add_window(self._window)
                        self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
                        self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
                        self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
 
-                       gtkMenu = self._widgetTree.get_widget("menubar1")
+                       gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
                        menu = gtk.Menu()
                        for child in gtkMenu.get_children():
                                child.reparent(menu)
@@ -248,17 +316,17 @@ class Dialpad(object):
                else:
                        warnings.warn("No Hildon", UserWarning, 2)
 
+               if hildon is not None:
+                       self._window.set_title("Keypad")
+               else:
+                       self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
+
                self._osso = None
                self._ebook = None
                if osso is not None:
                        self._osso = osso.Context(Dialpad.__app_name__, Dialpad.__version__, False)
                        device = osso.DeviceState(self._osso)
                        device.set_device_state_callback(self._on_device_state_change, 0)
-                       if abook is not None and evobook is not None:
-                               abook.init_with_name(Dialpad.__app_name__, self._osso)
-                               self._ebook = evobook.open_addressbook("default")
-                       else:
-                               warnings.warn("No abook and No evolution address book support", UserWarning, 2)
                else:
                        warnings.warn("No OSSO", UserWarning, 2)
 
@@ -275,7 +343,7 @@ class Dialpad(object):
                        "on_loginbutton_clicked": self._on_loginbutton_clicked,
                        "on_loginclose_clicked": self._on_loginclose_clicked,
 
-                       "on_dialpad_quit": (lambda data: gtk.main_quit()),
+                       "on_dialpad_quit": self._on_close,
                        "on_paste": self._on_paste,
                        "on_clear_number": self._on_clear_number,
 
@@ -290,6 +358,7 @@ class Dialpad(object):
                }
                self._widgetTree.signal_autoconnect(callbackMapping)
                self._widgetTree.get_widget("callbackcombo").get_child().connect("changed", self._on_callbackentry_changed)
+               self._widgetTree.get_widget("addressbook_combo").get_child().connect("changed", self._on_addressbook_entry_changed)
 
                if self._window:
                        self._window.connect("destroy", gtk.main_quit)
@@ -297,15 +366,45 @@ class Dialpad(object):
 
                self._gcBackend = GCDialer()
 
+               self._addressBookFactories = [
+                       DummyAddressBook(),
+                       EvolutionAddressBook(),
+                       self._gcBackend,
+               ]
+               self._addressBook = None
+               self.open_addressbook(*self.get_addressbooks().next()[0][0:2])
+
+               self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
+               for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
+                       if factoryName and bookName:
+                               entryName = "%s: %s" % (factoryName, bookName) 
+                       elif factoryName:
+                               entryName = factoryName
+                       elif bookName:
+                               entryName = bookName
+                       else:
+                               entryName = "Bad name (%d)" % factoryId
+                       row = (str(factoryId), bookId, entryName)
+                       self._booksList.append(row)
+
+               combobox = self._widgetTree.get_widget("addressbook_combo")
+               combobox.set_model(self._booksList)
+               combobox.set_text_column(2)
+               combobox.set_active(0)
+
                self._phoneTypeSelector = PhoneTypeSelector(self._widgetTree, self._gcBackend)
 
                if not self._gcBackend.is_authed():
                        self.attempt_login(2)
-               gobject.idle_add(self._init_recent_view)
-               gobject.idle_add(self._init_contacts_view)
+               else:
+                       self.set_account_number()
+               gobject.idle_add(self._idly_init_recent_view)
+               gobject.idle_add(self._idly_init_contacts_view)
 
-       def _init_recent_view(self):
-               """ Deferred initalization of the recent view treeview """
+       def _idly_init_recent_view(self):
+               """
+               Deferred initalization of the recent view treeview
+               """
 
                recentview = self._widgetTree.get_widget("recentview")
                recentview.set_model(self._recentmodel)
@@ -322,7 +421,7 @@ class Dialpad(object):
 
                return False
 
-       def _init_contacts_view(self):
+       def _idly_init_contacts_view(self):
                """ deferred initalization of the contacts view treeview """
 
                contactsview = self._widgetTree.get_widget("contactsview")
@@ -331,10 +430,6 @@ class Dialpad(object):
                # Add the column to the treeview
                column = gtk.TreeViewColumn("Contact")
 
-               iconrenderer = gtk.CellRendererPixbuf()
-               column.pack_start(iconrenderer, expand=False)
-               column.add_attribute(iconrenderer, 'pixbuf', 0)
-
                textrenderer = gtk.CellRendererText()
                column.pack_start(textrenderer, expand=True)
                column.add_attribute(textrenderer, 'text', 1)
@@ -367,28 +462,31 @@ class Dialpad(object):
 
                return False
 
-       def _setup_callback_combo(self):
-               combobox = self._widgetTree.get_widget("callbackcombo")
+       def _idly_setup_callback_combo(self):
                self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
+               for number, description in self._gcBackend.get_callback_numbers().iteritems():
+                       self.callbacklist.append((make_pretty(number),))
+
+               combobox = self._widgetTree.get_widget("callbackcombo")
                combobox.set_model(self.callbacklist)
                combobox.set_text_column(0)
-               for number, description in self._gcBackend.get_callback_numbers().iteritems():
-                       self.callbacklist.append([make_pretty(number)])
 
                combobox.get_child().set_text(make_pretty(self._gcBackend.get_callback_number()))
                self._callbackNeedsSetup = False
 
-       def populate_recentview(self):
+       def _idly_populate_recentview(self):
                self._recentmodel.clear()
 
                for personsName, phoneNumber, date, action in self._gcBackend.get_recent():
-                       item = (phoneNumber, "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber))
+                       description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
+                       item = (phoneNumber, description)
                        self._recentmodel.append(item)
 
                self._recenttime = time.time()
                return False
 
-       def populate_contactsview(self):
+       @make_idler
+       def _idly_populate_contactsview(self):
                self._contactsmodel.clear()
 
                # completely disable updating the treeview while we populate the data
@@ -396,17 +494,16 @@ class Dialpad(object):
                contactsview.freeze_child_notify()
                contactsview.set_model(None)
 
-               # get gc icon
-               gc_icon = gtk.gdk.pixbuf_new_from_file_at_size('gc_contact.png', 16, 16)
-               for contactId, contactName in self._gcBackend.get_contacts():
-                       self._contactsmodel.append((gc_icon,) + (contactName, "", contactId) + ("",))
+               contactType = (self._addressBook.factory_short_name(),)
+               for contactId, contactName in self._addressBook.get_contacts():
+                       self._contactsmodel.append(contactType + (contactName, "", contactId) + ("",))
+                       yield
 
                # restart the treeview data rendering
                contactsview.set_model(self._contactsmodel)
                contactsview.thaw_child_notify()
 
                self._contactstime = time.time()
-               return False
 
        def attempt_login(self, numOfAttempts = 1):
                """
@@ -441,14 +538,36 @@ class Dialpad(object):
                error_dialog.connect("response", close, self)
                error_dialog.run()
 
+       def get_addressbooks(self):
+               """
+               @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
+               """
+               for i, factory in enumerate(self._addressBookFactories):
+                       for bookFactory, bookId, bookName in factory.get_addressbooks():
+                               yield (i, bookId), (factory.factory_name(), bookName)
+       
+       def open_addressbook(self, bookFactoryId, bookId):
+               self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
+               self._contactstime = 0
+               gobject.idle_add(self._idly_populate_contactsview)
+
        def set_number(self, number):
+               """
+               Set the callback phonenumber
+               """
                self._phonenumber = make_ugly(number)
                self._prettynumber = make_pretty(self._phonenumber)
-               self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % ( self._prettynumber ) )
+               self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
 
        def set_account_number(self):
+               """
+               Displays current account number
+               """
                accountnumber = self._gcBackend.get_account_number()
-               self._widgetTree.get_widget("gcnumberlabel").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
+               self._widgetTree.get_widget("gcnumber_display").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
+
+       def _on_close(self, *args):
+               gtk.main_quit()
 
        def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
                """
@@ -486,7 +605,7 @@ class Dialpad(object):
                        self._isFullScreen = True
                else:
                        self._isFullScreen = False
-
+       
        def _on_key_press(self, widget, event, *args):
                """
                @note Hildon specific
@@ -501,11 +620,11 @@ class Dialpad(object):
                self._widgetTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
 
        def _on_loginclose_clicked(self, *args):
-               gtk.main_quit()
+               self._on_close()
                sys.exit(0)
 
        def _on_clearcookies_clicked(self, *args):
-               self._gcBackend.reset()
+               self._gcBackend.logout()
                self._callbackNeedsSetup = True
                self._recenttime = 0.0
                self._contactstime = 0.0
@@ -514,7 +633,7 @@ class Dialpad(object):
 
                # re-run the inital grandcentral setup
                self.attempt_login(2)
-               gobject.idle_add(self._setup_callback_combo)
+               gobject.idle_add(self._idly_setup_callback_combo)
 
        def _on_callbackentry_changed(self, *args):
                """
@@ -537,13 +656,21 @@ class Dialpad(object):
                self._notebook.set_current_page(0)
                self._recentviewselection.unselect_all()
 
+       def _on_addressbook_entry_changed(self, *args, **kwds):
+               combobox = self._widgetTree.get_widget("addressbook_combo")
+               itr = combobox.get_active_iter()
+
+               factoryId = int(self._booksList.get_value(itr, 0))
+               bookId = self._booksList.get_value(itr, 1)
+               self.open_addressbook(factoryId, bookId)
+
        def _on_contactsview_row_activated(self, treeview, path, view_column):
                model, itr = self._contactsviewselection.get_selected()
                if not itr:
                        return
 
                contactId = self._contactsmodel.get_value(itr, 3)
-               contactDetails = self._gcBackend.get_contact_details(contactId)
+               contactDetails = self._addressBook.get_contact_details(contactId)
                contactDetails = [phoneNumber for phoneNumber in contactDetails]
 
                if len(contactDetails) == 0:
@@ -561,20 +688,24 @@ class Dialpad(object):
 
        def _on_notebook_switch_page(self, notebook, page, page_num):
                if page_num == 1 and 300 < (time.time() - self._contactstime):
-                       gobject.idle_add(self.populate_contactsview)
+                       gobject.idle_add(self._idly_populate_contactsview)
                elif page_num == 2 and 300 < (time.time() - self._recenttime):
-                       gobject.idle_add(self.populate_recentview)
+                       gobject.idle_add(self._idly_populate_recentview)
                elif page_num == 3 and self._callbackNeedsSetup:
-                       gobject.idle_add(self._setup_callback_combo)
+                       gobject.idle_add(self._idly_setup_callback_combo)
 
-               if hildon:
-                       self._window.set_title(self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text())
+               tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
+               if hildon is not None:
+                       self._window.set_title(tabTitle)
+               else:
+                       self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
 
        def _on_dial_clicked(self, widget):
                """
                @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
                """
-               if not self._gcBackend.is_authed():
+               loggedIn = self._gcBackend.is_authed()
+               if not loggedIn:
                        loggedIn = self.attempt_login(2)
 
                if not loggedIn or not self._gcBackend.is_authed() or self._gcBackend.get_callback_number() == "":
@@ -635,7 +766,7 @@ class DummyOptions(object):
 
 if __name__ == "__main__":
        if hildon is not None:
-               gtk.set_application_name("Dialer")
+               gtk.set_application_name(Dialpad.__pretty_app_name__)
 
        if optparse is not None:
                parser = optparse.OptionParser()