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