0e6e768967b1f8c4de384915d094f9d7a4831328
[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 = None
117                 self._severityLabel = QtGui.QLabel()
118                 self._severityLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
119
120                 self._message = QtGui.QLabel()
121                 self._message.setText("Boo")
122                 self._message.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
123                 self._message.setWordWrap(True)
124
125                 self._closeLabel = None
126
127                 self._controlLayout = QtGui.QHBoxLayout()
128                 self._controlLayout.addWidget(self._severityLabel, 1, QtCore.Qt.AlignCenter)
129                 self._controlLayout.addWidget(self._message, 1000)
130
131                 self._widget = QtGui.QWidget()
132                 self._widget.setLayout(self._controlLayout)
133                 self._widget.hide()
134
135         @property
136         def toplevel(self):
137                 return self._widget
138
139         def _show_error(self):
140                 if self._icons is None:
141                         self._icons = {
142                                 ErrorMessage.LEVEL_BUSY:
143                                         get_theme_icon(
144                                                 #("process-working", "view-refresh", "general_refresh", "gtk-refresh")
145                                                 ("view-refresh", "general_refresh", "gtk-refresh", )
146                                         ).pixmap(32, 32),
147                                 ErrorMessage.LEVEL_INFO:
148                                         get_theme_icon(
149                                                 ("dialog-information", "general_notes", "gtk-info")
150                                         ).pixmap(32, 32),
151                                 ErrorMessage.LEVEL_ERROR:
152                                         get_theme_icon(
153                                                 ("dialog-error", "app_install_error", "gtk-dialog-error")
154                                         ).pixmap(32, 32),
155                         }
156                 if self._closeLabel is None:
157                         closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
158                         if closeIcon is not self._SENTINEL_ICON:
159                                 self._closeLabel = QtGui.QPushButton(closeIcon, "")
160                         else:
161                                 self._closeLabel = QtGui.QPushButton("X")
162                         self._closeLabel.clicked.connect(self._on_close)
163                         self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter)
164                 error = self._errorLog.peek_message()
165                 self._message.setText(error.message)
166                 self._severityLabel.setPixmap(self._icons[error.level])
167                 self._widget.show()
168
169         @QtCore.pyqtSlot()
170         @QtCore.pyqtSlot(bool)
171         @misc.log_exception(_moduleLogger)
172         def _on_close(self, checked = False):
173                 self._errorLog.pop()
174
175         @QtCore.pyqtSlot()
176         @misc.log_exception(_moduleLogger)
177         def _on_message_pushed(self):
178                 self._show_error()
179
180         @QtCore.pyqtSlot()
181         @misc.log_exception(_moduleLogger)
182         def _on_message_popped(self):
183                 if len(self._errorLog) == 0:
184                         self._message.setText("")
185                         self._widget.hide()
186                 else:
187                         self._show_error()
188
189
190 class QHtmlDelegate(QtGui.QStyledItemDelegate):
191
192         UNDEFINED_SIZE = -1
193
194         def __init__(self, *args, **kwd):
195                 QtGui.QStyledItemDelegate.__init__(*((self, ) + args), **kwd)
196                 self._width = self.UNDEFINED_SIZE
197
198         def paint(self, painter, option, index):
199                 newOption = QtGui.QStyleOptionViewItemV4(option)
200                 self.initStyleOption(newOption, index)
201                 if newOption.widget is not None:
202                         style = newOption.widget.style()
203                 else:
204                         style = QtGui.QApplication.style()
205
206                 doc = QtGui.QTextDocument()
207                 doc.setHtml(newOption.text)
208                 doc.setTextWidth(newOption.rect.width())
209
210                 newOption.text = ""
211                 style.drawControl(QtGui.QStyle.CE_ItemViewItem, newOption, painter)
212
213                 ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
214                 if newOption.state & QtGui.QStyle.State_Selected:
215                         ctx.palette.setColor(
216                                 QtGui.QPalette.Text,
217                                 newOption.palette.color(
218                                         QtGui.QPalette.Active,
219                                         QtGui.QPalette.HighlightedText
220                                 )
221                         )
222                 else:
223                         ctx.palette.setColor(
224                                 QtGui.QPalette.Text,
225                                 newOption.palette.color(
226                                         QtGui.QPalette.Active,
227                                         QtGui.QPalette.Text
228                                 )
229                         )
230
231                 textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, newOption)
232                 painter.save()
233                 painter.translate(textRect.topLeft())
234                 painter.setClipRect(textRect.translated(-textRect.topLeft()))
235                 doc.documentLayout().draw(painter, ctx)
236                 painter.restore()
237
238         def setWidth(self, width, model):
239                 if self._width == width:
240                         return
241                 self._width = width
242                 for c in xrange(model.rowCount()):
243                         cItem = model.item(c, 0)
244                         for r in xrange(model.rowCount()):
245                                 rItem = cItem.child(r, 0)
246                                 rIndex = model.indexFromItem(rItem)
247                                 self.sizeHintChanged.emit(rIndex)
248                                 return
249
250         def sizeHint(self, option, index):
251                 newOption = QtGui.QStyleOptionViewItemV4(option)
252                 self.initStyleOption(newOption, index)
253
254                 doc = QtGui.QTextDocument()
255                 doc.setHtml(newOption.text)
256                 if self._width != self.UNDEFINED_SIZE:
257                         width = self._width
258                 else:
259                         width = newOption.rect.width()
260                 doc.setTextWidth(width)
261                 size = QtCore.QSize(doc.idealWidth(), doc.size().height())
262                 return size
263
264
265 class QSignalingMainWindow(QtGui.QMainWindow):
266
267         closed = QtCore.pyqtSignal()
268         hidden = QtCore.pyqtSignal()
269         shown = QtCore.pyqtSignal()
270         resized = QtCore.pyqtSignal()
271
272         def __init__(self, *args, **kwd):
273                 QtGui.QMainWindow.__init__(*((self, )+args), **kwd)
274
275         def closeEvent(self, event):
276                 val = QtGui.QMainWindow.closeEvent(self, event)
277                 self.closed.emit()
278                 return val
279
280         def hideEvent(self, event):
281                 val = QtGui.QMainWindow.hideEvent(self, event)
282                 self.hidden.emit()
283                 return val
284
285         def showEvent(self, event):
286                 val = QtGui.QMainWindow.showEvent(self, event)
287                 self.shown.emit()
288                 return val
289
290         def resizeEvent(self, event):
291                 val = QtGui.QMainWindow.resizeEvent(self, event)
292                 self.resized.emit()
293                 return val
294
295
296 def _null_set_stackable(window, isStackable):
297         pass
298
299
300 def _maemo_set_stackable(window, isStackable):
301         window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
302
303
304 try:
305         QtCore.Qt.WA_Maemo5StackedWindow
306         set_stackable = _maemo_set_stackable
307 except AttributeError:
308         set_stackable = _null_set_stackable
309
310
311 def _null_set_autorient(window, isStackable):
312         pass
313
314
315 def _maemo_set_autorient(window, isStackable):
316         window.setAttribute(QtCore.Qt.WA_Maemo5AutoOrientation, isStackable)
317
318
319 try:
320         QtCore.Qt.WA_Maemo5AutoOrientation
321         set_autorient = _maemo_set_autorient
322 except AttributeError:
323         set_autorient = _null_set_autorient
324
325
326 def screen_orientation():
327         geom = QtGui.QApplication.desktop().screenGeometry()
328         if geom.width() <= geom.height():
329                 return QtCore.Qt.Vertical
330         else:
331                 return QtCore.Qt.Horizontal
332
333
334 def _null_set_window_orientation(window, orientation):
335         pass
336
337
338 def _maemo_set_window_orientation(window, orientation):
339         if orientation == QtCore.Qt.Vertical:
340                 oldHint = QtCore.Qt.WA_Maemo5LandscapeOrientation
341                 newHint = QtCore.Qt.WA_Maemo5PortraitOrientation
342         elif orientation == QtCore.Qt.Horizontal:
343                 oldHint = QtCore.Qt.WA_Maemo5PortraitOrientation
344                 newHint = QtCore.Qt.WA_Maemo5LandscapeOrientation
345         window.setAttribute(oldHint, False)
346         window.setAttribute(newHint, True)
347
348
349 try:
350         QtCore.Qt.WA_Maemo5LandscapeOrientation
351         QtCore.Qt.WA_Maemo5PortraitOrientation
352         set_window_orientation = _maemo_set_window_orientation
353 except AttributeError:
354         set_window_orientation = _null_set_window_orientation
355
356
357 def _null_show_progress_indicator(window, isStackable):
358         pass
359
360
361 def _maemo_show_progress_indicator(window, isStackable):
362         window.setAttribute(QtCore.Qt.WA_Maemo5ShowProgressIndicator, isStackable)
363
364
365 try:
366         QtCore.Qt.WA_Maemo5ShowProgressIndicator
367         show_progress_indicator = _maemo_show_progress_indicator
368 except AttributeError:
369         show_progress_indicator = _null_show_progress_indicator
370
371
372 def _null_mark_numbers_preferred(widget):
373         pass
374
375
376 def _newqt_mark_numbers_preferred(widget):
377         widget.setInputMethodHints(QtCore.Qt.ImhPreferNumbers)
378
379
380 try:
381         QtCore.Qt.ImhPreferNumbers
382         mark_numbers_preferred = _newqt_mark_numbers_preferred
383 except AttributeError:
384         mark_numbers_preferred = _null_mark_numbers_preferred
385
386
387 def _null_get_theme_icon(iconNames, fallback = None):
388         icon = fallback if fallback is not None else QtGui.QIcon()
389         return icon
390
391
392 def _newqt_get_theme_icon(iconNames, fallback = None):
393         for iconName in iconNames:
394                 if QtGui.QIcon.hasThemeIcon(iconName):
395                         icon = QtGui.QIcon.fromTheme(iconName)
396                         break
397         else:
398                 icon = fallback if fallback is not None else QtGui.QIcon()
399         return icon
400
401
402 try:
403         QtGui.QIcon.fromTheme
404         get_theme_icon = _newqt_get_theme_icon
405 except AttributeError:
406         get_theme_icon = _null_get_theme_icon
407