Reorging the code. Don't worry, no layoffs
[doneit] / src / common_view.py
1 """
2 @todo Add an agenda view to the task list
3         Tree of days, with each successive 7 days dropping the visibility of further lower priority items
4 @todo Add a map view
5         Using new api widgets people are developing)
6         Integrate GPS w/ fallback to default location
7         Use locations for mapping
8 @todo Add a quick search (OR within a property type, and between property types) view
9         Drop down for multi selecting priority
10         Drop down for multi selecting tags
11         Drop down for multi selecting locations
12         Calendar selector for choosing due date range
13 @todo Remove blocking operations from UI thread
14 """
15
16 import webbrowser
17 import datetime
18
19 import gobject
20 import gtk
21
22 import coroutines
23 import toolbox
24 import gtk_toolbox
25
26
27 def project_sort_by_type(projects):
28         sortedProjects = list(projects)
29         def advanced_key(proj):
30                 if proj["name"] == "Inbox":
31                         type = 0
32                 elif proj["name"] == "Sent":
33                         type = 1
34                 elif not proj["isMeta"]:
35                         type = 2
36                 else:
37                         type = 3
38                 return type, proj["name"]
39         sortedProjects.sort(key=advanced_key)
40         return sortedProjects
41
42
43 def item_sort_by_priority_then_date(items):
44         sortedTasks = list(items)
45         sortedTasks.sort(
46                 key = lambda taskDetails: (
47                         taskDetails["priority"].get_nothrow(4),
48                         taskDetails["dueDate"].get_nothrow(datetime.datetime.max),
49                 ),
50         )
51         return sortedTasks
52
53
54 def item_sort_by_date_then_priority(items):
55         sortedTasks = list(items)
56         sortedTasks.sort(
57                 key = lambda taskDetails: (
58                         taskDetails["dueDate"].get_nothrow(datetime.datetime.max),
59                         taskDetails["priority"].get_nothrow(4),
60                 ),
61         )
62         return sortedTasks
63
64
65 def item_in_agenda(item):
66         taskDate = item["dueDate"].get_nothrow(datetime.datetime.max)
67         today = datetime.datetime.now()
68         delta = taskDate - today
69         dayDelta = abs(delta.days)
70
71         priority = item["priority"].get_nothrow(4)
72         weeksVisible = 5 - priority
73
74         isVisible = not bool(dayDelta / (weeksVisible * 7))
75         return isVisible
76
77
78 def item_sort_by_fuzzydate_then_priority(items):
79         sortedTasks = list(items)
80
81         def advanced_key(taskDetails):
82                 dueDate = taskDetails["dueDate"].get_nothrow(datetime.datetime.max)
83                 priority = taskDetails["priority"].get_nothrow(4)
84                 isNotSameYear = not toolbox.is_same_year(dueDate)
85                 isNotSameMonth = not toolbox.is_same_month(dueDate)
86                 isNotSameDay = not toolbox.is_same_day(dueDate)
87                 return isNotSameDay, isNotSameMonth, isNotSameYear, priority, dueDate
88
89         sortedTasks.sort(key=advanced_key)
90         return sortedTasks
91
92
93 class ItemListView(object):
94
95         ID_IDX = 0
96         COMPLETION_IDX = 1
97         NAME_IDX = 2
98         PRIORITY_IDX = 3
99         DUE_IDX = 4
100         FUZZY_IDX = 5
101         LINK_IDX = 6
102         NOTES_IDX = 7
103
104         def __init__(self, widgetTree, errorDisplay):
105                 self._errorDisplay = errorDisplay
106                 self._manager = None
107                 self._projId = None
108                 self._showCompleted = False
109                 self._showIncomplete = True
110
111                 self._editDialog = EditTaskDialog(widgetTree)
112                 self._notesDialog = NotesDialog(widgetTree)
113
114                 self._itemList = gtk.ListStore(
115                         gobject.TYPE_STRING, # id
116                         gobject.TYPE_BOOLEAN, # is complete
117                         gobject.TYPE_STRING, # name
118                         gobject.TYPE_STRING, # priority
119                         gobject.TYPE_STRING, # due
120                         gobject.TYPE_STRING, # fuzzy due
121                         gobject.TYPE_STRING, # Link
122                         gobject.TYPE_STRING, # Notes
123                 )
124                 self._completionColumn = gtk.TreeViewColumn('') # Complete?
125                 self._completionCell = gtk.CellRendererToggle()
126                 self._completionCell.set_property("activatable", True)
127                 self._completionCell.connect("toggled", self._on_completion_change)
128                 self._completionColumn.pack_start(self._completionCell, False)
129                 self._completionColumn.set_attributes(self._completionCell, active=self.COMPLETION_IDX)
130                 self._priorityColumn = gtk.TreeViewColumn('') # Priority
131                 self._priorityCell = gtk.CellRendererText()
132                 self._priorityColumn.pack_start(self._priorityCell, False)
133                 self._priorityColumn.set_attributes(self._priorityCell, text=self.PRIORITY_IDX)
134                 self._nameColumn = gtk.TreeViewColumn('Name')
135                 self._nameCell = gtk.CellRendererText()
136                 self._nameColumn.pack_start(self._nameCell, True)
137                 self._nameColumn.set_attributes(self._nameCell, text=self.NAME_IDX)
138                 self._nameColumn.set_expand(True)
139                 self._dueColumn = gtk.TreeViewColumn('Due')
140                 self._dueCell = gtk.CellRendererText()
141                 self._dueColumn.pack_start(self._nameCell, False)
142                 self._dueColumn.set_attributes(self._nameCell, text=self.FUZZY_IDX)
143                 self._linkColumn = gtk.TreeViewColumn('') # Link
144                 self._linkCell = gtk.CellRendererText()
145                 self._linkColumn.pack_start(self._nameCell, False)
146                 self._linkColumn.set_attributes(self._nameCell, text=self.LINK_IDX)
147                 self._notesColumn = gtk.TreeViewColumn('Notes') # Notes
148                 self._notesCell = gtk.CellRendererText()
149                 self._notesColumn.pack_start(self._nameCell, False)
150                 self._notesColumn.set_attributes(self._nameCell, text=self.NOTES_IDX)
151
152                 self._todoBox = widgetTree.get_widget("todoBox")
153                 self._todoItemScroll = gtk.ScrolledWindow()
154                 self._todoItemScroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
155                 self._todoItemTree = gtk.TreeView()
156                 self._todoItemTree.set_headers_visible(True)
157                 self._todoItemTree.set_rules_hint(True)
158                 self._todoItemTree.set_search_column(self.NAME_IDX)
159                 self._todoItemTree.set_enable_search(True)
160                 self._todoItemTree.append_column(self._completionColumn)
161                 self._todoItemTree.append_column(self._priorityColumn)
162                 self._todoItemTree.append_column(self._nameColumn)
163                 self._todoItemTree.append_column(self._dueColumn)
164                 self._todoItemTree.append_column(self._linkColumn)
165                 self._todoItemTree.append_column(self._notesColumn)
166                 self._todoItemTree.connect("row-activated", self._on_item_select)
167                 self._todoItemScroll.add(self._todoItemTree)
168
169         def enable(self, manager, projId):
170                 self._manager = manager
171                 self._projId = projId
172
173                 self._todoBox.pack_start(self._todoItemScroll)
174                 self._todoItemScroll.show_all()
175
176                 self._itemList.clear()
177                 try:
178                         self.reset_task_list(self._projId)
179                 except StandardError, e:
180                         self._errorDisplay.push_exception()
181
182         def disable(self):
183                 self._manager = None
184                 self._projId = None
185
186                 self._todoBox.remove(self._todoItemScroll)
187                 self._todoItemScroll.hide_all()
188
189                 self._itemList.clear()
190                 self._todoItemTree.set_model(None)
191
192         def reset_task_list(self, projId):
193                 self._projId = projId
194                 self._itemList.clear()
195                 self._populate_items()
196
197         def _populate_items(self):
198                 projId = self._projId
199                 rawTasks = self._manager.get_tasks_with_details(projId)
200                 filteredTasks = (
201                         taskDetails
202                         for taskDetails in rawTasks
203                                 if self._showCompleted and taskDetails["isCompleted"] or self._showIncomplete and not taskDetails["isCompleted"]
204                 )
205                 # filteredTasks = (taskDetails for taskDetails in filteredTasks if item_in_agenda(taskDetails))
206                 sortedTasks = item_sort_by_priority_then_date(filteredTasks)
207                 # sortedTasks = item_sort_by_date_then_priority(filteredTasks)
208                 # sortedTasks = item_sort_by_fuzzydate_then_priority(filteredTasks)
209                 for taskDetails in sortedTasks:
210                         id = taskDetails["id"]
211                         isCompleted = taskDetails["isCompleted"]
212                         name = toolbox.abbreviate(taskDetails["name"], 100)
213                         priority = str(taskDetails["priority"].get_nothrow(""))
214                         if taskDetails["dueDate"].is_good():
215                                 dueDate = taskDetails["dueDate"].get()
216                                 dueDescription = dueDate.strftime("%Y-%m-%d %H:%M:%S")
217                                 fuzzyDue = toolbox.to_fuzzy_date(dueDate)
218                         else:
219                                 dueDescription = ""
220                                 fuzzyDue = ""
221
222                         linkDisplay = taskDetails["url"]
223                         linkDisplay = toolbox.abbreviate_url(linkDisplay, 20, 10)
224
225                         notes = taskDetails["notes"]
226                         notesDisplay = "%d Notes" % len(notes) if notes else ""
227
228                         row = (id, isCompleted, name, priority, dueDescription, fuzzyDue, linkDisplay, notesDisplay)
229                         self._itemList.append(row)
230                 self._todoItemTree.set_model(self._itemList)
231
232         def _on_item_select(self, treeView, path, viewColumn):
233                 try:
234                         # @todo See if there is a way to use the new gtk_toolbox.ContextHandler
235                         taskId = self._itemList[path[0]][self.ID_IDX]
236
237                         if viewColumn is self._priorityColumn:
238                                 pass
239                         elif viewColumn is self._nameColumn:
240                                 self._editDialog.enable(self._manager)
241                                 try:
242                                         self._editDialog.request_task(self._manager, taskId)
243                                 finally:
244                                         self._editDialog.disable()
245                                 self.reset_task_list(self._projId)
246                         elif viewColumn is self._dueColumn:
247                                 due = self._manager.get_task_details(taskId)["dueDate"]
248                                 if due.is_good():
249                                         # @todo Pass to calendar the parent widget
250                                         calendar = gtk_toolbox.PopupCalendar(None, due.get(), "Due Date")
251                                         calendar.run()
252                         elif viewColumn is self._linkColumn:
253                                 webbrowser.open(self._manager.get_task_details(taskId)["url"])
254                         elif viewColumn is self._notesColumn:
255                                 self._notesDialog.enable()
256                                 try:
257                                         # @todo Need to pass in parent window
258                                         self._notesDialog.run(self._manager, taskId)
259                                 finally:
260                                         self._notesDialog.disable()
261                 except StandardError, e:
262                         self._errorDisplay.push_exception()
263
264         def _on_completion_change(self, cell, path):
265                 try:
266                         taskId = self._itemList[path[0]][self.ID_IDX]
267                         self._manager.complete_task(taskId)
268                         self.reset_task_list(self._projId)
269                 except StandardError, e:
270                         self._errorDisplay.push_exception()
271
272
273 class NotesDialog(object):
274
275         def __init__(self, widgetTree):
276                 self._dialog = widgetTree.get_widget("notesDialog")
277                 self._notesBox = widgetTree.get_widget("notes-notesBox")
278                 self._addButton = widgetTree.get_widget("notes-addButton")
279                 self._saveButton = widgetTree.get_widget("notes-saveButton")
280                 self._cancelButton = widgetTree.get_widget("notes-cancelButton")
281                 self._onAddId = None
282                 self._onSaveId = None
283                 self._onCancelId = None
284
285                 self._notes = []
286                 self._notesToDelete = []
287
288         def enable(self):
289                 self._dialog.set_default_size(800, 300)
290                 self._onAddId = self._addButton.connect("clicked", self._on_add_clicked)
291                 self._onSaveId = self._saveButton.connect("clicked", self._on_save_clicked)
292                 self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked)
293
294         def disable(self):
295                 self._addButton.disconnect(self._onAddId)
296                 self._saveButton.disconnect(self._onSaveId)
297                 self._cancelButton.disconnect(self._onCancelId)
298
299         def run(self, todoManager, taskId, parentWindow = None):
300                 if parentWindow is not None:
301                         self._dialog.set_transient_for(parentWindow)
302
303                 taskDetails = todoManager.get_task_details(taskId)
304
305                 self._dialog.set_default_response(gtk.RESPONSE_OK)
306                 for note in taskDetails["notes"].itervalues():
307                         noteBox, titleEntry, noteDeleteButton, noteEntry = self._append_notebox(note)
308                         noteDeleteButton.connect("clicked", self._on_delete_existing, note["id"], noteBox)
309
310                 try:
311                         response = self._dialog.run()
312                         if response != gtk.RESPONSE_OK:
313                                 raise RuntimeError("Edit Cancelled")
314                 finally:
315                         self._dialog.hide()
316
317                 for note in self._notes:
318                         noteId = note[0]
319                         noteTitle = note[2].get_text()
320                         noteBody = note[4].get_buffer().get_text()
321                         if noteId is None:
322                                 print "New note:", note
323                                 todoManager.add_note(taskId, noteTitle, noteBody)
324                         else:
325                                 # @todo Provide way to only update on change
326                                 print "Updating note:", note
327                                 todoManager.update_note(noteId, noteTitle, noteBody)
328
329                 for deletedNoteId in self._notesToDelete:
330                         print "Deleted note:", deletedNoteId
331                         todoManager.delete_note(noteId)
332
333         def _append_notebox(self, noteDetails = None):
334                 if noteDetails is None:
335                         noteDetails = {"id": None, "title": "", "body": ""}
336
337                 noteBox = gtk.VBox()
338
339                 titleBox = gtk.HBox()
340                 titleEntry = gtk.Entry()
341                 titleEntry.set_text(noteDetails["title"])
342                 titleBox.pack_start(titleEntry, True, True)
343                 noteDeleteButton = gtk.Button(stock=gtk.STOCK_DELETE)
344                 titleBox.pack_end(noteDeleteButton, False, False)
345                 noteBox.pack_start(titleBox, False, True)
346
347                 noteEntryScroll = gtk.ScrolledWindow()
348                 noteEntryScroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
349                 noteEntry = gtk.TextView()
350                 noteEntry.set_editable(True)
351                 noteEntry.set_wrap_mode(gtk.WRAP_WORD)
352                 noteEntry.get_buffer().set_text(noteDetails["body"])
353                 noteEntry.set_size_request(-1, 150)
354                 noteEntryScroll.add(noteEntry)
355                 noteBox.pack_start(noteEntryScroll, True, True)
356
357                 self._notesBox.pack_start(noteBox, True, True)
358                 noteBox.show_all()
359
360                 note = noteDetails["id"], noteBox, titleEntry, noteDeleteButton, noteEntry
361                 self._notes.append(note)
362                 return note[1:]
363
364         def _on_add_clicked(self, *args):
365                 noteBox, titleEntry, noteDeleteButton, noteEntry = self._append_notebox()
366                 noteDeleteButton.connect("clicked", self._on_delete_new, noteBox)
367
368         def _on_save_clicked(self, *args):
369                 self._dialog.response(gtk.RESPONSE_OK)
370
371         def _on_cancel_clicked(self, *args):
372                 self._dialog.response(gtk.RESPONSE_CANCEL)
373
374         def _on_delete_new(self, widget, noteBox):
375                 self._notesBox.remove(noteBox)
376                 self._notes = [note for note in self._notes if note[1] is not noteBox]
377
378         def _on_delete_existing(self, widget, noteId, noteBox):
379                 self._notesBox.remove(noteBox)
380                 self._notes = [note for note in self._notes if note[1] is not noteBox]
381                 self._notesToDelete.append(noteId)
382
383
384 class EditTaskDialog(object):
385
386         def __init__(self, widgetTree):
387                 self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
388
389                 self._dialog = widgetTree.get_widget("editTaskDialog")
390                 self._projectCombo = widgetTree.get_widget("edit-targetProjectCombo")
391                 self._taskName = widgetTree.get_widget("edit-taskNameEntry")
392                 self._pasteTaskNameButton = widgetTree.get_widget("edit-pasteTaskNameButton")
393                 self._priorityChoiceCombo = widgetTree.get_widget("edit-priorityChoiceCombo")
394                 self._dueDateDisplay = widgetTree.get_widget("edit-dueDateCalendar")
395                 self._clearDueDate = widgetTree.get_widget("edit-clearDueDate")
396
397                 self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
398                 self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
399
400                 self._onPasteTaskId = None
401                 self._onClearDueDateId = None
402                 self._onAddId = None
403                 self._onCancelId = None
404
405         def enable(self, todoManager):
406                 self._populate_projects(todoManager)
407
408                 self._onPasteTaskId = self._pasteTaskNameButton.connect("clicked", self._on_name_paste)
409                 self._onClearDueDateId = self._clearDueDate.connect("clicked", self._on_clear_duedate)
410                 self._onAddId = self._addButton.connect("clicked", self._on_add_clicked)
411                 self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked)
412
413         def disable(self):
414                 self._pasteTaskNameButton.disconnect(self._onPasteTaskId)
415                 self._clearDueDate.disconnect(self._onClearDueDateId)
416                 self._addButton.disconnect(self._onAddId)
417                 self._cancelButton.disconnect(self._onCancelId)
418
419                 self._projectsList.clear()
420                 self._projectCombo.set_model(None)
421
422         def request_task(self, todoManager, taskId, parentWindow = None):
423                 if parentWindow is not None:
424                         self._dialog.set_transient_for(parentWindow)
425
426                 taskDetails = todoManager.get_task_details(taskId)
427                 originalProjectId = taskDetails["projId"]
428                 originalProjectName = todoManager.get_project(originalProjectId)["name"]
429                 originalName = taskDetails["name"]
430                 originalPriority = str(taskDetails["priority"].get_nothrow(0))
431                 if taskDetails["dueDate"].is_good():
432                         originalDue = taskDetails["dueDate"].get()
433                 else:
434                         originalDue = None
435
436                 self._dialog.set_default_response(gtk.RESPONSE_OK)
437                 self._taskName.set_text(originalName)
438                 self._set_active_proj(originalProjectName)
439                 self._priorityChoiceCombo.set_active(int(originalPriority))
440                 if originalDue is not None:
441                         # Months are 0 indexed
442                         self._dueDateDisplay.select_month(originalDue.month - 1, originalDue.year)
443                         self._dueDateDisplay.select_day(originalDue.day)
444                 else:
445                         now = datetime.datetime.now()
446                         self._dueDateDisplay.select_month(now.month, now.year)
447                         self._dueDateDisplay.select_day(0)
448
449                 try:
450                         response = self._dialog.run()
451                         if response != gtk.RESPONSE_OK:
452                                 raise RuntimeError("Edit Cancelled")
453                 finally:
454                         self._dialog.hide()
455
456                 newProjectName = self._get_project(todoManager)
457                 newName = self._taskName.get_text()
458                 newPriority = self._get_priority()
459                 year, month, day = self._dueDateDisplay.get_date()
460                 if day != 0:
461                         # Months are 0 indexed
462                         date = datetime.date(year, month + 1, day)
463                         time = datetime.time()
464                         newDueDate = datetime.datetime.combine(date, time)
465                 else:
466                         newDueDate = None
467
468                 isProjDifferent = newProjectName != originalProjectName
469                 isNameDifferent = newName != originalName
470                 isPriorityDifferent = newPriority != originalPriority
471                 isDueDifferent = newDueDate != originalDue
472
473                 if isProjDifferent:
474                         newProjectId = todoManager.lookup_project(newProjectName)
475                         todoManager.set_project(taskId, newProjectId)
476                         print "PROJ CHANGE"
477                 if isNameDifferent:
478                         todoManager.set_name(taskId, newName)
479                         print "NAME CHANGE"
480                 if isPriorityDifferent:
481                         try:
482                                 priority = toolbox.Optional(int(newPriority))
483                         except ValueError:
484                                 priority = toolbox.Optional()
485                         todoManager.set_priority(taskId, priority)
486                         print "PRIO CHANGE"
487                 if isDueDifferent:
488                         if newDueDate:
489                                 due = toolbox.Optional(newDueDate)
490                         else:
491                                 due = toolbox.Optional()
492
493                         todoManager.set_duedate(taskId, due)
494                         print "DUE CHANGE"
495
496                 return {
497                         "projId": isProjDifferent,
498                         "name": isNameDifferent,
499                         "priority": isPriorityDifferent,
500                         "due": isDueDifferent,
501                 }
502
503         def _populate_projects(self, todoManager):
504                 for projectName in todoManager.get_projects():
505                         row = (projectName["name"], )
506                         self._projectsList.append(row)
507                 self._projectCombo.set_model(self._projectsList)
508                 cell = gtk.CellRendererText()
509                 self._projectCombo.pack_start(cell, True)
510                 self._projectCombo.add_attribute(cell, 'text', 0)
511                 self._projectCombo.set_active(0)
512
513         def _set_active_proj(self, projName):
514                 for i, row in enumerate(self._projectsList):
515                         if row[0] == projName:
516                                 self._projectCombo.set_active(i)
517                                 break
518                 else:
519                         raise ValueError("%s not in list" % projName)
520
521         def _get_project(self, todoManager):
522                 name = self._projectCombo.get_active_text()
523                 return name
524
525         def _get_priority(self):
526                 index = self._priorityChoiceCombo.get_active()
527                 assert index != -1
528                 if index < 1:
529                         return ""
530                 else:
531                         return str(index)
532
533         def _on_name_paste(self, *args):
534                 clipboard = gtk.clipboard_get()
535                 contents = clipboard.wait_for_text()
536                 if contents is not None:
537                         self._taskName.set_text(contents)
538
539         def _on_clear_duedate(self, *args):
540                 self._dueDateDisplay.select_day(0)
541
542         def _on_add_clicked(self, *args):
543                 self._dialog.response(gtk.RESPONSE_OK)
544
545         def _on_cancel_clicked(self, *args):
546                 self._dialog.response(gtk.RESPONSE_CANCEL)
547
548
549 class ProjectsDialog(object):
550
551         ID_IDX = 0
552         NAME_IDX = 1
553         VISIBILITY_IDX = 2
554
555         def __init__(self, widgetTree):
556                 self._manager = None
557
558                 self._dialog = widgetTree.get_widget("projectsDialog")
559                 self._projView = widgetTree.get_widget("proj-projectView")
560
561                 addSink = coroutines.CoSwitch(["add", "add-edit"])
562                 addSink.register_sink("add", coroutines.func_sink(self._on_add))
563                 addSink.register_sink("add-edit", coroutines.func_sink(self._on_add_edit))
564                 self._addView = gtk_toolbox.QuickAddView(widgetTree, gtk_toolbox.DummyErrorDisplay(), addSink, "proj")
565
566                 self._projList = gtk.ListStore(
567                         gobject.TYPE_STRING, # id
568                         gobject.TYPE_STRING, # name
569                         gobject.TYPE_BOOLEAN, # is visible
570                 )
571                 self._visibilityColumn = gtk.TreeViewColumn('') # Complete?
572                 self._visibilityCell = gtk.CellRendererToggle()
573                 self._visibilityCell.set_property("activatable", True)
574                 self._visibilityCell.connect("toggled", self._on_toggle_visibility)
575                 self._visibilityColumn.pack_start(self._visibilityCell, False)
576                 self._visibilityColumn.set_attributes(self._visibilityCell, active=self.VISIBILITY_IDX)
577                 self._nameColumn = gtk.TreeViewColumn('Name')
578                 self._nameCell = gtk.CellRendererText()
579                 self._nameColumn.pack_start(self._nameCell, True)
580                 self._nameColumn.set_attributes(self._nameCell, text=self.NAME_IDX)
581                 self._nameColumn.set_expand(True)
582
583                 self._projView.append_column(self._visibilityColumn)
584                 self._projView.append_column(self._nameColumn)
585                 self._projView.connect("row-activated", self._on_proj_select)
586
587         def enable(self, manager):
588                 self._manager = manager
589
590                 self._populate_projects()
591                 self._dialog.show_all()
592
593         def disable(self):
594                 self._dialog.hide_all()
595                 self._manager = None
596
597                 self._projList.clear()
598                 self._projView.set_model(None)
599
600         def _populate_projects(self):
601                 self._projList.clear()
602
603                 projects = self._manager.get_projects()
604                 for project in projects:
605                         projectId = project["id"]
606                         projectName = project["name"]
607                         isVisible = project["isVisible"]
608                         row = (projectId, projectName, isVisible)
609                         self._projList.append(row)
610                 self._projView.set_model(self._projList)
611
612         def _on_add(self, eventData):
613                 eventName, projectName, = eventData
614                 self._manager.add_project(projectName)
615                 self._populate_projects()
616
617         def _on_add_edit(self, eventData):
618                 self._on_add(eventData)
619
620         def _on_toggle_visibility(self, cell, path):
621                 listIndex = path[0]
622                 row = self._projList[listIndex]
623                 projId = row[self.ID_IDX]
624                 oldValue = row[self.VISIBILITY_IDX]
625                 newValue = not oldValue
626                 print oldValue, newValue
627                 self._manager.set_project_visibility(projId, newValue)
628                 row[self.VISIBILITY_IDX] = newValue
629
630         def _on_proj_select(self, *args):
631                 # @todo Implement project renaming
632                 pass
633
634
635 class CommonView(object):
636
637         def __init__(self, widgetTree, errorDisplay):
638                 """
639                 @note Thread agnostic
640                 """
641                 self._errorDisplay = errorDisplay
642                 self._manager = None
643
644                 self._editDialog = EditTaskDialog(widgetTree)
645                 self._projDialog = ProjectsDialog(widgetTree)
646                 self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
647                 self._projectsCombo = widgetTree.get_widget("projectsCombo")
648                 self._projectCell = gtk.CellRendererText()
649                 self._onListActivateId = 0
650
651                 self._projectMenuItem = widgetTree.get_widget("projectMenuItem")
652                 self._onProjectMenuItemActivated = 0
653
654                 self._itemView = ItemListView(widgetTree, self._errorDisplay)
655                 addSink = coroutines.CoSwitch(["add", "add-edit"])
656                 addSink.register_sink("add", coroutines.func_sink(self._on_add))
657                 addSink.register_sink("add-edit", coroutines.func_sink(self._on_add_edit))
658                 self._addView = gtk_toolbox.QuickAddView(widgetTree, self._errorDisplay, addSink, "add")
659
660         @staticmethod
661         def name():
662                 raise NotImplementedError
663
664         def load_settings(self, config):
665                 """
666                 @note Thread Agnostic
667                 """
668                 raise NotImplementedError
669
670         def save_settings(self, config):
671                 """
672                 @note Thread Agnostic
673                 """
674                 raise NotImplementedError
675
676         def login(self):
677                 """
678                 @note UI Thread
679                 """
680                 raise NotImplementedError
681
682         def logout(self):
683                 """
684                 @note Thread Agnostic
685                 """
686                 raise NotImplementedError
687
688         def enable(self):
689                 """
690                 @note UI Thread
691                 """
692                 self._projectsList.clear()
693                 self._populate_projects()
694                 cell = self._projectCell
695                 self._projectsCombo.pack_start(cell, True)
696                 self._projectsCombo.add_attribute(cell, 'text', 0)
697
698                 currentProject = self._get_project()
699                 projId = self._manager.lookup_project(currentProject)["id"]
700                 self._addView.enable(self._manager)
701                 self._itemView.enable(self._manager, projId)
702
703                 self._onListActivateId = self._projectsCombo.connect("changed", self._on_list_activate)
704                 self._onProjectMenuItemActivated = self._projectMenuItem.connect("activate", self._on_proj_activate)
705
706         def disable(self):
707                 """
708                 @note UI Thread
709                 """
710                 self._projectsCombo.disconnect(self._onListActivateId)
711                 self._projectMenuItem.disconnect(self._onProjectMenuItemActivated)
712                 self._onListActivateId = 0
713
714                 self._addView.disable()
715                 self._itemView.disable()
716
717                 self._projectsList.clear()
718                 self._projectsCombo.set_model(None)
719
720         def _populate_projects(self):
721                 projects = self._manager.get_projects()
722                 sortedProjects = project_sort_by_type(projects)
723                 for project in sortedProjects:
724                         projectName = project["name"]
725                         isVisible = project["isVisible"]
726                         row = (projectName, )
727                         if isVisible:
728                                 self._projectsList.append(row)
729                 self._projectsCombo.set_model(self._projectsList)
730                 self._projectsCombo.set_active(0)
731
732         def _reset_task_list(self):
733                 projectName = self._get_project()
734                 projId = self._manager.lookup_project(projectName)["id"]
735                 self._itemView.reset_task_list(projId)
736
737                 isMeta = self._manager.get_project(projId)["isMeta"]
738                 # @todo RTM handles this by defaulting to a specific list
739                 self._addView.set_addability(not isMeta)
740
741         def _get_project(self):
742                 currentProjectName = self._projectsCombo.get_active_text()
743                 return currentProjectName
744
745         def _on_list_activate(self, *args):
746                 try:
747                         self._reset_task_list()
748                 except StandardError, e:
749                         self._errorDisplay.push_exception()
750
751         def _on_add(self, eventData):
752                 eventName, taskName, = eventData
753                 projectName = self._get_project()
754                 projId = self._manager.lookup_project(projectName)["id"]
755
756                 taskId = self._manager.add_task(projId, taskName)
757
758                 self._itemView.reset_task_list(projId)
759
760         def _on_add_edit(self, eventData):
761                 eventName, taskName, = eventData
762                 projectName = self._get_project()
763                 projId = self._manager.lookup_project(projectName)["id"]
764
765                 taskId = self._manager.add_task(projId, taskName)
766
767                 self._editDialog.enable(self._manager)
768                 try:
769                         # @todo Need to pass in parent
770                         self._editDialog.request_task(self._manager, taskId)
771                 finally:
772                         self._editDialog.disable()
773                 self._itemView.reset_task_list(projId)
774
775         def _on_proj_activate(self, *args):
776                 # @todo Need to pass in parent
777                 self._projDialog.enable(self._manager)