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