Adding the file backend as a choice in the UI
[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
216                 import file_view
217                 defaultStoragePath = "%s/data.txt" % self._user_data
218                 with gtk_toolbox.gtk_lock():
219                         fileView = file_view.FileView(self._widgetTree, self.__errorDisplay, defaultStoragePath)
220                 self._todoUIs[fileView.name()] = fileView
221
222                 self._defaultUIName = fileView.name()
223
224                 config = ConfigParser.SafeConfigParser()
225                 config.read(self._user_settings)
226                 with gtk_toolbox.gtk_lock():
227                         self.load_settings(config)
228                         self._widgetTree.get_widget("connectMenuItem").connect("activate", lambda *args: self.switch_ui(self._defaultUIName))
229                         self._widgetTree.get_widget("preferencesMenuItem").connect("activate", self._on_prefs)
230
231                 self._initDone = True
232
233         def display_error_message(self, msg):
234                 """
235                 @note UI Thread
236                 """
237                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
238
239                 def close(dialog, response, editor):
240                         editor.about_dialog = None
241                         dialog.destroy()
242                 error_dialog.connect("response", close, self)
243                 error_dialog.run()
244
245         def load_settings(self, config):
246                 """
247                 @note UI Thread
248                 """
249                 for todoUI in self._todoUIs.itervalues():
250                         try:
251                                 todoUI.load_settings(config)
252                         except ConfigParser.NoSectionError, e:
253                                 warnings.warn(
254                                         "Settings file %s is missing section %s" % (
255                                                 self._user_settings,
256                                                 e.section,
257                                         ),
258                                         stacklevel=2
259                                 )
260
261                 try:
262                         activeUIName = config.get(self.__pretty_app_name__, "active")
263                 except ConfigParser.NoSectionError, e:
264                         activeUIName = ""
265                         warnings.warn(
266                                 "Settings file %s is missing section %s" % (
267                                         self._user_settings,
268                                         e.section,
269                                 ),
270                                 stacklevel=2
271                         )
272
273                 try:
274                         self.switch_ui(activeUIName)
275                 except KeyError, e:
276                         self.switch_ui(self._defaultUIName)
277
278         def save_settings(self, config):
279                 """
280                 @note Thread Agnostic
281                 """
282                 config.add_section(self.__pretty_app_name__)
283                 config.set(self.__pretty_app_name__, "active", self._todoUI.name())
284
285                 for todoUI in self._todoUIs.itervalues():
286                         todoUI.save_settings(config)
287
288         def get_uis(self):
289                 return (ui for ui in self._todoUIs.iteritems())
290
291         def get_default_ui(self):
292                 return self._defaultUIName
293
294         def switch_ui(self, uiName):
295                 """
296                 @note UI Thread
297                 """
298                 newActiveUI = self._todoUIs[uiName]
299                 try:
300                         newActiveUI.login()
301                 except RuntimeError:
302                         return # User cancelled the operation
303
304                 self._todoUI.disable()
305                 self._todoUI = newActiveUI
306                 self._todoUI.enable()
307
308                 if uiName != self._fallbackUIName:
309                         self._defaultUIName = uiName
310
311         def _save_settings(self):
312                 """
313                 @note Thread Agnostic
314                 """
315                 config = ConfigParser.SafeConfigParser()
316                 self.save_settings(config)
317                 with open(self._user_settings, "wb") as configFile:
318                         config.write(configFile)
319
320         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
321                 """
322                 For system_inactivity, we have no background tasks to pause
323
324                 @note Hildon specific
325                 """
326                 if memory_low:
327                         gc.collect()
328
329                 if save_unsaved_data or shutdown:
330                         self._save_settings()
331
332         def _on_connection_change(self, connection, event, magicIdentifier):
333                 """
334                 @note Hildon specific
335                 """
336                 import conic
337
338                 status = event.get_status()
339                 error = event.get_error()
340                 iap_id = event.get_iap_id()
341                 bearer = event.get_bearer_type()
342
343                 if status == conic.STATUS_CONNECTED:
344                         self._deviceIsOnline = True
345                         if self._initDone:
346                                 self.switch_ui(self._defaultUIName)
347                 elif status == conic.STATUS_DISCONNECTED:
348                         self._deviceIsOnline = False
349                         if self._initDone:
350                                 self.switch_ui(self._fallbackUIName)
351
352         def _on_window_state_change(self, widget, event, *args):
353                 """
354                 @note Hildon specific
355                 """
356                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
357                         self._isFullScreen = True
358                 else:
359                         self._isFullScreen = False
360
361         def _on_close(self, *args, **kwds):
362                 try:
363                         if self._osso is not None:
364                                 self._osso.close()
365
366                         if self._initDone:
367                                 self._save_settings()
368                 finally:
369                         gtk.main_quit()
370
371         def _on_key_press(self, widget, event, *args):
372                 """
373                 @note Hildon specific
374                 """
375                 if event.keyval == gtk.keysyms.F6:
376                         if self._isFullScreen:
377                                 self.__window.unfullscreen()
378                         else:
379                                 self.__window.fullscreen()
380
381         def _on_logout(self, *args):
382                 if not self._initDone:
383                         return
384
385                 self._todoUI.logout()
386                 self.switch_ui(self._fallbackUIName)
387
388         def _on_prefs(self, *args):
389                 if not self._initDone:
390                         return
391
392                 self._prefsDialog.enable()
393                 try:
394                         self._prefsDialog.run(self)
395                 finally:
396                         self._prefsDialog.disable()
397
398         def _on_about_activate(self, *args):
399                 dlg = gtk.AboutDialog()
400                 dlg.set_name(self.__pretty_app_name__)
401                 dlg.set_version(self.__version__)
402                 dlg.set_copyright("Copyright 2008 - LGPL")
403                 dlg.set_comments("")
404                 dlg.set_website("http://doneit.garage.maemo.org")
405                 dlg.set_authors(["Ed Page"])
406                 dlg.run()
407                 dlg.destroy()
408
409
410 def run_doctest():
411         import doctest
412
413         failureCount, testCount = doctest.testmod()
414         if not failureCount:
415                 print "Tests Successful"
416                 sys.exit(0)
417         else:
418                 sys.exit(1)
419
420
421 def run_doneit():
422         gtk.gdk.threads_init()
423
424         if hildon is not None:
425                 gtk.set_application_name(DoneIt.__pretty_app_name__)
426         handle = DoneIt()
427         gtk.main()
428
429
430 class DummyOptions(object):
431
432         def __init__(self):
433                 self.test = False
434
435
436 if __name__ == "__main__":
437         if len(sys.argv) > 1:
438                 try:
439                         import optparse
440                 except ImportError:
441                         optparse = None
442
443                 if optparse is not None:
444                         parser = optparse.OptionParser()
445                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
446                         (commandOptions, commandArgs) = parser.parse_args()
447         else:
448                 commandOptions = DummyOptions()
449                 commandArgs = []
450
451         if commandOptions.test:
452                 run_doctest()
453         else:
454                 run_doneit()