Fixing line endings and adding a function sink
[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
128 class LoginWindow(object):
129
130         def __init__(self, widgetTree):
131                 """
132                 @note Thread agnostic
133                 """
134                 self._dialog = widgetTree.get_widget("loginDialog")
135                 self._parentWindow = widgetTree.get_widget("mainWindow")
136                 self._serviceCombo = widgetTree.get_widget("serviceCombo")
137                 self._usernameEntry = widgetTree.get_widget("usernameentry")
138                 self._passwordEntry = widgetTree.get_widget("passwordentry")
139
140                 self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING)
141                 self._serviceCombo.set_model(self._serviceList)
142                 cell = gtk.CellRendererText()
143                 self._serviceCombo.pack_start(cell, True)
144                 self._serviceCombo.add_attribute(cell, 'text', 1)
145                 self._serviceCombo.set_active(0)
146
147                 callbackMapping = {
148                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
149                         "on_loginclose_clicked": self._on_loginclose_clicked,
150                 }
151                 widgetTree.signal_autoconnect(callbackMapping)
152
153         def request_credentials(self, parentWindow = None):
154                 """
155                 @note UI Thread
156                 """
157                 if parentWindow is None:
158                         parentWindow = self._parentWindow
159
160                 self._serviceCombo.hide()
161                 self._serviceList.clear()
162
163                 try:
164                         self._dialog.set_transient_for(parentWindow)
165                         self._dialog.set_default_response(gtk.RESPONSE_OK)
166                         response = self._dialog.run()
167                         if response != gtk.RESPONSE_OK:
168                                 raise RuntimeError("Login Cancelled")
169
170                         username = self._usernameEntry.get_text()
171                         password = self._passwordEntry.get_text()
172                         self._passwordEntry.set_text("")
173                 finally:
174                         self._dialog.hide()
175
176                 return username, password
177
178         def request_credentials_from(self, services, parentWindow = None):
179                 """
180                 @note UI Thread
181                 """
182                 if parentWindow is None:
183                         parentWindow = self._parentWindow
184
185                 self._serviceList.clear()
186                 for serviceIdserviceName in services.iteritems():
187                         self._serviceList.append(serviceIdserviceName)
188                 self._serviceCombo.set_active(0)
189                 self._serviceCombo.show()
190
191                 try:
192                         self._dialog.set_transient_for(parentWindow)
193                         self._dialog.set_default_response(gtk.RESPONSE_OK)
194                         response = self._dialog.run()
195                         if response != gtk.RESPONSE_OK:
196                                 raise RuntimeError("Login Cancelled")
197
198                         username = self._usernameEntry.get_text()
199                         password = self._passwordEntry.get_text()
200                         self._passwordEntry.set_text("")
201                 finally:
202                         self._dialog.hide()
203
204                 itr = self._serviceCombo.get_active_iter()
205                 serviceId = int(self._serviceList.get_value(itr, 0))
206                 self._serviceList.clear()
207                 return serviceId, username, password
208
209         def _on_loginbutton_clicked(self, *args):
210                 self._dialog.response(gtk.RESPONSE_OK)
211
212         def _on_loginclose_clicked(self, *args):
213                 self._dialog.response(gtk.RESPONSE_CANCEL)
214
215
216 class ErrorDisplay(object):
217
218         def __init__(self, widgetTree):
219                 super(ErrorDisplay, self).__init__()
220                 self.__errorBox = widgetTree.get_widget("errorEventBox")
221                 self.__errorDescription = widgetTree.get_widget("errorDescription")
222                 self.__errorClose = widgetTree.get_widget("errorClose")
223                 self.__parentBox = self.__errorBox.get_parent()
224
225                 self.__errorBox.connect("button_release_event", self._on_close)
226
227                 self.__messages = []
228                 self.__parentBox.remove(self.__errorBox)
229
230         def push_message_with_lock(self, message):
231                 gtk.gdk.threads_enter()
232                 try:
233                         self.push_message(message)
234                 finally:
235                         gtk.gdk.threads_leave()
236
237         def push_message(self, message):
238                 if 0 < len(self.__messages):
239                         self.__messages.append(message)
240                 else:
241                         self.__show_message(message)
242
243         def push_exception(self, exception):
244                 self.push_message(exception.message)
245                 warnings.warn(exception, stacklevel=3)
246
247         def pop_message(self):
248                 if 0 < len(self.__messages):
249                         self.__show_message(self.__messages[0])
250                         del self.__messages[0]
251                 else:
252                         self.__hide_message()
253
254         def _on_close(self, *args):
255                 self.pop_message()
256
257         def __show_message(self, message):
258                 self.__errorDescription.set_text(message)
259                 self.__parentBox.pack_start(self.__errorBox, False, False)
260                 self.__parentBox.reorder_child(self.__errorBox, 1)
261
262         def __hide_message(self):
263                 self.__errorDescription.set_text("")
264                 self.__parentBox.remove(self.__errorBox)
265
266
267 class MessageBox(gtk.MessageDialog):
268
269         def __init__(self, message):
270                 parent = None
271                 gtk.MessageDialog.__init__(
272                         self,
273                         parent,
274                         gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
275                         gtk.MESSAGE_ERROR,
276                         gtk.BUTTONS_OK,
277                         message,
278                 )
279                 self.set_default_response(gtk.RESPONSE_OK)
280                 self.connect('response', self._handle_clicked)
281
282         def _handle_clicked(self, *args):
283                 self.destroy()
284
285
286 class MessageBox2(gtk.MessageDialog):
287
288         def __init__(self, message):
289                 parent = None
290                 gtk.MessageDialog.__init__(
291                         self,
292                         parent,
293                         gtk.DIALOG_DESTROY_WITH_PARENT,
294                         gtk.MESSAGE_ERROR,
295                         gtk.BUTTONS_OK,
296                         message,
297                 )
298                 self.set_default_response(gtk.RESPONSE_OK)
299                 self.connect('response', self._handle_clicked)
300
301         def _handle_clicked(self, *args):
302                 self.destroy()
303
304
305 class PopupCalendar(object):
306
307         def __init__(self, parent):
308                 self.__calendar = gtk.Calendar()
309                 self.__calendar.connect("day-selected-double-click", self._on_date_select)
310
311                 self.__popupWindow = gtk.Window(type = gtk.WINDOW_POPUP)
312                 self.__popupWindow.set_title("")
313                 self.__popupWindow.add(self.__calendar)
314                 self.__popupWindow.set_transient_for(parent)
315                 self.__popupWindow.set_modal(True)
316
317                 self.callback = lambda: None
318
319         def get_date(self):
320                 year, month, day = self.__calendar.get_date()
321                 month += 1 # Seems to be 0 indexed
322                 return datetime.date(year, month, day)
323
324         def run(self):
325                 self.__popupWindow.show_all()
326
327         def _on_date_select(self, *args):
328                 self.__popupWindow.hide()
329                 self.callback()
330
331         def callback(self):
332                 pass
333
334
335 class EditTaskDialog(object):
336
337         def __init__(self, widgetTree):
338                 self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
339
340                 self._dialog = widgetTree.get_widget("editTaskDialog")
341                 self._projectCombo = widgetTree.get_widget("edit-targetProjectCombo")
342                 self._taskName = widgetTree.get_widget("edit-taskNameEntry")
343                 self._pasteTaskNameButton = widgetTree.get_widget("edit-pasteTaskNameButton")
344                 self._priorityChoiceCombo = widgetTree.get_widget("edit-priorityChoiceCombo")
345                 self._dueDateDisplay = widgetTree.get_widget("edit-dueDateDisplay")
346                 self._dueDateProperties = widgetTree.get_widget("edit-dueDateProperties")
347                 self._clearDueDate = widgetTree.get_widget("edit-clearDueDate")
348
349                 self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
350                 self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
351
352                 self._popupCalendar = PopupCalendar(self._dialog)
353
354         def enable(self, todoManager):
355                 self._populate_projects(todoManager)
356                 self._pasteTaskNameButton.connect("clicked", self._on_name_paste)
357                 self._dueDateProperties.connect("clicked", self._on_choose_duedate)
358                 self._clearDueDate.connect("clicked", self._on_clear_duedate)
359
360                 self._addButton.connect("clicked", self._on_add_clicked)
361                 self._cancelButton.connect("clicked", self._on_cancel_clicked)
362
363                 self._popupCalendar.callback = self._update_duedate
364
365         def disable(self):
366                 self._popupCalendar.callback = lambda: None
367
368                 self._pasteTaskNameButton.disconnect("clicked", self._on_name_paste)
369                 self._dueDateProperties.disconnect("clicked", self._on_choose_duedate)
370                 self._clearDueDate.disconnect("clicked", self._on_clear_duedate)
371                 self._projectsList.clear()
372                 self._projectCombo.set_model(None)
373
374         def request_task(self, todoManager, taskId, parentWindow = None):
375                 if parentWindow is not None:
376                         self._dialog.set_transient_for(parentWindow)
377
378                 taskDetails = todoManager.get_task_details(taskId)
379                 originalProjectId = taskDetails["projId"]
380                 originalProjectName = todoManager.get_project(originalProjectId)["name"]
381                 originalName = taskDetails["name"]
382                 originalPriority = str(taskDetails["priority"].get_nothrow(0))
383                 if taskDetails["dueDate"].is_good():
384                         originalDue = taskDetails["dueDate"].get().strftime("%Y-%m-%d %H:%M:%S")
385                 else:
386                         originalDue = ""
387
388                 self._dialog.set_default_response(gtk.RESPONSE_OK)
389                 self._taskName.set_text(originalName)
390                 self._set_active_proj(originalProjectName)
391                 self._priorityChoiceCombo.set_active(int(originalPriority))
392                 self._dueDateDisplay.set_text(originalDue)
393
394                 try:
395                         response = self._dialog.run()
396                         if response != gtk.RESPONSE_OK:
397                                 raise RuntimeError("Edit Cancelled")
398                 finally:
399                         self._dialog.hide()
400
401                 newProjectName = self._get_project(todoManager)
402                 newName = self._taskName.get_text()
403                 newPriority = self._get_priority()
404                 newDueDate = self._dueDateDisplay.get_text()
405
406                 isProjDifferent = newProjectName != originalProjectName
407                 isNameDifferent = newName != originalName
408                 isPriorityDifferent = newPriority != originalPriority
409                 isDueDifferent = newDueDate != originalDue
410
411                 if isProjDifferent:
412                         newProjectId = todoManager.lookup_project(newProjectName)
413                         todoManager.set_project(taskId, newProjectId)
414                         print "PROJ CHANGE"
415                 if isNameDifferent:
416                         todoManager.set_name(taskId, newName)
417                         print "NAME CHANGE"
418                 if isPriorityDifferent:
419                         try:
420                                 priority = toolbox.Optional(int(newPriority))
421                         except ValueError:
422                                 priority = toolbox.Optional()
423                         todoManager.set_priority(taskId, priority)
424                         print "PRIO CHANGE"
425                 if isDueDifferent:
426                         if newDueDate:
427                                 due = datetime.datetime.strptime(newDueDate, "%Y-%m-%d %H:%M:%S")
428                                 due = toolbox.Optional(due)
429                         else:
430                                 due = toolbox.Optional()
431
432                         todoManager.set_duedate(taskId, due)
433                         print "DUE CHANGE"
434
435                 return {
436                         "projId": isProjDifferent,
437                         "name": isNameDifferent,
438                         "priority": isPriorityDifferent,
439                         "due": isDueDifferent,
440                 }
441
442         def _populate_projects(self, todoManager):
443                 for projectName in todoManager.get_projects():
444                         row = (projectName["name"], )
445                         self._projectsList.append(row)
446                 self._projectCombo.set_model(self._projectsList)
447                 cell = gtk.CellRendererText()
448                 self._projectCombo.pack_start(cell, True)
449                 self._projectCombo.add_attribute(cell, 'text', 0)
450                 self._projectCombo.set_active(0)
451
452         def _set_active_proj(self, projName):
453                 for i, row in enumerate(self._projectsList):
454                         if row[0] == projName:
455                                 self._projectCombo.set_active(i)
456                                 break
457                 else:
458                         raise ValueError("%s not in list" % projName)
459
460         def _get_project(self, todoManager):
461                 name = self._projectCombo.get_active_text()
462                 return name
463
464         def _get_priority(self):
465                 index = self._priorityChoiceCombo.get_active()
466                 assert index != -1
467                 if index < 1:
468                         return ""
469                 else:
470                         return str(index)
471
472         def _update_duedate(self):
473                 date = self._popupCalendar.get_date()
474                 time = datetime.time()
475                 dueDate = datetime.datetime.combine(date, time)
476
477                 formttedDate = dueDate.strftime("%Y-%m-%d %H:%M:%S")
478                 self._dueDateDisplay.set_text(formttedDate)
479
480         def _on_name_paste(self, *args):
481                 clipboard = gtk.clipboard_get()
482                 contents = clipboard.wait_for_text()
483                 if contents is not None:
484                         self._taskName.set_text(contents)
485
486         def _on_choose_duedate(self, *args):
487                 self._popupCalendar.run()
488
489         def _on_clear_duedate(self, *args):
490                 self._dueDateDisplay.set_text("")
491
492         def _on_add_clicked(self, *args):
493                 self._dialog.response(gtk.RESPONSE_OK)
494
495         def _on_cancel_clicked(self, *args):
496                 self._dialog.response(gtk.RESPONSE_CANCEL)
497
498
499 if __name__ == "__main__":
500         if True:
501                 win = gtk.Window()
502                 win.set_title("Tap'N'Hold")
503                 eventBox = gtk.EventBox()
504                 win.add(eventBox)
505
506                 context = ContextHandler(eventBox, coroutines.printer_sink())
507                 context.enable()
508                 win.connect("destroy", lambda w: gtk.main_quit())
509
510                 win.show_all()
511                 gtk.main()