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