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