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, activationTarget = coroutines.null_sink()):
36 self._actionWidget = actionWidget
37 self._activationTarget = activationTarget
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._activationTarget.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)
127 class LoginWindow(object):
129 def __init__(self, widgetTree):
131 @note Thread agnostic
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")
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)
147 "on_loginbutton_clicked": self._on_loginbutton_clicked,
148 "on_loginclose_clicked": self._on_loginclose_clicked,
150 widgetTree.signal_autoconnect(callbackMapping)
152 def request_credentials(self, parentWindow = None):
156 if parentWindow is None:
157 parentWindow = self._parentWindow
159 self._serviceCombo.hide()
160 self._serviceList.clear()
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")
169 username = self._usernameEntry.get_text()
170 password = self._passwordEntry.get_text()
171 self._passwordEntry.set_text("")
175 return username, password
177 def request_credentials_from(self, services, parentWindow = None):
181 if parentWindow is None:
182 parentWindow = self._parentWindow
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()
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")
197 username = self._usernameEntry.get_text()
198 password = self._passwordEntry.get_text()
199 self._passwordEntry.set_text("")
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
208 def _on_loginbutton_clicked(self, *args):
209 self._dialog.response(gtk.RESPONSE_OK)
211 def _on_loginclose_clicked(self, *args):
212 self._dialog.response(gtk.RESPONSE_CANCEL)
215 class ErrorDisplay(object):
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()
224 self.__errorBox.connect("button_release_event", self._on_close)
227 self.__parentBox.remove(self.__errorBox)
229 def push_message_with_lock(self, message):
230 gtk.gdk.threads_enter()
232 self.push_message(message)
234 gtk.gdk.threads_leave()
236 def push_message(self, message):
237 if 0 < len(self.__messages):
238 self.__messages.append(message)
240 self.__show_message(message)
242 def push_exception(self, exception):
243 self.push_message(exception.message)
244 warnings.warn(exception, stacklevel=3)
246 def pop_message(self):
247 if 0 < len(self.__messages):
248 self.__show_message(self.__messages[0])
249 del self.__messages[0]
251 self.__hide_message()
253 def _on_close(self, *args):
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)
261 def __hide_message(self):
262 self.__errorDescription.set_text("")
263 self.__parentBox.remove(self.__errorBox)
266 class MessageBox(gtk.MessageDialog):
268 def __init__(self, message):
270 gtk.MessageDialog.__init__(
273 gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
278 self.set_default_response(gtk.RESPONSE_OK)
279 self.connect('response', self._handle_clicked)
281 def _handle_clicked(self, *args):
285 class MessageBox2(gtk.MessageDialog):
287 def __init__(self, message):
289 gtk.MessageDialog.__init__(
292 gtk.DIALOG_DESTROY_WITH_PARENT,
297 self.set_default_response(gtk.RESPONSE_OK)
298 self.connect('response', self._handle_clicked)
300 def _handle_clicked(self, *args):
304 class PopupCalendar(object):
306 def __init__(self, parent):
307 self.__calendar = gtk.Calendar()
308 self.__calendar.connect("day-selected-double-click", self._on_date_select)
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)
316 self.callback = lambda: None
319 year, month, day = self.__calendar.get_date()
320 month += 1 # Seems to be 0 indexed
321 return datetime.date(year, month, day)
324 self.__popupWindow.show_all()
326 def _on_date_select(self, *args):
327 self.__popupWindow.hide()
334 class EditTaskDialog(object):
336 def __init__(self, widgetTree):
337 self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
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")
348 self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
349 self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
351 self._popupCalendar = PopupCalendar(self._dialog)
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)
359 self._addButton.connect("clicked", self._on_add_clicked)
360 self._cancelButton.connect("clicked", self._on_cancel_clicked)
362 self._popupCalendar.callback = self._update_duedate
365 self._popupCalendar.callback = lambda: None
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)
373 def request_task(self, todoManager, taskId, parentWindow = None):
374 if parentWindow is not None:
375 self._dialog.set_transient_for(parentWindow)
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")
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)
394 response = self._dialog.run()
395 if response != gtk.RESPONSE_OK:
396 raise RuntimeError("Edit Cancelled")
400 newProjectName = self._get_project(todoManager)
401 newName = self._taskName.get_text()
402 newPriority = self._get_priority()
403 newDueDate = self._dueDateDisplay.get_text()
405 isProjDifferent = newProjectName != originalProjectName
406 isNameDifferent = newName != originalName
407 isPriorityDifferent = newPriority != originalPriority
408 isDueDifferent = newDueDate != originalDue
411 newProjectId = todoManager.lookup_project(newProjectName)
412 todoManager.set_project(taskId, newProjectId)
415 todoManager.set_name(taskId, newName)
417 if isPriorityDifferent:
419 priority = toolbox.Optional(int(newPriority))
421 priority = toolbox.Optional()
422 todoManager.set_priority(taskId, priority)
426 due = datetime.datetime.strptime(newDueDate, "%Y-%m-%d %H:%M:%S")
427 due = toolbox.Optional(due)
429 due = toolbox.Optional()
431 todoManager.set_duedate(taskId, due)
435 "projId": isProjDifferent,
436 "name": isNameDifferent,
437 "priority": isPriorityDifferent,
438 "due": isDueDifferent,
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)
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)
457 raise ValueError("%s not in list" % projName)
459 def _get_project(self, todoManager):
460 name = self._projectCombo.get_active_text()
463 def _get_priority(self):
464 index = self._priorityChoiceCombo.get_active()
471 def _update_duedate(self):
472 date = self._popupCalendar.get_date()
473 time = datetime.time()
474 dueDate = datetime.datetime.combine(date, time)
476 formttedDate = dueDate.strftime("%Y-%m-%d %H:%M:%S")
477 self._dueDateDisplay.set_text(formttedDate)
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)
485 def _on_choose_duedate(self, *args):
486 self._popupCalendar.run()
488 def _on_clear_duedate(self, *args):
489 self._dueDateDisplay.set_text("")
491 def _on_add_clicked(self, *args):
492 self._dialog.response(gtk.RESPONSE_OK)
494 def _on_cancel_clicked(self, *args):
495 self._dialog.response(gtk.RESPONSE_CANCEL)
498 if __name__ == "__main__":
501 win.set_title("Tap'N'Hold")
502 eventBox = gtk.EventBox()
505 context = ContextHandler(eventBox, coroutines.printer_sink())
507 win.connect("destroy", lambda w: gtk.main_quit())