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