More wild brainstorming
[doneit] / src / doneit_glade.py
1 #!/usr/bin/python
2
3 """
4 @todo Alt Views (Besides task list)
5         Map
6                 Using new api widgets people are developing)
7                 Integrate GPS w/ fallback to default location
8                 Use locations for mapping
9         Agenda view
10                 Grid of days
11                 Using an adjustable version of the following RTM quiery 'dueBefore:Today OR dueWithin:"1 week of today" OR (priority:3 AND dueWithin"2 weeks of today") OR (priority:2 AND dueWithin"3 weeks of today") OR (priority:1 AND dueWithin"4 weeks of today")')
12         Quick search (OR within a property type, and between property types)
13                 Drop down for multi selecting priority
14                 Drop down for multi selecting tags
15                 Drop down for multi selecting locations
16                 Calendar selector for choosing due date range
17 """
18
19 from __future__ import with_statement
20
21
22 import sys
23 import gc
24 import os
25 import threading
26 import warnings
27 import ConfigParser
28
29 import gtk
30 import gtk.glade
31
32 try:
33         import hildon
34 except ImportError:
35         hildon = None
36
37 import gtk_toolbox
38
39
40 class DoneIt(object):
41
42         __pretty_app_name__ = "DoneIt"
43         __app_name__ = "doneit"
44         __version__ = "0.3.0"
45         __app_magic__ = 0xdeadbeef
46
47         _glade_files = [
48                 '/usr/lib/doneit/doneit.glade',
49                 os.path.join(os.path.dirname(__file__), "doneit.glade"),
50                 os.path.join(os.path.dirname(__file__), "../lib/doneit.glade"),
51         ]
52
53         _user_data = os.path.expanduser("~/.%s/" % __app_name__)
54         _user_settings = "%s/settings.ini" % _user_data
55
56         def __init__(self):
57                 self._todoUIs = {}
58                 self._todoUI = None
59                 self._osso = None
60                 self._deviceIsOnline = True
61                 self._connection = None
62                 self._fallbackUIName = ""
63                 self._defaultUIName = ""
64
65                 for path in self._glade_files:
66                         if os.path.isfile(path):
67                                 self._widgetTree = gtk.glade.XML(path)
68                                 break
69                 else:
70                         self.display_error_message("Cannot find doneit.glade")
71                         gtk.main_quit()
72                 try:
73                         os.makedirs(self._user_data)
74                 except OSError, e:
75                         if e.errno != 17:
76                                 raise
77
78                 self._clipboard = gtk.clipboard_get()
79                 self.__window = self._widgetTree.get_widget("mainWindow")
80                 self.__errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
81
82                 self._app = None
83                 self._isFullScreen = False
84                 if hildon is not None:
85                         self._app = hildon.Program()
86                         self.__window = hildon.Window()
87                         self._widgetTree.get_widget("mainLayout").reparent(self.__window)
88                         self._app.add_window(self.__window)
89                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
90                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
91                         self._widgetTree.get_widget("projectsCombo").get_child().set_property('hildon-input-mode', (1 << 4))
92
93                         gtkMenu = self._widgetTree.get_widget("mainMenubar")
94                         menu = gtk.Menu()
95                         for child in gtkMenu.get_children():
96                                 child.reparent(menu)
97                         self.__window.set_menu(menu)
98                         gtkMenu.destroy()
99
100                         self.__window.connect("key-press-event", self._on_key_press)
101                         self.__window.connect("window-state-event", self._on_window_state_change)
102                 else:
103                         pass # warnings.warn("No Hildon", UserWarning, 2)
104
105                 callbackMapping = {
106                         "on_doneit_quit": self._on_close,
107                         "on_paste": self._on_paste,
108                         "on_about": self._on_about_activate,
109                 }
110                 self._widgetTree.signal_autoconnect(callbackMapping)
111
112                 if self.__window:
113                         if hildon is None:
114                                 self.__window.set_title("%s" % self.__pretty_app_name__)
115                         self.__window.connect("destroy", self._on_close)
116                         self.__window.show_all()
117
118                 backgroundSetup = threading.Thread(target=self._idle_setup)
119                 backgroundSetup.setDaemon(True)
120                 backgroundSetup.start()
121
122         def _idle_setup(self):
123                 # Barebones UI handlers
124                 import null_view
125                 with gtk_toolbox.gtk_lock():
126                         nullView = null_view.GtkNull(self._widgetTree)
127                         self._todoUIs[nullView.name()] = nullView
128                         self._todoUI = nullView
129                         self._todoUI.enable()
130                         self._fallbackUIName = nullView.name()
131
132                 # Setup maemo specifics
133                 try:
134                         import osso
135                 except ImportError:
136                         osso = None
137                 self._osso = None
138                 if osso is not None:
139                         self._osso = osso.Context(DoneIt.__app_name__, DoneIt.__version__, False)
140                         device = osso.DeviceState(self._osso)
141                         device.set_device_state_callback(self._on_device_state_change, 0)
142                 else:
143                         pass # warnings.warn("No OSSO", UserWarning, 2)
144
145                 try:
146                         import conic
147                 except ImportError:
148                         conic = None
149                 self._connection = None
150                 if conic is not None:
151                         self._connection = conic.Connection()
152                         self._connection.connect("connection-event", self._on_connection_change, self.__app_magic__)
153                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
154                 else:
155                         pass # warnings.warn("No Internet Connectivity API ", UserWarning)
156
157                 # Setup costly backends
158                 import rtm_view
159                 with gtk_toolbox.gtk_lock():
160                         rtmView = rtm_view.GtkRtMilk(self._widgetTree)
161                 self._todoUIs[rtmView.name()] = rtmView
162                 self._defaultUIName = rtmView.name()
163
164                 config = ConfigParser.SafeConfigParser()
165                 config.read(self._user_settings)
166                 with gtk_toolbox.gtk_lock():
167                         self.load_settings(config)
168
169         def display_error_message(self, msg):
170                 """
171                 @note UI Thread
172                 """
173                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
174
175                 def close(dialog, response, editor):
176                         editor.about_dialog = None
177                         dialog.destroy()
178                 error_dialog.connect("response", close, self)
179                 error_dialog.run()
180
181         def load_settings(self, config):
182                 """
183                 @note UI Thread
184                 """
185                 for todoUI in self._todoUIs.itervalues():
186                         try:
187                                 todoUI.load_settings(config)
188                         except ConfigParser.NoSectionError, e:
189                                 warnings.warn(
190                                         "Settings file %s is missing section %s" % (
191                                                 self._user_settings,
192                                                 e.section,
193                                         ),
194                                         stacklevel=2
195                                 )
196
197                 try:
198                         activeUIName = config.get(self.__pretty_app_name__, "active")
199                 except ConfigParser.NoSectionError, e:
200                         activeUIName = ""
201                         warnings.warn(
202                                 "Settings file %s is missing section %s" % (
203                                         self._user_settings,
204                                         e.section,
205                                 ),
206                                 stacklevel=2
207                         )
208
209                 try:
210                         self._switch_ui(activeUIName)
211                 except KeyError, e:
212                         self._switch_ui(self._defaultUIName)
213
214         def save_settings(self, config):
215                 """
216                 @note Thread Agnostic
217                 """
218                 config.add_section(self.__pretty_app_name__)
219                 config.set(self.__pretty_app_name__, "active", self._todoUI.name())
220
221                 for todoUI in self._todoUIs.itervalues():
222                         todoUI.save_settings(config)
223
224         def _switch_ui(self, uiName):
225                 """
226                 @note UI Thread
227                 """
228                 newActiveUI = self._todoUIs[uiName]
229                 try:
230                         newActiveUI.login()
231                 except RuntimeError:
232                         return # User cancelled the operation
233
234                 self._todoUI.disable()
235                 self._todoUI = newActiveUI
236                 self._todoUI.enable()
237
238                 if uiName != self._fallbackUIName:
239                         self._defaultUIName = uiName
240
241         def _save_settings(self):
242                 """
243                 @note Thread Agnostic
244                 """
245                 config = ConfigParser.SafeConfigParser()
246                 self.save_settings(config)
247                 with open(self._user_settings, "wb") as configFile:
248                         config.write(configFile)
249
250         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
251                 """
252                 For system_inactivity, we have no background tasks to pause
253
254                 @note Hildon specific
255                 """
256                 if memory_low:
257                         gc.collect()
258
259                 if save_unsaved_data or shutdown:
260                         self._save_settings()
261
262         def _on_connection_change(self, connection, event, magicIdentifier):
263                 """
264                 @note Hildon specific
265                 """
266                 import conic
267
268                 status = event.get_status()
269                 error = event.get_error()
270                 iap_id = event.get_iap_id()
271                 bearer = event.get_bearer_type()
272
273                 if status == conic.STATUS_CONNECTED:
274                         self._deviceIsOnline = True
275                         self._switch_ui(self._defaultUIName)
276                 elif status == conic.STATUS_DISCONNECTED:
277                         self._deviceIsOnline = False
278                         self._switch_ui(self._fallbackUIName)
279
280         def _on_window_state_change(self, widget, event, *args):
281                 """
282                 @note Hildon specific
283                 """
284                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
285                         self._isFullScreen = True
286                 else:
287                         self._isFullScreen = False
288
289         def _on_close(self, *args, **kwds):
290                 try:
291                         if self._osso is not None:
292                                 self._osso.close()
293
294                         self._save_settings()
295                 finally:
296                         gtk.main_quit()
297
298         def _on_paste(self, *args):
299                 pass
300
301         def _on_key_press(self, widget, event, *args):
302                 """
303                 @note Hildon specific
304                 """
305                 if event.keyval == gtk.keysyms.F6:
306                         if self._isFullScreen:
307                                 self.__window.unfullscreen()
308                         else:
309                                 self.__window.fullscreen()
310
311         def _on_logout(self, *args):
312                 self._todoUI.logout()
313                 self._switch_ui(self._fallbackUIName)
314
315         def _on_about_activate(self, *args):
316                 dlg = gtk.AboutDialog()
317                 dlg.set_name(self.__pretty_app_name__)
318                 dlg.set_version(self.__version__)
319                 dlg.set_copyright("Copyright 2008 - LGPL")
320                 dlg.set_comments("")
321                 dlg.set_website("")
322                 dlg.set_authors([""])
323                 dlg.run()
324                 dlg.destroy()
325
326
327 def run_doctest():
328         import doctest
329
330         failureCount, testCount = doctest.testmod()
331         if not failureCount:
332                 print "Tests Successful"
333                 sys.exit(0)
334         else:
335                 sys.exit(1)
336
337
338 def run_doneit():
339         gtk.gdk.threads_init()
340
341         if hildon is not None:
342                 gtk.set_application_name(DoneIt.__pretty_app_name__)
343         handle = DoneIt()
344         gtk.main()
345
346
347 class DummyOptions(object):
348
349         def __init__(self):
350                 self.test = False
351
352
353 if __name__ == "__main__":
354         if len(sys.argv) > 1:
355                 try:
356                         import optparse
357                 except ImportError:
358                         optparse = None
359
360                 if optparse is not None:
361                         parser = optparse.OptionParser()
362                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
363                         (commandOptions, commandArgs) = parser.parse_args()
364         else:
365                 commandOptions = DummyOptions()
366                 commandArgs = []
367
368         if commandOptions.test:
369                 run_doctest()
370         else:
371                 run_doneit()