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