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