Addign todos and cleaning up the warnings
[gc-dialer] / src / dc_glade.py
1 #!/usr/bin/python2.5
2
3 """
4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21 @bug Completely broken on Maemo
22 @todo For every X minutes, if logged in, attempt login
23 @todo Force login on connect if not already done
24 @todo Add logging support to make debugging issues for people a lot easier
25 """
26
27
28 from __future__ import with_statement
29
30 import sys
31 import gc
32 import os
33 import threading
34 import base64
35 import ConfigParser
36 import itertools
37 import warnings
38 import traceback
39
40 import gtk
41 import gtk.glade
42
43 try:
44         import hildon
45 except ImportError:
46         hildon = None
47
48 import gtk_toolbox
49
50
51 def getmtime_nothrow(path):
52         try:
53                 return os.path.getmtime(path)
54         except StandardError:
55                 return 0
56
57
58 def display_error_message(msg):
59         error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
60
61         def close(dialog, response):
62                 dialog.destroy()
63         error_dialog.connect("response", close)
64         error_dialog.run()
65
66
67 class Dialcentral(object):
68
69         __pretty_app_name__ = "DialCentral"
70         __app_name__ = "dialcentral"
71         __version__ = "0.9.6"
72         __app_magic__ = 0xdeadbeef
73
74         _glade_files = [
75                 '/usr/lib/dialcentral/dialcentral.glade',
76                 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
77                 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
78         ]
79
80         KEYPAD_TAB = 0
81         RECENT_TAB = 1
82         MESSAGES_TAB = 2
83         CONTACTS_TAB = 3
84         ACCOUNT_TAB = 4
85
86         NULL_BACKEND = 0
87         GC_BACKEND = 1
88         GV_BACKEND = 2
89         BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
90
91         _data_path = os.path.join(os.path.expanduser("~"), ".dialcentral")
92         _user_settings = "%s/settings.ini" % _data_path
93
94         def __init__(self):
95                 self._initDone = False
96                 self._connection = None
97                 self._osso = None
98                 self._clipboard = gtk.clipboard_get()
99
100                 self._deviceIsOnline = True
101                 self._credentials = ("", "")
102                 self._selectedBackendId = self.NULL_BACKEND
103                 self._defaultBackendId = self.GC_BACKEND
104                 self._phoneBackends = None
105                 self._dialpads = None
106                 self._accountViews = None
107                 self._messagesViews = None
108                 self._recentViews = None
109                 self._contactsViews = None
110
111                 for path in self._glade_files:
112                         if os.path.isfile(path):
113                                 self._widgetTree = gtk.glade.XML(path)
114                                 break
115                 else:
116                         display_error_message("Cannot find dialcentral.glade")
117                         gtk.main_quit()
118                         return
119
120                 self._window = self._widgetTree.get_widget("mainWindow")
121                 self._notebook = self._widgetTree.get_widget("notebook")
122                 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
123                 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
124
125                 self._app = None
126                 self._isFullScreen = False
127                 if hildon is not None:
128                         self._app = hildon.Program()
129                         self._window = hildon.Window()
130                         self._widgetTree.get_widget("vbox1").reparent(self._window)
131                         self._app.add_window(self._window)
132                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
133                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
134                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
135                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
136                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
137
138                         gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
139                         menu = gtk.Menu()
140                         for child in gtkMenu.get_children():
141                                 child.reparent(menu)
142                         self._window.set_menu(menu)
143                         gtkMenu.destroy()
144
145                         self._window.connect("key-press-event", self._on_key_press)
146                         self._window.connect("window-state-event", self._on_window_state_change)
147                 else:
148                         pass # warnings.warn("No Hildon", UserWarning, 2)
149
150                 if hildon is not None:
151                         self._window.set_title("Keypad")
152                 else:
153                         self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
154
155                 callbackMapping = {
156                         "on_dialpad_quit": self._on_close,
157                 }
158                 self._widgetTree.signal_autoconnect(callbackMapping)
159
160                 if self._window:
161                         self._window.connect("destroy", self._on_close)
162                         self._window.show_all()
163                         self._window.set_default_size(800, 300)
164
165                 backgroundSetup = threading.Thread(target=self._idle_setup)
166                 backgroundSetup.setDaemon(True)
167                 backgroundSetup.start()
168
169         def _idle_setup(self):
170                 """
171                 If something can be done after the UI loads, push it here so it's not blocking the UI
172                 """
173                 # Barebones UI handlers
174                 import null_backend
175                 import null_views
176
177                 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
178                 with gtk_toolbox.gtk_lock():
179                         self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
180                         self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
181                         self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
182                         self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
183                         self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
184
185                         self._dialpads[self._selectedBackendId].enable()
186                         self._accountViews[self._selectedBackendId].enable()
187                         self._recentViews[self._selectedBackendId].enable()
188                         self._messagesViews[self._selectedBackendId].enable()
189                         self._contactsViews[self._selectedBackendId].enable()
190
191                 # Setup maemo specifics
192                 try:
193                         import osso
194                 except ImportError:
195                         osso = None
196                 self._osso = None
197                 if osso is not None:
198                         self._osso = osso.Context(Dialcentral.__app_name__, Dialcentral.__version__, False)
199                         device = osso.DeviceState(self._osso)
200                         device.set_device_state_callback(self._on_device_state_change, 0)
201                 else:
202                         pass # warnings.warn("No OSSO", UserWarning)
203
204                 try:
205                         import conic
206                 except ImportError:
207                         conic = None
208                 self._connection = None
209                 if conic is not None:
210                         self._connection = conic.Connection()
211                         self._connection.connect("connection-event", self._on_connection_change, Dialcentral.__app_magic__)
212                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
213                 else:
214                         pass # warnings.warn("No Internet Connectivity API ", UserWarning)
215
216                 # Setup costly backends
217                 import gv_backend
218                 import gc_backend
219                 import file_backend
220                 import evo_backend
221                 import gc_views
222
223                 try:
224                         os.makedirs(self._data_path)
225                 except OSError, e:
226                         if e.errno != 17:
227                                 raise
228                 gcCookiePath = os.path.join(self._data_path, "gc_cookies.txt")
229                 gvCookiePath = os.path.join(self._data_path, "gv_cookies.txt")
230                 self._defaultBackendId = self._guess_preferred_backend((
231                         (self.GC_BACKEND, gcCookiePath),
232                         (self.GV_BACKEND, gvCookiePath),
233                 ))
234
235                 self._phoneBackends.update({
236                         self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
237                         self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
238                 })
239                 with gtk_toolbox.gtk_lock():
240                         unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
241                         unifiedDialpad.set_number("")
242                         self._dialpads.update({
243                                 self.GC_BACKEND: unifiedDialpad,
244                                 self.GV_BACKEND: unifiedDialpad,
245                         })
246                         self._accountViews.update({
247                                 self.GC_BACKEND: gc_views.AccountInfo(
248                                         self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
249                                 ),
250                                 self.GV_BACKEND: gc_views.AccountInfo(
251                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
252                                 ),
253                         })
254                         self._recentViews.update({
255                                 self.GC_BACKEND: gc_views.RecentCallsView(
256                                         self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
257                                 ),
258                                 self.GV_BACKEND: gc_views.RecentCallsView(
259                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
260                                 ),
261                         })
262                         self._messagesViews.update({
263                                 self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
264                                 self.GV_BACKEND: gc_views.MessagesView(
265                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
266                                 ),
267                         })
268                         self._contactsViews.update({
269                                 self.GC_BACKEND: gc_views.ContactsView(
270                                         self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
271                                 ),
272                                 self.GV_BACKEND: gc_views.ContactsView(
273                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
274                                 ),
275                         })
276
277                 evoBackend = evo_backend.EvolutionAddressBook()
278                 fsContactsPath = os.path.join(self._data_path, "contacts")
279                 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
280                 for backendId in (self.GV_BACKEND, self.GC_BACKEND):
281                         self._dialpads[backendId].dial = self._on_dial_clicked
282                         self._recentViews[backendId].number_selected = self._on_number_selected
283                         self._contactsViews[backendId].number_selected = self._on_number_selected
284
285                         addressBooks = [
286                                 self._phoneBackends[backendId],
287                                 evoBackend,
288                                 fileBackend,
289                         ]
290                         mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
291                         self._contactsViews[backendId].append(mergedBook)
292                         self._contactsViews[backendId].extend(addressBooks)
293                         self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
294
295                 callbackMapping = {
296                         "on_paste": self._on_paste,
297                         "on_refresh": self._on_refresh,
298                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
299                         "on_notebook_switch_page": self._on_notebook_switch_page,
300                         "on_about_activate": self._on_about_activate,
301                 }
302                 self._widgetTree.signal_autoconnect(callbackMapping)
303
304                 self._initDone = True
305
306                 config = ConfigParser.SafeConfigParser()
307                 config.read(self._user_settings)
308                 with gtk_toolbox.gtk_lock():
309                         self.load_settings(config)
310
311                 self.attempt_login(2)
312
313                 return False
314
315         def attempt_login(self, numOfAttempts = 10):
316                 """
317                 @todo Handle user notification better like attempting to login and failed login
318
319                 @note Not meant to be called directly, but run as a seperate thread.
320                 """
321                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
322
323                 if not self._deviceIsOnline:
324                         warnings.warn("Attempted to login while device was offline")
325                         return False
326                 elif self._phoneBackends is None or len(self._phoneBackends) < len(self.BACKENDS):
327                         warnings.warn(
328                                 "Attempted to login before initialization is complete, did an event fire early?"
329                         )
330                         return False
331
332                 loggedIn = False
333                 try:
334                         username, password = self._credentials
335                         serviceId = self._defaultBackendId
336
337                         # Attempt using the cookies
338                         loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
339                         if loggedIn:
340                                 warnings.warn(
341                                         "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
342                                         UserWarning, 2
343                                 )
344
345                         # Attempt using the settings file
346                         if not loggedIn and username and password:
347                                 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
348                                 if loggedIn:
349                                         warnings.warn(
350                                                 "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
351                                                 UserWarning, 2
352                                         )
353
354                         # Query the user for credentials
355                         for attemptCount in xrange(numOfAttempts):
356                                 if loggedIn:
357                                         break
358                                 with gtk_toolbox.gtk_lock():
359                                         availableServices = {
360                                                 self.GV_BACKEND: "Google Voice",
361                                                 self.GC_BACKEND: "Grand Central",
362                                         }
363                                         credentials = self._credentialsDialog.request_credentials_from(
364                                                 availableServices, defaultCredentials = self._credentials
365                                         )
366                                         serviceId, username, password = credentials
367
368                                 loggedIn = self._phoneBackends[serviceId].login(username, password)
369                         if 0 < attemptCount:
370                                 warnings.warn(
371                                         "Logged into %r through user request" % self._phoneBackends[serviceId],
372                                         UserWarning, 2
373                                 )
374                 except RuntimeError, e:
375                         warnings.warn(traceback.format_exc())
376                         self._errorDisplay.push_exception_with_lock(e)
377
378                 with gtk_toolbox.gtk_lock():
379                         if loggedIn:
380                                 self._credentials = username, password
381                                 self._change_loggedin_status(serviceId)
382                         else:
383                                 self._errorDisplay.push_message("Login Failed")
384                                 self._change_loggedin_status(self.NULL_BACKEND)
385                 return loggedIn
386
387         def _on_close(self, *args, **kwds):
388                 try:
389                         if self._osso is not None:
390                                 self._osso.close()
391
392                         if self._initDone:
393                                 self._save_settings()
394                 finally:
395                         gtk.main_quit()
396
397         def _change_loggedin_status(self, newStatus):
398                 oldStatus = self._selectedBackendId
399                 if oldStatus == newStatus:
400                         return
401
402                 self._dialpads[oldStatus].disable()
403                 self._accountViews[oldStatus].disable()
404                 self._recentViews[oldStatus].disable()
405                 self._messagesViews[oldStatus].disable()
406                 self._contactsViews[oldStatus].disable()
407
408                 self._dialpads[newStatus].enable()
409                 self._accountViews[newStatus].enable()
410                 self._recentViews[newStatus].enable()
411                 self._messagesViews[newStatus].enable()
412                 self._contactsViews[newStatus].enable()
413
414                 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
415                         self._phoneBackends[self._selectedBackendId].set_sane_callback()
416                 self._accountViews[self._selectedBackendId].update()
417
418                 self._selectedBackendId = newStatus
419
420         def load_settings(self, config):
421                 """
422                 @note UI Thread
423                 """
424                 self._defaultBackendId = int(config.get(self.__pretty_app_name__, "active"))
425                 blobs = (
426                         config.get(self.__pretty_app_name__, "bin_blob_%i" % i)
427                         for i in xrange(len(self._credentials))
428                 )
429                 creds = (
430                         base64.b64decode(blob)
431                         for blob in blobs
432                 )
433                 self._credentials = tuple(creds)
434                 for backendId, view in itertools.chain(
435                         self._dialpads.iteritems(),
436                         self._accountViews.iteritems(),
437                         self._messagesViews.iteritems(),
438                         self._recentViews.iteritems(),
439                         self._contactsViews.iteritems(),
440                 ):
441                         sectionName = "%s - %s" % (backendId, view.name())
442                         view.load_settings(config, sectionName)
443
444         def save_settings(self, config):
445                 """
446                 @note Thread Agnostic
447                 """
448                 config.add_section(self.__pretty_app_name__)
449                 config.set(self.__pretty_app_name__, "active", str(self._selectedBackendId))
450                 for i, value in enumerate(self._credentials):
451                         blob = base64.b64encode(value)
452                         config.set(self.__pretty_app_name__, "bin_blob_%i" % i, blob)
453                 for backendId, view in itertools.chain(
454                         self._dialpads.iteritems(),
455                         self._accountViews.iteritems(),
456                         self._messagesViews.iteritems(),
457                         self._recentViews.iteritems(),
458                         self._contactsViews.iteritems(),
459                 ):
460                         sectionName = "%s - %s" % (backendId, view.name())
461                         config.add_section(sectionName)
462                         view.save_settings(config, sectionName)
463
464         def _guess_preferred_backend(self, backendAndCookiePaths):
465                 modTimeAndPath = [
466                         (getmtime_nothrow(path), backendId, path)
467                         for backendId, path in backendAndCookiePaths
468                 ]
469                 modTimeAndPath.sort()
470                 return modTimeAndPath[-1][1]
471
472         def _save_settings(self):
473                 """
474                 @note Thread Agnostic
475                 """
476                 config = ConfigParser.SafeConfigParser()
477                 self.save_settings(config)
478                 with open(self._user_settings, "wb") as configFile:
479                         config.write(configFile)
480
481         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
482                 """
483                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
484                 For system_inactivity, we have no background tasks to pause
485
486                 @note Hildon specific
487                 """
488                 if memory_low:
489                         for backendId in self.BACKENDS:
490                                 self._phoneBackends[backendId].clear_caches()
491                         self._contactsViews[self._selectedBackendId].clear_caches()
492                         gc.collect()
493
494                 if save_unsaved_data or shutdown:
495                         self._save_settings()
496
497         def _on_connection_change(self, connection, event, magicIdentifier):
498                 """
499                 @note Hildon specific
500                 """
501                 import conic
502
503                 status = event.get_status()
504                 error = event.get_error()
505                 iap_id = event.get_iap_id()
506                 bearer = event.get_bearer_type()
507
508                 if status == conic.STATUS_CONNECTED:
509                         self._deviceIsOnline = True
510                         backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
511                         backgroundLogin.setDaemon(True)
512                         backgroundLogin.start()
513                 elif status == conic.STATUS_DISCONNECTED:
514                         self._deviceIsOnline = False
515                         self._defaultBackendId = self._selectedBackendId
516                         self._change_loggedin_status(self.NULL_BACKEND)
517
518         def _on_window_state_change(self, widget, event, *args):
519                 """
520                 @note Hildon specific
521                 """
522                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
523                         self._isFullScreen = True
524                 else:
525                         self._isFullScreen = False
526
527         def _on_key_press(self, widget, event, *args):
528                 """
529                 @note Hildon specific
530                 """
531                 if event.keyval == gtk.keysyms.F6:
532                         if self._isFullScreen:
533                                 self._window.unfullscreen()
534                         else:
535                                 self._window.fullscreen()
536
537         def _on_clearcookies_clicked(self, *args):
538                 self._phoneBackends[self._selectedBackendId].logout()
539                 self._accountViews[self._selectedBackendId].clear()
540                 self._recentViews[self._selectedBackendId].clear()
541                 self._messagesViews[self._selectedBackendId].clear()
542                 self._contactsViews[self._selectedBackendId].clear()
543                 self._change_loggedin_status(self.NULL_BACKEND)
544
545                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
546                 backgroundLogin.setDaemon(True)
547                 backgroundLogin.start()
548
549         def _on_notebook_switch_page(self, notebook, page, page_num):
550                 if page_num == self.RECENT_TAB:
551                         self._recentViews[self._selectedBackendId].update()
552                 elif page_num == self.MESSAGES_TAB:
553                         self._messagesViews[self._selectedBackendId].update()
554                 elif page_num == self.CONTACTS_TAB:
555                         self._contactsViews[self._selectedBackendId].update()
556                 elif page_num == self.ACCOUNT_TAB:
557                         self._accountViews[self._selectedBackendId].update()
558
559                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
560                 if hildon is not None:
561                         self._window.set_title(tabTitle)
562                 else:
563                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
564
565         def _on_number_selected(self, action, number, message):
566                 if action == "select":
567                         self._dialpads[self._selectedBackendId].set_number(number)
568                         self._notebook.set_current_page(self.KEYPAD_TAB)
569                 elif action == "dial":
570                         self._on_dial_clicked(number)
571                 elif action == "sms":
572                         self._on_sms_clicked(number, message)
573                 else:
574                         assert False, "Unknown action: %s" % action
575
576         def _on_sms_clicked(self, number, message):
577                 """
578                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
579                 """
580                 assert number
581                 assert message
582                 try:
583                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
584                 except RuntimeError, e:
585                         loggedIn = False
586                         self._errorDisplay.push_exception(e)
587                         return
588
589                 if not loggedIn:
590                         self._errorDisplay.push_message(
591                                 "Backend link with grandcentral is not working, please try again"
592                         )
593                         return
594
595                 dialed = False
596                 try:
597                         self._phoneBackends[self._selectedBackendId].send_sms(number, message)
598                         dialed = True
599                 except RuntimeError, e:
600                         self._errorDisplay.push_exception(e)
601                 except ValueError, e:
602                         self._errorDisplay.push_exception(e)
603
604         def _on_dial_clicked(self, number):
605                 """
606                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
607                 """
608                 assert number
609                 try:
610                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
611                 except RuntimeError, e:
612                         loggedIn = False
613                         self._errorDisplay.push_exception(e)
614                         return
615
616                 if not loggedIn:
617                         self._errorDisplay.push_message(
618                                 "Backend link with grandcentral is not working, please try again"
619                         )
620                         return
621
622                 dialed = False
623                 try:
624                         assert self._phoneBackends[self._selectedBackendId].get_callback_number() != ""
625                         self._phoneBackends[self._selectedBackendId].dial(number)
626                         dialed = True
627                 except RuntimeError, e:
628                         self._errorDisplay.push_exception(e)
629                 except ValueError, e:
630                         self._errorDisplay.push_exception(e)
631
632                 if dialed:
633                         self._dialpads[self._selectedBackendId].clear()
634
635         def _on_refresh(self, *args):
636                 page_num = self._notebook.get_current_page()
637                 if page_num == self.CONTACTS_TAB:
638                         self._contactsViews[self._selectedBackendId].update(force=True)
639                 elif page_num == self.RECENT_TAB:
640                         self._recentViews[self._selectedBackendId].update(force=True)
641                 elif page_num == self.MESSAGES_TAB:
642                         self._messagesViews[self._selectedBackendId].update(force=True)
643
644         def _on_paste(self, *args):
645                 contents = self._clipboard.wait_for_text()
646                 self._dialpads[self._selectedBackendId].set_number(contents)
647
648         def _on_about_activate(self, *args):
649                 dlg = gtk.AboutDialog()
650                 dlg.set_name(self.__pretty_app_name__)
651                 dlg.set_version(self.__version__)
652                 dlg.set_copyright("Copyright 2008 - LGPL")
653                 dlg.set_comments("Dialer is designed to interface with your Google Grandcentral account.  This application is not affiliated with Google or Grandcentral in any way")
654                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
655                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
656                 dlg.run()
657                 dlg.destroy()
658
659
660 def run_doctest():
661         import doctest
662
663         failureCount, testCount = doctest.testmod()
664         if not failureCount:
665                 print "Tests Successful"
666                 sys.exit(0)
667         else:
668                 sys.exit(1)
669
670
671 def run_dialpad():
672         gtk.gdk.threads_init()
673         if hildon is not None:
674                 gtk.set_application_name(Dialcentral.__pretty_app_name__)
675         handle = Dialcentral()
676         gtk.main()
677
678
679 class DummyOptions(object):
680
681         def __init__(self):
682                 self.test = False
683
684
685 if __name__ == "__main__":
686         if len(sys.argv) > 1:
687                 try:
688                         import optparse
689                 except ImportError:
690                         optparse = None
691
692                 if optparse is not None:
693                         parser = optparse.OptionParser()
694                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
695                         (commandOptions, commandArgs) = parser.parse_args()
696         else:
697                 commandOptions = DummyOptions()
698                 commandArgs = []
699
700         if commandOptions.test:
701                 run_doctest()
702         else:
703                 run_dialpad()