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