Reorging the code. Don't worry, no layoffs
[doneit] / src / doneit_glade.py
1 #!/usr/bin/python
2
3 """
4 @todo Add logging support to make debugging random user issues a lot easier
5 """
6
7 from __future__ import with_statement
8
9
10 import sys
11 import gc
12 import os
13 import threading
14 import warnings
15 import ConfigParser
16 import socket
17
18 import gobject
19 import gtk
20 import gtk.glade
21
22 try:
23         import hildon
24 except ImportError:
25         hildon = None
26
27 import gtk_toolbox
28
29
30 socket.setdefaulttimeout(10)
31
32
33 class PreferencesDialog(object):
34
35         def __init__(self, widgetTree):
36                 self._backendList = gtk.ListStore(gobject.TYPE_STRING)
37                 self._backendCell = gtk.CellRendererText()
38
39                 self._dialog = widgetTree.get_widget("preferencesDialog")
40                 self._backendSelector = widgetTree.get_widget("prefsBackendSelector")
41                 self._applyButton = widgetTree.get_widget("applyPrefsButton")
42                 self._cancelButton = widgetTree.get_widget("cancelPrefsButton")
43
44                 self._onApplyId = None
45                 self._onCancelId = None
46
47         def enable(self):
48                 self._dialog.set_default_size(800, 300)
49                 self._onApplyId = self._applyButton.connect("clicked", self._on_apply_clicked)
50                 self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked)
51
52                 cell = self._backendCell
53                 self._backendSelector.pack_start(cell, True)
54                 self._backendSelector.add_attribute(cell, 'text', 0)
55                 self._backendSelector.set_model(self._backendList)
56
57         def disable(self):
58                 self._applyButton.disconnect(self._onApplyId)
59                 self._cancelButton.disconnect(self._onCancelId)
60
61                 self._backendList.clear()
62                 self._backendSelector.set_model(None)
63
64         def run(self, app, parentWindow = None):
65                 if parentWindow is not None:
66                         self._dialog.set_transient_for(parentWindow)
67
68                 self._backendList.clear()
69                 activeIndex = 0
70                 for i, (uiName, ui) in enumerate(app.get_uis()):
71                         self._backendList.append((uiName, ))
72                         if uiName == app.get_default_ui():
73                                 activeIndex = i
74                 self._backendSelector.set_active(activeIndex)
75
76                 try:
77                         response = self._dialog.run()
78                         if response != gtk.RESPONSE_OK:
79                                 raise RuntimeError("Edit Cancelled")
80                 finally:
81                         self._dialog.hide()
82
83                 backendName = self._backendSelector.get_active_text()
84                 app.switch_ui(backendName)
85
86         def _on_apply_clicked(self, *args):
87                 self._dialog.response(gtk.RESPONSE_OK)
88
89         def _on_cancel_clicked(self, *args):
90                 self._dialog.response(gtk.RESPONSE_CANCEL)
91
92
93 class DoneIt(object):
94
95         __pretty_app_name__ = "DoneIt"
96         __app_name__ = "doneit"
97         __version__ = "0.3.1"
98         __app_magic__ = 0xdeadbeef
99
100         _glade_files = [
101                 '/usr/lib/doneit/doneit.glade',
102                 os.path.join(os.path.dirname(__file__), "doneit.glade"),
103                 os.path.join(os.path.dirname(__file__), "../lib/doneit.glade"),
104         ]
105
106         _user_data = os.path.expanduser("~/.%s/" % __app_name__)
107         _user_settings = "%s/settings.ini" % _user_data
108
109         def __init__(self):
110                 self._initDone = False
111                 self._todoUIs = {}
112                 self._todoUI = None
113                 self._osso = None
114                 self._deviceIsOnline = True
115                 self._connection = None
116                 self._fallbackUIName = ""
117                 self._defaultUIName = ""
118
119                 for path in self._glade_files:
120                         if os.path.isfile(path):
121                                 self._widgetTree = gtk.glade.XML(path)
122                                 break
123                 else:
124                         self.display_error_message("Cannot find doneit.glade")
125                         gtk.main_quit()
126                 try:
127                         os.makedirs(self._user_data)
128                 except OSError, e:
129                         if e.errno != 17:
130                                 raise
131
132                 self._clipboard = gtk.clipboard_get()
133                 self.__window = self._widgetTree.get_widget("mainWindow")
134                 self.__errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
135                 self._prefsDialog = PreferencesDialog(self._widgetTree)
136
137                 self._app = None
138                 self._isFullScreen = False
139                 if hildon is not None:
140                         self._app = hildon.Program()
141                         self.__window = hildon.Window()
142                         self._widgetTree.get_widget("mainLayout").reparent(self.__window)
143                         self._app.add_window(self.__window)
144                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
145                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
146
147                         gtkMenu = self._widgetTree.get_widget("mainMenubar")
148                         menu = gtk.Menu()
149                         for child in gtkMenu.get_children():
150                                 child.reparent(menu)
151                         self.__window.set_menu(menu)
152                         gtkMenu.destroy()
153
154                         self.__window.connect("key-press-event", self._on_key_press)
155                         self.__window.connect("window-state-event", self._on_window_state_change)
156                 else:
157                         pass # warnings.warn("No Hildon", UserWarning, 2)
158
159                 callbackMapping = {
160                         "on_doneit_quit": self._on_close,
161                         "on_about": self._on_about_activate,
162                 }
163                 self._widgetTree.signal_autoconnect(callbackMapping)
164
165                 if self.__window:
166                         if hildon is None:
167                                 self.__window.set_title("%s" % self.__pretty_app_name__)
168                         self.__window.connect("destroy", self._on_close)
169                         self.__window.show_all()
170
171                 backgroundSetup = threading.Thread(target=self._idle_setup)
172                 backgroundSetup.setDaemon(True)
173                 backgroundSetup.start()
174
175         def _idle_setup(self):
176                 # Barebones UI handlers
177                 import null_view
178                 with gtk_toolbox.gtk_lock():
179                         nullView = null_view.GtkNull(self._widgetTree)
180                         self._todoUIs[nullView.name()] = nullView
181                         self._todoUI = nullView
182                         self._todoUI.enable()
183                         self._fallbackUIName = nullView.name()
184
185                 # Setup maemo specifics
186                 try:
187                         import osso
188                 except ImportError:
189                         osso = None
190                 self._osso = None
191                 if osso is not None:
192                         self._osso = osso.Context(DoneIt.__app_name__, DoneIt.__version__, False)
193                         device = osso.DeviceState(self._osso)
194                         device.set_device_state_callback(self._on_device_state_change, 0)
195                 else:
196                         pass # warnings.warn("No OSSO", UserWarning, 2)
197
198                 try:
199                         import conic
200                 except ImportError:
201                         conic = None
202                 self._connection = None
203                 if conic is not None:
204                         self._connection = conic.Connection()
205                         self._connection.connect("connection-event", self._on_connection_change, self.__app_magic__)
206                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
207                 else:
208                         pass # warnings.warn("No Internet Connectivity API ", UserWarning)
209
210                 # Setup costly backends
211                 import rtm_view
212                 with gtk_toolbox.gtk_lock():
213                         rtmView = rtm_view.RtmView(self._widgetTree, self.__errorDisplay)
214                 self._todoUIs[rtmView.name()] = rtmView
215                 self._defaultUIName = rtmView.name()
216
217                 config = ConfigParser.SafeConfigParser()
218                 config.read(self._user_settings)
219                 with gtk_toolbox.gtk_lock():
220                         self.load_settings(config)
221                         self._widgetTree.get_widget("connectMenuItem").connect("activate", lambda *args: self.switch_ui(self._defaultUIName))
222                         self._widgetTree.get_widget("preferencesMenuItem").connect("activate", self._on_prefs)
223
224                 self._initDone = True
225
226         def display_error_message(self, msg):
227                 """
228                 @note UI Thread
229                 """
230                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
231
232                 def close(dialog, response, editor):
233                         editor.about_dialog = None
234                         dialog.destroy()
235                 error_dialog.connect("response", close, self)
236                 error_dialog.run()
237
238         def load_settings(self, config):
239                 """
240                 @note UI Thread
241                 """
242                 for todoUI in self._todoUIs.itervalues():
243                         try:
244                                 todoUI.load_settings(config)
245                         except ConfigParser.NoSectionError, e:
246                                 warnings.warn(
247                                         "Settings file %s is missing section %s" % (
248                                                 self._user_settings,
249                                                 e.section,
250                                         ),
251                                         stacklevel=2
252                                 )
253
254                 try:
255                         activeUIName = config.get(self.__pretty_app_name__, "active")
256                 except ConfigParser.NoSectionError, e:
257                         activeUIName = ""
258                         warnings.warn(
259                                 "Settings file %s is missing section %s" % (
260                                         self._user_settings,
261                                         e.section,
262                                 ),
263                                 stacklevel=2
264                         )
265
266                 try:
267                         self.switch_ui(activeUIName)
268                 except KeyError, e:
269                         self.switch_ui(self._defaultUIName)
270
271         def save_settings(self, config):
272                 """
273                 @note Thread Agnostic
274                 """
275                 config.add_section(self.__pretty_app_name__)
276                 config.set(self.__pretty_app_name__, "active", self._todoUI.name())
277
278                 for todoUI in self._todoUIs.itervalues():
279                         todoUI.save_settings(config)
280
281         def get_uis(self):
282                 return (ui for ui in self._todoUIs.iteritems())
283
284         def get_default_ui(self):
285                 return self._defaultUIName
286
287         def switch_ui(self, uiName):
288                 """
289                 @note UI Thread
290                 """
291                 newActiveUI = self._todoUIs[uiName]
292                 try:
293                         newActiveUI.login()
294                 except RuntimeError:
295                         return # User cancelled the operation
296
297                 self._todoUI.disable()
298                 self._todoUI = newActiveUI
299                 self._todoUI.enable()
300
301                 if uiName != self._fallbackUIName:
302                         self._defaultUIName = uiName
303
304         def _save_settings(self):
305                 """
306                 @note Thread Agnostic
307                 """
308                 config = ConfigParser.SafeConfigParser()
309                 self.save_settings(config)
310                 with open(self._user_settings, "wb") as configFile:
311                         config.write(configFile)
312
313         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
314                 """
315                 For system_inactivity, we have no background tasks to pause
316
317                 @note Hildon specific
318                 """
319                 if memory_low:
320                         gc.collect()
321
322                 if save_unsaved_data or shutdown:
323                         self._save_settings()
324
325         def _on_connection_change(self, connection, event, magicIdentifier):
326                 """
327                 @note Hildon specific
328                 """
329                 import conic
330
331                 status = event.get_status()
332                 error = event.get_error()
333                 iap_id = event.get_iap_id()
334                 bearer = event.get_bearer_type()
335
336                 if status == conic.STATUS_CONNECTED:
337                         self._deviceIsOnline = True
338                         if self._initDone:
339                                 self.switch_ui(self._defaultUIName)
340                 elif status == conic.STATUS_DISCONNECTED:
341                         self._deviceIsOnline = False
342                         if self._initDone:
343                                 self.switch_ui(self._fallbackUIName)
344
345         def _on_window_state_change(self, widget, event, *args):
346                 """
347                 @note Hildon specific
348                 """
349                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
350                         self._isFullScreen = True
351                 else:
352                         self._isFullScreen = False
353
354         def _on_close(self, *args, **kwds):
355                 try:
356                         if self._osso is not None:
357                                 self._osso.close()
358
359                         if self._initDone:
360                                 self._save_settings()
361                 finally:
362                         gtk.main_quit()
363
364         def _on_key_press(self, widget, event, *args):
365                 """
366                 @note Hildon specific
367                 """
368                 if event.keyval == gtk.keysyms.F6:
369                         if self._isFullScreen:
370                                 self.__window.unfullscreen()
371                         else:
372                                 self.__window.fullscreen()
373
374         def _on_logout(self, *args):
375                 if not self._initDone:
376                         return
377
378                 self._todoUI.logout()
379                 self.switch_ui(self._fallbackUIName)
380
381         def _on_prefs(self, *args):
382                 if not self._initDone:
383                         return
384
385                 self._prefsDialog.enable()
386                 try:
387                         self._prefsDialog.run(self)
388                 finally:
389                         self._prefsDialog.disable()
390
391         def _on_about_activate(self, *args):
392                 dlg = gtk.AboutDialog()
393                 dlg.set_name(self.__pretty_app_name__)
394                 dlg.set_version(self.__version__)
395                 dlg.set_copyright("Copyright 2008 - LGPL")
396                 dlg.set_comments("")
397                 dlg.set_website("http://doneit.garage.maemo.org")
398                 dlg.set_authors(["Ed Page"])
399                 dlg.run()
400                 dlg.destroy()
401
402
403 def run_doctest():
404         import doctest
405
406         failureCount, testCount = doctest.testmod()
407         if not failureCount:
408                 print "Tests Successful"
409                 sys.exit(0)
410         else:
411                 sys.exit(1)
412
413
414 def run_doneit():
415         gtk.gdk.threads_init()
416
417         if hildon is not None:
418                 gtk.set_application_name(DoneIt.__pretty_app_name__)
419         handle = DoneIt()
420         gtk.main()
421
422
423 class DummyOptions(object):
424
425         def __init__(self):
426                 self.test = False
427
428
429 if __name__ == "__main__":
430         if len(sys.argv) > 1:
431                 try:
432                         import optparse
433                 except ImportError:
434                         optparse = None
435
436                 if optparse is not None:
437                         parser = optparse.OptionParser()
438                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
439                         (commandOptions, commandArgs) = parser.parse_args()
440         else:
441                 commandOptions = DummyOptions()
442                 commandArgs = []
443
444         if commandOptions.test:
445                 run_doctest()
446         else:
447                 run_doneit()