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
21 @contextlib.contextmanager
23 gtk.gdk.threads_enter()
27 gtk.gdk.threads_leave()
30 class ContextHandler(object):
35 def __init__(self, actionWidget, eventTarget = coroutines.null_sink()):
36 self._actionWidget = actionWidget
37 self._eventTarget = eventTarget
39 self._actionPressId = None
40 self._actionReleaseId = None
41 self._motionNotifyId = None
42 self._popupMenuId = None
43 self._holdTimerId = None
45 self._respondOnRelease = False
46 self._startPosition = None
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)
56 self._actionWidget.disconnect(self._actionPressId)
57 self._actionWidget.disconnect(self._actionReleaseId)
58 self._actionWidget.disconnect(self._motionNotifyId)
59 self._actionWidget.disconnect(self._popupMenuId)
61 def _respond(self, position):
62 widgetPosition = 0, 0 # @todo Figure out how to get a widgets root position
64 widgetPosition[0] + position[0],
65 widgetPosition[1] + position[1],
67 self._eventTarget.send((self._actionWidget, responsePosition))
70 if self._holdTimerId is not None:
71 gobject.source_remove(self._holdTimerId)
72 self._respondOnRelease = False
73 self._startPosition = None
75 def _is_cleared(self):
76 return self._startPosition is None
78 def _on_press(self, widget, event):
79 if not self._is_cleared():
82 self._startPosition = event.get_coords()
85 self._holdTimerId = gobject.timeout_add(self.HOLD_TIMEOUT, self._on_hold_timeout)
88 self._respondOnRelease = True
90 def _on_release(self, widget, event):
91 if self._is_cleared():
94 if self._respondOnRelease:
95 position = self._startPosition
97 self._respond(position)
101 def _on_hold_timeout(self):
102 assert not self._is_cleared()
103 gobject.source_remove(self._holdTimerId)
104 self._holdTimerId = None
106 position = self._startPosition
108 self._respond(position)
110 def _on_motion(self, widget, event):
111 if self._is_cleared():
113 curPosition = event.get_coords()
115 curPosition[1] - self._startPosition[1],
116 curPosition[1] - self._startPosition[1],
118 delta = (dx ** 2 + dy ** 2) ** (0.5)
119 if self.MOVE_THRESHHOLD <= delta:
122 def _on_popup(self, widget):
125 self._respond(position)
128 class LoginWindow(object):
130 def __init__(self, widgetTree):
132 @note Thread agnostic
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")
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)
148 "on_loginbutton_clicked": self._on_loginbutton_clicked,
149 "on_loginclose_clicked": self._on_loginclose_clicked,
151 widgetTree.signal_autoconnect(callbackMapping)
153 def request_credentials(self, parentWindow = None):
157 if parentWindow is None:
158 parentWindow = self._parentWindow
160 self._serviceCombo.hide()
161 self._serviceList.clear()
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")
170 username = self._usernameEntry.get_text()
171 password = self._passwordEntry.get_text()
172 self._passwordEntry.set_text("")
176 return username, password
178 def request_credentials_from(self, services, parentWindow = None):
182 if parentWindow is None:
183 parentWindow = self._parentWindow
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()
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")
198 username = self._usernameEntry.get_text()
199 password = self._passwordEntry.get_text()
200 self._passwordEntry.set_text("")
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
209 def _on_loginbutton_clicked(self, *args):
210 self._dialog.response(gtk.RESPONSE_OK)
212 def _on_loginclose_clicked(self, *args):
213 self._dialog.response(gtk.RESPONSE_CANCEL)
216 class ErrorDisplay(object):
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()
225 self.__errorBox.connect("button_release_event", self._on_close)
228 self.__parentBox.remove(self.__errorBox)
230 def push_message_with_lock(self, message):
231 gtk.gdk.threads_enter()
233 self.push_message(message)
235 gtk.gdk.threads_leave()
237 def push_message(self, message):
238 if 0 < len(self.__messages):
239 self.__messages.append(message)
241 self.__show_message(message)
243 def push_exception(self, exception):
244 self.push_message(exception.message)
245 warnings.warn(exception, stacklevel=3)
247 def pop_message(self):
248 if 0 < len(self.__messages):
249 self.__show_message(self.__messages[0])
250 del self.__messages[0]
252 self.__hide_message()
254 def _on_close(self, *args):
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)
262 def __hide_message(self):
263 self.__errorDescription.set_text("")
264 self.__parentBox.remove(self.__errorBox)
267 class MessageBox(gtk.MessageDialog):
269 def __init__(self, message):
271 gtk.MessageDialog.__init__(
274 gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
279 self.set_default_response(gtk.RESPONSE_OK)
280 self.connect('response', self._handle_clicked)
282 def _handle_clicked(self, *args):
286 class MessageBox2(gtk.MessageDialog):
288 def __init__(self, message):
290 gtk.MessageDialog.__init__(
293 gtk.DIALOG_DESTROY_WITH_PARENT,
298 self.set_default_response(gtk.RESPONSE_OK)
299 self.connect('response', self._handle_clicked)
301 def _handle_clicked(self, *args):
305 class PopupCalendar(object):
307 def __init__(self, parent, eventTarget = coroutines.null_sink()):
308 self._eventTarget = eventTarget
310 self.__calendar = gtk.Calendar()
311 self.__calendar.connect("day-selected-double-click", self._on_date_select)
313 self.__popupWindow = gtk.Window(type = gtk.WINDOW_POPUP)
314 self.__popupWindow.set_title("")
315 self.__popupWindow.add(self.__calendar)
316 self.__popupWindow.set_transient_for(parent)
317 self.__popupWindow.set_modal(True)
320 year, month, day = self.__calendar.get_date()
321 month += 1 # Seems to be 0 indexed
322 return datetime.date(year, month, day)
325 self.__popupWindow.show_all()
327 def _on_date_select(self, *args):
328 self.__popupWindow.hide()
329 self._eventTarget.send((self, self.get_date()))
332 class EditTaskDialog(object):
334 def __init__(self, widgetTree):
335 self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
337 self._dialog = widgetTree.get_widget("editTaskDialog")
338 self._projectCombo = widgetTree.get_widget("edit-targetProjectCombo")
339 self._taskName = widgetTree.get_widget("edit-taskNameEntry")
340 self._pasteTaskNameButton = widgetTree.get_widget("edit-pasteTaskNameButton")
341 self._priorityChoiceCombo = widgetTree.get_widget("edit-priorityChoiceCombo")
342 self._dueDateDisplay = widgetTree.get_widget("edit-dueDateDisplay")
343 self._dueDateProperties = widgetTree.get_widget("edit-dueDateProperties")
344 self._clearDueDate = widgetTree.get_widget("edit-clearDueDate")
346 self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
347 self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
349 self._popupCalendar = PopupCalendar(self._dialog, coroutines.func_sink(self._update_duedate))
351 def enable(self, todoManager):
352 self._populate_projects(todoManager)
353 self._pasteTaskNameButton.connect("clicked", self._on_name_paste)
354 self._dueDateProperties.connect("clicked", self._on_choose_duedate)
355 self._clearDueDate.connect("clicked", self._on_clear_duedate)
357 self._addButton.connect("clicked", self._on_add_clicked)
358 self._cancelButton.connect("clicked", self._on_cancel_clicked)
361 self._pasteTaskNameButton.disconnect("clicked", self._on_name_paste)
362 self._dueDateProperties.disconnect("clicked", self._on_choose_duedate)
363 self._clearDueDate.disconnect("clicked", self._on_clear_duedate)
364 self._projectsList.clear()
365 self._projectCombo.set_model(None)
367 def request_task(self, todoManager, taskId, parentWindow = None):
368 if parentWindow is not None:
369 self._dialog.set_transient_for(parentWindow)
371 taskDetails = todoManager.get_task_details(taskId)
372 originalProjectId = taskDetails["projId"]
373 originalProjectName = todoManager.get_project(originalProjectId)["name"]
374 originalName = taskDetails["name"]
375 originalPriority = str(taskDetails["priority"].get_nothrow(0))
376 if taskDetails["dueDate"].is_good():
377 originalDue = taskDetails["dueDate"].get().strftime("%Y-%m-%d %H:%M:%S")
381 self._dialog.set_default_response(gtk.RESPONSE_OK)
382 self._taskName.set_text(originalName)
383 self._set_active_proj(originalProjectName)
384 self._priorityChoiceCombo.set_active(int(originalPriority))
385 self._dueDateDisplay.set_text(originalDue)
388 response = self._dialog.run()
389 if response != gtk.RESPONSE_OK:
390 raise RuntimeError("Edit Cancelled")
394 newProjectName = self._get_project(todoManager)
395 newName = self._taskName.get_text()
396 newPriority = self._get_priority()
397 newDueDate = self._dueDateDisplay.get_text()
399 isProjDifferent = newProjectName != originalProjectName
400 isNameDifferent = newName != originalName
401 isPriorityDifferent = newPriority != originalPriority
402 isDueDifferent = newDueDate != originalDue
405 newProjectId = todoManager.lookup_project(newProjectName)
406 todoManager.set_project(taskId, newProjectId)
409 todoManager.set_name(taskId, newName)
411 if isPriorityDifferent:
413 priority = toolbox.Optional(int(newPriority))
415 priority = toolbox.Optional()
416 todoManager.set_priority(taskId, priority)
420 due = datetime.datetime.strptime(newDueDate, "%Y-%m-%d %H:%M:%S")
421 due = toolbox.Optional(due)
423 due = toolbox.Optional()
425 todoManager.set_duedate(taskId, due)
429 "projId": isProjDifferent,
430 "name": isNameDifferent,
431 "priority": isPriorityDifferent,
432 "due": isDueDifferent,
435 def _populate_projects(self, todoManager):
436 for projectName in todoManager.get_projects():
437 row = (projectName["name"], )
438 self._projectsList.append(row)
439 self._projectCombo.set_model(self._projectsList)
440 cell = gtk.CellRendererText()
441 self._projectCombo.pack_start(cell, True)
442 self._projectCombo.add_attribute(cell, 'text', 0)
443 self._projectCombo.set_active(0)
445 def _set_active_proj(self, projName):
446 for i, row in enumerate(self._projectsList):
447 if row[0] == projName:
448 self._projectCombo.set_active(i)
451 raise ValueError("%s not in list" % projName)
453 def _get_project(self, todoManager):
454 name = self._projectCombo.get_active_text()
457 def _get_priority(self):
458 index = self._priorityChoiceCombo.get_active()
465 def _update_duedate(self, eventData):
466 widget, date = eventData
467 time = datetime.time()
468 dueDate = datetime.datetime.combine(date, time)
470 formttedDate = dueDate.strftime("%Y-%m-%d %H:%M:%S")
471 self._dueDateDisplay.set_text(formttedDate)
473 def _on_name_paste(self, *args):
474 clipboard = gtk.clipboard_get()
475 contents = clipboard.wait_for_text()
476 if contents is not None:
477 self._taskName.set_text(contents)
479 def _on_choose_duedate(self, *args):
480 self._popupCalendar.run()
482 def _on_clear_duedate(self, *args):
483 self._dueDateDisplay.set_text("")
485 def _on_add_clicked(self, *args):
486 self._dialog.response(gtk.RESPONSE_OK)
488 def _on_cancel_clicked(self, *args):
489 self._dialog.response(gtk.RESPONSE_CANCEL)
492 if __name__ == "__main__":
495 win.set_title("Tap'N'Hold")
496 eventBox = gtk.EventBox()
499 context = ContextHandler(eventBox, coroutines.printer_sink())
501 win.connect("destroy", lambda w: gtk.main_quit())