More gui cleanup
[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._contactstime = 0.0
272                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
273                 self._contactsviewselection = None
274
275                 for path in Dialpad._glade_files:
276                         if os.path.isfile(path):
277                                 self._widgetTree = gtk.glade.XML(path)
278                                 break
279                 else:
280                         self.display_error_message("Cannot find gc_dialer.glade")
281                         gtk.main_quit()
282                         return
283
284                 aboutHeader = self._widgetTree.get_widget("about_title")
285                 aboutHeader.set_label("%s\nVersion %s" % (aboutHeader.get_label(), Dialpad.__version__))
286
287                 #Get the buffer associated with the number display
288                 self._numberdisplay = self._widgetTree.get_widget("numberdisplay")
289                 self.set_number("")
290                 self._notebook = self._widgetTree.get_widget("notebook")
291
292                 self._window = self._widgetTree.get_widget("Dialpad")
293
294                 global hildon
295                 self._app = None
296                 self._isFullScreen = False
297                 if hildon is not None and self._window is gtk.Window:
298                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
299                         hildon = None
300                 elif hildon is not None:
301                         self._app = hildon.Program()
302                         self._app.add_window(self._window)
303                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
304                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
305                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
306
307                         gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
308                         menu = gtk.Menu()
309                         for child in gtkMenu.get_children():
310                                 child.reparent(menu)
311                         self._window.set_menu(menu)
312                         gtkMenu.destroy()
313
314                         self._window.connect("key-press-event", self._on_key_press)
315                         self._window.connect("window-state-event", self._on_window_state_change)
316                 else:
317                         warnings.warn("No Hildon", UserWarning, 2)
318
319                 if hildon is not None:
320                         self._window.set_title("Keypad")
321                 else:
322                         self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
323
324                 self._osso = None
325                 self._ebook = None
326                 if osso is not None:
327                         self._osso = osso.Context(Dialpad.__app_name__, Dialpad.__version__, False)
328                         device = osso.DeviceState(self._osso)
329                         device.set_device_state_callback(self._on_device_state_change, 0)
330                 else:
331                         warnings.warn("No OSSO", UserWarning, 2)
332
333                 self._connection = None
334                 if conic is not None:
335                         self._connection = conic.Connection()
336                         self._connection.connect("connection-event", self._on_connection_change, Dialpad.__app_magic__)
337                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
338                 else:
339                         warnings.warn("No Internet Connectivity API ", UserWarning, 2)
340
341                 callbackMapping = {
342                         # Process signals from buttons
343                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
344                         "on_loginclose_clicked": self._on_loginclose_clicked,
345
346                         "on_dialpad_quit": self._on_close,
347                         "on_paste": self._on_paste,
348                         "on_clear_number": self._on_clear_number,
349
350                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
351                         "on_notebook_switch_page": self._on_notebook_switch_page,
352                         "on_recentview_row_activated": self._on_recentview_row_activated,
353                         "on_contactsview_row_activated" : self._on_contactsview_row_activated,
354
355                         "on_digit_clicked": self._on_digit_clicked,
356                         "on_back_clicked": self._on_backspace,
357                         "on_dial_clicked": self._on_dial_clicked,
358                 }
359                 self._widgetTree.signal_autoconnect(callbackMapping)
360                 self._widgetTree.get_widget("callbackcombo").get_child().connect("changed", self._on_callbackentry_changed)
361                 self._widgetTree.get_widget("addressbook_combo").get_child().connect("changed", self._on_addressbook_entry_changed)
362
363                 if self._window:
364                         self._window.connect("destroy", gtk.main_quit)
365                         self._window.show_all()
366
367                 self._gcBackend = GCDialer()
368
369                 self._addressBookFactories = [
370                         DummyAddressBook(),
371                         EvolutionAddressBook(),
372                         self._gcBackend,
373                 ]
374                 self._addressBook = None
375                 self.open_addressbook(*self.get_addressbooks().next()[0][0:2])
376
377                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
378                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
379                         if factoryName and bookName:
380                                 entryName = "%s: %s" % (factoryName, bookName) 
381                         elif factoryName:
382                                 entryName = factoryName
383                         elif bookName:
384                                 entryName = bookName
385                         else:
386                                 entryName = "Bad name (%d)" % factoryId
387                         row = (str(factoryId), bookId, entryName)
388                         self._booksList.append(row)
389
390                 combobox = self._widgetTree.get_widget("addressbook_combo")
391                 combobox.set_model(self._booksList)
392                 combobox.set_text_column(2)
393                 combobox.set_active(0)
394
395                 self._phoneTypeSelector = PhoneTypeSelector(self._widgetTree, self._gcBackend)
396
397                 if not self._gcBackend.is_authed():
398                         self.attempt_login(2)
399                 else:
400                         self.set_account_number()
401                 gobject.idle_add(self._idly_init_recent_view)
402                 gobject.idle_add(self._idly_init_contacts_view)
403
404         def _idly_init_recent_view(self):
405                 """
406                 Deferred initalization of the recent view treeview
407                 """
408
409                 recentview = self._widgetTree.get_widget("recentview")
410                 recentview.set_model(self._recentmodel)
411                 textrenderer = gtk.CellRendererText()
412
413                 # Add the column to the treeview
414                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
415                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
416
417                 recentview.append_column(column)
418
419                 self._recentviewselection = recentview.get_selection()
420                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
421
422                 return False
423
424         def _idly_init_contacts_view(self):
425                 """ deferred initalization of the contacts view treeview """
426
427                 contactsview = self._widgetTree.get_widget("contactsview")
428                 contactsview.set_model(self._contactsmodel)
429
430                 # Add the column to the treeview
431                 column = gtk.TreeViewColumn("Contact")
432
433                 textrenderer = gtk.CellRendererText()
434                 column.pack_start(textrenderer, expand=True)
435                 column.add_attribute(textrenderer, 'text', 1)
436
437                 textrenderer = gtk.CellRendererText()
438                 column.pack_start(textrenderer, expand=True)
439                 column.add_attribute(textrenderer, 'text', 4)
440
441                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
442                 column.set_sort_column_id(1)
443                 column.set_visible(True)
444                 contactsview.append_column(column)
445
446                 #textrenderer = gtk.CellRendererText()
447                 #column = gtk.TreeViewColumn("Location", textrenderer, text=2)
448                 #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
449                 #column.set_sort_column_id(2)
450                 #column.set_visible(True)
451                 #contactsview.append_column(column)
452
453                 #textrenderer = gtk.CellRendererText()
454                 #column = gtk.TreeViewColumn("Phone", textrenderer, text=3)
455                 #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
456                 #column.set_sort_column_id(3)
457                 #column.set_visible(True)
458                 #contactsview.append_column(column)
459
460                 self._contactsviewselection = contactsview.get_selection()
461                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
462
463                 return False
464
465         def _idly_setup_callback_combo(self):
466                 self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
467                 for number, description in self._gcBackend.get_callback_numbers().iteritems():
468                         self.callbacklist.append((make_pretty(number),))
469
470                 combobox = self._widgetTree.get_widget("callbackcombo")
471                 combobox.set_model(self.callbacklist)
472                 combobox.set_text_column(0)
473
474                 combobox.get_child().set_text(make_pretty(self._gcBackend.get_callback_number()))
475                 self._callbackNeedsSetup = False
476
477         def _idly_populate_recentview(self):
478                 self._recentmodel.clear()
479
480                 for personsName, phoneNumber, date, action in self._gcBackend.get_recent():
481                         description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
482                         item = (phoneNumber, description)
483                         self._recentmodel.append(item)
484
485                 self._recenttime = time.time()
486                 return False
487
488         @make_idler
489         def _idly_populate_contactsview(self):
490                 self._contactsmodel.clear()
491
492                 # completely disable updating the treeview while we populate the data
493                 contactsview = self._widgetTree.get_widget("contactsview")
494                 contactsview.freeze_child_notify()
495                 contactsview.set_model(None)
496
497                 contactType = (self._addressBook.factory_short_name(),)
498                 for contactId, contactName in self._addressBook.get_contacts():
499                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("",))
500                         yield
501
502                 # restart the treeview data rendering
503                 contactsview.set_model(self._contactsmodel)
504                 contactsview.thaw_child_notify()
505
506                 self._contactstime = time.time()
507
508         def attempt_login(self, numOfAttempts = 1):
509                 """
510                 @note Assumes that you are already logged in
511                 """
512                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
513                 dialog = self._widgetTree.get_widget("login_dialog")
514
515                 for i in range(numOfAttempts):
516                         dialog.run()
517
518                         username = self._widgetTree.get_widget("usernameentry").get_text()
519                         password = self._widgetTree.get_widget("passwordentry").get_text()
520                         self._widgetTree.get_widget("passwordentry").set_text("")
521
522                         loggedIn = self._gcBackend.login(username, password)
523                         dialog.hide()
524                         if loggedIn:
525                                 if self._gcBackend.get_callback_number() is None:
526                                         self._gcBackend.set_sane_callback()
527                                 self.set_account_number()
528                                 return True
529
530                 return False
531
532         def display_error_message(self, msg):
533                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
534
535                 def close(dialog, response, editor):
536                         editor.about_dialog = None
537                         dialog.destroy()
538                 error_dialog.connect("response", close, self)
539                 error_dialog.run()
540
541         def get_addressbooks(self):
542                 """
543                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
544                 """
545                 for i, factory in enumerate(self._addressBookFactories):
546                         for bookFactory, bookId, bookName in factory.get_addressbooks():
547                                 yield (i, bookId), (factory.factory_name(), bookName)
548         
549         def open_addressbook(self, bookFactoryId, bookId):
550                 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
551                 self._contactstime = 0
552                 gobject.idle_add(self._idly_populate_contactsview)
553
554         def set_number(self, number):
555                 """
556                 Set the callback phonenumber
557                 """
558                 self._phonenumber = make_ugly(number)
559                 self._prettynumber = make_pretty(self._phonenumber)
560                 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
561
562         def set_account_number(self):
563                 """
564                 Displays current account number
565                 """
566                 accountnumber = self._gcBackend.get_account_number()
567                 self._widgetTree.get_widget("gcnumber_display").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
568
569         def _on_close(self, *args):
570                 gtk.main_quit()
571
572         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
573                 """
574                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
575                 For system_inactivity, we have no background tasks to pause
576
577                 @note Hildon specific
578                 """
579                 if memory_low:
580                         self._gcBackend.clear_caches()
581                         re.purge()
582                         gc.collect()
583
584         def _on_connection_change(self, connection, event, magicIdentifier):
585                 """
586                 @note Hildon specific
587                 """
588                 status = event.get_status()
589                 error = event.get_error()
590                 iap_id = event.get_iap_id()
591                 bearer = event.get_bearer_type()
592
593                 if status == conic.STATUS_CONNECTED:
594                         self._window.set_sensitive(True)
595                         self._deviceIsOnline = True
596                 elif status == conic.STATUS_DISCONNECTED:
597                         self._window.set_sensitive(False)
598                         self._deviceIsOnline = False
599
600         def _on_window_state_change(self, widget, event, *args):
601                 """
602                 @note Hildon specific
603                 """
604                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
605                         self._isFullScreen = True
606                 else:
607                         self._isFullScreen = False
608         
609         def _on_key_press(self, widget, event, *args):
610                 """
611                 @note Hildon specific
612                 """
613                 if event.keyval == gtk.keysyms.F6:
614                         if self._isFullScreen:
615                                 self._window.unfullscreen()
616                         else:
617                                 self._window.fullscreen()
618
619         def _on_loginbutton_clicked(self, *args):
620                 self._widgetTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
621
622         def _on_loginclose_clicked(self, *args):
623                 self._on_close()
624                 sys.exit(0)
625
626         def _on_clearcookies_clicked(self, *args):
627                 self._gcBackend.logout()
628                 self._callbackNeedsSetup = True
629                 self._recenttime = 0.0
630                 self._contactstime = 0.0
631                 self._recentmodel.clear()
632                 self._widgetTree.get_widget("callbackcombo").get_child().set_text("")
633
634                 # re-run the inital grandcentral setup
635                 self.attempt_login(2)
636                 gobject.idle_add(self._idly_setup_callback_combo)
637
638         def _on_callbackentry_changed(self, *args):
639                 """
640                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
641                 """
642                 text = make_ugly(self._widgetTree.get_widget("callbackcombo").get_child().get_text())
643                 if not self._gcBackend.is_valid_syntax(text):
644                         warnings.warn("%s is not a valid callback number" % text, UserWarning, 2)
645                 elif text != self._gcBackend.get_callback_number():
646                         self._gcBackend.set_callback_number(text)
647                 else:
648                         warnings.warn("Callback number already is %s" % self._gcBackend.get_callback_number(), UserWarning, 2)
649
650         def _on_recentview_row_activated(self, treeview, path, view_column):
651                 model, itr = self._recentviewselection.get_selected()
652                 if not itr:
653                         return
654
655                 self.set_number(self._recentmodel.get_value(itr, 0))
656                 self._notebook.set_current_page(0)
657                 self._recentviewselection.unselect_all()
658
659         def _on_addressbook_entry_changed(self, *args, **kwds):
660                 combobox = self._widgetTree.get_widget("addressbook_combo")
661                 itr = combobox.get_active_iter()
662
663                 factoryId = int(self._booksList.get_value(itr, 0))
664                 bookId = self._booksList.get_value(itr, 1)
665                 self.open_addressbook(factoryId, bookId)
666
667         def _on_contactsview_row_activated(self, treeview, path, view_column):
668                 model, itr = self._contactsviewselection.get_selected()
669                 if not itr:
670                         return
671
672                 contactId = self._contactsmodel.get_value(itr, 3)
673                 contactDetails = self._addressBook.get_contact_details(contactId)
674                 contactDetails = [phoneNumber for phoneNumber in contactDetails]
675
676                 if len(contactDetails) == 0:
677                         phoneNumber = ""
678                 elif len(contactDetails) == 1:
679                         phoneNumber = contactDetails[0][1]
680                 else:
681                         phoneNumber = self._phoneTypeSelector.run(contactDetails)
682
683                 if 0 < len(phoneNumber):
684                         self.set_number(phoneNumber)
685                         self._notebook.set_current_page(0)
686
687                 self._contactsviewselection.unselect_all()
688
689         def _on_notebook_switch_page(self, notebook, page, page_num):
690                 if page_num == 1 and 300 < (time.time() - self._contactstime):
691                         gobject.idle_add(self._idly_populate_contactsview)
692                 elif page_num == 2 and 300 < (time.time() - self._recenttime):
693                         gobject.idle_add(self._idly_populate_recentview)
694                 elif page_num == 3 and self._callbackNeedsSetup:
695                         gobject.idle_add(self._idly_setup_callback_combo)
696
697                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
698                 if hildon is not None:
699                         self._window.set_title(tabTitle)
700                 else:
701                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
702
703         def _on_dial_clicked(self, widget):
704                 """
705                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
706                 """
707                 loggedIn = self._gcBackend.is_authed()
708                 if not loggedIn:
709                         loggedIn = self.attempt_login(2)
710
711                 if not loggedIn or not self._gcBackend.is_authed() or self._gcBackend.get_callback_number() == "":
712                         self.display_error_message("Backend link with grandcentral is not working, please try again")
713                         warnings.warn("Backend Status: Logged in? %s, Authenticated? %s, Callback=%s" % (loggedIn, self._gcBackend.is_authed(), self._gcBackend.get_callback_number()), UserWarning, 2)
714                         return
715
716                 try:
717                         callSuccess = self._gcBackend.dial(self._phonenumber)
718                 except ValueError, e:
719                         self._gcBackend._msg = e.message
720                         callSuccess = False
721
722                 if not callSuccess:
723                         self.display_error_message(self._gcBackend._msg)
724                 else:
725                         self.set_number("")
726
727                 self._recentmodel.clear()
728                 self._recenttime = 0.0
729
730         def _on_paste(self, *args):
731                 contents = self._clipboard.wait_for_text()
732                 phoneNumber = re.sub('\D', '', contents)
733                 self.set_number(phoneNumber)
734
735         def _on_clear_number(self, *args):
736                 self.set_number("")
737
738         def _on_digit_clicked(self, widget):
739                 self.set_number(self._phonenumber + widget.get_name()[5])
740
741         def _on_backspace(self, widget):
742                 self.set_number(self._phonenumber[:-1])
743
744
745 def run_doctest():
746         failureCount, testCount = doctest.testmod()
747         if not failureCount:
748                 print "Tests Successful"
749                 sys.exit(0)
750         else:
751                 sys.exit(1)
752
753
754 def run_dialpad():
755         gtk.gdk.threads_init()
756         title = 'Dialpad'
757         handle = Dialpad()
758         gtk.main()
759
760
761 class DummyOptions(object):
762
763         def __init__(self):
764                 self.test = False
765
766
767 if __name__ == "__main__":
768         if hildon is not None:
769                 gtk.set_application_name(Dialpad.__pretty_app_name__)
770
771         if optparse is not None:
772                 parser = optparse.OptionParser()
773                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
774                 (commandOptions, commandArgs) = parser.parse_args()
775         else:
776                 commandOptions = DummyOptions()
777                 commandArgs = []
778
779         if commandOptions.test:
780                 run_doctest()
781         else:
782                 run_dialpad()