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