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