Prioritizing Error messages over Busy Indicators
[gc-dialer] / src / util / qui_utils.py
1 import sys
2 import contextlib
3 import datetime
4 import logging
5
6 from PyQt4 import QtCore
7 from PyQt4 import QtGui
8
9 import misc
10
11
12 _moduleLogger = logging.getLogger(__name__)
13
14
15 @contextlib.contextmanager
16 def notify_error(log):
17         try:
18                 yield
19         except:
20                 log.push_exception()
21
22
23 @contextlib.contextmanager
24 def notify_busy(log, message):
25         log.push_busy(message)
26         try:
27                 yield
28         finally:
29                 log.pop(message)
30
31
32 class ErrorMessage(object):
33
34         LEVEL_ERROR = 0
35         LEVEL_BUSY = 1
36         LEVEL_INFO = 2
37
38         def __init__(self, message, level):
39                 self._message = message
40                 self._level = level
41                 self._time = datetime.datetime.now()
42
43         @property
44         def level(self):
45                 return self._level
46
47         @property
48         def message(self):
49                 return self._message
50
51         def __repr__(self):
52                 return "%s.%s(%r, %r)" % (__name__, self.__class__.__name__, self._message, self._level)
53
54
55 class QErrorLog(QtCore.QObject):
56
57         messagePushed = QtCore.pyqtSignal()
58         messagePopped = QtCore.pyqtSignal()
59
60         def __init__(self):
61                 QtCore.QObject.__init__(self)
62                 self._messages = []
63
64         def push_busy(self, message):
65                 _moduleLogger.info("Entering state: %s" % message)
66                 self._push_message(message, ErrorMessage.LEVEL_BUSY)
67
68         def push_message(self, message):
69                 self._push_message(message, ErrorMessage.LEVEL_INFO)
70
71         def push_error(self, message):
72                 self._push_message(message, ErrorMessage.LEVEL_ERROR)
73
74         def push_exception(self):
75                 userMessage = str(sys.exc_info()[1])
76                 _moduleLogger.exception(userMessage)
77                 self.push_error(userMessage)
78
79         def pop(self, message = None):
80                 if message is None:
81                         del self._messages[0]
82                 else:
83                         _moduleLogger.info("Exiting state: %s" % message)
84                         messageIndex = [
85                                 i
86                                 for (i, error) in enumerate(self._messages)
87                                 if error.message == message
88                         ]
89                         # Might be removed out of order
90                         if messageIndex:
91                                 del self._messages[messageIndex[0]]
92                 self.messagePopped.emit()
93
94         def peek_message(self):
95                 return self._messages[0]
96
97         def _push_message(self, message, level):
98                 self._messages.append(ErrorMessage(message, level))
99                 # Sort is defined as stable, so this should be fine
100                 self._messages.sort(key=lambda x: x.level)
101                 self.messagePushed.emit()
102
103         def __len__(self):
104                 return len(self._messages)
105
106
107 class ErrorDisplay(object):
108
109         _SENTINEL_ICON = QtGui.QIcon()
110
111         def __init__(self, errorLog):
112                 self._errorLog = errorLog
113                 self._errorLog.messagePushed.connect(self._on_message_pushed)
114                 self._errorLog.messagePopped.connect(self._on_message_popped)
115
116                 self._icons = {
117                         ErrorMessage.LEVEL_BUSY:
118                                 get_theme_icon(
119                                         #("process-working", "view-refresh", "general_refresh", "gtk-refresh")
120                                         ("view-refresh", "general_refresh", "gtk-refresh", )
121                                 ).pixmap(32, 32),
122                         ErrorMessage.LEVEL_INFO:
123                                 get_theme_icon(
124                                         ("dialog-information", "general_notes", "gtk-info")
125                                 ).pixmap(32, 32),
126                         ErrorMessage.LEVEL_ERROR:
127                                 get_theme_icon(
128                                         ("dialog-error", "app_install_error", "gtk-dialog-error")
129                                 ).pixmap(32, 32),
130                 }
131                 self._severityLabel = QtGui.QLabel()
132                 self._severityLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
133
134                 self._message = QtGui.QLabel()
135                 self._message.setText("Boo")
136                 self._message.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
137                 self._message.setWordWrap(True)
138
139                 closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
140                 if closeIcon is not self._SENTINEL_ICON:
141                         self._closeLabel = QtGui.QPushButton(closeIcon, "")
142                 else:
143                         self._closeLabel = QtGui.QPushButton("X")
144                 self._closeLabel.clicked.connect(self._on_close)
145
146                 self._controlLayout = QtGui.QHBoxLayout()
147                 self._controlLayout.addWidget(self._severityLabel, 1, QtCore.Qt.AlignCenter)
148                 self._controlLayout.addWidget(self._message, 1000)
149                 self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter)
150
151                 self._topLevelLayout = QtGui.QHBoxLayout()
152                 self._topLevelLayout.addLayout(self._controlLayout)
153                 self._widget = QtGui.QWidget()
154                 self._widget.setLayout(self._topLevelLayout)
155                 self._widget.hide()
156
157         @property
158         def toplevel(self):
159                 return self._widget
160
161         def _show_error(self):
162                 error = self._errorLog.peek_message()
163                 self._message.setText(error.message)
164                 self._severityLabel.setPixmap(self._icons[error.level])
165                 self._widget.show()
166
167         @QtCore.pyqtSlot()
168         @QtCore.pyqtSlot(bool)
169         @misc.log_exception(_moduleLogger)
170         def _on_close(self, checked = False):
171                 self._errorLog.pop()
172
173         @QtCore.pyqtSlot()
174         @misc.log_exception(_moduleLogger)
175         def _on_message_pushed(self):
176                 self._show_error()
177
178         @QtCore.pyqtSlot()
179         @misc.log_exception(_moduleLogger)
180         def _on_message_popped(self):
181                 if len(self._errorLog) == 0:
182                         self._message.setText("")
183                         self._widget.hide()
184                 else:
185                         self._show_error()
186
187
188 class QHtmlDelegate(QtGui.QStyledItemDelegate):
189
190         # @bug Not showing all of a message
191
192         def paint(self, painter, option, index):
193                 newOption = QtGui.QStyleOptionViewItemV4(option)
194                 self.initStyleOption(newOption, index)
195                 if newOption.widget is not None:
196                         style = newOption.widget.style()
197                 else:
198                         style = QtGui.QApplication.style()
199
200                 doc = QtGui.QTextDocument()
201                 doc.setHtml(newOption.text)
202                 doc.setTextWidth(newOption.rect.width())
203
204                 newOption.text = ""
205                 style.drawControl(QtGui.QStyle.CE_ItemViewItem, newOption, painter)
206
207                 ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
208                 if newOption.state & QtGui.QStyle.State_Selected:
209                         ctx.palette.setColor(
210                                 QtGui.QPalette.Text,
211                                 newOption.palette.color(
212                                         QtGui.QPalette.Active,
213                                         QtGui.QPalette.HighlightedText
214                                 )
215                         )
216                 else:
217                         ctx.palette.setColor(
218                                 QtGui.QPalette.Text,
219                                 newOption.palette.color(
220                                         QtGui.QPalette.Active,
221                                         QtGui.QPalette.Text
222                                 )
223                         )
224
225                 textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, newOption)
226                 painter.save()
227                 painter.translate(textRect.topLeft())
228                 painter.setClipRect(textRect.translated(-textRect.topLeft()))
229                 doc.documentLayout().draw(painter, ctx)
230                 painter.restore()
231
232         def sizeHint(self, option, index):
233                 newOption = QtGui.QStyleOptionViewItemV4(option)
234                 self.initStyleOption(newOption, index)
235
236                 doc = QtGui.QTextDocument()
237                 doc.setHtml(newOption.text)
238                 doc.setTextWidth(newOption.rect.width())
239                 size = QtCore.QSize(doc.idealWidth(), doc.size().height())
240                 return size
241
242
243 def _null_set_stackable(window, isStackable):
244         pass
245
246
247 def _maemo_set_stackable(window, isStackable):
248         window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
249
250
251 try:
252         QtCore.Qt.WA_Maemo5StackedWindow
253         set_stackable = _maemo_set_stackable
254 except AttributeError:
255         set_stackable = _null_set_stackable
256
257
258 def _null_set_autorient(window, isStackable):
259         pass
260
261
262 def _maemo_set_autorient(window, isStackable):
263         window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
264
265
266 try:
267         QtCore.Qt.WA_Maemo5AutoOrientation
268         set_autorient = _maemo_set_autorient
269 except AttributeError:
270         set_autorient = _null_set_autorient
271
272
273 def _null_set_landscape(window, isStackable):
274         pass
275
276
277 def _maemo_set_landscape(window, isStackable):
278         window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
279
280
281 try:
282         QtCore.Qt.WA_Maemo5LandscapeOrientation
283         set_landscape = _maemo_set_landscape
284 except AttributeError:
285         set_landscape = _null_set_landscape
286
287
288 def _null_set_portrait(window, isStackable):
289         pass
290
291
292 def _maemo_set_portrait(window, isStackable):
293         window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
294
295
296 try:
297         QtCore.Qt.WA_Maemo5PortraitOrientation
298         set_portrait = _maemo_set_portrait
299 except AttributeError:
300         set_portrait = _null_set_portrait
301
302
303 def _null_show_progress_indicator(window, isStackable):
304         pass
305
306
307 def _maemo_show_progress_indicator(window, isStackable):
308         window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
309
310
311 try:
312         QtCore.Qt.WA_Maemo5ShowProgressIndicator
313         show_progress_indicator = _maemo_show_progress_indicator
314 except AttributeError:
315         show_progress_indicator = _null_show_progress_indicator
316
317
318 def _null_mark_numbers_preferred(widget):
319         pass
320
321
322 def _newqt_mark_numbers_preferred(widget):
323         widget.setInputMethodHints(QtCore.Qt.ImhPreferNumbers)
324
325
326 try:
327         QtCore.Qt.ImhPreferNumbers
328         mark_numbers_preferred = _newqt_mark_numbers_preferred
329 except AttributeError:
330         mark_numbers_preferred = _null_mark_numbers_preferred
331
332
333 def screen_orientation():
334         geom = QtGui.QApplication.desktop().screenGeometry()
335         if geom.width() <= geom.height():
336                 return QtCore.Qt.Vertical
337         else:
338                 return QtCore.Qt.Horizontal
339
340
341 def _null_get_theme_icon(iconNames, fallback = None):
342         icon = fallback if fallback is not None else QtGui.QIcon()
343         return icon
344
345
346 def _newqt_get_theme_icon(iconNames, fallback = None):
347         for iconName in iconNames:
348                 if QtGui.QIcon.hasThemeIcon(iconName):
349                         icon = QtGui.QIcon.fromTheme(iconName)
350                         break
351         else:
352                 icon = fallback if fallback is not None else QtGui.QIcon()
353         return icon
354
355
356 try:
357         QtGui.QIcon.fromTheme
358         get_theme_icon = _newqt_get_theme_icon
359 except AttributeError:
360         get_theme_icon = _null_get_theme_icon
361