Adding progress notification
[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         def __init__(self, errorLog):
92                 self._errorLog = errorLog
93                 self._errorLog.messagePushed.connect(self._on_message_pushed)
94                 self._errorLog.messagePopped.connect(self._on_message_popped)
95
96                 self._icons = {
97                         ErrorMessage.LEVEL_BUSY:
98                                 get_theme_icon(
99                                         #("process-working", "gtk-refresh")
100                                         ("gtk-refresh", )
101                                 ).pixmap(32, 32),
102                         ErrorMessage.LEVEL_INFO:
103                                 get_theme_icon(
104                                         ("dialog-information", "general_notes", "gtk-info")
105                                 ).pixmap(32, 32),
106                         ErrorMessage.LEVEL_ERROR:
107                                 get_theme_icon(
108                                         ("dialog-error", "app_install_error", "gtk-dialog-error")
109                                 ).pixmap(32, 32),
110                 }
111                 self._severityLabel = QtGui.QLabel()
112                 self._severityLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
113
114                 self._message = QtGui.QLabel()
115                 self._message.setText("Boo")
116                 self._message.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
117
118                 closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"))
119                 self._closeLabel = QtGui.QPushButton(closeIcon, "")
120                 self._closeLabel.clicked.connect(self._on_close)
121
122                 self._controlLayout = QtGui.QHBoxLayout()
123                 self._controlLayout.addWidget(self._severityLabel, 1, QtCore.Qt.AlignCenter)
124                 self._controlLayout.addWidget(self._message, 1000)
125                 self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter)
126
127                 self._topLevelLayout = QtGui.QHBoxLayout()
128                 self._topLevelLayout.addLayout(self._controlLayout)
129                 self._widget = QtGui.QWidget()
130                 self._widget.setLayout(self._topLevelLayout)
131                 self._widget.hide()
132
133         @property
134         def toplevel(self):
135                 return self._widget
136
137         @QtCore.pyqtSlot()
138         @QtCore.pyqtSlot(bool)
139         @misc.log_exception(_moduleLogger)
140         def _on_close(self, checked = False):
141                 self._errorLog.pop()
142
143         @QtCore.pyqtSlot()
144         @misc.log_exception(_moduleLogger)
145         def _on_message_pushed(self):
146                 if 1 <= len(self._errorLog) and self._widget.isHidden():
147                         error = self._errorLog.peek_message()
148                         self._message.setText(error.message)
149                         self._severityLabel.setPixmap(self._icons[error.level])
150                         self._widget.show()
151
152         @QtCore.pyqtSlot()
153         @misc.log_exception(_moduleLogger)
154         def _on_message_popped(self):
155                 if len(self._errorLog) == 0:
156                         self._message.setText("")
157                         self._widget.hide()
158                 else:
159                         error = self._errorLog.peek_message()
160                         self._message.setText(error.message)
161                         self._severityLabel.setPixmap(self._icons[error.level])
162
163
164 class QHtmlDelegate(QtGui.QStyledItemDelegate):
165
166         # @bug Not showing all of a message
167
168         def paint(self, painter, option, index):
169                 newOption = QtGui.QStyleOptionViewItemV4(option)
170                 self.initStyleOption(newOption, index)
171                 if newOption.widget is not None:
172                         style = newOption.widget.style()
173                 else:
174                         style = QtGui.QApplication.style()
175
176                 doc = QtGui.QTextDocument()
177                 doc.setHtml(newOption.text)
178                 doc.setTextWidth(newOption.rect.width())
179
180                 newOption.text = ""
181                 style.drawControl(QtGui.QStyle.CE_ItemViewItem, newOption, painter)
182
183                 ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
184                 if newOption.state & QtGui.QStyle.State_Selected:
185                         ctx.palette.setColor(
186                                 QtGui.QPalette.Text,
187                                 newOption.palette.color(
188                                         QtGui.QPalette.Active,
189                                         QtGui.QPalette.HighlightedText
190                                 )
191                         )
192                 else:
193                         ctx.palette.setColor(
194                                 QtGui.QPalette.Text,
195                                 newOption.palette.color(
196                                         QtGui.QPalette.Active,
197                                         QtGui.QPalette.Text
198                                 )
199                         )
200
201                 textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, newOption)
202                 painter.save()
203                 painter.translate(textRect.topLeft())
204                 painter.setClipRect(textRect.translated(-textRect.topLeft()))
205                 doc.documentLayout().draw(painter, ctx)
206                 painter.restore()
207
208         def sizeHint(self, option, index):
209                 newOption = QtGui.QStyleOptionViewItemV4(option)
210                 self.initStyleOption(newOption, index)
211
212                 doc = QtGui.QTextDocument()
213                 doc.setHtml(newOption.text)
214                 doc.setTextWidth(newOption.rect.width())
215                 size = QtCore.QSize(doc.idealWidth(), doc.size().height())
216                 return size
217
218
219 def _null_set_stackable(window, isStackable):
220         pass
221
222
223 def _maemo_set_stackable(window, isStackable):
224         window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
225
226
227 try:
228         QtCore.Qt.WA_Maemo5StackedWindow
229         set_stackable = _maemo_set_stackable
230 except AttributeError:
231         set_stackable = _null_set_stackable
232
233
234 def _null_set_autorient(window, isStackable):
235         pass
236
237
238 def _maemo_set_autorient(window, isStackable):
239         window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
240
241
242 try:
243         QtCore.Qt.WA_Maemo5AutoOrientation
244         set_autorient = _maemo_set_autorient
245 except AttributeError:
246         set_autorient = _null_set_autorient
247
248
249 def _null_set_landscape(window, isStackable):
250         pass
251
252
253 def _maemo_set_landscape(window, isStackable):
254         window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
255
256
257 try:
258         QtCore.Qt.WA_Maemo5LandscapeOrientation
259         set_landscape = _maemo_set_landscape
260 except AttributeError:
261         set_landscape = _null_set_landscape
262
263
264 def _null_set_portrait(window, isStackable):
265         pass
266
267
268 def _maemo_set_portrait(window, isStackable):
269         window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
270
271
272 try:
273         QtCore.Qt.WA_Maemo5PortraitOrientation
274         set_portrait = _maemo_set_portrait
275 except AttributeError:
276         set_portrait = _null_set_portrait
277
278
279 def _null_show_progress_indicator(window, isStackable):
280         pass
281
282
283 def _maemo_show_progress_indicator(window, isStackable):
284         window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
285
286
287 try:
288         QtCore.Qt.WA_Maemo5ShowProgressIndicator
289         show_progress_indicator = _maemo_show_progress_indicator
290 except AttributeError:
291         show_progress_indicator = _null_show_progress_indicator
292
293
294 def _null_mark_numbers_preferred(widget):
295         pass
296
297
298 def _newqt_mark_numbers_preferred(widget):
299         widget.setInputMethodHints(QtCore.Qt.ImhPreferNumbers)
300
301
302 try:
303         QtCore.Qt.ImhPreferNumbers
304         mark_numbers_preferred = _newqt_mark_numbers_preferred
305 except AttributeError:
306         mark_numbers_preferred = _null_mark_numbers_preferred
307
308
309 def screen_orientation():
310         geom = QtGui.QApplication.desktop().screenGeometry()
311         if geom.width() <= geom.height():
312                 return QtCore.Qt.Vertical
313         else:
314                 return QtCore.Qt.Horizontal
315
316
317 def _null_get_theme_icon(iconNames, fallback = None):
318         icon = fallback if fallback is not None else QtGui.QIcon()
319         return icon
320
321
322 def _newqt_get_theme_icon(iconNames, fallback = None):
323         for iconName in iconNames:
324                 if QtGui.QIcon.hasThemeIcon(iconName):
325                         icon = QtGui.QIcon.fromTheme(iconName)
326                         break
327         else:
328                 icon = fallback if fallback is not None else QtGui.QIcon()
329         return icon
330
331
332 try:
333         QtGui.QIcon.fromTheme
334         get_theme_icon = _newqt_get_theme_icon
335 except AttributeError:
336         get_theme_icon = _null_get_theme_icon
337