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