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