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