Pulling in helpers and improvements from other projects
[gc-dialer] / src / gtk_toolbox.py
1 #!/usr/bin/python
2
3 from __future__ import with_statement
4
5 import sys
6 import traceback
7 import functools
8 import contextlib
9 import warnings
10
11 import gobject
12 import gtk
13
14
15 @contextlib.contextmanager
16 def gtk_lock():
17         gtk.gdk.threads_enter()
18         try:
19                 yield
20         finally:
21                 gtk.gdk.threads_leave()
22
23
24 def make_idler(func):
25         """
26         Decorator that makes a generator-function into a function that will continue execution on next call
27         """
28         a = []
29
30         @functools.wraps(func)
31         def decorated_func(*args, **kwds):
32                 if not a:
33                         a.append(func(*args, **kwds))
34                 try:
35                         a[0].next()
36                         return True
37                 except StopIteration:
38                         del a[:]
39                         return False
40
41         return decorated_func
42
43
44 def asynchronous_gtk_message(original_func):
45         """
46         @note Idea came from http://www.aclevername.com/articles/python-webgui/
47         """
48
49         def execute(allArgs):
50                 args, kwargs = allArgs
51                 original_func(*args, **kwargs)
52
53         @functools.wraps(original_func)
54         def delayed_func(*args, **kwargs):
55                 gobject.idle_add(execute, (args, kwargs))
56
57         return delayed_func
58
59
60 def synchronous_gtk_message(original_func):
61         """
62         @note Idea came from http://www.aclevername.com/articles/python-webgui/
63         """
64
65         @functools.wraps(original_func)
66         def immediate_func(*args, **kwargs):
67                 with gtk_lock():
68                         return original_func(*args, **kwargs)
69
70         return immediate_func
71
72
73 class LoginWindow(object):
74
75         def __init__(self, widgetTree):
76                 """
77                 @note Thread agnostic
78                 """
79                 self._dialog = widgetTree.get_widget("loginDialog")
80                 self._parentWindow = widgetTree.get_widget("mainWindow")
81                 self._serviceCombo = widgetTree.get_widget("serviceCombo")
82                 self._usernameEntry = widgetTree.get_widget("usernameentry")
83                 self._passwordEntry = widgetTree.get_widget("passwordentry")
84
85                 self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING)
86                 self._serviceCombo.set_model(self._serviceList)
87                 cell = gtk.CellRendererText()
88                 self._serviceCombo.pack_start(cell, True)
89                 self._serviceCombo.add_attribute(cell, 'text', 1)
90                 self._serviceCombo.set_active(0)
91
92                 callbackMapping = {
93                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
94                         "on_loginclose_clicked": self._on_loginclose_clicked,
95                 }
96                 widgetTree.signal_autoconnect(callbackMapping)
97
98         def request_credentials(self, parentWindow = None):
99                 """
100                 @note UI Thread
101                 """
102                 if parentWindow is None:
103                         parentWindow = self._parentWindow
104
105                 self._serviceCombo.hide()
106                 self._serviceList.clear()
107
108                 try:
109                         self._dialog.set_transient_for(parentWindow)
110                         self._dialog.set_default_response(gtk.RESPONSE_OK)
111                         response = self._dialog.run()
112                         if response != gtk.RESPONSE_OK:
113                                 raise RuntimeError("Login Cancelled")
114
115                         username = self._usernameEntry.get_text()
116                         password = self._passwordEntry.get_text()
117                         self._passwordEntry.set_text("")
118                 finally:
119                         self._dialog.hide()
120
121                 return username, password
122
123         def request_credentials_from(self, services, parentWindow = None):
124                 """
125                 @note UI Thread
126                 """
127                 if parentWindow is None:
128                         parentWindow = self._parentWindow
129
130                 self._serviceList.clear()
131                 for serviceIdserviceName in services.iteritems():
132                         self._serviceList.append(serviceIdserviceName)
133                 self._serviceCombo.set_active(0)
134                 self._serviceCombo.show()
135
136                 try:
137                         self._dialog.set_transient_for(parentWindow)
138                         self._dialog.set_default_response(gtk.RESPONSE_OK)
139                         response = self._dialog.run()
140                         if response != gtk.RESPONSE_OK:
141                                 raise RuntimeError("Login Cancelled")
142
143                         username = self._usernameEntry.get_text()
144                         password = self._passwordEntry.get_text()
145                         self._passwordEntry.set_text("")
146                 finally:
147                         self._dialog.hide()
148
149                 itr = self._serviceCombo.get_active_iter()
150                 serviceId = int(self._serviceList.get_value(itr, 0))
151                 self._serviceList.clear()
152                 return serviceId, username, password
153
154         def _on_loginbutton_clicked(self, *args):
155                 self._dialog.response(gtk.RESPONSE_OK)
156
157         def _on_loginclose_clicked(self, *args):
158                 self._dialog.response(gtk.RESPONSE_CANCEL)
159
160
161 class ErrorDisplay(object):
162
163         def __init__(self, widgetTree):
164                 super(ErrorDisplay, self).__init__()
165                 self.__errorBox = widgetTree.get_widget("errorEventBox")
166                 self.__errorDescription = widgetTree.get_widget("errorDescription")
167                 self.__errorClose = widgetTree.get_widget("errorClose")
168                 self.__parentBox = self.__errorBox.get_parent()
169
170                 self.__errorBox.connect("button_release_event", self._on_close)
171
172                 self.__messages = []
173                 self.__parentBox.remove(self.__errorBox)
174
175         def push_message_with_lock(self, message):
176                 with gtk_lock():
177                         self.push_message(message)
178
179         def push_message(self, message):
180                 if 0 < len(self.__messages):
181                         self.__messages.append(message)
182                 else:
183                         self.__show_message(message)
184
185         def push_exception_with_lock(self, exception = None):
186                 with gtk_lock():
187                         self.push_exception(exception)
188
189         def push_exception(self, exception = None):
190                 if exception is None:
191                         userMessage = str(sys.exc_value)
192                         warningMessage = str(traceback.format_exc())
193                 else:
194                         userMessage = str(exception)
195                         warningMessage = str(exception)
196                 self.push_message(userMessage)
197                 warnings.warn(warningMessage, stacklevel=3)
198
199         def pop_message(self):
200                 if 0 < len(self.__messages):
201                         self.__show_message(self.__messages[0])
202                         del self.__messages[0]
203                 else:
204                         self.__hide_message()
205
206         def _on_close(self, *args):
207                 self.pop_message()
208
209         def __show_message(self, message):
210                 self.__errorDescription.set_text(message)
211                 self.__parentBox.pack_start(self.__errorBox, False, False)
212                 self.__parentBox.reorder_child(self.__errorBox, 1)
213
214         def __hide_message(self):
215                 self.__errorDescription.set_text("")
216                 self.__parentBox.remove(self.__errorBox)
217
218
219 class DummyErrorDisplay(object):
220
221         def __init__(self):
222                 super(DummyErrorDisplay, self).__init__()
223
224                 self.__messages = []
225
226         def push_message_with_lock(self, message):
227                 self.push_message(message)
228
229         def push_message(self, message):
230                 if 0 < len(self.__messages):
231                         self.__messages.append(message)
232                 else:
233                         self.__show_message(message)
234
235         def push_exception(self, exception = None):
236                 if exception is None:
237                         warningMessage = traceback.format_exc()
238                 else:
239                         warningMessage = exception
240                 warnings.warn(warningMessage, stacklevel=3)
241
242         def pop_message(self):
243                 if 0 < len(self.__messages):
244                         self.__show_message(self.__messages[0])
245                         del self.__messages[0]
246
247         def __show_message(self, message):
248                 warnings.warn(message, stacklevel=2)
249
250
251 class MessageBox(gtk.MessageDialog):
252
253         def __init__(self, message):
254                 parent = None
255                 gtk.MessageDialog.__init__(
256                         self,
257                         parent,
258                         gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
259                         gtk.MESSAGE_ERROR,
260                         gtk.BUTTONS_OK,
261                         message,
262                 )
263                 self.set_default_response(gtk.RESPONSE_OK)
264                 self.connect('response', self._handle_clicked)
265
266         def _handle_clicked(self, *args):
267                 self.destroy()
268
269
270 class MessageBox2(gtk.MessageDialog):
271
272         def __init__(self, message):
273                 parent = None
274                 gtk.MessageDialog.__init__(
275                         self,
276                         parent,
277                         gtk.DIALOG_DESTROY_WITH_PARENT,
278                         gtk.MESSAGE_ERROR,
279                         gtk.BUTTONS_OK,
280                         message,
281                 )
282                 self.set_default_response(gtk.RESPONSE_OK)
283                 self.connect('response', self._handle_clicked)
284
285         def _handle_clicked(self, *args):
286                 self.destroy()
287
288
289 class PopupCalendar(object):
290
291         def __init__(self, parent, displayDate, title = ""):
292                 self._displayDate = displayDate
293
294                 self._calendar = gtk.Calendar()
295                 self._calendar.select_month(self._displayDate.month, self._displayDate.year)
296                 self._calendar.select_day(self._displayDate.day)
297                 self._calendar.set_display_options(
298                         gtk.CALENDAR_SHOW_HEADING |
299                         gtk.CALENDAR_SHOW_DAY_NAMES |
300                         gtk.CALENDAR_NO_MONTH_CHANGE |
301                         0
302                 )
303                 self._calendar.connect("day-selected", self._on_day_selected)
304
305                 self._popupWindow = gtk.Window()
306                 self._popupWindow.set_title(title)
307                 self._popupWindow.add(self._calendar)
308                 self._popupWindow.set_transient_for(parent)
309                 self._popupWindow.set_modal(True)
310                 self._popupWindow.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
311                 self._popupWindow.set_skip_pager_hint(True)
312                 self._popupWindow.set_skip_taskbar_hint(True)
313
314         def run(self):
315                 self._popupWindow.show_all()
316
317         def _on_day_selected(self, *args):
318                 try:
319                         self._calendar.select_month(self._displayDate.month, self._displayDate.year)
320                         self._calendar.select_day(self._displayDate.day)
321                 except StandardError, e:
322                         warnings.warn(e.message)
323
324
325 class QuickAddView(object):
326
327         def __init__(self, widgetTree, errorDisplay, signalSink, prefix):
328                 self._errorDisplay = errorDisplay
329                 self._manager = None
330                 self._signalSink = signalSink
331
332                 self._clipboard = gtk.clipboard_get()
333
334                 self._taskNameEntry = widgetTree.get_widget(prefix+"-nameEntry")
335                 self._addTaskButton = widgetTree.get_widget(prefix+"-addButton")
336                 self._pasteTaskNameButton = widgetTree.get_widget(prefix+"-pasteNameButton")
337                 self._clearTaskNameButton = widgetTree.get_widget(prefix+"-clearNameButton")
338                 self._onAddId = None
339                 self._onAddClickedId = None
340                 self._onAddReleasedId = None
341                 self._addToEditTimerId = None
342                 self._onClearId = None
343                 self._onPasteId = None
344
345         def enable(self, manager):
346                 self._manager = manager
347
348                 self._onAddId = self._addTaskButton.connect("clicked", self._on_add)
349                 self._onAddClickedId = self._addTaskButton.connect("pressed", self._on_add_pressed)
350                 self._onAddReleasedId = self._addTaskButton.connect("released", self._on_add_released)
351                 self._onPasteId = self._pasteTaskNameButton.connect("clicked", self._on_paste)
352                 self._onClearId = self._clearTaskNameButton.connect("clicked", self._on_clear)
353
354         def disable(self):
355                 self._manager = None
356
357                 self._addTaskButton.disconnect(self._onAddId)
358                 self._addTaskButton.disconnect(self._onAddClickedId)
359                 self._addTaskButton.disconnect(self._onAddReleasedId)
360                 self._pasteTaskNameButton.disconnect(self._onPasteId)
361                 self._clearTaskNameButton.disconnect(self._onClearId)
362
363         def set_addability(self, addability):
364                 self._addTaskButton.set_sensitive(addability)
365
366         def _on_add(self, *args):
367                 try:
368                         name = self._taskNameEntry.get_text()
369                         self._taskNameEntry.set_text("")
370
371                         self._signalSink.stage.send(("add", name))
372                 except StandardError, e:
373                         self._errorDisplay.push_exception()
374
375         def _on_add_edit(self, *args):
376                 try:
377                         name = self._taskNameEntry.get_text()
378                         self._taskNameEntry.set_text("")
379
380                         self._signalSink.stage.send(("add-edit", name))
381                 except StandardError, e:
382                         self._errorDisplay.push_exception()
383
384         def _on_add_pressed(self, widget):
385                 try:
386                         self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit)
387                 except StandardError, e:
388                         self._errorDisplay.push_exception()
389
390         def _on_add_released(self, widget):
391                 try:
392                         if self._addToEditTimerId is not None:
393                                 gobject.source_remove(self._addToEditTimerId)
394                         self._addToEditTimerId = None
395                 except StandardError, e:
396                         self._errorDisplay.push_exception()
397
398         def _on_paste(self, *args):
399                 try:
400                         entry = self._taskNameEntry.get_text()
401                         addedText = self._clipboard.wait_for_text()
402                         if addedText:
403                                 entry += addedText
404                         self._taskNameEntry.set_text(entry)
405                 except StandardError, e:
406                         self._errorDisplay.push_exception()
407
408         def _on_clear(self, *args):
409                 try:
410                         self._taskNameEntry.set_text("")
411                 except StandardError, e:
412                         self._errorDisplay.push_exception()
413
414
415 if __name__ == "__main__":
416         if False:
417                 import datetime
418                 cal = PopupCalendar(None, datetime.datetime.now())
419                 cal._popupWindow.connect("destroy", lambda w: gtk.main_quit())
420                 cal.run()
421
422         gtk.main()