Cleaning up some further todos
[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 warnings
11 import contextlib
12 import datetime
13
14 import gobject
15 import gtk
16
17 import toolbox
18 import coroutines
19
20
21 @contextlib.contextmanager
22 def gtk_lock():
23         gtk.gdk.threads_enter()
24         try:
25                 yield
26         finally:
27                 gtk.gdk.threads_leave()
28
29
30 class ContextHandler(object):
31
32         HOLD_TIMEOUT = 1000
33         MOVE_THRESHHOLD = 10
34
35         def __init__(self, actionWidget, activationTarget = coroutines.null_sink()):
36                 self._actionWidget = actionWidget
37                 self._activationTarget = activationTarget
38
39                 self._actionPressId = None
40                 self._actionReleaseId = None
41                 self._motionNotifyId = None
42                 self._popupMenuId = None
43                 self._holdTimerId = None
44
45                 self._respondOnRelease = False
46                 self._startPosition = None
47
48         def enable(self):
49                 self._actionPressId = self._actionWidget.connect("button-press-event", self._on_press)
50                 self._actionReleaseId = self._actionWidget.connect("button-release-event", self._on_release)
51                 self._motionNotifyId = self._actionWidget.connect("motion-notify-event", self._on_motion)
52                 self._popupMenuId = self._actionWidget.connect("popup-menu", self._on_popup)
53
54         def disable(self):
55                 self._clear()
56                 self._actionWidget.disconnect(self._actionPressId)
57                 self._actionWidget.disconnect(self._actionReleaseId)
58                 self._actionWidget.disconnect(self._motionNotifyId)
59                 self._actionWidget.disconnect(self._popupMenuId)
60
61         def _respond(self, position):
62                 widgetPosition = 0, 0 # @todo Figure out how to get a widgets root position
63                 responsePosition = (
64                         widgetPosition[0] + position[0],
65                         widgetPosition[1] + position[1],
66                 )
67                 self._activationTarget.send((self._actionWidget, responsePosition))
68
69         def _clear(self):
70                 if self._holdTimerId is not None:
71                         gobject.source_remove(self._holdTimerId)
72                 self._respondOnRelease = False
73                 self._startPosition = None
74
75         def _is_cleared(self):
76                 return self._startPosition is None
77
78         def _on_press(self, widget, event):
79                 if not self._is_cleared():
80                         return
81
82                 self._startPosition = event.get_coords()
83                 if event.button == 1:
84                         # left click
85                         self._holdTimerId = gobject.timeout_add(self.HOLD_TIMEOUT, self._on_hold_timeout)
86                 else:
87                         # any other click
88                         self._respondOnRelease = True
89
90         def _on_release(self, widget, event):
91                 if self._is_cleared():
92                         return
93
94                 if self._respondOnRelease:
95                         position = self._startPosition
96                         self._clear()
97                         self._respond(position)
98                 else:
99                         self._clear()
100
101         def _on_hold_timeout(self):
102                 assert not self._is_cleared()
103                 gobject.source_remove(self._holdTimerId)
104                 self._holdTimerId = None
105
106                 position = self._startPosition
107                 self._clear()
108                 self._respond(position)
109
110         def _on_motion(self, widget, event):
111                 if self._is_cleared():
112                         return
113                 curPosition = event.get_coords()
114                 dx, dy = (
115                         curPosition[1] - self._startPosition[1],
116                         curPosition[1] - self._startPosition[1],
117                 )
118                 delta = (dx ** 2 + dy ** 2) ** (0.5)
119                 if self.MOVE_THRESHHOLD <= delta:
120                         self._clear()
121
122         def _on_popup(self, widget):
123                 self._clear()
124                 position = 0, 0
125                 self._respond(position)
126
127 class LoginWindow(object):
128
129         def __init__(self, widgetTree):
130                 """
131                 @note Thread agnostic
132                 """
133                 self._dialog = widgetTree.get_widget("loginDialog")
134                 self._parentWindow = widgetTree.get_widget("mainWindow")
135                 self._serviceCombo = widgetTree.get_widget("serviceCombo")
136                 self._usernameEntry = widgetTree.get_widget("usernameentry")
137                 self._passwordEntry = widgetTree.get_widget("passwordentry")
138
139                 self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING)
140                 self._serviceCombo.set_model(self._serviceList)
141                 cell = gtk.CellRendererText()
142                 self._serviceCombo.pack_start(cell, True)
143                 self._serviceCombo.add_attribute(cell, 'text', 1)
144                 self._serviceCombo.set_active(0)
145
146                 callbackMapping = {
147                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
148                         "on_loginclose_clicked": self._on_loginclose_clicked,
149                 }
150                 widgetTree.signal_autoconnect(callbackMapping)
151
152         def request_credentials(self, parentWindow = None):
153                 """
154                 @note UI Thread
155                 """
156                 if parentWindow is None:
157                         parentWindow = self._parentWindow
158
159                 self._serviceCombo.hide()
160                 self._serviceList.clear()
161
162                 try:
163                         self._dialog.set_transient_for(parentWindow)
164                         self._dialog.set_default_response(gtk.RESPONSE_OK)
165                         response = self._dialog.run()
166                         if response != gtk.RESPONSE_OK:
167                                 raise RuntimeError("Login Cancelled")
168
169                         username = self._usernameEntry.get_text()
170                         password = self._passwordEntry.get_text()
171                         self._passwordEntry.set_text("")
172                 finally:
173                         self._dialog.hide()
174
175                 return username, password
176
177         def request_credentials_from(self, services, parentWindow = None):
178                 """
179                 @note UI Thread
180                 """
181                 if parentWindow is None:
182                         parentWindow = self._parentWindow
183
184                 self._serviceList.clear()
185                 for serviceIdserviceName in services.iteritems():
186                         self._serviceList.append(serviceIdserviceName)
187                 self._serviceCombo.set_active(0)
188                 self._serviceCombo.show()
189
190                 try:
191                         self._dialog.set_transient_for(parentWindow)
192                         self._dialog.set_default_response(gtk.RESPONSE_OK)
193                         response = self._dialog.run()
194                         if response != gtk.RESPONSE_OK:
195                                 raise RuntimeError("Login Cancelled")
196
197                         username = self._usernameEntry.get_text()
198                         password = self._passwordEntry.get_text()
199                         self._passwordEntry.set_text("")
200                 finally:
201                         self._dialog.hide()
202
203                 itr = self._serviceCombo.get_active_iter()
204                 serviceId = int(self._serviceList.get_value(itr, 0))
205                 self._serviceList.clear()
206                 return serviceId, username, password
207
208         def _on_loginbutton_clicked(self, *args):
209                 self._dialog.response(gtk.RESPONSE_OK)
210
211         def _on_loginclose_clicked(self, *args):
212                 self._dialog.response(gtk.RESPONSE_CANCEL)
213
214
215 class ErrorDisplay(object):
216
217         def __init__(self, widgetTree):
218                 super(ErrorDisplay, self).__init__()
219                 self.__errorBox = widgetTree.get_widget("errorEventBox")
220                 self.__errorDescription = widgetTree.get_widget("errorDescription")
221                 self.__errorClose = widgetTree.get_widget("errorClose")
222                 self.__parentBox = self.__errorBox.get_parent()
223
224                 self.__errorBox.connect("button_release_event", self._on_close)
225
226                 self.__messages = []
227                 self.__parentBox.remove(self.__errorBox)
228
229         def push_message_with_lock(self, message):
230                 gtk.gdk.threads_enter()
231                 try:
232                         self.push_message(message)
233                 finally:
234                         gtk.gdk.threads_leave()
235
236         def push_message(self, message):
237                 if 0 < len(self.__messages):
238                         self.__messages.append(message)
239                 else:
240                         self.__show_message(message)
241
242         def push_exception(self, exception):
243                 self.push_message(exception.message)
244                 warnings.warn(exception, stacklevel=3)
245
246         def pop_message(self):
247                 if 0 < len(self.__messages):
248                         self.__show_message(self.__messages[0])
249                         del self.__messages[0]
250                 else:
251                         self.__hide_message()
252
253         def _on_close(self, *args):
254                 self.pop_message()
255
256         def __show_message(self, message):
257                 self.__errorDescription.set_text(message)
258                 self.__parentBox.pack_start(self.__errorBox, False, False)
259                 self.__parentBox.reorder_child(self.__errorBox, 1)
260
261         def __hide_message(self):
262                 self.__errorDescription.set_text("")
263                 self.__parentBox.remove(self.__errorBox)
264
265
266 class MessageBox(gtk.MessageDialog):
267
268         def __init__(self, message):
269                 parent = None
270                 gtk.MessageDialog.__init__(
271                         self,
272                         parent,
273                         gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
274                         gtk.MESSAGE_ERROR,
275                         gtk.BUTTONS_OK,
276                         message,
277                 )
278                 self.set_default_response(gtk.RESPONSE_OK)
279                 self.connect('response', self._handle_clicked)
280
281         def _handle_clicked(self, *args):
282                 self.destroy()
283
284
285 class MessageBox2(gtk.MessageDialog):
286
287         def __init__(self, message):
288                 parent = None
289                 gtk.MessageDialog.__init__(
290                         self,
291                         parent,
292                         gtk.DIALOG_DESTROY_WITH_PARENT,
293                         gtk.MESSAGE_ERROR,
294                         gtk.BUTTONS_OK,
295                         message,
296                 )
297                 self.set_default_response(gtk.RESPONSE_OK)
298                 self.connect('response', self._handle_clicked)
299
300         def _handle_clicked(self, *args):
301                 self.destroy()
302
303
304 class PopupCalendar(object):
305
306         def __init__(self, parent):
307                 self.__calendar = gtk.Calendar()
308                 self.__calendar.connect("day-selected-double-click", self._on_date_select)
309
310                 self.__popupWindow = gtk.Window(type = gtk.WINDOW_POPUP)
311                 self.__popupWindow.set_title("")
312                 self.__popupWindow.add(self.__calendar)
313                 self.__popupWindow.set_transient_for(parent)
314                 self.__popupWindow.set_modal(True)
315
316                 self.callback = lambda: None
317
318         def get_date(self):
319                 year, month, day = self.__calendar.get_date()
320                 month += 1 # Seems to be 0 indexed
321                 return datetime.date(year, month, day)
322
323         def run(self):
324                 self.__popupWindow.show_all()
325
326         def _on_date_select(self, *args):
327                 self.__popupWindow.hide()
328                 self.callback()
329
330         def callback(self):
331                 pass
332
333
334 class EditTaskDialog(object):
335
336         def __init__(self, widgetTree):
337                 self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
338
339                 self._dialog = widgetTree.get_widget("editTaskDialog")
340                 self._projectCombo = widgetTree.get_widget("edit-targetProjectCombo")
341                 self._taskName = widgetTree.get_widget("edit-taskNameEntry")
342                 self._pasteTaskNameButton = widgetTree.get_widget("edit-pasteTaskNameButton")
343                 self._priorityChoiceCombo = widgetTree.get_widget("edit-priorityChoiceCombo")
344                 self._dueDateDisplay = widgetTree.get_widget("edit-dueDateDisplay")
345                 self._dueDateProperties = widgetTree.get_widget("edit-dueDateProperties")
346                 self._clearDueDate = widgetTree.get_widget("edit-clearDueDate")
347
348                 self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
349                 self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
350
351                 self._popupCalendar = PopupCalendar(self._dialog)
352
353         def enable(self, todoManager):
354                 self._populate_projects(todoManager)
355                 self._pasteTaskNameButton.connect("clicked", self._on_name_paste)
356                 self._dueDateProperties.connect("clicked", self._on_choose_duedate)
357                 self._clearDueDate.connect("clicked", self._on_clear_duedate)
358
359                 self._addButton.connect("clicked", self._on_add_clicked)
360                 self._cancelButton.connect("clicked", self._on_cancel_clicked)
361
362                 self._popupCalendar.callback = self._update_duedate
363
364         def disable(self):
365                 self._popupCalendar.callback = lambda: None
366
367                 self._pasteTaskNameButton.disconnect("clicked", self._on_name_paste)
368                 self._dueDateProperties.disconnect("clicked", self._on_choose_duedate)
369                 self._clearDueDate.disconnect("clicked", self._on_clear_duedate)
370                 self._projectsList.clear()
371                 self._projectCombo.set_model(None)
372
373         def request_task(self, todoManager, taskId, parentWindow = None):
374                 if parentWindow is not None:
375                         self._dialog.set_transient_for(parentWindow)
376
377                 taskDetails = todoManager.get_task_details(taskId)
378                 originalProjectId = taskDetails["projId"]
379                 originalProjectName = todoManager.get_project(originalProjectId)["name"]
380                 originalName = taskDetails["name"]
381                 originalPriority = str(taskDetails["priority"].get_nothrow(0))
382                 if taskDetails["dueDate"].is_good():
383                         originalDue = taskDetails["dueDate"].get().strftime("%Y-%m-%d %H:%M:%S")
384                 else:
385                         originalDue = ""
386
387                 self._dialog.set_default_response(gtk.RESPONSE_OK)
388                 self._taskName.set_text(originalName)
389                 self._set_active_proj(originalProjectName)
390                 self._priorityChoiceCombo.set_active(int(originalPriority))
391                 self._dueDateDisplay.set_text(originalDue)
392
393                 try:
394                         response = self._dialog.run()
395                         if response != gtk.RESPONSE_OK:
396                                 raise RuntimeError("Edit Cancelled")
397                 finally:
398                         self._dialog.hide()
399
400                 newProjectName = self._get_project(todoManager)
401                 newName = self._taskName.get_text()
402                 newPriority = self._get_priority()
403                 newDueDate = self._dueDateDisplay.get_text()
404
405                 isProjDifferent = newProjectName != originalProjectName
406                 isNameDifferent = newName != originalName
407                 isPriorityDifferent = newPriority != originalPriority
408                 isDueDifferent = newDueDate != originalDue
409
410                 if isProjDifferent:
411                         newProjectId = todoManager.lookup_project(newProjectName)
412                         todoManager.set_project(taskId, newProjectId)
413                         print "PROJ CHANGE"
414                 if isNameDifferent:
415                         todoManager.set_name(taskId, newName)
416                         print "NAME CHANGE"
417                 if isPriorityDifferent:
418                         try:
419                                 priority = toolbox.Optional(int(newPriority))
420                         except ValueError:
421                                 priority = toolbox.Optional()
422                         todoManager.set_priority(taskId, priority)
423                         print "PRIO CHANGE"
424                 if isDueDifferent:
425                         if newDueDate:
426                                 due = datetime.datetime.strptime(newDueDate, "%Y-%m-%d %H:%M:%S")
427                                 due = toolbox.Optional(due)
428                         else:
429                                 due = toolbox.Optional()
430
431                         todoManager.set_duedate(taskId, due)
432                         print "DUE CHANGE"
433
434                 return {
435                         "projId": isProjDifferent,
436                         "name": isNameDifferent,
437                         "priority": isPriorityDifferent,
438                         "due": isDueDifferent,
439                 }
440
441         def _populate_projects(self, todoManager):
442                 for projectName in todoManager.get_projects():
443                         row = (projectName["name"], )
444                         self._projectsList.append(row)
445                 self._projectCombo.set_model(self._projectsList)
446                 cell = gtk.CellRendererText()
447                 self._projectCombo.pack_start(cell, True)
448                 self._projectCombo.add_attribute(cell, 'text', 0)
449                 self._projectCombo.set_active(0)
450
451         def _set_active_proj(self, projName):
452                 for i, row in enumerate(self._projectsList):
453                         if row[0] == projName:
454                                 self._projectCombo.set_active(i)
455                                 break
456                 else:
457                         raise ValueError("%s not in list" % projName)
458
459         def _get_project(self, todoManager):
460                 name = self._projectCombo.get_active_text()
461                 return name
462
463         def _get_priority(self):
464                 index = self._priorityChoiceCombo.get_active()
465                 assert index != -1
466                 if index < 1:
467                         return ""
468                 else:
469                         return str(index)
470
471         def _update_duedate(self):
472                 date = self._popupCalendar.get_date()
473                 time = datetime.time()
474                 dueDate = datetime.datetime.combine(date, time)
475
476                 formttedDate = dueDate.strftime("%Y-%m-%d %H:%M:%S")
477                 self._dueDateDisplay.set_text(formttedDate)
478
479         def _on_name_paste(self, *args):
480                 clipboard = gtk.clipboard_get()
481                 contents = clipboard.wait_for_text()
482                 if contents is not None:
483                         self._taskName.set_text(contents)
484
485         def _on_choose_duedate(self, *args):
486                 self._popupCalendar.run()
487
488         def _on_clear_duedate(self, *args):
489                 self._dueDateDisplay.set_text("")
490
491         def _on_add_clicked(self, *args):
492                 self._dialog.response(gtk.RESPONSE_OK)
493
494         def _on_cancel_clicked(self, *args):
495                 self._dialog.response(gtk.RESPONSE_CANCEL)
496
497
498 if __name__ == "__main__":
499         if True:
500                 win = gtk.Window()
501                 win.set_title("Tap'N'Hold")
502                 eventBox = gtk.EventBox()
503                 win.add(eventBox)
504
505                 context = ContextHandler(eventBox, coroutines.printer_sink())
506                 context.enable()
507                 win.connect("destroy", lambda w: gtk.main_quit())
508
509                 win.show_all()
510                 gtk.main()