More wild brainstorming
[doneit] / src / gtk_toolbox.py
1 #!/usr/bin/python
2
3 """
4 @todo Implement my own tap-n-hold
5         Constructor takes
6                 a widget (Event Box)
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
15 """
16
17 import warnings
18 import contextlib
19
20 import gobject
21 import gtk
22
23
24 @contextlib.contextmanager
25 def gtk_lock():
26         gtk.gdk.threads_enter()
27         try:
28                 yield
29         finally:
30                 gtk.gdk.threads_leave()
31
32
33 class LoginWindow(object):
34
35         def __init__(self, widgetTree):
36                 """
37                 @note Thread agnostic
38                 """
39                 self._dialog = widgetTree.get_widget("loginDialog")
40                 self._parentWindow = widgetTree.get_widget("mainWindow")
41                 self._serviceCombo = widgetTree.get_widget("serviceCombo")
42                 self._usernameEntry = widgetTree.get_widget("usernameentry")
43                 self._passwordEntry = widgetTree.get_widget("passwordentry")
44
45                 self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING)
46                 self._serviceCombo.set_model(self._serviceList)
47                 cell = gtk.CellRendererText()
48                 self._serviceCombo.pack_start(cell, True)
49                 self._serviceCombo.add_attribute(cell, 'text', 1)
50                 self._serviceCombo.set_active(0)
51
52                 callbackMapping = {
53                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
54                         "on_loginclose_clicked": self._on_loginclose_clicked,
55                 }
56                 widgetTree.signal_autoconnect(callbackMapping)
57
58         def request_credentials(self, parentWindow = None):
59                 """
60                 @note UI Thread
61                 """
62                 if parentWindow is None:
63                         parentWindow = self._parentWindow
64
65                 self._serviceCombo.hide()
66                 self._serviceList.clear()
67
68                 try:
69                         self._dialog.set_transient_for(parentWindow)
70                         self._dialog.set_default_response(gtk.RESPONSE_OK)
71                         response = self._dialog.run()
72                         if response != gtk.RESPONSE_OK:
73                                 raise RuntimeError("Login Cancelled")
74
75                         username = self._usernameEntry.get_text()
76                         password = self._passwordEntry.get_text()
77                         self._passwordEntry.set_text("")
78                 finally:
79                         self._dialog.hide()
80
81                 return username, password
82
83         def request_credentials_from(self, services, parentWindow = None):
84                 """
85                 @note UI Thread
86                 """
87                 if parentWindow is None:
88                         parentWindow = self._parentWindow
89
90                 self._serviceList.clear()
91                 for serviceIdserviceName in services.iteritems():
92                         self._serviceList.append(serviceIdserviceName)
93                 self._serviceCombo.set_active(0)
94                 self._serviceCombo.show()
95
96                 try:
97                         self._dialog.set_transient_for(parentWindow)
98                         self._dialog.set_default_response(gtk.RESPONSE_OK)
99                         response = self._dialog.run()
100                         if response != gtk.RESPONSE_OK:
101                                 raise RuntimeError("Login Cancelled")
102
103                         username = self._usernameEntry.get_text()
104                         password = self._passwordEntry.get_text()
105                         self._passwordEntry.set_text("")
106                 finally:
107                         self._dialog.hide()
108
109                 itr = self._serviceCombo.get_active_iter()
110                 serviceId = int(self._serviceList.get_value(itr, 0))
111                 self._serviceList.clear()
112                 return serviceId, username, password
113
114         def _on_loginbutton_clicked(self, *args):
115                 self._dialog.response(gtk.RESPONSE_OK)
116
117         def _on_loginclose_clicked(self, *args):
118                 self._dialog.response(gtk.RESPONSE_CANCEL)
119
120
121 class ErrorDisplay(object):
122
123         def __init__(self, widgetTree):
124                 super(ErrorDisplay, self).__init__()
125                 self.__errorBox = widgetTree.get_widget("errorEventBox")
126                 self.__errorDescription = widgetTree.get_widget("errorDescription")
127                 self.__errorClose = widgetTree.get_widget("errorClose")
128                 self.__parentBox = self.__errorBox.get_parent()
129
130                 self.__errorBox.connect("button_release_event", self._on_close)
131
132                 self.__messages = []
133                 self.__parentBox.remove(self.__errorBox)
134
135         def push_message_with_lock(self, message):
136                 gtk.gdk.threads_enter()
137                 try:
138                         self.push_message(message)
139                 finally:
140                         gtk.gdk.threads_leave()
141
142         def push_message(self, message):
143                 if 0 < len(self.__messages):
144                         self.__messages.append(message)
145                 else:
146                         self.__show_message(message)
147
148         def push_exception(self, exception):
149                 self.push_message(exception.message)
150                 warnings.warn(exception, stacklevel=3)
151
152         def pop_message(self):
153                 if 0 < len(self.__messages):
154                         self.__show_message(self.__messages[0])
155                         del self.__messages[0]
156                 else:
157                         self.__hide_message()
158
159         def _on_close(self, *args):
160                 self.pop_message()
161
162         def __show_message(self, message):
163                 self.__errorDescription.set_text(message)
164                 self.__parentBox.pack_start(self.__errorBox, False, False)
165                 self.__parentBox.reorder_child(self.__errorBox, 1)
166
167         def __hide_message(self):
168                 self.__errorDescription.set_text("")
169                 self.__parentBox.remove(self.__errorBox)
170
171
172 class MessageBox(gtk.MessageDialog):
173
174         def __init__(self, message):
175                 parent = None
176                 gtk.MessageDialog.__init__(
177                         self,
178                         parent,
179                         gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
180                         gtk.MESSAGE_ERROR,
181                         gtk.BUTTONS_OK,
182                         message,
183                 )
184                 self.set_default_response(gtk.RESPONSE_OK)
185                 self.connect('response', self._handle_clicked)
186
187         def _handle_clicked(self, *args):
188                 self.destroy()
189
190
191 class MessageBox2(gtk.MessageDialog):
192
193         def __init__(self, message):
194                 parent = None
195                 gtk.MessageDialog.__init__(
196                         self,
197                         parent,
198                         gtk.DIALOG_DESTROY_WITH_PARENT,
199                         gtk.MESSAGE_ERROR,
200                         gtk.BUTTONS_OK,
201                         message,
202                 )
203                 self.set_default_response(gtk.RESPONSE_OK)
204                 self.connect('response', self._handle_clicked)
205
206         def _handle_clicked(self, *args):
207                 self.destroy()
208
209
210 class PopupCalendar(object):
211
212         def __init__(self, parent):
213                 self.__calendar = gtk.Calendar()
214                 self.__calendar.connect("day-selected-double-click", self._on_date_select)
215
216                 self.__popupWindow = gtk.Window(type = gtk.WINDOW_POPUP)
217                 self.__popupWindow.set_title("")
218                 self.__popupWindow.add(self.__calendar)
219                 self.__popupWindow.set_transient_for(parent)
220                 self.__popupWindow.set_modal(True)
221
222         def get_date(self):
223                 year, month, day = self.__calendar.get_date()
224                 month += 1 # Seems to be 0 indexed
225                 return year, month, day
226
227         def run(self):
228                 self.__popupWindow.show_all()
229
230         def _on_date_select(self, *args):
231                 self.__popupWindow.hide()
232                 self.callback()
233
234         def callback(self):
235                 pass
236
237
238 class EditTaskDialog(object):
239
240         def __init__(self, widgetTree):
241                 self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
242
243                 self._dialog = widgetTree.get_widget("editTaskDialog")
244                 self._projectCombo = widgetTree.get_widget("edit-targetProjectCombo")
245                 self._taskName = widgetTree.get_widget("edit-taskNameEntry")
246                 self._pasteTaskNameButton = widgetTree.get_widget("edit-pasteTaskNameButton")
247                 self._priorityChoiceCombo = widgetTree.get_widget("edit-priorityChoiceCombo")
248                 self._dueDateDisplay = widgetTree.get_widget("edit-dueDateDisplay")
249                 self._dueDateProperties = widgetTree.get_widget("edit-dueDateProperties")
250                 self._clearDueDate = widgetTree.get_widget("edit-clearDueDate")
251
252                 self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
253                 self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
254
255                 self._popupCalendar = PopupCalendar(self._dialog)
256
257         def enable(self, todoManager):
258                 self._populate_projects(todoManager)
259                 self._pasteTaskNameButton.connect("clicked", self._on_name_paste)
260                 self._dueDateProperties.connect("clicked", self._on_choose_duedate)
261                 self._clearDueDate.connect("clicked", self._on_clear_duedate)
262
263                 self._addButton.connect("clicked", self._on_add_clicked)
264                 self._cancelButton.connect("clicked", self._on_cancel_clicked)
265
266                 self._popupCalendar.callback = self._update_duedate
267
268         def disable(self):
269                 self._popupCalendar.callback = lambda: None
270
271                 self._pasteTaskNameButton.disconnect("clicked", self._on_name_paste)
272                 self._dueDateProperties.disconnect("clicked", self._on_choose_duedate)
273                 self._clearDueDate.disconnect("clicked", self._on_clear_duedate)
274                 self._projectsList.clear()
275                 self._projectCombo.set_model(None)
276
277         def request_task(self, todoManager, taskId, parentWindow = None):
278                 if parentWindow is not None:
279                         self._dialog.set_transient_for(parentWindow)
280
281                 taskDetails = todoManager.get_task_details(taskId)
282                 originalProjectId = taskDetails["projId"]
283                 originalProjectName = todoManager.get_project(originalProjectId)["name"]
284                 originalName = taskDetails["name"]
285                 try:
286                         originalPriority = int(taskDetails["priority"])
287                 except ValueError:
288                         originalPriority = 0
289                 originalDue = taskDetails["due"]
290
291                 self._dialog.set_default_response(gtk.RESPONSE_OK)
292                 self._taskName.set_text(originalName)
293                 self._set_active_proj(originalProjectName)
294                 self._priorityChoiceCombo.set_active(originalPriority)
295                 self._dueDateDisplay.set_text(originalDue)
296
297                 try:
298                         response = self._dialog.run()
299                         if response != gtk.RESPONSE_OK:
300                                 raise RuntimeError("Edit Cancelled")
301                 finally:
302                         self._dialog.hide()
303
304                 newProjectName = self._get_project(todoManager)
305                 newName = self._taskName.get_text()
306                 newPriority = self._get_priority()
307                 newDueDate = self._dueDateDisplay.get_text()
308
309                 isProjDifferent = newProjectName != originalProjectName
310                 isNameDifferent = newName != originalName
311                 isPriorityDifferent = newPriority != originalPriority
312                 isDueDifferent = newDueDate != originalDue
313
314                 if isProjDifferent:
315                         newProjectId = todoManager.lookup_project(newProjectName)
316                         todoManager.set_project(taskId, newProjectId)
317                         print "PROJ CHANGE"
318                 if isNameDifferent:
319                         todoManager.set_name(taskId, newName)
320                         print "NAME CHANGE"
321                 if isPriorityDifferent:
322                         todoManager.set_priority(taskId, newPriority)
323                         print "PRIO CHANGE"
324                 if isDueDifferent:
325                         todoManager.set_duedate(taskId, newDueDate)
326                         print "DUE CHANGE"
327
328                 return {
329                         "projId": isProjDifferent,
330                         "name": isNameDifferent,
331                         "priority": isPriorityDifferent,
332                         "due": isDueDifferent,
333                 }
334
335         def _populate_projects(self, todoManager):
336                 for projectName in todoManager.get_projects():
337                         row = (projectName["name"], )
338                         self._projectsList.append(row)
339                 self._projectCombo.set_model(self._projectsList)
340                 cell = gtk.CellRendererText()
341                 self._projectCombo.pack_start(cell, True)
342                 self._projectCombo.add_attribute(cell, 'text', 0)
343                 self._projectCombo.set_active(0)
344
345         def _set_active_proj(self, projName):
346                 for i, row in enumerate(self._projectsList):
347                         if row[0] == projName:
348                                 self._projectCombo.set_active(i)
349                                 break
350                 else:
351                         raise ValueError("%s not in list" % projName)
352
353         def _get_project(self, todoManager):
354                 name = self._projectCombo.get_active_text()
355                 return name
356
357         def _get_priority(self):
358                 index = self._priorityChoiceCombo.get_active()
359                 assert index != -1
360                 if index < 1:
361                         return ""
362                 else:
363                         return str(index)
364
365         def _get_date(self):
366                 # @bug The date is not used in a consistent manner causing ... issues
367                 dateParts = self._popupCalendar.get_date()
368                 dateParts = (str(part) for part in dateParts)
369                 return "-".join(dateParts)
370
371         def _update_duedate(self):
372                 self._dueDateDisplay.set_text(self._get_date())
373
374         def _on_name_paste(self, *args):
375                 clipboard = gtk.clipboard_get()
376                 contents = clipboard.wait_for_text()
377                 if contents is not None:
378                         self._taskName.set_text(contents)
379
380         def _on_choose_duedate(self, *args):
381                 self._popupCalendar.run()
382
383         def _on_clear_duedate(self, *args):
384                 self._dueDateDisplay.set_text("")
385
386         def _on_add_clicked(self, *args):
387                 self._dialog.response(gtk.RESPONSE_OK)
388
389         def _on_cancel_clicked(self, *args):
390                 self._dialog.response(gtk.RESPONSE_CANCEL)
391