4 @todo Implement my own tap-n-hold
7 Callback function with default to a "callback widget" that places the widget in the appropriate place
8 Change of system default (class variable) for hold time
9 Follows enable/disable pattern
10 Cares about mouse move for disqualifying a tap
11 @todo Implement a custom label that encodes random info (Not set in stone, just throwing these out there)
12 Background color based on Location
13 Text intensity based on time estimate
14 Short/long version with long including tags colored specially
28 @contextlib.contextmanager
30 gtk.gdk.threads_enter()
34 gtk.gdk.threads_leave()
37 class ContextHandler(object):
42 def __init__(self, actionWidget, activationTarget = coroutines.null_sink()):
43 self._actionWidget = actionWidget
44 self._activationTarget = activationTarget
46 self._actionPressId = None
47 self._actionReleaseId = None
48 self._motionNotifyId = None
49 self._popupMenuId = None
50 self._holdTimerId = None
52 self._respondOnRelease = False
53 self._startPosition = None
56 self._actionPressId = self._actionWidget.connect("button-press-event", self._on_press)
57 self._actionReleaseId = self._actionWidget.connect("button-release-event", self._on_release)
58 self._motionNotifyId = self._actionWidget.connect("motion-notify-event", self._on_motion)
59 self._popupMenuId = self._actionWidget.connect("popup-menu", self._on_popup)
63 self._actionWidget.disconnect(self._actionPressId)
64 self._actionWidget.disconnect(self._actionReleaseId)
65 self._actionWidget.disconnect(self._motionNotifyId)
66 self._actionWidget.disconnect(self._popupMenuId)
68 def _respond(self, position):
69 widgetPosition = 0, 0 # @todo Figure out how to get a widgets root position
71 widgetPosition[0] + position[0],
72 widgetPosition[1] + position[1],
74 self._activationTarget.send((self._actionWidget, responsePosition))
77 if self._holdTimerId is not None:
78 gobject.source_remove(self._holdTimerId)
79 self._respondOnRelease = False
80 self._startPosition = None
82 def _is_cleared(self):
83 return self._startPosition is None
85 def _on_press(self, widget, event):
86 if not self._is_cleared():
89 self._startPosition = event.get_coords()
92 self._holdTimerId = gobject.timeout_add(self.HOLD_TIMEOUT, self._on_hold_timeout)
95 self._respondOnRelease = True
97 def _on_release(self, widget, event):
98 if self._is_cleared():
101 if self._respondOnRelease:
102 position = self._startPosition
104 self._respond(position)
108 def _on_hold_timeout(self):
109 assert not self._is_cleared()
110 gobject.source_remove(self._holdTimerId)
111 self._holdTimerId = None
113 position = self._startPosition
115 self._respond(position)
117 def _on_motion(self, widget, event):
118 if self._is_cleared():
120 curPosition = event.get_coords()
122 curPosition[1] - self._startPosition[1],
123 curPosition[1] - self._startPosition[1],
125 delta = (dx ** 2 + dy ** 2) ** (0.5)
126 if self.MOVE_THRESHHOLD <= delta:
129 def _on_popup(self, widget):
132 self._respond(position)
134 class LoginWindow(object):
136 def __init__(self, widgetTree):
138 @note Thread agnostic
140 self._dialog = widgetTree.get_widget("loginDialog")
141 self._parentWindow = widgetTree.get_widget("mainWindow")
142 self._serviceCombo = widgetTree.get_widget("serviceCombo")
143 self._usernameEntry = widgetTree.get_widget("usernameentry")
144 self._passwordEntry = widgetTree.get_widget("passwordentry")
146 self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING)
147 self._serviceCombo.set_model(self._serviceList)
148 cell = gtk.CellRendererText()
149 self._serviceCombo.pack_start(cell, True)
150 self._serviceCombo.add_attribute(cell, 'text', 1)
151 self._serviceCombo.set_active(0)
154 "on_loginbutton_clicked": self._on_loginbutton_clicked,
155 "on_loginclose_clicked": self._on_loginclose_clicked,
157 widgetTree.signal_autoconnect(callbackMapping)
159 def request_credentials(self, parentWindow = None):
163 if parentWindow is None:
164 parentWindow = self._parentWindow
166 self._serviceCombo.hide()
167 self._serviceList.clear()
170 self._dialog.set_transient_for(parentWindow)
171 self._dialog.set_default_response(gtk.RESPONSE_OK)
172 response = self._dialog.run()
173 if response != gtk.RESPONSE_OK:
174 raise RuntimeError("Login Cancelled")
176 username = self._usernameEntry.get_text()
177 password = self._passwordEntry.get_text()
178 self._passwordEntry.set_text("")
182 return username, password
184 def request_credentials_from(self, services, parentWindow = None):
188 if parentWindow is None:
189 parentWindow = self._parentWindow
191 self._serviceList.clear()
192 for serviceIdserviceName in services.iteritems():
193 self._serviceList.append(serviceIdserviceName)
194 self._serviceCombo.set_active(0)
195 self._serviceCombo.show()
198 self._dialog.set_transient_for(parentWindow)
199 self._dialog.set_default_response(gtk.RESPONSE_OK)
200 response = self._dialog.run()
201 if response != gtk.RESPONSE_OK:
202 raise RuntimeError("Login Cancelled")
204 username = self._usernameEntry.get_text()
205 password = self._passwordEntry.get_text()
206 self._passwordEntry.set_text("")
210 itr = self._serviceCombo.get_active_iter()
211 serviceId = int(self._serviceList.get_value(itr, 0))
212 self._serviceList.clear()
213 return serviceId, username, password
215 def _on_loginbutton_clicked(self, *args):
216 self._dialog.response(gtk.RESPONSE_OK)
218 def _on_loginclose_clicked(self, *args):
219 self._dialog.response(gtk.RESPONSE_CANCEL)
222 class ErrorDisplay(object):
224 def __init__(self, widgetTree):
225 super(ErrorDisplay, self).__init__()
226 self.__errorBox = widgetTree.get_widget("errorEventBox")
227 self.__errorDescription = widgetTree.get_widget("errorDescription")
228 self.__errorClose = widgetTree.get_widget("errorClose")
229 self.__parentBox = self.__errorBox.get_parent()
231 self.__errorBox.connect("button_release_event", self._on_close)
234 self.__parentBox.remove(self.__errorBox)
236 def push_message_with_lock(self, message):
237 gtk.gdk.threads_enter()
239 self.push_message(message)
241 gtk.gdk.threads_leave()
243 def push_message(self, message):
244 if 0 < len(self.__messages):
245 self.__messages.append(message)
247 self.__show_message(message)
249 def push_exception(self, exception):
250 self.push_message(exception.message)
251 warnings.warn(exception, stacklevel=3)
253 def pop_message(self):
254 if 0 < len(self.__messages):
255 self.__show_message(self.__messages[0])
256 del self.__messages[0]
258 self.__hide_message()
260 def _on_close(self, *args):
263 def __show_message(self, message):
264 self.__errorDescription.set_text(message)
265 self.__parentBox.pack_start(self.__errorBox, False, False)
266 self.__parentBox.reorder_child(self.__errorBox, 1)
268 def __hide_message(self):
269 self.__errorDescription.set_text("")
270 self.__parentBox.remove(self.__errorBox)
273 class MessageBox(gtk.MessageDialog):
275 def __init__(self, message):
277 gtk.MessageDialog.__init__(
280 gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
285 self.set_default_response(gtk.RESPONSE_OK)
286 self.connect('response', self._handle_clicked)
288 def _handle_clicked(self, *args):
292 class MessageBox2(gtk.MessageDialog):
294 def __init__(self, message):
296 gtk.MessageDialog.__init__(
299 gtk.DIALOG_DESTROY_WITH_PARENT,
304 self.set_default_response(gtk.RESPONSE_OK)
305 self.connect('response', self._handle_clicked)
307 def _handle_clicked(self, *args):
311 class PopupCalendar(object):
313 def __init__(self, parent):
314 self.__calendar = gtk.Calendar()
315 self.__calendar.connect("day-selected-double-click", self._on_date_select)
317 self.__popupWindow = gtk.Window(type = gtk.WINDOW_POPUP)
318 self.__popupWindow.set_title("")
319 self.__popupWindow.add(self.__calendar)
320 self.__popupWindow.set_transient_for(parent)
321 self.__popupWindow.set_modal(True)
323 self.callback = lambda: None
326 year, month, day = self.__calendar.get_date()
327 month += 1 # Seems to be 0 indexed
328 return datetime.date(year, month, day)
331 self.__popupWindow.show_all()
333 def _on_date_select(self, *args):
334 self.__popupWindow.hide()
341 class EditTaskDialog(object):
343 def __init__(self, widgetTree):
344 self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
346 self._dialog = widgetTree.get_widget("editTaskDialog")
347 self._projectCombo = widgetTree.get_widget("edit-targetProjectCombo")
348 self._taskName = widgetTree.get_widget("edit-taskNameEntry")
349 self._pasteTaskNameButton = widgetTree.get_widget("edit-pasteTaskNameButton")
350 self._priorityChoiceCombo = widgetTree.get_widget("edit-priorityChoiceCombo")
351 self._dueDateDisplay = widgetTree.get_widget("edit-dueDateDisplay")
352 self._dueDateProperties = widgetTree.get_widget("edit-dueDateProperties")
353 self._clearDueDate = widgetTree.get_widget("edit-clearDueDate")
355 self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
356 self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
358 self._popupCalendar = PopupCalendar(self._dialog)
360 def enable(self, todoManager):
361 self._populate_projects(todoManager)
362 self._pasteTaskNameButton.connect("clicked", self._on_name_paste)
363 self._dueDateProperties.connect("clicked", self._on_choose_duedate)
364 self._clearDueDate.connect("clicked", self._on_clear_duedate)
366 self._addButton.connect("clicked", self._on_add_clicked)
367 self._cancelButton.connect("clicked", self._on_cancel_clicked)
369 self._popupCalendar.callback = self._update_duedate
372 self._popupCalendar.callback = lambda: None
374 self._pasteTaskNameButton.disconnect("clicked", self._on_name_paste)
375 self._dueDateProperties.disconnect("clicked", self._on_choose_duedate)
376 self._clearDueDate.disconnect("clicked", self._on_clear_duedate)
377 self._projectsList.clear()
378 self._projectCombo.set_model(None)
380 def request_task(self, todoManager, taskId, parentWindow = None):
381 if parentWindow is not None:
382 self._dialog.set_transient_for(parentWindow)
384 taskDetails = todoManager.get_task_details(taskId)
385 originalProjectId = taskDetails["projId"]
386 originalProjectName = todoManager.get_project(originalProjectId)["name"]
387 originalName = taskDetails["name"]
388 originalPriority = str(taskDetails["priority"].get_nothrow(0))
389 if taskDetails["dueDate"].is_good():
390 originalDue = taskDetails["dueDate"].get().strftime("%Y-%m-%d %H:%M:%S")
394 self._dialog.set_default_response(gtk.RESPONSE_OK)
395 self._taskName.set_text(originalName)
396 self._set_active_proj(originalProjectName)
397 self._priorityChoiceCombo.set_active(int(originalPriority))
398 self._dueDateDisplay.set_text(originalDue)
401 response = self._dialog.run()
402 if response != gtk.RESPONSE_OK:
403 raise RuntimeError("Edit Cancelled")
407 newProjectName = self._get_project(todoManager)
408 newName = self._taskName.get_text()
409 newPriority = self._get_priority()
410 newDueDate = self._dueDateDisplay.get_text()
412 isProjDifferent = newProjectName != originalProjectName
413 isNameDifferent = newName != originalName
414 isPriorityDifferent = newPriority != originalPriority
415 isDueDifferent = newDueDate != originalDue
418 newProjectId = todoManager.lookup_project(newProjectName)
419 todoManager.set_project(taskId, newProjectId)
422 todoManager.set_name(taskId, newName)
424 if isPriorityDifferent:
426 priority = toolbox.Optional(int(newPriority))
428 priority = toolbox.Optional()
429 todoManager.set_priority(taskId, priority)
433 due = datetime.datetime.strptime(newDueDate, "%Y-%m-%d %H:%M:%S")
434 due = toolbox.Optional(due)
436 due = toolbox.Optional()
438 todoManager.set_duedate(taskId, due)
442 "projId": isProjDifferent,
443 "name": isNameDifferent,
444 "priority": isPriorityDifferent,
445 "due": isDueDifferent,
448 def _populate_projects(self, todoManager):
449 for projectName in todoManager.get_projects():
450 row = (projectName["name"], )
451 self._projectsList.append(row)
452 self._projectCombo.set_model(self._projectsList)
453 cell = gtk.CellRendererText()
454 self._projectCombo.pack_start(cell, True)
455 self._projectCombo.add_attribute(cell, 'text', 0)
456 self._projectCombo.set_active(0)
458 def _set_active_proj(self, projName):
459 for i, row in enumerate(self._projectsList):
460 if row[0] == projName:
461 self._projectCombo.set_active(i)
464 raise ValueError("%s not in list" % projName)
466 def _get_project(self, todoManager):
467 name = self._projectCombo.get_active_text()
470 def _get_priority(self):
471 index = self._priorityChoiceCombo.get_active()
478 def _update_duedate(self):
479 date = self._popupCalendar.get_date()
480 time = datetime.time()
481 dueDate = datetime.datetime.combine(date, time)
483 formttedDate = dueDate.strftime("%Y-%m-%d %H:%M:%S")
484 self._dueDateDisplay.set_text(formttedDate)
486 def _on_name_paste(self, *args):
487 clipboard = gtk.clipboard_get()
488 contents = clipboard.wait_for_text()
489 if contents is not None:
490 self._taskName.set_text(contents)
492 def _on_choose_duedate(self, *args):
493 self._popupCalendar.run()
495 def _on_clear_duedate(self, *args):
496 self._dueDateDisplay.set_text("")
498 def _on_add_clicked(self, *args):
499 self._dialog.response(gtk.RESPONSE_OK)
501 def _on_cancel_clicked(self, *args):
502 self._dialog.response(gtk.RESPONSE_CANCEL)
505 if __name__ == "__main__":
508 win.set_title("Tap'N'Hold")
509 eventBox = gtk.EventBox()
512 context = ContextHandler(eventBox, coroutines.printer_sink())
514 win.connect("destroy", lambda w: gtk.main_quit())