Displaying build number to use
[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
22
23 from __future__ import with_statement
24
25 import sys
26 import gc
27 import os
28 import threading
29 import base64
30 import ConfigParser
31 import itertools
32 import warnings
33
34 import gtk
35 import gtk.glade
36
37 import constants
38 import hildonize
39 import gtk_toolbox
40
41
42 def getmtime_nothrow(path):
43         try:
44                 return os.path.getmtime(path)
45         except StandardError:
46                 return 0
47
48
49 def display_error_message(msg):
50         error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
51
52         def close(dialog, response):
53                 dialog.destroy()
54         error_dialog.connect("response", close)
55         error_dialog.run()
56
57
58 class Dialcentral(object):
59
60         _glade_files = [
61                 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
62                 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
63                 '/usr/lib/dialcentral/dialcentral.glade',
64         ]
65
66         KEYPAD_TAB = 0
67         RECENT_TAB = 1
68         MESSAGES_TAB = 2
69         CONTACTS_TAB = 3
70         ACCOUNT_TAB = 4
71
72         NULL_BACKEND = 0
73         GV_BACKEND = 2
74         BACKENDS = (NULL_BACKEND, GV_BACKEND)
75
76         def __init__(self):
77                 self._initDone = False
78                 self._connection = None
79                 self._osso = None
80                 self._clipboard = gtk.clipboard_get()
81
82                 self._credentials = ("", "")
83                 self._selectedBackendId = self.NULL_BACKEND
84                 self._defaultBackendId = self.GV_BACKEND
85                 self._phoneBackends = None
86                 self._dialpads = None
87                 self._accountViews = None
88                 self._messagesViews = None
89                 self._recentViews = None
90                 self._contactsViews = None
91                 self._alarmHandler = None
92                 self._ledHandler = None
93                 self._originalCurrentLabels = []
94
95                 for path in self._glade_files:
96                         if os.path.isfile(path):
97                                 self._widgetTree = gtk.glade.XML(path)
98                                 break
99                 else:
100                         display_error_message("Cannot find dialcentral.glade")
101                         gtk.main_quit()
102                         return
103
104                 self._window = self._widgetTree.get_widget("mainWindow")
105                 self._notebook = self._widgetTree.get_widget("notebook")
106                 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
107                 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
108
109                 self._isFullScreen = False
110                 self._app = hildonize.get_app_class()()
111                 self._window = hildonize.hildonize_window(self._app, self._window)
112                 hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry"))
113                 hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry"))
114                 hildonize.hildonize_combo_entry(self._widgetTree.get_widget("callbackcombo").get_child())
115
116                 for scrollingWidget in (
117                         'recent_scrolledwindow',
118                         'message_scrolledwindow',
119                         'contacts_scrolledwindow',
120                         "phoneSelectionMessage_scrolledwindow",
121                         "phonetypes_scrolledwindow",
122                         "smsMessage_scrolledwindow",
123                         "smsMessage_scrolledEntry",
124                 ):
125                         hildonize.set_thumb_scrollbar(self._widgetTree.get_widget(scrollingWidget))
126
127                 hildonize.hildonize_menu(self._window, self._widgetTree.get_widget("dialpad_menubar"))
128
129                 if hildonize.IS_HILDON:
130                         self._window.connect("key-press-event", self._on_key_press)
131                         self._window.connect("window-state-event", self._on_window_state_change)
132                 else:
133                         pass # warnings.warn("No Hildon", UserWarning, 2)
134
135                 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
136
137                 callbackMapping = {
138                         "on_dialpad_quit": self._on_close,
139                 }
140                 self._widgetTree.signal_autoconnect(callbackMapping)
141
142                 self._window.connect("destroy", self._on_close)
143                 self._window.set_default_size(800, 300)
144                 self._window.show_all()
145
146                 self._loginSink = gtk_toolbox.threaded_stage(
147                         gtk_toolbox.comap(
148                                 self.attempt_login,
149                                 gtk_toolbox.null_sink(),
150                         )
151                 )
152
153                 backgroundSetup = threading.Thread(target=self._idle_setup)
154                 backgroundSetup.setDaemon(True)
155                 backgroundSetup.start()
156
157         def _idle_setup(self):
158                 """
159                 If something can be done after the UI loads, push it here so it's not blocking the UI
160                 """
161                 try:
162                         # Barebones UI handlers
163                         import null_backend
164                         import null_views
165
166                         self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
167                         with gtk_toolbox.gtk_lock():
168                                 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
169                                 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
170                                 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
171                                 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
172                                 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
173
174                                 self._dialpads[self._selectedBackendId].enable()
175                                 self._accountViews[self._selectedBackendId].enable()
176                                 self._recentViews[self._selectedBackendId].enable()
177                                 self._messagesViews[self._selectedBackendId].enable()
178                                 self._contactsViews[self._selectedBackendId].enable()
179
180                         # Setup maemo specifics
181                         try:
182                                 import osso
183                         except ImportError:
184                                 osso = None
185                         self._osso = None
186                         if osso is not None:
187                                 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
188                                 device = osso.DeviceState(self._osso)
189                                 device.set_device_state_callback(self._on_device_state_change, 0)
190                         else:
191                                 pass # warnings.warn("No OSSO", UserWarning, 2)
192
193                         try:
194                                 import alarm_handler
195                                 self._alarmHandler = alarm_handler.AlarmHandler()
196                         except ImportError:
197                                 alarm_handler = None
198                         except Exception:
199                                 with gtk_toolbox.gtk_lock():
200                                         self._errorDisplay.push_exception()
201                                 alarm_handler = None
202                         if hildonize.IS_HILDON:
203                                 import led_handler
204                                 self._ledHandler = led_handler.LedHandler()
205
206                         # Setup maemo specifics
207                         try:
208                                 import conic
209                         except ImportError:
210                                 conic = None
211                         self._connection = None
212                         if conic is not None:
213                                 self._connection = conic.Connection()
214                                 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
215                                 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
216                         else:
217                                 pass # warnings.warn("No Internet Connectivity API ", UserWarning)
218
219                         # Setup costly backends
220                         import gv_backend
221                         import file_backend
222                         import gv_views
223
224                         try:
225                                 os.makedirs(constants._data_path_)
226                         except OSError, e:
227                                 if e.errno != 17:
228                                         raise
229                         gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
230
231                         self._phoneBackends.update({
232                                 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
233                         })
234                         with gtk_toolbox.gtk_lock():
235                                 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
236                                 self._dialpads.update({
237                                         self.GV_BACKEND: unifiedDialpad,
238                                 })
239                                 self._accountViews.update({
240                                         self.GV_BACKEND: gv_views.AccountInfo(
241                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
242                                         ),
243                                 })
244                                 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
245                                 self._recentViews.update({
246                                         self.GV_BACKEND: gv_views.RecentCallsView(
247                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
248                                         ),
249                                 })
250                                 self._messagesViews.update({
251                                         self.GV_BACKEND: gv_views.MessagesView(
252                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
253                                         ),
254                                 })
255                                 self._contactsViews.update({
256                                         self.GV_BACKEND: gv_views.ContactsView(
257                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
258                                         ),
259                                 })
260
261                         fsContactsPath = os.path.join(constants._data_path_, "contacts")
262                         fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
263
264                         self._dialpads[self.GV_BACKEND].number_selected = self._select_action
265                         self._recentViews[self.GV_BACKEND].number_selected = self._select_action
266                         self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
267                         self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
268
269                         addressBooks = [
270                                 self._phoneBackends[self.GV_BACKEND],
271                                 fileBackend,
272                         ]
273                         mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
274                         self._contactsViews[self.GV_BACKEND].append(mergedBook)
275                         self._contactsViews[self.GV_BACKEND].extend(addressBooks)
276                         self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
277
278                         callbackMapping = {
279                                 "on_paste": self._on_paste,
280                                 "on_refresh": self._on_menu_refresh,
281                                 "on_clearcookies_clicked": self._on_clearcookies_clicked,
282                                 "on_notebook_switch_page": self._on_notebook_switch_page,
283                                 "on_about_activate": self._on_about_activate,
284                         }
285                         self._widgetTree.signal_autoconnect(callbackMapping)
286
287                         with gtk_toolbox.gtk_lock():
288                                 self._originalCurrentLabels = [
289                                         self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
290                                         for pageIndex in xrange(self._notebook.get_n_pages())
291                                 ]
292                                 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
293                                 self._notebookTapHandler.enable()
294                         self._notebookTapHandler.on_tap = self._reset_tab_refresh
295                         self._notebookTapHandler.on_hold = self._on_tab_refresh
296                         self._notebookTapHandler.on_holding = self._set_tab_refresh
297                         self._notebookTapHandler.on_cancel = self._reset_tab_refresh
298
299                         self._initDone = True
300
301                         config = ConfigParser.SafeConfigParser()
302                         config.read(constants._user_settings_)
303                         with gtk_toolbox.gtk_lock():
304                                 self.load_settings(config)
305
306                         self._spawn_attempt_login(2)
307                 except Exception, e:
308                         with gtk_toolbox.gtk_lock():
309                                 self._errorDisplay.push_exception()
310
311         def attempt_login(self, numOfAttempts = 10, force = False):
312                 """
313                 @todo Handle user notification better like attempting to login and failed login
314
315                 @note This must be run outside of the UI lock
316                 """
317                 try:
318                         assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
319                         assert self._initDone, "Attempting login before app is fully loaded"
320
321                         serviceId = self.NULL_BACKEND
322                         loggedIn = False
323                         if not force:
324                                 try:
325                                         self.refresh_session()
326                                         serviceId = self._defaultBackendId
327                                         loggedIn = True
328                                 except StandardError, e:
329                                         warnings.warn('Session refresh failed with the following message "%s"' % e.message, UserWarning, 2)
330
331                         if not loggedIn:
332                                 loggedIn, serviceId = self._login_by_user(numOfAttempts)
333
334                         with gtk_toolbox.gtk_lock():
335                                 self._change_loggedin_status(serviceId)
336                 except StandardError, e:
337                         with gtk_toolbox.gtk_lock():
338                                 self._errorDisplay.push_exception()
339
340         def _spawn_attempt_login(self, *args):
341                 self._loginSink.send(args)
342
343         def refresh_session(self):
344                 """
345                 @note Thread agnostic
346                 """
347                 assert self._initDone, "Attempting login before app is fully loaded"
348
349                 loggedIn = False
350                 if not loggedIn:
351                         loggedIn = self._login_by_cookie()
352                 if not loggedIn:
353                         loggedIn = self._login_by_settings()
354
355                 if not loggedIn:
356                         raise RuntimeError("Login Failed")
357
358         def _login_by_cookie(self):
359                 """
360                 @note Thread agnostic
361                 """
362                 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
363                 if loggedIn:
364                         warnings.warn(
365                                 "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
366                                 UserWarning, 2
367                         )
368                 return loggedIn
369
370         def _login_by_settings(self):
371                 """
372                 @note Thread agnostic
373                 """
374                 username, password = self._credentials
375                 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
376                 if loggedIn:
377                         self._credentials = username, password
378                         warnings.warn(
379                                 "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
380                                 UserWarning, 2
381                         )
382                 return loggedIn
383
384         def _login_by_user(self, numOfAttempts):
385                 """
386                 @note This must be run outside of the UI lock
387                 """
388                 loggedIn, (username, password) = False, self._credentials
389                 tmpServiceId = self.NULL_BACKEND
390                 for attemptCount in xrange(numOfAttempts):
391                         if loggedIn:
392                                 break
393                         with gtk_toolbox.gtk_lock():
394                                 credentials = self._credentialsDialog.request_credentials(
395                                         defaultCredentials = self._credentials
396                                 )
397                         tmpServiceId, username, password = credentials
398                         loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
399
400                 if loggedIn:
401                         serviceId = tmpServiceId
402                         self._credentials = username, password
403                         warnings.warn(
404                                 "Logged into %r through user request" % self._phoneBackends[serviceId],
405                                 UserWarning, 2
406                         )
407                 else:
408                         serviceId = self.NULL_BACKEND
409
410                 return loggedIn, serviceId
411
412         def _select_action(self, action, number, message):
413                 self.refresh_session()
414                 if action == "select":
415                         self._dialpads[self._selectedBackendId].set_number(number)
416                         self._notebook.set_current_page(self.KEYPAD_TAB)
417                 elif action == "dial":
418                         self._on_dial_clicked(number)
419                 elif action == "sms":
420                         self._on_sms_clicked(number, message)
421                 else:
422                         assert False, "Unknown action: %s" % action
423
424         def _change_loggedin_status(self, newStatus):
425                 oldStatus = self._selectedBackendId
426                 if oldStatus == newStatus:
427                         return
428
429                 self._dialpads[oldStatus].disable()
430                 self._accountViews[oldStatus].disable()
431                 self._recentViews[oldStatus].disable()
432                 self._messagesViews[oldStatus].disable()
433                 self._contactsViews[oldStatus].disable()
434
435                 self._dialpads[newStatus].enable()
436                 self._accountViews[newStatus].enable()
437                 self._recentViews[newStatus].enable()
438                 self._messagesViews[newStatus].enable()
439                 self._contactsViews[newStatus].enable()
440
441                 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
442                         self._phoneBackends[self._selectedBackendId].set_sane_callback()
443                 self._accountViews[self._selectedBackendId].update()
444
445                 self._selectedBackendId = newStatus
446
447         def load_settings(self, config):
448                 """
449                 @note UI Thread
450                 """
451                 try:
452                         self._defaultBackendId = int(config.get(constants.__pretty_app_name__, "active"))
453                         blobs = (
454                                 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
455                                 for i in xrange(len(self._credentials))
456                         )
457                         creds = (
458                                 base64.b64decode(blob)
459                                 for blob in blobs
460                         )
461                         self._credentials = tuple(creds)
462
463                         if self._alarmHandler is not None:
464                                 self._alarmHandler.load_settings(config, "alarm")
465                 except ConfigParser.NoOptionError, e:
466                         warnings.warn(
467                                 "Settings file %s is missing section %s" % (
468                                         constants._user_settings_,
469                                         e.section,
470                                 ),
471                                 stacklevel=2
472                         )
473                 except ConfigParser.NoSectionError, e:
474                         warnings.warn(
475                                 "Settings file %s is missing section %s" % (
476                                         constants._user_settings_,
477                                         e.section,
478                                 ),
479                                 stacklevel=2
480                         )
481
482                 for backendId, view in itertools.chain(
483                         self._dialpads.iteritems(),
484                         self._accountViews.iteritems(),
485                         self._messagesViews.iteritems(),
486                         self._recentViews.iteritems(),
487                         self._contactsViews.iteritems(),
488                 ):
489                         sectionName = "%s - %s" % (backendId, view.name())
490                         try:
491                                 view.load_settings(config, sectionName)
492                         except ConfigParser.NoOptionError, e:
493                                 warnings.warn(
494                                         "Settings file %s is missing section %s" % (
495                                                 constants._user_settings_,
496                                                 e.section,
497                                         ),
498                                         stacklevel=2
499                                 )
500                         except ConfigParser.NoSectionError, e:
501                                 warnings.warn(
502                                         "Settings file %s is missing section %s" % (
503                                                 constants._user_settings_,
504                                                 e.section,
505                                         ),
506                                         stacklevel=2
507                                 )
508
509         def save_settings(self, config):
510                 """
511                 @note Thread Agnostic
512                 """
513                 config.add_section(constants.__pretty_app_name__)
514                 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
515                 for i, value in enumerate(self._credentials):
516                         blob = base64.b64encode(value)
517                         config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
518                 config.add_section("alarm")
519                 if self._alarmHandler is not None:
520                         self._alarmHandler.save_settings(config, "alarm")
521
522                 for backendId, view in itertools.chain(
523                         self._dialpads.iteritems(),
524                         self._accountViews.iteritems(),
525                         self._messagesViews.iteritems(),
526                         self._recentViews.iteritems(),
527                         self._contactsViews.iteritems(),
528                 ):
529                         sectionName = "%s - %s" % (backendId, view.name())
530                         config.add_section(sectionName)
531                         view.save_settings(config, sectionName)
532
533         def _save_settings(self):
534                 """
535                 @note Thread Agnostic
536                 """
537                 config = ConfigParser.SafeConfigParser()
538                 self.save_settings(config)
539                 with open(constants._user_settings_, "wb") as configFile:
540                         config.write(configFile)
541
542         def _refresh_active_tab(self):
543                 pageIndex = self._notebook.get_current_page()
544                 if pageIndex == self.CONTACTS_TAB:
545                         self._contactsViews[self._selectedBackendId].update(force=True)
546                 elif pageIndex == self.RECENT_TAB:
547                         self._recentViews[self._selectedBackendId].update(force=True)
548                 elif pageIndex == self.MESSAGES_TAB:
549                         self._messagesViews[self._selectedBackendId].update(force=True)
550
551                 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
552                         if self._ledHandler is not None:
553                                 self._ledHandler.off()
554
555         def _on_close(self, *args, **kwds):
556                 try:
557                         if self._osso is not None:
558                                 self._osso.close()
559
560                         if self._initDone:
561                                 self._save_settings()
562                 finally:
563                         gtk.main_quit()
564
565         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
566                 """
567                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
568                 For system_inactivity, we have no background tasks to pause
569
570                 @note Hildon specific
571                 """
572                 if memory_low:
573                         for backendId in self.BACKENDS:
574                                 self._phoneBackends[backendId].clear_caches()
575                         self._contactsViews[self._selectedBackendId].clear_caches()
576                         gc.collect()
577
578                 if save_unsaved_data or shutdown:
579                         self._save_settings()
580
581         def _on_connection_change(self, connection, event, magicIdentifier):
582                 """
583                 @note Hildon specific
584                 """
585                 import conic
586
587                 status = event.get_status()
588                 error = event.get_error()
589                 iap_id = event.get_iap_id()
590                 bearer = event.get_bearer_type()
591
592                 if status == conic.STATUS_CONNECTED:
593                         if self._initDone:
594                                 self._spawn_attempt_login(2)
595                 elif status == conic.STATUS_DISCONNECTED:
596                         if self._initDone:
597                                 self._defaultBackendId = self._selectedBackendId
598                                 self._change_loggedin_status(self.NULL_BACKEND)
599
600         def _on_window_state_change(self, widget, event, *args):
601                 """
602                 @note Hildon specific
603                 """
604                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
605                         self._isFullScreen = True
606                 else:
607                         self._isFullScreen = False
608
609         def _on_key_press(self, widget, event, *args):
610                 """
611                 @note Hildon specific
612                 """
613                 if event.keyval == gtk.keysyms.F6:
614                         if self._isFullScreen:
615                                 self._window.unfullscreen()
616                         else:
617                                 self._window.fullscreen()
618
619         def _on_clearcookies_clicked(self, *args):
620                 self._phoneBackends[self._selectedBackendId].logout()
621                 self._accountViews[self._selectedBackendId].clear()
622                 self._recentViews[self._selectedBackendId].clear()
623                 self._messagesViews[self._selectedBackendId].clear()
624                 self._contactsViews[self._selectedBackendId].clear()
625                 self._change_loggedin_status(self.NULL_BACKEND)
626
627                 self._spawn_attempt_login(2, True)
628
629         def _on_notebook_switch_page(self, notebook, page, pageIndex):
630                 self._reset_tab_refresh()
631
632                 didRecentUpdate = False
633                 didMessagesUpdate = False
634
635                 if pageIndex == self.RECENT_TAB:
636                         didRecentUpdate = self._recentViews[self._selectedBackendId].update()
637                 elif pageIndex == self.MESSAGES_TAB:
638                         didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
639                 elif pageIndex == self.CONTACTS_TAB:
640                         self._contactsViews[self._selectedBackendId].update()
641                 elif pageIndex == self.ACCOUNT_TAB:
642                         self._accountViews[self._selectedBackendId].update()
643
644                 if didRecentUpdate or didMessagesUpdate:
645                         if self._ledHandler is not None:
646                                 self._ledHandler.off()
647
648         def _set_tab_refresh(self, *args):
649                 pageIndex = self._notebook.get_current_page()
650                 child = self._notebook.get_nth_page(pageIndex)
651                 self._notebook.get_tab_label(child).set_text("Refresh?")
652                 return False
653
654         def _reset_tab_refresh(self, *args):
655                 pageIndex = self._notebook.get_current_page()
656                 child = self._notebook.get_nth_page(pageIndex)
657                 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
658                 return False
659
660         def _on_tab_refresh(self, *args):
661                 self._refresh_active_tab()
662                 self._reset_tab_refresh()
663                 return False
664
665         def _on_sms_clicked(self, number, message):
666                 assert number, "No number specified"
667                 assert message, "Empty message"
668                 try:
669                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
670                 except StandardError, e:
671                         loggedIn = False
672                         self._errorDisplay.push_exception()
673                         return
674
675                 if not loggedIn:
676                         self._errorDisplay.push_message(
677                                 "Backend link with grandcentral is not working, please try again"
678                         )
679                         return
680
681                 dialed = False
682                 try:
683                         self._phoneBackends[self._selectedBackendId].send_sms(number, message)
684                         dialed = True
685                 except StandardError, e:
686                         self._errorDisplay.push_exception()
687                 except ValueError, e:
688                         self._errorDisplay.push_exception()
689
690                 if dialed:
691                         self._dialpads[self._selectedBackendId].clear()
692
693         def _on_dial_clicked(self, number):
694                 assert number, "No number to call"
695                 try:
696                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
697                 except StandardError, e:
698                         loggedIn = False
699                         self._errorDisplay.push_exception()
700                         return
701
702                 if not loggedIn:
703                         self._errorDisplay.push_message(
704                                 "Backend link with grandcentral is not working, please try again"
705                         )
706                         return
707
708                 dialed = False
709                 try:
710                         assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
711                         self._phoneBackends[self._selectedBackendId].dial(number)
712                         dialed = True
713                 except StandardError, e:
714                         self._errorDisplay.push_exception()
715                 except ValueError, e:
716                         self._errorDisplay.push_exception()
717
718                 if dialed:
719                         self._dialpads[self._selectedBackendId].clear()
720
721         def _on_menu_refresh(self, *args):
722                 self._refresh_active_tab()
723
724         def _on_paste(self, *args):
725                 contents = self._clipboard.wait_for_text()
726                 self._dialpads[self._selectedBackendId].set_number(contents)
727
728         def _on_about_activate(self, *args):
729                 dlg = gtk.AboutDialog()
730                 dlg.set_name(constants.__pretty_app_name__)
731                 dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
732                 dlg.set_copyright("Copyright 2008 - LGPL")
733                 dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice/Grandcentral account.  This application is not affiliated with Google in any way")
734                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
735                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
736                 dlg.run()
737                 dlg.destroy()
738
739
740 def run_doctest():
741         import doctest
742
743         failureCount, testCount = doctest.testmod()
744         if not failureCount:
745                 print "Tests Successful"
746                 sys.exit(0)
747         else:
748                 sys.exit(1)
749
750
751 def run_dialpad():
752         _lock_file = os.path.join(constants._data_path_, ".lock")
753         #with gtk_toolbox.flock(_lock_file, 0):
754         gtk.gdk.threads_init()
755
756         if hildonize.IS_HILDON:
757                 gtk.set_application_name(constants.__pretty_app_name__)
758         handle = Dialcentral()
759         gtk.main()
760
761
762 class DummyOptions(object):
763
764         def __init__(self):
765                 self.test = False
766
767
768 if __name__ == "__main__":
769         if len(sys.argv) > 1:
770                 try:
771                         import optparse
772                 except ImportError:
773                         optparse = None
774
775                 if optparse is not None:
776                         parser = optparse.OptionParser()
777                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
778                         (commandOptions, commandArgs) = parser.parse_args()
779         else:
780                 commandOptions = DummyOptions()
781                 commandArgs = []
782
783         if commandOptions.test:
784                 run_doctest()
785         else:
786                 run_dialpad()