Filing in what is needed for a todo
[doneit] / src / gtk_toolbox.py
1 #!/usr/bin/python
2
3 """
4 @todo Implement a custom label that encodes random info (Not set in stone, just throwing these out there)
5         Background color based on Location
6         Text intensity based on time estimate
7         Short/long version with long including tags colored specially
8 """
9
10 import sys
11 import traceback
12 import datetime
13 import contextlib
14 import warnings
15
16 import gobject
17 import gtk
18
19 import toolbox
20 import coroutines
21
22
23 @contextlib.contextmanager
24 def gtk_lock():
25         gtk.gdk.threads_enter()
26         try:
27                 yield
28         finally:
29                 gtk.gdk.threads_leave()
30
31
32 class ContextHandler(object):
33
34         HOLD_TIMEOUT = 1000
35         MOVE_THRESHHOLD = 10
36
37         def __init__(self, actionWidget, eventTarget = coroutines.null_sink()):
38                 self._actionWidget = actionWidget
39                 self._eventTarget = eventTarget
40
41                 self._actionPressId = None
42                 self._actionReleaseId = None
43                 self._motionNotifyId = None
44                 self._popupMenuId = None
45                 self._holdTimerId = None
46
47                 self._respondOnRelease = False
48                 self._startPosition = None
49
50         def enable(self):
51                 self._actionPressId = self._actionWidget.connect("button-press-event", self._on_press)
52                 self._actionReleaseId = self._actionWidget.connect("button-release-event", self._on_release)
53                 self._motionNotifyId = self._actionWidget.connect("motion-notify-event", self._on_motion)
54                 self._popupMenuId = self._actionWidget.connect("popup-menu", self._on_popup)
55
56         def disable(self):
57                 self._clear()
58                 self._actionWidget.disconnect(self._actionPressId)
59                 self._actionWidget.disconnect(self._actionReleaseId)
60                 self._actionWidget.disconnect(self._motionNotifyId)
61                 self._actionWidget.disconnect(self._popupMenuId)
62
63         def _respond(self, position):
64                 widgetPosition = 0, 0 # @todo Figure out how to get a widgets root position
65                 responsePosition = (
66                         widgetPosition[0] + position[0],
67                         widgetPosition[1] + position[1],
68                 )
69                 self._eventTarget.send((self._actionWidget, responsePosition))
70
71         def _clear(self):
72                 if self._holdTimerId is not None:
73                         gobject.source_remove(self._holdTimerId)
74                 self._respondOnRelease = False
75                 self._startPosition = None
76
77         def _is_cleared(self):
78                 return self._startPosition is None
79
80         def _on_press(self, widget, event):
81                 if not self._is_cleared():
82                         return
83
84                 self._startPosition = event.get_coords()
85                 if event.button == 1:
86                         # left click
87                         self._holdTimerId = gobject.timeout_add(self.HOLD_TIMEOUT, self._on_hold_timeout)
88                 else:
89                         # any other click
90                         self._respondOnRelease = True
91
92         def _on_release(self, widget, event):
93                 if self._is_cleared():
94                         return
95
96                 if self._respondOnRelease:
97                         position = self._startPosition
98                         self._clear()
99                         self._respond(position)
100                 else:
101                         self._clear()
102
103         def _on_hold_timeout(self):
104                 assert not self._is_cleared()
105                 gobject.source_remove(self._holdTimerId)
106                 self._holdTimerId = None
107
108                 position = self._startPosition
109                 self._clear()
110                 self._respond(position)
111
112         def _on_motion(self, widget, event):
113                 if self._is_cleared():
114                         return
115                 curPosition = event.get_coords()
116                 dx, dy = (
117                         curPosition[1] - self._startPosition[1],
118                         curPosition[1] - self._startPosition[1],
119                 )
120                 delta = (dx ** 2 + dy ** 2) ** (0.5)
121                 if self.MOVE_THRESHHOLD <= delta:
122                         self._clear()
123
124         def _on_popup(self, widget):
125                 self._clear()
126                 position = 0, 0
127                 self._respond(position)
128
129
130 class LoginWindow(object):
131
132         def __init__(self, widgetTree):
133                 """
134                 @note Thread agnostic
135                 """
136                 self._dialog = widgetTree.get_widget("loginDialog")
137                 self._parentWindow = widgetTree.get_widget("mainWindow")
138                 self._serviceCombo = widgetTree.get_widget("serviceCombo")
139                 self._usernameEntry = widgetTree.get_widget("usernameentry")
140                 self._passwordEntry = widgetTree.get_widget("passwordentry")
141
142                 self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING)
143                 self._serviceCombo.set_model(self._serviceList)
144                 cell = gtk.CellRendererText()
145                 self._serviceCombo.pack_start(cell, True)
146                 self._serviceCombo.add_attribute(cell, 'text', 1)
147                 self._serviceCombo.set_active(0)
148
149                 callbackMapping = {
150                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
151                         "on_loginclose_clicked": self._on_loginclose_clicked,
152                 }
153                 widgetTree.signal_autoconnect(callbackMapping)
154
155         def request_credentials(self, parentWindow = None):
156                 """
157                 @note UI Thread
158                 """
159                 if parentWindow is None:
160                         parentWindow = self._parentWindow
161
162                 self._serviceCombo.hide()
163                 self._serviceList.clear()
164
165                 try:
166                         self._dialog.set_transient_for(parentWindow)
167                         self._dialog.set_default_response(gtk.RESPONSE_OK)
168                         response = self._dialog.run()
169                         if response != gtk.RESPONSE_OK:
170                                 raise RuntimeError("Login Cancelled")
171
172                         username = self._usernameEntry.get_text()
173                         password = self._passwordEntry.get_text()
174                         self._passwordEntry.set_text("")
175                 finally:
176                         self._dialog.hide()
177
178                 return username, password
179
180         def request_credentials_from(self, services, parentWindow = None):
181                 """
182                 @note UI Thread
183                 """
184                 if parentWindow is None:
185                         parentWindow = self._parentWindow
186
187                 self._serviceList.clear()
188                 for serviceIdserviceName in services.iteritems():
189                         self._serviceList.append(serviceIdserviceName)
190                 self._serviceCombo.set_active(0)
191                 self._serviceCombo.show()
192
193                 try:
194                         self._dialog.set_transient_for(parentWindow)
195                         self._dialog.set_default_response(gtk.RESPONSE_OK)
196                         response = self._dialog.run()
197                         if response != gtk.RESPONSE_OK:
198                                 raise RuntimeError("Login Cancelled")
199
200                         username = self._usernameEntry.get_text()
201                         password = self._passwordEntry.get_text()
202                         self._passwordEntry.set_text("")
203                 finally:
204                         self._dialog.hide()
205
206                 itr = self._serviceCombo.get_active_iter()
207                 serviceId = int(self._serviceList.get_value(itr, 0))
208                 self._serviceList.clear()
209                 return serviceId, username, password
210
211         def _on_loginbutton_clicked(self, *args):
212                 self._dialog.response(gtk.RESPONSE_OK)
213
214         def _on_loginclose_clicked(self, *args):
215                 self._dialog.response(gtk.RESPONSE_CANCEL)
216
217
218 class ErrorDisplay(object):
219
220         def __init__(self, widgetTree):
221                 super(ErrorDisplay, self).__init__()
222                 self.__errorBox = widgetTree.get_widget("errorEventBox")
223                 self.__errorDescription = widgetTree.get_widget("errorDescription")
224                 self.__errorClose = widgetTree.get_widget("errorClose")
225                 self.__parentBox = self.__errorBox.get_parent()
226
227                 self.__errorBox.connect("button_release_event", self._on_close)
228
229                 self.__messages = []
230                 self.__parentBox.remove(self.__errorBox)
231
232         def push_message_with_lock(self, message):
233                 gtk.gdk.threads_enter()
234                 try:
235                         self.push_message(message)
236                 finally:
237                         gtk.gdk.threads_leave()
238
239         def push_message(self, message):
240                 if 0 < len(self.__messages):
241                         self.__messages.append(message)
242                 else:
243                         self.__show_message(message)
244
245         def push_exception(self, exception = None):
246                 if exception is None:
247                         userMessage = sys.exc_value.message
248                         warningMessage = traceback.format_exc()
249                 else:
250                         userMessage = exception.message
251                         warningMessage = exception
252                 self.push_message(userMessage)
253                 warnings.warn(warningMessage, stacklevel=3)
254
255         def pop_message(self):
256                 if 0 < len(self.__messages):
257                         self.__show_message(self.__messages[0])
258                         del self.__messages[0]
259                 else:
260                         self.__hide_message()
261
262         def _on_close(self, *args):
263                 self.pop_message()
264
265         def __show_message(self, message):
266                 self.__errorDescription.set_text(message)
267                 self.__parentBox.pack_start(self.__errorBox, False, False)
268                 self.__parentBox.reorder_child(self.__errorBox, 1)
269
270         def __hide_message(self):
271                 self.__errorDescription.set_text("")
272                 self.__parentBox.remove(self.__errorBox)
273
274
275 class DummyErrorDisplay(object):
276
277         def __init__(self):
278                 super(DummyErrorDisplay, self).__init__()
279
280                 self.__messages = []
281
282         def push_message_with_lock(self, message):
283                 self.push_message(message)
284
285         def push_message(self, message):
286                 if 0 < len(self.__messages):
287                         self.__messages.append(message)
288                 else:
289                         self.__show_message(message)
290
291         def push_exception(self, exception = None):
292                 if exception is None:
293                         warningMessage = traceback.format_exc()
294                 else:
295                         warningMessage = exception
296                 warnings.warn(warningMessage, stacklevel=3)
297
298         def pop_message(self):
299                 if 0 < len(self.__messages):
300                         self.__show_message(self.__messages[0])
301                         del self.__messages[0]
302
303         def __show_message(self, message):
304                 warnings.warn(message, stacklevel=2)
305
306
307 class MessageBox(gtk.MessageDialog):
308
309         def __init__(self, message):
310                 parent = None
311                 gtk.MessageDialog.__init__(
312                         self,
313                         parent,
314                         gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
315                         gtk.MESSAGE_ERROR,
316                         gtk.BUTTONS_OK,
317                         message,
318                 )
319                 self.set_default_response(gtk.RESPONSE_OK)
320                 self.connect('response', self._handle_clicked)
321
322         def _handle_clicked(self, *args):
323                 self.destroy()
324
325
326 class MessageBox2(gtk.MessageDialog):
327
328         def __init__(self, message):
329                 parent = None
330                 gtk.MessageDialog.__init__(
331                         self,
332                         parent,
333                         gtk.DIALOG_DESTROY_WITH_PARENT,
334                         gtk.MESSAGE_ERROR,
335                         gtk.BUTTONS_OK,
336                         message,
337                 )
338                 self.set_default_response(gtk.RESPONSE_OK)
339                 self.connect('response', self._handle_clicked)
340
341         def _handle_clicked(self, *args):
342                 self.destroy()
343
344
345 class PopupCalendar(object):
346
347         def __init__(self, parent, displayDate, title = ""):
348                 self._displayDate = displayDate
349
350                 self._calendar = gtk.Calendar()
351                 self._calendar.select_month(self._displayDate.month, self._displayDate.year)
352                 self._calendar.select_day(self._displayDate.day)
353                 self._calendar.set_display_options(
354                         gtk.CALENDAR_SHOW_HEADING |
355                         gtk.CALENDAR_SHOW_DAY_NAMES |
356                         gtk.CALENDAR_NO_MONTH_CHANGE |
357                         0
358                 )
359                 self._calendar.connect("day-selected", self._on_day_selected)
360
361                 self._popupWindow = gtk.Window()
362                 self._popupWindow.set_title(title)
363                 self._popupWindow.add(self._calendar)
364                 self._popupWindow.set_transient_for(parent)
365                 self._popupWindow.set_modal(True)
366                 self._popupWindow.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
367                 self._popupWindow.set_skip_pager_hint(True)
368                 self._popupWindow.set_skip_taskbar_hint(True)
369
370         def run(self):
371                 self._popupWindow.show_all()
372
373         def _on_day_selected(self, *args):
374                 try:
375                         self._calendar.select_month(self._displayDate.month, self._displayDate.year)
376                         self._calendar.select_day(self._displayDate.day)
377                 except StandardError, e:
378                         warnings.warn(e.message)
379
380
381 class QuickAddView(object):
382
383         def __init__(self, widgetTree, errorDisplay, signalSink, prefix):
384                 self._errorDisplay = errorDisplay
385                 self._manager = None
386                 self._signalSink = signalSink
387
388                 self._clipboard = gtk.clipboard_get()
389
390                 self._taskNameEntry = widgetTree.get_widget(prefix+"-nameEntry")
391                 self._addTaskButton = widgetTree.get_widget(prefix+"-addButton")
392                 self._pasteTaskNameButton = widgetTree.get_widget(prefix+"-pasteNameButton")
393                 self._clearTaskNameButton = widgetTree.get_widget(prefix+"-clearNameButton")
394                 self._onAddId = None
395                 self._onAddClickedId = None
396                 self._onAddReleasedId = None
397                 self._addToEditTimerId = None
398                 self._onClearId = None
399                 self._onPasteId = None
400
401         def enable(self, manager):
402                 self._manager = manager
403
404                 self._onAddId = self._addTaskButton.connect("clicked", self._on_add)
405                 self._onAddClickedId = self._addTaskButton.connect("pressed", self._on_add_pressed)
406                 self._onAddReleasedId = self._addTaskButton.connect("released", self._on_add_released)
407                 self._onPasteId = self._pasteTaskNameButton.connect("clicked", self._on_paste)
408                 self._onClearId = self._clearTaskNameButton.connect("clicked", self._on_clear)
409
410         def disable(self):
411                 self._manager = None
412
413                 self._addTaskButton.disconnect(self._onAddId)
414                 self._addTaskButton.disconnect(self._onAddClickedId)
415                 self._addTaskButton.disconnect(self._onAddReleasedId)
416                 self._pasteTaskNameButton.disconnect(self._onPasteId)
417                 self._clearTaskNameButton.disconnect(self._onClearId)
418
419         def set_addability(self, addability):
420                 self._addTaskButton.set_sensitive(addability)
421
422         def _on_add(self, *args):
423                 try:
424                         name = self._taskNameEntry.get_text()
425                         self._taskNameEntry.set_text("")
426
427                         self._signalSink.stage.send(("add", name))
428                 except StandardError, e:
429                         self._errorDisplay.push_exception()
430
431         def _on_add_edit(self, *args):
432                 try:
433                         name = self._taskNameEntry.get_text()
434                         self._taskNameEntry.set_text("")
435
436                         self._signalSink.stage.send(("add-edit", name))
437                 except StandardError, e:
438                         self._errorDisplay.push_exception()
439
440         def _on_add_pressed(self, widget):
441                 try:
442                         self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit)
443                 except StandardError, e:
444                         self._errorDisplay.push_exception()
445
446         def _on_add_released(self, widget):
447                 try:
448                         if self._addToEditTimerId is not None:
449                                 gobject.source_remove(self._addToEditTimerId)
450                         self._addToEditTimerId = None
451                 except StandardError, e:
452                         self._errorDisplay.push_exception()
453
454         def _on_paste(self, *args):
455                 try:
456                         entry = self._taskNameEntry.get_text()
457                         addedText = self._clipboard.wait_for_text()
458                         if addedText:
459                                 entry += addedText
460                         self._taskNameEntry.set_text(entry)
461                 except StandardError, e:
462                         self._errorDisplay.push_exception()
463
464         def _on_clear(self, *args):
465                 try:
466                         self._taskNameEntry.set_text("")
467                 except StandardError, e:
468                         self._errorDisplay.push_exception()
469
470
471 class NotesDialog(object):
472
473         def __init__(self, widgetTree):
474                 self._dialog = widgetTree.get_widget("notesDialog")
475                 self._notesBox = widgetTree.get_widget("notes-notesBox")
476                 self._addButton = widgetTree.get_widget("notes-addButton")
477                 self._saveButton = widgetTree.get_widget("notes-saveButton")
478                 self._cancelButton = widgetTree.get_widget("notes-cancelButton")
479                 self._onAddId = None
480                 self._onSaveId = None
481                 self._onCancelId = None
482
483                 self._notes = []
484                 self._notesToDelete = []
485
486         def enable(self):
487                 self._dialog.set_default_size(800, 300)
488                 self._onAddId = self._addButton.connect("clicked", self._on_add_clicked)
489                 self._onSaveId = self._saveButton.connect("clicked", self._on_save_clicked)
490                 self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked)
491
492         def disable(self):
493                 self._addButton.disconnect(self._onAddId)
494                 self._saveButton.disconnect(self._onSaveId)
495                 self._cancelButton.disconnect(self._onCancelId)
496
497         def run(self, todoManager, taskId, parentWindow = None):
498                 if parentWindow is not None:
499                         self._dialog.set_transient_for(parentWindow)
500
501                 taskDetails = todoManager.get_task_details(taskId)
502
503                 self._dialog.set_default_response(gtk.RESPONSE_OK)
504                 for note in taskDetails["notes"].itervalues():
505                         noteBox, titleEntry, noteDeleteButton, noteEntry = self._append_notebox(note)
506                         noteDeleteButton.connect("clicked", self._on_delete_existing, note["id"], noteBox)
507
508                 try:
509                         response = self._dialog.run()
510                         if response != gtk.RESPONSE_OK:
511                                 raise RuntimeError("Edit Cancelled")
512                 finally:
513                         self._dialog.hide()
514
515                 for note in self._notes:
516                         noteId = note[0]
517                         noteTitle = note[2].get_text()
518                         noteBody = note[4].get_buffer().get_text()
519                         if noteId is None:
520                                 print "New note:", note
521                                 todoManager.add_note(taskId, noteTitle, noteBody)
522                         else:
523                                 # @todo Provide way to only update on change
524                                 print "Updating note:", note
525                                 todoManager.update_note(noteId, noteTitle, noteBody)
526
527                 for deletedNoteId in self._notesToDelete:
528                         print "Deleted note:", deletedNoteId
529                         todoManager.delete_note(noteId)
530
531         def _append_notebox(self, noteDetails = None):
532                 if noteDetails is None:
533                         noteDetails = {"id": None, "title": "", "body": ""}
534
535                 noteBox = gtk.VBox()
536
537                 titleBox = gtk.HBox()
538                 titleEntry = gtk.Entry()
539                 titleEntry.set_text(noteDetails["title"])
540                 titleBox.pack_start(titleEntry, True, True)
541                 noteDeleteButton = gtk.Button(stock=gtk.STOCK_DELETE)
542                 titleBox.pack_end(noteDeleteButton, False, False)
543                 noteBox.pack_start(titleBox, False, True)
544
545                 noteEntryScroll = gtk.ScrolledWindow()
546                 noteEntryScroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
547                 noteEntry = gtk.TextView()
548                 noteEntry.set_editable(True)
549                 noteEntry.set_wrap_mode(gtk.WRAP_WORD)
550                 noteEntry.get_buffer().set_text(noteDetails["body"])
551                 noteEntry.set_size_request(-1, 150)
552                 noteEntryScroll.add(noteEntry)
553                 noteBox.pack_start(noteEntryScroll, True, True)
554
555                 self._notesBox.pack_start(noteBox, True, True)
556                 noteBox.show_all()
557
558                 note = noteDetails["id"], noteBox, titleEntry, noteDeleteButton, noteEntry
559                 self._notes.append(note)
560                 return note[1:]
561
562         def _on_add_clicked(self, *args):
563                 noteBox, titleEntry, noteDeleteButton, noteEntry = self._append_notebox()
564                 noteDeleteButton.connect("clicked", self._on_delete_new, noteBox)
565
566         def _on_save_clicked(self, *args):
567                 self._dialog.response(gtk.RESPONSE_OK)
568
569         def _on_cancel_clicked(self, *args):
570                 self._dialog.response(gtk.RESPONSE_CANCEL)
571
572         def _on_delete_new(self, widget, noteBox):
573                 self._notesBox.remove(noteBox)
574                 self._notes = [note for note in self._notes if note[1] is not noteBox]
575
576         def _on_delete_existing(self, widget, noteId, noteBox):
577                 self._notesBox.remove(noteBox)
578                 self._notes = [note for note in self._notes if note[1] is not noteBox]
579                 self._notesToDelete.append(noteId)
580
581
582 class EditTaskDialog(object):
583
584         def __init__(self, widgetTree):
585                 self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
586
587                 self._dialog = widgetTree.get_widget("editTaskDialog")
588                 self._projectCombo = widgetTree.get_widget("edit-targetProjectCombo")
589                 self._taskName = widgetTree.get_widget("edit-taskNameEntry")
590                 self._pasteTaskNameButton = widgetTree.get_widget("edit-pasteTaskNameButton")
591                 self._priorityChoiceCombo = widgetTree.get_widget("edit-priorityChoiceCombo")
592                 self._dueDateDisplay = widgetTree.get_widget("edit-dueDateCalendar")
593                 self._clearDueDate = widgetTree.get_widget("edit-clearDueDate")
594
595                 self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
596                 self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
597
598                 self._onPasteTaskId = None
599                 self._onClearDueDateId = None
600                 self._onAddId = None
601                 self._onCancelId = None
602
603         def enable(self, todoManager):
604                 self._populate_projects(todoManager)
605
606                 self._onPasteTaskId = self._pasteTaskNameButton.connect("clicked", self._on_name_paste)
607                 self._onClearDueDateId = self._clearDueDate.connect("clicked", self._on_clear_duedate)
608                 self._onAddId = self._addButton.connect("clicked", self._on_add_clicked)
609                 self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked)
610
611         def disable(self):
612                 self._pasteTaskNameButton.disconnect(self._onPasteTaskId)
613                 self._clearDueDate.disconnect(self._onClearDueDateId)
614                 self._addButton.disconnect(self._onAddId)
615                 self._cancelButton.disconnect(self._onCancelId)
616
617                 self._projectsList.clear()
618                 self._projectCombo.set_model(None)
619
620         def request_task(self, todoManager, taskId, parentWindow = None):
621                 if parentWindow is not None:
622                         self._dialog.set_transient_for(parentWindow)
623
624                 taskDetails = todoManager.get_task_details(taskId)
625                 originalProjectId = taskDetails["projId"]
626                 originalProjectName = todoManager.get_project(originalProjectId)["name"]
627                 originalName = taskDetails["name"]
628                 originalPriority = str(taskDetails["priority"].get_nothrow(0))
629                 if taskDetails["dueDate"].is_good():
630                         originalDue = taskDetails["dueDate"].get()
631                 else:
632                         originalDue = None
633
634                 self._dialog.set_default_response(gtk.RESPONSE_OK)
635                 self._taskName.set_text(originalName)
636                 self._set_active_proj(originalProjectName)
637                 self._priorityChoiceCombo.set_active(int(originalPriority))
638                 if originalDue is not None:
639                         # Months are 0 indexed
640                         self._dueDateDisplay.select_month(originalDue.month - 1, originalDue.year)
641                         self._dueDateDisplay.select_day(originalDue.day)
642                 else:
643                         now = datetime.datetime.now()
644                         self._dueDateDisplay.select_month(now.month, now.year)
645                         self._dueDateDisplay.select_day(0)
646
647                 try:
648                         response = self._dialog.run()
649                         if response != gtk.RESPONSE_OK:
650                                 raise RuntimeError("Edit Cancelled")
651                 finally:
652                         self._dialog.hide()
653
654                 newProjectName = self._get_project(todoManager)
655                 newName = self._taskName.get_text()
656                 newPriority = self._get_priority()
657                 year, month, day = self._dueDateDisplay.get_date()
658                 if day != 0:
659                         # Months are 0 indexed
660                         date = datetime.date(year, month + 1, day)
661                         time = datetime.time()
662                         newDueDate = datetime.datetime.combine(date, time)
663                 else:
664                         newDueDate = None
665
666                 isProjDifferent = newProjectName != originalProjectName
667                 isNameDifferent = newName != originalName
668                 isPriorityDifferent = newPriority != originalPriority
669                 isDueDifferent = newDueDate != originalDue
670
671                 if isProjDifferent:
672                         newProjectId = todoManager.lookup_project(newProjectName)
673                         todoManager.set_project(taskId, newProjectId)
674                         print "PROJ CHANGE"
675                 if isNameDifferent:
676                         todoManager.set_name(taskId, newName)
677                         print "NAME CHANGE"
678                 if isPriorityDifferent:
679                         try:
680                                 priority = toolbox.Optional(int(newPriority))
681                         except ValueError:
682                                 priority = toolbox.Optional()
683                         todoManager.set_priority(taskId, priority)
684                         print "PRIO CHANGE"
685                 if isDueDifferent:
686                         if newDueDate:
687                                 due = toolbox.Optional(newDueDate)
688                         else:
689                                 due = toolbox.Optional()
690
691                         todoManager.set_duedate(taskId, due)
692                         print "DUE CHANGE"
693
694                 return {
695                         "projId": isProjDifferent,
696                         "name": isNameDifferent,
697                         "priority": isPriorityDifferent,
698                         "due": isDueDifferent,
699                 }
700
701         def _populate_projects(self, todoManager):
702                 for projectName in todoManager.get_projects():
703                         row = (projectName["name"], )
704                         self._projectsList.append(row)
705                 self._projectCombo.set_model(self._projectsList)
706                 cell = gtk.CellRendererText()
707                 self._projectCombo.pack_start(cell, True)
708                 self._projectCombo.add_attribute(cell, 'text', 0)
709                 self._projectCombo.set_active(0)
710
711         def _set_active_proj(self, projName):
712                 for i, row in enumerate(self._projectsList):
713                         if row[0] == projName:
714                                 self._projectCombo.set_active(i)
715                                 break
716                 else:
717                         raise ValueError("%s not in list" % projName)
718
719         def _get_project(self, todoManager):
720                 name = self._projectCombo.get_active_text()
721                 return name
722
723         def _get_priority(self):
724                 index = self._priorityChoiceCombo.get_active()
725                 assert index != -1
726                 if index < 1:
727                         return ""
728                 else:
729                         return str(index)
730
731         def _on_name_paste(self, *args):
732                 clipboard = gtk.clipboard_get()
733                 contents = clipboard.wait_for_text()
734                 if contents is not None:
735                         self._taskName.set_text(contents)
736
737         def _on_clear_duedate(self, *args):
738                 self._dueDateDisplay.select_day(0)
739
740         def _on_add_clicked(self, *args):
741                 self._dialog.response(gtk.RESPONSE_OK)
742
743         def _on_cancel_clicked(self, *args):
744                 self._dialog.response(gtk.RESPONSE_CANCEL)
745
746
747 class PreferencesDialog(object):
748
749         def __init__(self, widgetTree):
750                 self._backendList = gtk.ListStore(gobject.TYPE_STRING)
751                 self._backendCell = gtk.CellRendererText()
752
753                 self._dialog = widgetTree.get_widget("preferencesDialog")
754                 self._backendSelector = widgetTree.get_widget("prefsBackendSelector")
755                 self._applyButton = widgetTree.get_widget("applyPrefsButton")
756                 self._cancelButton = widgetTree.get_widget("cancelPrefsButton")
757
758                 self._onApplyId = None
759                 self._onCancelId = None
760
761         def enable(self):
762                 self._dialog.set_default_size(800, 300)
763                 self._onApplyId = self._applyButton.connect("clicked", self._on_apply_clicked)
764                 self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked)
765
766                 cell = self._backendCell
767                 self._backendSelector.pack_start(cell, True)
768                 self._backendSelector.add_attribute(cell, 'text', 0)
769                 self._backendSelector.set_model(self._backendList)
770
771         def disable(self):
772                 self._applyButton.disconnect(self._onApplyId)
773                 self._cancelButton.disconnect(self._onCancelId)
774
775                 self._backendList.clear()
776                 self._backendSelector.set_model(None)
777
778         def run(self, app, parentWindow = None):
779                 if parentWindow is not None:
780                         self._dialog.set_transient_for(parentWindow)
781
782                 self._backendList.clear()
783                 activeIndex = 0
784                 for i, (uiName, ui) in enumerate(app.get_uis()):
785                         self._backendList.append((uiName, ))
786                         if uiName == app.get_default_ui():
787                                 activeIndex = i
788                 self._backendSelector.set_active(activeIndex)
789
790                 try:
791                         response = self._dialog.run()
792                         if response != gtk.RESPONSE_OK:
793                                 raise RuntimeError("Edit Cancelled")
794                 finally:
795                         self._dialog.hide()
796
797                 backendName = self._backendSelector.get_active_text()
798                 app.switch_ui(backendName)
799
800         def _on_apply_clicked(self, *args):
801                 self._dialog.response(gtk.RESPONSE_OK)
802
803         def _on_cancel_clicked(self, *args):
804                 self._dialog.response(gtk.RESPONSE_CANCEL)
805
806
807 class ProjectsDialog(object):
808
809         ID_IDX = 0
810         NAME_IDX = 1
811         VISIBILITY_IDX = 2
812
813         def __init__(self, widgetTree):
814                 self._manager = None
815
816                 self._dialog = widgetTree.get_widget("projectsDialog")
817                 self._projView = widgetTree.get_widget("proj-projectView")
818
819                 addSink = coroutines.CoSwitch(["add", "add-edit"])
820                 addSink.register_sink("add", coroutines.func_sink(self._on_add))
821                 addSink.register_sink("add-edit", coroutines.func_sink(self._on_add_edit))
822                 self._addView = QuickAddView(widgetTree, DummyErrorDisplay(), addSink, "proj")
823
824                 self._projList = gtk.ListStore(
825                         gobject.TYPE_STRING, # id
826                         gobject.TYPE_STRING, # name
827                         gobject.TYPE_BOOLEAN, # is visible
828                 )
829                 self._visibilityColumn = gtk.TreeViewColumn('') # Complete?
830                 self._visibilityCell = gtk.CellRendererToggle()
831                 self._visibilityCell.set_property("activatable", True)
832                 self._visibilityCell.connect("toggled", self._on_toggle_visibility)
833                 self._visibilityColumn.pack_start(self._visibilityCell, False)
834                 self._visibilityColumn.set_attributes(self._visibilityCell, active=self.VISIBILITY_IDX)
835                 self._nameColumn = gtk.TreeViewColumn('Name')
836                 self._nameCell = gtk.CellRendererText()
837                 self._nameColumn.pack_start(self._nameCell, True)
838                 self._nameColumn.set_attributes(self._nameCell, text=self.NAME_IDX)
839                 self._nameColumn.set_expand(True)
840
841                 self._projView.append_column(self._visibilityColumn)
842                 self._projView.append_column(self._nameColumn)
843                 self._projView.connect("row-activated", self._on_proj_select)
844
845         def enable(self, manager):
846                 self._manager = manager
847
848                 self._populate_projects()
849                 self._dialog.show_all()
850
851         def disable(self):
852                 self._dialog.hide_all()
853                 self._manager = None
854
855                 self._projList.clear()
856                 self._projView.set_model(None)
857
858         def _populate_projects(self):
859                 self._projList.clear()
860
861                 projects = self._manager.get_projects()
862                 for project in projects:
863                         projectId = project["id"]
864                         projectName = project["name"]
865                         isVisible = project["isVisible"]
866                         row = (projectId, projectName, isVisible)
867                         self._projList.append(row)
868                 self._projView.set_model(self._projList)
869
870         def _on_add(self, eventData):
871                 eventName, projectName, = eventData
872                 self._manager.add_project(projectName)
873                 self._populate_projects()
874
875         def _on_add_edit(self, eventData):
876                 self._on_add(eventData)
877
878         def _on_toggle_visibility(self, cell, path):
879                 listIndex = path[0]
880                 row = self._projList[listIndex]
881                 projId = row[self.ID_IDX]
882                 oldValue = row[self.VISIBILITY_IDX]
883                 self._manager.set_project_visibility(projId, not oldValue)
884                 row[self.VISIBILITY_IDX] = not oldValue
885
886         def _on_proj_select(self, *args):
887                 # @todo Implement project renaming
888                 pass
889
890
891 if __name__ == "__main__":
892         if True:
893                 win = gtk.Window()
894                 win.set_title("Tap'N'Hold")
895                 eventBox = gtk.EventBox()
896                 win.add(eventBox)
897
898                 context = ContextHandler(eventBox, coroutines.printer_sink())
899                 context.enable()
900                 win.connect("destroy", lambda w: gtk.main_quit())
901
902                 win.show_all()
903
904         if False:
905                 cal = PopupCalendar(None, datetime.datetime.now())
906                 cal._popupWindow.connect("destroy", lambda w: gtk.main_quit())
907                 cal.run()
908
909         gtk.main()