Some minor cleanups to the new GUI for addressbooks
[gc-dialer] / src / gc_dialer.py
1 #!/usr/bin/python
2
3 # GC Dialer - Front end for Google's Grand Central service.
4 # Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
5
6 # This library is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU Lesser General Public
8 # License as published by the Free Software Foundation; either
9 # version 2.1 of the License, or (at your option) any later version.
10
11 # This library is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # Lesser General Public License for more details.
15
16 # You should have received a copy of the GNU Lesser General Public
17 # License along with this library; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19
20
21 """
22 Grandcentral Dialer
23 """
24
25
26 import sys
27 import gc
28 import os
29 import threading
30 import time
31 import re
32 import warnings
33
34 import gobject
35 import gtk
36 import gtk.glade
37
38 try:
39         import hildon
40 except ImportError:
41         hildon = None
42
43 try:
44         import osso
45 except ImportError:
46         osso = None
47
48 try:
49         import conic
50 except ImportError:
51         conic = None
52
53 try:
54         import doctest
55         import optparse
56 except ImportError:
57         doctest = None
58         optparse = None
59
60 from gc_backend import GCDialer
61 from evo_backend import EvolutionAddressBook
62
63 import socket
64
65
66 socket.setdefaulttimeout(5)
67
68
69 def make_ugly(prettynumber):
70         """
71         function to take a phone number and strip out all non-numeric
72         characters
73
74         >>> make_ugly("+012-(345)-678-90")
75         '01234567890'
76         """
77         uglynumber = re.sub('\D', '', prettynumber)
78         return uglynumber
79
80
81 def make_pretty(phonenumber):
82         """
83         Function to take a phone number and return the pretty version
84         pretty numbers:
85                 if phonenumber begins with 0:
86                         ...-(...)-...-....
87                 if phonenumber begins with 1: ( for gizmo callback numbers )
88                         1 (...)-...-....
89                 if phonenumber is 13 digits:
90                         (...)-...-....
91                 if phonenumber is 10 digits:
92                         ...-....
93         >>> make_pretty("12")
94         '12'
95         >>> make_pretty("1234567")
96         '123-4567'
97         >>> make_pretty("2345678901")
98         '(234)-567-8901'
99         >>> make_pretty("12345678901")
100         '1 (234)-567-8901'
101         >>> make_pretty("01234567890")
102         '+012-(345)-678-90'
103         """
104         if phonenumber is None:
105                 return ""
106
107         if len(phonenumber) < 3:
108                 return phonenumber
109
110         if phonenumber[0] == "0":
111                 prettynumber = ""
112                 prettynumber += "+%s" % phonenumber[0:3]
113                 if 3 < len(phonenumber):
114                         prettynumber += "-(%s)" % phonenumber[3:6]
115                         if 6 < len(phonenumber):
116                                 prettynumber += "-%s" % phonenumber[6:9]
117                                 if 9 < len(phonenumber):
118                                         prettynumber += "-%s" % phonenumber[9:]
119                 return prettynumber
120         elif len(phonenumber) <= 7:
121                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
122         elif len(phonenumber) > 8 and phonenumber[0] == "1":
123                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
124         elif len(phonenumber) > 7:
125                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
126         return prettynumber
127
128
129 def make_idler(func):
130         """
131         Decorator that makes a generator-function into a function that will continue execution on next call
132         """
133         a = []
134
135         def decorated_func(*args, **kwds):
136                 if not a:
137                         a.append(func(*args, **kwds))
138                 try:
139                         a[0].next()
140                         return True
141                 except StopIteration:
142                         del a[:]
143                         return False
144         
145         decorated_func.__name__ = func.__name__
146         decorated_func.__doc__ = func.__doc__
147         decorated_func.__dict__.update(func.__dict__)
148
149         return decorated_func
150
151
152 class DummyAddressBook(object):
153         """
154         Minimal example of both an addressbook factory and an addressbook
155         """
156
157         def get_addressbooks(self):
158                 """
159                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
160                 """
161                 yield self, "", "None"
162         
163         def open_addressbook(self, bookId):
164                 return self
165
166         @staticmethod
167         def factory_short_name():
168                 return ""
169
170         @staticmethod
171         def factory_name():
172                 return ""
173
174         def get_contacts(self):
175                 """
176                 @returns Iterable of (contact id, contact name)
177                 """
178                 return []
179
180         def get_contact_details(self, contactId):
181                 """
182                 @returns Iterable of (Phone Type, Phone Number)
183                 """
184                 return []
185
186
187 class PhoneTypeSelector(object):
188
189         def __init__(self, widgetTree, gcBackend):
190                 self._gcBackend = gcBackend
191                 self._widgetTree = widgetTree
192                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
193
194                 self._selectButton = self._widgetTree.get_widget("select_button")
195                 self._selectButton.connect("clicked", self._on_phonetype_select)
196
197                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
198                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
199
200                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
201                 self._typeviewselection = None
202
203                 typeview = self._widgetTree.get_widget("phonetypes")
204                 typeview.connect("row-activated", self._on_phonetype_select)
205                 typeview.set_model(self._typemodel)
206                 textrenderer = gtk.CellRendererText()
207
208                 # Add the column to the treeview
209                 column = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=1)
210                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
211
212                 typeview.append_column(column)
213
214                 self._typeviewselection = typeview.get_selection()
215                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
216
217         def run(self, contactDetails):
218                 self._typemodel.clear()
219
220                 for phoneType, phoneNumber in contactDetails:
221                         self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType)))
222
223                 userResponse = self._dialog.run()
224
225                 if userResponse == gtk.RESPONSE_OK:
226                         model, itr = self._typeviewselection.get_selected()
227                         if itr:
228                                 phoneNumber = self._typemodel.get_value(itr, 0)
229                 else:
230                         phoneNumber = ""
231
232                 self._typeviewselection.unselect_all()
233                 self._dialog.hide()
234                 return phoneNumber
235         
236         def _on_phonetype_select(self, *args):
237                 self._dialog.response(gtk.RESPONSE_OK)
238
239         def _on_phonetype_cancel(self, *args):
240                 self._dialog.response(gtk.RESPONSE_CANCEL)
241
242
243 class Dialpad(object):
244
245         __pretty_app_name__ = "Dialer"
246         __app_name__ = "gc_dialer"
247         __version__ = "0.8.0"
248         __app_magic__ = 0xdeadbeef
249
250         _glade_files = [
251                 './gc_dialer.glade',
252                 '../lib/gc_dialer.glade',
253                 '/usr/local/lib/gc_dialer.glade',
254         ]
255
256         def __init__(self):
257                 self._phonenumber = ""
258                 self._prettynumber = ""
259                 self._areacode = "518"
260
261                 self._clipboard = gtk.clipboard_get()
262
263                 self._deviceIsOnline = True
264                 self.callbacklist = None
265                 self._callbackNeedsSetup = True
266
267                 self._recenttime = 0.0
268                 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
269                 self._recentviewselection = None
270
271                 self._gcContactText = "GC"
272                 try:
273                         self._gcContactIcon = gtk.gdk.pixbuf_new_from_file_at_size('gc_contact.png', 16, 16)
274                 except gobject.GError:
275                         self._gcContactIcon = None
276                 self._contactstime = 0.0
277                 if self._gcContactIcon is not None:
278                         self._contactsmodel = gtk.ListStore(
279                                 gtk.gdk.Pixbuf,
280                                 gobject.TYPE_STRING,
281                                 gobject.TYPE_STRING,
282                                 gobject.TYPE_STRING,
283                                 gobject.TYPE_STRING
284                         )
285                 else:
286                         self._contactsmodel = gtk.ListStore(
287                                 gobject.TYPE_STRING,
288                                 gobject.TYPE_STRING,
289                                 gobject.TYPE_STRING,
290                                 gobject.TYPE_STRING,
291                                 gobject.TYPE_STRING
292                         )
293                 self._contactsviewselection = None
294
295                 for path in Dialpad._glade_files:
296                         if os.path.isfile(path):
297                                 self._widgetTree = gtk.glade.XML(path)
298                                 break
299                 else:
300                         self.display_error_message("Cannot find gc_dialer.glade")
301                         gtk.main_quit()
302                         return
303
304                 aboutHeader = self._widgetTree.get_widget("about_title")
305                 aboutHeader.set_label("%s\nVersion %s" % (aboutHeader.get_label(), Dialpad.__version__))
306
307                 #Get the buffer associated with the number display
308                 self._numberdisplay = self._widgetTree.get_widget("numberdisplay")
309                 self.set_number("")
310                 self._notebook = self._widgetTree.get_widget("notebook")
311
312                 self._window = self._widgetTree.get_widget("Dialpad")
313
314                 global hildon
315                 self._app = None
316                 self._isFullScreen = False
317                 if hildon is not None and self._window is gtk.Window:
318                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
319                         hildon = None
320                 elif hildon is not None:
321                         self._app = hildon.Program()
322                         self._app.add_window(self._window)
323                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
324                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
325                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
326
327                         gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
328                         menu = gtk.Menu()
329                         for child in gtkMenu.get_children():
330                                 child.reparent(menu)
331                         self._window.set_menu(menu)
332                         gtkMenu.destroy()
333
334                         self._window.connect("key-press-event", self._on_key_press)
335                         self._window.connect("window-state-event", self._on_window_state_change)
336                 else:
337                         warnings.warn("No Hildon", UserWarning, 2)
338
339                 if hildon is not None:
340                         self._window.set_title("Keypad")
341                 else:
342                         self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
343
344                 self._osso = None
345                 self._ebook = None
346                 if osso is not None:
347                         self._osso = osso.Context(Dialpad.__app_name__, Dialpad.__version__, False)
348                         device = osso.DeviceState(self._osso)
349                         device.set_device_state_callback(self._on_device_state_change, 0)
350                 else:
351                         warnings.warn("No OSSO", UserWarning, 2)
352
353                 self._connection = None
354                 if conic is not None:
355                         self._connection = conic.Connection()
356                         self._connection.connect("connection-event", self._on_connection_change, Dialpad.__app_magic__)
357                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
358                 else:
359                         warnings.warn("No Internet Connectivity API ", UserWarning, 2)
360
361                 callbackMapping = {
362                         # Process signals from buttons
363                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
364                         "on_loginclose_clicked": self._on_loginclose_clicked,
365
366                         "on_dialpad_quit": self._on_close,
367                         "on_paste": self._on_paste,
368                         "on_clear_number": self._on_clear_number,
369
370                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
371                         "on_notebook_switch_page": self._on_notebook_switch_page,
372                         "on_recentview_row_activated": self._on_recentview_row_activated,
373                         "on_contactsview_row_activated" : self._on_contactsview_row_activated,
374
375                         "on_digit_clicked": self._on_digit_clicked,
376                         "on_back_clicked": self._on_backspace,
377                         "on_dial_clicked": self._on_dial_clicked,
378                 }
379                 self._widgetTree.signal_autoconnect(callbackMapping)
380                 self._widgetTree.get_widget("callbackcombo").get_child().connect("changed", self._on_callbackentry_changed)
381                 self._widgetTree.get_widget("addressbook_combo").get_child().connect("changed", self._on_addressbook_entry_changed)
382
383                 if self._window:
384                         self._window.connect("destroy", gtk.main_quit)
385                         self._window.show_all()
386
387                 self._gcBackend = GCDialer()
388
389                 self._addressBookFactories = [
390                         DummyAddressBook(),
391                         EvolutionAddressBook(),
392                         self._gcBackend,
393                 ]
394                 self._addressBook = None
395                 self.open_addressbook(*self.get_addressbooks().next()[0][0:2])
396
397                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
398                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
399                         if factoryName and bookName:
400                                 entryName = "%s: %s" % (factoryName, bookName) 
401                         elif factoryName:
402                                 entryName = factoryName
403                         elif bookName:
404                                 entryName = bookName
405                         else:
406                                 entryName = "Bad name (%d)" % factoryId
407                         row = (str(factoryId), bookId, entryName)
408                         self._booksList.append(row)
409
410                 combobox = self._widgetTree.get_widget("addressbook_combo")
411                 combobox.set_model(self._booksList)
412                 combobox.set_text_column(2)
413                 combobox.set_active(0)
414
415                 self._phoneTypeSelector = PhoneTypeSelector(self._widgetTree, self._gcBackend)
416
417                 if not self._gcBackend.is_authed():
418                         self.attempt_login(2)
419                 else:
420                         self.set_account_number()
421                 gobject.idle_add(self._idly_init_recent_view)
422                 gobject.idle_add(self._idly_init_contacts_view)
423
424         def _idly_init_recent_view(self):
425                 """
426                 Deferred initalization of the recent view treeview
427                 """
428
429                 recentview = self._widgetTree.get_widget("recentview")
430                 recentview.set_model(self._recentmodel)
431                 textrenderer = gtk.CellRendererText()
432
433                 # Add the column to the treeview
434                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
435                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
436
437                 recentview.append_column(column)
438
439                 self._recentviewselection = recentview.get_selection()
440                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
441
442                 return False
443
444         def _idly_init_contacts_view(self):
445                 """ deferred initalization of the contacts view treeview """
446
447                 contactsview = self._widgetTree.get_widget("contactsview")
448                 contactsview.set_model(self._contactsmodel)
449
450                 # Add the column to the treeview
451                 column = gtk.TreeViewColumn("Contact")
452
453                 if self._gcContactIcon is not None:
454                         iconrenderer = gtk.CellRendererPixbuf()
455                         column.pack_start(iconrenderer, expand=False)
456                         column.add_attribute(iconrenderer, 'pixbuf', 0)
457                 else:
458                         warnings.warn("Contact icon unavailable", UserWarning, 1)
459                         textrenderer = gtk.CellRendererText()
460                         column.pack_start(textrenderer, expand=False)
461                         column.add_attribute(textrenderer, 'text', 0)
462
463                 textrenderer = gtk.CellRendererText()
464                 column.pack_start(textrenderer, expand=True)
465                 column.add_attribute(textrenderer, 'text', 1)
466
467                 textrenderer = gtk.CellRendererText()
468                 column.pack_start(textrenderer, expand=True)
469                 column.add_attribute(textrenderer, 'text', 4)
470
471                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
472                 column.set_sort_column_id(1)
473                 column.set_visible(True)
474                 contactsview.append_column(column)
475
476                 #textrenderer = gtk.CellRendererText()
477                 #column = gtk.TreeViewColumn("Location", textrenderer, text=2)
478                 #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
479                 #column.set_sort_column_id(2)
480                 #column.set_visible(True)
481                 #contactsview.append_column(column)
482
483                 #textrenderer = gtk.CellRendererText()
484                 #column = gtk.TreeViewColumn("Phone", textrenderer, text=3)
485                 #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
486                 #column.set_sort_column_id(3)
487                 #column.set_visible(True)
488                 #contactsview.append_column(column)
489
490                 self._contactsviewselection = contactsview.get_selection()
491                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
492
493                 return False
494
495         def _idly_setup_callback_combo(self):
496                 self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
497                 for number, description in self._gcBackend.get_callback_numbers().iteritems():
498                         self.callbacklist.append((make_pretty(number),))
499
500                 combobox = self._widgetTree.get_widget("callbackcombo")
501                 combobox.set_model(self.callbacklist)
502                 combobox.set_text_column(0)
503
504                 combobox.get_child().set_text(make_pretty(self._gcBackend.get_callback_number()))
505                 self._callbackNeedsSetup = False
506
507         def _idly_populate_recentview(self):
508                 self._recentmodel.clear()
509
510                 for personsName, phoneNumber, date, action in self._gcBackend.get_recent():
511                         description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
512                         item = (phoneNumber, description)
513                         self._recentmodel.append(item)
514
515                 self._recenttime = time.time()
516                 return False
517
518         @make_idler
519         def _idly_populate_contactsview(self):
520                 self._contactsmodel.clear()
521
522                 # completely disable updating the treeview while we populate the data
523                 contactsview = self._widgetTree.get_widget("contactsview")
524                 contactsview.freeze_child_notify()
525                 contactsview.set_model(None)
526
527                 # get gc icon
528                 if self._gcContactIcon is not None:
529                         contactType = (self._gcContactIcon,)
530                 else:
531                         contactType = (self._addressBook.factory_short_name(),)
532                 for contactId, contactName in self._addressBook.get_contacts():
533                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("",))
534                         yield
535
536                 # restart the treeview data rendering
537                 contactsview.set_model(self._contactsmodel)
538                 contactsview.thaw_child_notify()
539
540                 self._contactstime = time.time()
541
542         def attempt_login(self, numOfAttempts = 1):
543                 """
544                 @note Assumes that you are already logged in
545                 """
546                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
547                 dialog = self._widgetTree.get_widget("login_dialog")
548
549                 for i in range(numOfAttempts):
550                         dialog.run()
551
552                         username = self._widgetTree.get_widget("usernameentry").get_text()
553                         password = self._widgetTree.get_widget("passwordentry").get_text()
554                         self._widgetTree.get_widget("passwordentry").set_text("")
555
556                         loggedIn = self._gcBackend.login(username, password)
557                         dialog.hide()
558                         if loggedIn:
559                                 if self._gcBackend.get_callback_number() is None:
560                                         self._gcBackend.set_sane_callback()
561                                 self.set_account_number()
562                                 return True
563
564                 return False
565
566         def display_error_message(self, msg):
567                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
568
569                 def close(dialog, response, editor):
570                         editor.about_dialog = None
571                         dialog.destroy()
572                 error_dialog.connect("response", close, self)
573                 error_dialog.run()
574
575         def get_addressbooks(self):
576                 """
577                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
578                 """
579                 for i, factory in enumerate(self._addressBookFactories):
580                         for bookFactory, bookId, bookName in factory.get_addressbooks():
581                                 yield (i, bookId), (factory.factory_name(), bookName)
582         
583         def open_addressbook(self, bookFactoryId, bookId):
584                 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
585                 self._contactstime = 0
586                 gobject.idle_add(self._idly_populate_contactsview)
587
588         def set_number(self, number):
589                 """
590                 Set the callback phonenumber
591                 """
592                 self._phonenumber = make_ugly(number)
593                 self._prettynumber = make_pretty(self._phonenumber)
594                 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
595
596         def set_account_number(self):
597                 """
598                 Displays current account number
599                 """
600                 accountnumber = self._gcBackend.get_account_number()
601                 self._widgetTree.get_widget("gcnumber_display").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
602
603         def _on_close(self, *args):
604                 gtk.main_quit()
605
606         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
607                 """
608                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
609                 For system_inactivity, we have no background tasks to pause
610
611                 @note Hildon specific
612                 """
613                 if memory_low:
614                         self._gcBackend.clear_caches()
615                         re.purge()
616                         gc.collect()
617
618         def _on_connection_change(self, connection, event, magicIdentifier):
619                 """
620                 @note Hildon specific
621                 """
622                 status = event.get_status()
623                 error = event.get_error()
624                 iap_id = event.get_iap_id()
625                 bearer = event.get_bearer_type()
626
627                 if status == conic.STATUS_CONNECTED:
628                         self._window.set_sensitive(True)
629                         self._deviceIsOnline = True
630                 elif status == conic.STATUS_DISCONNECTED:
631                         self._window.set_sensitive(False)
632                         self._deviceIsOnline = False
633
634         def _on_window_state_change(self, widget, event, *args):
635                 """
636                 @note Hildon specific
637                 """
638                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
639                         self._isFullScreen = True
640                 else:
641                         self._isFullScreen = False
642         
643         def _on_key_press(self, widget, event, *args):
644                 """
645                 @note Hildon specific
646                 """
647                 if event.keyval == gtk.keysyms.F6:
648                         if self._isFullScreen:
649                                 self._window.unfullscreen()
650                         else:
651                                 self._window.fullscreen()
652
653         def _on_loginbutton_clicked(self, *args):
654                 self._widgetTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
655
656         def _on_loginclose_clicked(self, *args):
657                 self._on_close()
658                 sys.exit(0)
659
660         def _on_clearcookies_clicked(self, *args):
661                 self._gcBackend.logout()
662                 self._callbackNeedsSetup = True
663                 self._recenttime = 0.0
664                 self._contactstime = 0.0
665                 self._recentmodel.clear()
666                 self._widgetTree.get_widget("callbackcombo").get_child().set_text("")
667
668                 # re-run the inital grandcentral setup
669                 self.attempt_login(2)
670                 gobject.idle_add(self._idly_setup_callback_combo)
671
672         def _on_callbackentry_changed(self, *args):
673                 """
674                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
675                 """
676                 text = make_ugly(self._widgetTree.get_widget("callbackcombo").get_child().get_text())
677                 if not self._gcBackend.is_valid_syntax(text):
678                         warnings.warn("%s is not a valid callback number" % text, UserWarning, 2)
679                 elif text != self._gcBackend.get_callback_number():
680                         self._gcBackend.set_callback_number(text)
681                 else:
682                         warnings.warn("Callback number already is %s" % self._gcBackend.get_callback_number(), UserWarning, 2)
683
684         def _on_recentview_row_activated(self, treeview, path, view_column):
685                 model, itr = self._recentviewselection.get_selected()
686                 if not itr:
687                         return
688
689                 self.set_number(self._recentmodel.get_value(itr, 0))
690                 self._notebook.set_current_page(0)
691                 self._recentviewselection.unselect_all()
692
693         def _on_addressbook_entry_changed(self, *args, **kwds):
694                 combobox = self._widgetTree.get_widget("addressbook_combo")
695                 itr = combobox.get_active_iter()
696
697                 factoryId = int(self._booksList.get_value(itr, 0))
698                 bookId = self._booksList.get_value(itr, 1)
699                 self.open_addressbook(factoryId, bookId)
700
701         def _on_contactsview_row_activated(self, treeview, path, view_column):
702                 model, itr = self._contactsviewselection.get_selected()
703                 if not itr:
704                         return
705
706                 contactId = self._contactsmodel.get_value(itr, 3)
707                 contactDetails = self._addressBook.get_contact_details(contactId)
708                 contactDetails = [phoneNumber for phoneNumber in contactDetails]
709
710                 if len(contactDetails) == 0:
711                         phoneNumber = ""
712                 elif len(contactDetails) == 1:
713                         phoneNumber = contactDetails[0][1]
714                 else:
715                         phoneNumber = self._phoneTypeSelector.run(contactDetails)
716
717                 if 0 < len(phoneNumber):
718                         self.set_number(phoneNumber)
719                         self._notebook.set_current_page(0)
720
721                 self._contactsviewselection.unselect_all()
722
723         def _on_notebook_switch_page(self, notebook, page, page_num):
724                 if page_num == 1 and 300 < (time.time() - self._contactstime):
725                         gobject.idle_add(self._idly_populate_contactsview)
726                 elif page_num == 2 and 300 < (time.time() - self._recenttime):
727                         gobject.idle_add(self._idly_populate_recentview)
728                 elif page_num == 3 and self._callbackNeedsSetup:
729                         gobject.idle_add(self._idly_setup_callback_combo)
730
731                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
732                 if hildon is not None:
733                         self._window.set_title(tabTitle)
734                 else:
735                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
736
737         def _on_dial_clicked(self, widget):
738                 """
739                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
740                 """
741                 loggedIn = self._gcBackend.is_authed()
742                 if not loggedIn:
743                         loggedIn = self.attempt_login(2)
744
745                 if not loggedIn or not self._gcBackend.is_authed() or self._gcBackend.get_callback_number() == "":
746                         self.display_error_message("Backend link with grandcentral is not working, please try again")
747                         warnings.warn("Backend Status: Logged in? %s, Authenticated? %s, Callback=%s" % (loggedIn, self._gcBackend.is_authed(), self._gcBackend.get_callback_number()), UserWarning, 2)
748                         return
749
750                 try:
751                         callSuccess = self._gcBackend.dial(self._phonenumber)
752                 except ValueError, e:
753                         self._gcBackend._msg = e.message
754                         callSuccess = False
755
756                 if not callSuccess:
757                         self.display_error_message(self._gcBackend._msg)
758                 else:
759                         self.set_number("")
760
761                 self._recentmodel.clear()
762                 self._recenttime = 0.0
763
764         def _on_paste(self, *args):
765                 contents = self._clipboard.wait_for_text()
766                 phoneNumber = re.sub('\D', '', contents)
767                 self.set_number(phoneNumber)
768
769         def _on_clear_number(self, *args):
770                 self.set_number("")
771
772         def _on_digit_clicked(self, widget):
773                 self.set_number(self._phonenumber + widget.get_name()[5])
774
775         def _on_backspace(self, widget):
776                 self.set_number(self._phonenumber[:-1])
777
778
779 def run_doctest():
780         failureCount, testCount = doctest.testmod()
781         if not failureCount:
782                 print "Tests Successful"
783                 sys.exit(0)
784         else:
785                 sys.exit(1)
786
787
788 def run_dialpad():
789         gtk.gdk.threads_init()
790         title = 'Dialpad'
791         handle = Dialpad()
792         gtk.main()
793
794
795 class DummyOptions(object):
796
797         def __init__(self):
798                 self.test = False
799
800
801 if __name__ == "__main__":
802         if hildon is not None:
803                 gtk.set_application_name(Dialpad.__pretty_app_name__)
804
805         if optparse is not None:
806                 parser = optparse.OptionParser()
807                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
808                 (commandOptions, commandArgs) = parser.parse_args()
809         else:
810                 commandOptions = DummyOptions()
811                 commandArgs = []
812
813         if commandOptions.test:
814                 run_doctest()
815         else:
816                 run_dialpad()