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
23 @contextlib.contextmanager
25 gtk.gdk.threads_enter()
29 gtk.gdk.threads_leave()
32 class ContextHandler(object):
37 def __init__(self, actionWidget, eventTarget = coroutines.null_sink()):
38 self._actionWidget = actionWidget
39 self._eventTarget = eventTarget
41 self._actionPressId = None
42 self._actionReleaseId = None
43 self._motionNotifyId = None
44 self._popupMenuId = None
45 self._holdTimerId = None
47 self._respondOnRelease = False
48 self._startPosition = None
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)
58 self._actionWidget.disconnect(self._actionPressId)
59 self._actionWidget.disconnect(self._actionReleaseId)
60 self._actionWidget.disconnect(self._motionNotifyId)
61 self._actionWidget.disconnect(self._popupMenuId)
63 def _respond(self, position):
64 widgetPosition = 0, 0 # @todo Figure out how to get a widgets root position
66 widgetPosition[0] + position[0],
67 widgetPosition[1] + position[1],
69 self._eventTarget.send((self._actionWidget, responsePosition))
72 if self._holdTimerId is not None:
73 gobject.source_remove(self._holdTimerId)
74 self._respondOnRelease = False
75 self._startPosition = None
77 def _is_cleared(self):
78 return self._startPosition is None
80 def _on_press(self, widget, event):
81 if not self._is_cleared():
84 self._startPosition = event.get_coords()
87 self._holdTimerId = gobject.timeout_add(self.HOLD_TIMEOUT, self._on_hold_timeout)
90 self._respondOnRelease = True
92 def _on_release(self, widget, event):
93 if self._is_cleared():
96 if self._respondOnRelease:
97 position = self._startPosition
99 self._respond(position)
103 def _on_hold_timeout(self):
104 assert not self._is_cleared()
105 gobject.source_remove(self._holdTimerId)
106 self._holdTimerId = None
108 position = self._startPosition
110 self._respond(position)
112 def _on_motion(self, widget, event):
113 if self._is_cleared():
115 curPosition = event.get_coords()
117 curPosition[1] - self._startPosition[1],
118 curPosition[1] - self._startPosition[1],
120 delta = (dx ** 2 + dy ** 2) ** (0.5)
121 if self.MOVE_THRESHHOLD <= delta:
124 def _on_popup(self, widget):
127 self._respond(position)
130 class LoginWindow(object):
132 def __init__(self, widgetTree):
134 @note Thread agnostic
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")
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)
150 "on_loginbutton_clicked": self._on_loginbutton_clicked,
151 "on_loginclose_clicked": self._on_loginclose_clicked,
153 widgetTree.signal_autoconnect(callbackMapping)
155 def request_credentials(self, parentWindow = None):
159 if parentWindow is None:
160 parentWindow = self._parentWindow
162 self._serviceCombo.hide()
163 self._serviceList.clear()
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")
172 username = self._usernameEntry.get_text()
173 password = self._passwordEntry.get_text()
174 self._passwordEntry.set_text("")
178 return username, password
180 def request_credentials_from(self, services, parentWindow = None):
184 if parentWindow is None:
185 parentWindow = self._parentWindow
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()
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")
200 username = self._usernameEntry.get_text()
201 password = self._passwordEntry.get_text()
202 self._passwordEntry.set_text("")
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
211 def _on_loginbutton_clicked(self, *args):
212 self._dialog.response(gtk.RESPONSE_OK)
214 def _on_loginclose_clicked(self, *args):
215 self._dialog.response(gtk.RESPONSE_CANCEL)
218 class ErrorDisplay(object):
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()
227 self.__errorBox.connect("button_release_event", self._on_close)
230 self.__parentBox.remove(self.__errorBox)
232 def push_message_with_lock(self, message):
233 gtk.gdk.threads_enter()
235 self.push_message(message)
237 gtk.gdk.threads_leave()
239 def push_message(self, message):
240 if 0 < len(self.__messages):
241 self.__messages.append(message)
243 self.__show_message(message)
245 def push_exception(self, exception = None):
246 if exception is None:
247 userMessage = sys.exc_value.message
248 warningMessage = traceback.format_exc()
250 userMessage = exception.message
251 warningMessage = exception
252 self.push_message(userMessage)
253 warnings.warn(warningMessage, stacklevel=3)
255 def pop_message(self):
256 if 0 < len(self.__messages):
257 self.__show_message(self.__messages[0])
258 del self.__messages[0]
260 self.__hide_message()
262 def _on_close(self, *args):
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)
270 def __hide_message(self):
271 self.__errorDescription.set_text("")
272 self.__parentBox.remove(self.__errorBox)
275 class MessageBox(gtk.MessageDialog):
277 def __init__(self, message):
279 gtk.MessageDialog.__init__(
282 gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
287 self.set_default_response(gtk.RESPONSE_OK)
288 self.connect('response', self._handle_clicked)
290 def _handle_clicked(self, *args):
294 class MessageBox2(gtk.MessageDialog):
296 def __init__(self, message):
298 gtk.MessageDialog.__init__(
301 gtk.DIALOG_DESTROY_WITH_PARENT,
306 self.set_default_response(gtk.RESPONSE_OK)
307 self.connect('response', self._handle_clicked)
309 def _handle_clicked(self, *args):
313 class PopupCalendar(object):
315 def __init__(self, parent, displayDate):
316 self._displayDate = displayDate
318 self._calendar = gtk.Calendar()
319 self._calendar.select_month(self._displayDate.month, self._displayDate.year)
320 self._calendar.select_day(self._displayDate.day)
321 self._calendar.set_display_options(
322 gtk.CALENDAR_SHOW_HEADING |
323 gtk.CALENDAR_SHOW_DAY_NAMES |
324 gtk.CALENDAR_NO_MONTH_CHANGE |
327 self._calendar.connect("day-selected", self._on_day_selected)
329 self._popupWindow = gtk.Window()
330 self._popupWindow.set_title("")
331 self._popupWindow.add(self._calendar)
332 self._popupWindow.set_transient_for(parent)
333 self._popupWindow.set_modal(True)
336 self._popupWindow.show_all()
338 def _on_day_selected(self, *args):
340 self._calendar.select_month(self._displayDate.month, self._displayDate.year)
341 self._calendar.select_day(self._displayDate.day)
342 except StandardError, e:
343 warnings.warn(e.message)
346 class EditTaskDialog(object):
348 def __init__(self, widgetTree):
349 self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
351 self._dialog = widgetTree.get_widget("editTaskDialog")
352 self._projectCombo = widgetTree.get_widget("edit-targetProjectCombo")
353 self._taskName = widgetTree.get_widget("edit-taskNameEntry")
354 self._pasteTaskNameButton = widgetTree.get_widget("edit-pasteTaskNameButton")
355 self._priorityChoiceCombo = widgetTree.get_widget("edit-priorityChoiceCombo")
356 self._dueDateDisplay = widgetTree.get_widget("edit-dueDateCalendar")
357 self._clearDueDate = widgetTree.get_widget("edit-clearDueDate")
359 self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
360 self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
362 self._onPasteTaskId = None
363 self._onClearDueDateId = None
365 self._onCancelId = None
367 def enable(self, todoManager):
368 self._populate_projects(todoManager)
370 self._onPasteTaskId = self._pasteTaskNameButton.connect("clicked", self._on_name_paste)
371 self._onClearDueDateId = self._clearDueDate.connect("clicked", self._on_clear_duedate)
372 self._onAddId = self._addButton.connect("clicked", self._on_add_clicked)
373 self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked)
376 self._pasteTaskNameButton.disconnect(self._onPasteTaskId)
377 self._clearDueDate.disconnect(self._onClearDueDateId)
378 self._addButton.disconnect(self._onAddId)
379 self._cancelButton.disconnect(self._onAddId)
381 self._projectsList.clear()
382 self._projectCombo.set_model(None)
384 def request_task(self, todoManager, taskId, parentWindow = None):
385 if parentWindow is not None:
386 self._dialog.set_transient_for(parentWindow)
388 taskDetails = todoManager.get_task_details(taskId)
389 originalProjectId = taskDetails["projId"]
390 originalProjectName = todoManager.get_project(originalProjectId)["name"]
391 originalName = taskDetails["name"]
392 originalPriority = str(taskDetails["priority"].get_nothrow(0))
393 if taskDetails["dueDate"].is_good():
394 originalDue = taskDetails["dueDate"].get()
398 self._dialog.set_default_response(gtk.RESPONSE_OK)
399 self._taskName.set_text(originalName)
400 self._set_active_proj(originalProjectName)
401 self._priorityChoiceCombo.set_active(int(originalPriority))
402 if originalDue is not None:
403 # Months are 0 indexed
404 self._dueDateDisplay.select_month(originalDue.month - 1, originalDue.year)
405 self._dueDateDisplay.select_day(originalDue.day)
407 now = datetime.datetime.now()
408 self._dueDateDisplay.select_month(now.month, now.year)
409 self._dueDateDisplay.select_day(0)
412 response = self._dialog.run()
413 if response != gtk.RESPONSE_OK:
414 raise RuntimeError("Edit Cancelled")
418 newProjectName = self._get_project(todoManager)
419 newName = self._taskName.get_text()
420 newPriority = self._get_priority()
421 year, month, day = self._dueDateDisplay.get_date()
423 # Months are 0 indexed
424 date = datetime.date(year, month + 1, day)
425 time = datetime.time()
426 newDueDate = datetime.datetime.combine(date, time)
430 isProjDifferent = newProjectName != originalProjectName
431 isNameDifferent = newName != originalName
432 isPriorityDifferent = newPriority != originalPriority
433 isDueDifferent = newDueDate != originalDue
436 newProjectId = todoManager.lookup_project(newProjectName)
437 todoManager.set_project(taskId, newProjectId)
440 todoManager.set_name(taskId, newName)
442 if isPriorityDifferent:
444 priority = toolbox.Optional(int(newPriority))
446 priority = toolbox.Optional()
447 todoManager.set_priority(taskId, priority)
451 due = toolbox.Optional(newDueDate)
453 due = toolbox.Optional()
455 todoManager.set_duedate(taskId, due)
459 "projId": isProjDifferent,
460 "name": isNameDifferent,
461 "priority": isPriorityDifferent,
462 "due": isDueDifferent,
465 def _populate_projects(self, todoManager):
466 for projectName in todoManager.get_projects():
467 row = (projectName["name"], )
468 self._projectsList.append(row)
469 self._projectCombo.set_model(self._projectsList)
470 cell = gtk.CellRendererText()
471 self._projectCombo.pack_start(cell, True)
472 self._projectCombo.add_attribute(cell, 'text', 0)
473 self._projectCombo.set_active(0)
475 def _set_active_proj(self, projName):
476 for i, row in enumerate(self._projectsList):
477 if row[0] == projName:
478 self._projectCombo.set_active(i)
481 raise ValueError("%s not in list" % projName)
483 def _get_project(self, todoManager):
484 name = self._projectCombo.get_active_text()
487 def _get_priority(self):
488 index = self._priorityChoiceCombo.get_active()
495 def _on_name_paste(self, *args):
496 clipboard = gtk.clipboard_get()
497 contents = clipboard.wait_for_text()
498 if contents is not None:
499 self._taskName.set_text(contents)
501 def _on_clear_duedate(self, *args):
502 self._dueDateDisplay.select_day(0)
504 def _on_add_clicked(self, *args):
505 self._dialog.response(gtk.RESPONSE_OK)
507 def _on_cancel_clicked(self, *args):
508 self._dialog.response(gtk.RESPONSE_CANCEL)
511 if __name__ == "__main__":
514 win.set_title("Tap'N'Hold")
515 eventBox = gtk.EventBox()
518 context = ContextHandler(eventBox, coroutines.printer_sink())
520 win.connect("destroy", lambda w: gtk.main_quit())
525 cal = PopupCalendar(None, datetime.datetime.now())
526 cal._popupWindow.connect("destroy", lambda w: gtk.main_quit())