X-Git-Url: http://git.maemo.org/git/?p=gonvert;a=blobdiff_plain;f=src%2Futil%2Fqui_utils.py;fp=src%2Futil%2Fqui_utils.py;h=56a34085eac3757c915b81a83afcb850e6d4abed;hp=0000000000000000000000000000000000000000;hb=3f92789cfd41f67f89f69801d032b98de93ba923;hpb=7c7cf777ed5a70550f8a7becca6a81515fb1e4b6 diff --git a/src/util/qui_utils.py b/src/util/qui_utils.py new file mode 100644 index 0000000..56a3408 --- /dev/null +++ b/src/util/qui_utils.py @@ -0,0 +1,364 @@ +import sys +import contextlib +import datetime +import logging + +from PyQt4 import QtCore +from PyQt4 import QtGui + +import misc + + +_moduleLogger = logging.getLogger(__name__) + + +@contextlib.contextmanager +def notify_error(log): + try: + yield + except: + log.push_exception() + + +@contextlib.contextmanager +def notify_busy(log, message): + log.push_busy(message) + try: + yield + finally: + log.pop(message) + + +class ErrorMessage(object): + + LEVEL_ERROR = 0 + LEVEL_BUSY = 1 + LEVEL_INFO = 2 + + def __init__(self, message, level): + self._message = message + self._level = level + self._time = datetime.datetime.now() + + @property + def level(self): + return self._level + + @property + def message(self): + return self._message + + def __repr__(self): + return "%s.%s(%r, %r)" % (__name__, self.__class__.__name__, self._message, self._level) + + +class QErrorLog(QtCore.QObject): + + messagePushed = QtCore.pyqtSignal() + messagePopped = QtCore.pyqtSignal() + + def __init__(self): + QtCore.QObject.__init__(self) + self._messages = [] + + def push_busy(self, message): + _moduleLogger.info("Entering state: %s" % message) + self._push_message(message, ErrorMessage.LEVEL_BUSY) + + def push_message(self, message): + self._push_message(message, ErrorMessage.LEVEL_INFO) + + def push_error(self, message): + self._push_message(message, ErrorMessage.LEVEL_ERROR) + + def push_exception(self): + userMessage = str(sys.exc_info()[1]) + _moduleLogger.exception(userMessage) + self.push_error(userMessage) + + def pop(self, message = None): + if message is None: + del self._messages[0] + else: + _moduleLogger.info("Exiting state: %s" % message) + messageIndex = [ + i + for (i, error) in enumerate(self._messages) + if error.message == message + ] + # Might be removed out of order + if messageIndex: + del self._messages[messageIndex[0]] + self.messagePopped.emit() + + def peek_message(self): + return self._messages[0] + + def _push_message(self, message, level): + self._messages.append(ErrorMessage(message, level)) + # Sort is defined as stable, so this should be fine + self._messages.sort(key=lambda x: x.level) + self.messagePushed.emit() + + def __len__(self): + return len(self._messages) + + +class ErrorDisplay(object): + + _SENTINEL_ICON = QtGui.QIcon() + + def __init__(self, errorLog): + self._errorLog = errorLog + self._errorLog.messagePushed.connect(self._on_message_pushed) + self._errorLog.messagePopped.connect(self._on_message_popped) + + self._icons = { + ErrorMessage.LEVEL_BUSY: + get_theme_icon( + #("process-working", "view-refresh", "general_refresh", "gtk-refresh") + ("view-refresh", "general_refresh", "gtk-refresh", ) + ).pixmap(32, 32), + ErrorMessage.LEVEL_INFO: + get_theme_icon( + ("dialog-information", "general_notes", "gtk-info") + ).pixmap(32, 32), + ErrorMessage.LEVEL_ERROR: + get_theme_icon( + ("dialog-error", "app_install_error", "gtk-dialog-error") + ).pixmap(32, 32), + } + self._severityLabel = QtGui.QLabel() + self._severityLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + + self._message = QtGui.QLabel() + self._message.setText("Boo") + self._message.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + self._message.setWordWrap(True) + + closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON) + if closeIcon is not self._SENTINEL_ICON: + self._closeLabel = QtGui.QPushButton(closeIcon, "") + else: + self._closeLabel = QtGui.QPushButton("X") + self._closeLabel.clicked.connect(self._on_close) + + self._controlLayout = QtGui.QHBoxLayout() + self._controlLayout.addWidget(self._severityLabel, 1, QtCore.Qt.AlignCenter) + self._controlLayout.addWidget(self._message, 1000) + self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter) + + self._widget = QtGui.QWidget() + self._widget.setLayout(self._controlLayout) + self._widget.hide() + + @property + def toplevel(self): + return self._widget + + def _show_error(self): + error = self._errorLog.peek_message() + self._message.setText(error.message) + self._severityLabel.setPixmap(self._icons[error.level]) + self._widget.show() + + @QtCore.pyqtSlot() + @QtCore.pyqtSlot(bool) + @misc.log_exception(_moduleLogger) + def _on_close(self, checked = False): + self._errorLog.pop() + + @QtCore.pyqtSlot() + @misc.log_exception(_moduleLogger) + def _on_message_pushed(self): + self._show_error() + + @QtCore.pyqtSlot() + @misc.log_exception(_moduleLogger) + def _on_message_popped(self): + if len(self._errorLog) == 0: + self._message.setText("") + self._widget.hide() + else: + self._show_error() + + +class QHtmlDelegate(QtGui.QStyledItemDelegate): + + UNDEFINED_SIZE = -1 + + def __init__(self, *args, **kwd): + QtGui.QStyledItemDelegate.__init__(*((self, ) + args), **kwd) + self._width = self.UNDEFINED_SIZE + + def paint(self, painter, option, index): + newOption = QtGui.QStyleOptionViewItemV4(option) + self.initStyleOption(newOption, index) + if newOption.widget is not None: + style = newOption.widget.style() + else: + style = QtGui.QApplication.style() + + doc = QtGui.QTextDocument() + doc.setHtml(newOption.text) + doc.setTextWidth(newOption.rect.width()) + + newOption.text = "" + style.drawControl(QtGui.QStyle.CE_ItemViewItem, newOption, painter) + + ctx = QtGui.QAbstractTextDocumentLayout.PaintContext() + if newOption.state & QtGui.QStyle.State_Selected: + ctx.palette.setColor( + QtGui.QPalette.Text, + newOption.palette.color( + QtGui.QPalette.Active, + QtGui.QPalette.HighlightedText + ) + ) + else: + ctx.palette.setColor( + QtGui.QPalette.Text, + newOption.palette.color( + QtGui.QPalette.Active, + QtGui.QPalette.Text + ) + ) + + textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, newOption) + painter.save() + painter.translate(textRect.topLeft()) + painter.setClipRect(textRect.translated(-textRect.topLeft())) + doc.documentLayout().draw(painter, ctx) + painter.restore() + + def setWidth(self, width): + # @bug we need to be emitting sizeHintChanged but it requires an index + self._width = width + + def sizeHint(self, option, index): + newOption = QtGui.QStyleOptionViewItemV4(option) + self.initStyleOption(newOption, index) + + doc = QtGui.QTextDocument() + doc.setHtml(newOption.text) + if self._width != self.UNDEFINED_SIZE: + width = self._width + else: + width = newOption.rect.width() + doc.setTextWidth(width) + size = QtCore.QSize(doc.idealWidth(), doc.size().height()) + return size + + +def _null_set_stackable(window, isStackable): + pass + + +def _maemo_set_stackable(window, isStackable): + window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable) + + +try: + QtCore.Qt.WA_Maemo5StackedWindow + set_stackable = _maemo_set_stackable +except AttributeError: + set_stackable = _null_set_stackable + + +def _null_set_autorient(window, isStackable): + pass + + +def _maemo_set_autorient(window, isStackable): + window.setAttribute(QtCore.Qt.WA_Maemo5AutoOrientation, isStackable) + + +try: + QtCore.Qt.WA_Maemo5AutoOrientation + set_autorient = _maemo_set_autorient +except AttributeError: + set_autorient = _null_set_autorient + + +def screen_orientation(): + geom = QtGui.QApplication.desktop().screenGeometry() + if geom.width() <= geom.height(): + return QtCore.Qt.Vertical + else: + return QtCore.Qt.Horizontal + + +def _null_set_window_orientation(window, orientation): + pass + + +def _maemo_set_window_orientation(window, orientation): + if orientation == QtCore.Qt.Vertical: + oldHint = QtCore.Qt.WA_Maemo5LandscapeOrientation + newHint = QtCore.Qt.WA_Maemo5PortraitOrientation + elif orientation == QtCore.Qt.Horizontal: + oldHint = QtCore.Qt.WA_Maemo5PortraitOrientation + newHint = QtCore.Qt.WA_Maemo5LandscapeOrientation + window.setAttribute(oldHint, False) + window.setAttribute(newHint, True) + + +try: + QtCore.Qt.WA_Maemo5LandscapeOrientation + QtCore.Qt.WA_Maemo5PortraitOrientation + set_window_orientation = _maemo_set_window_orientation +except AttributeError: + set_window_orientation = _null_set_window_orientation + + +def _null_show_progress_indicator(window, isStackable): + pass + + +def _maemo_show_progress_indicator(window, isStackable): + window.setAttribute(QtCore.Qt.WA_Maemo5ShowProgressIndicator, isStackable) + + +try: + QtCore.Qt.WA_Maemo5ShowProgressIndicator + show_progress_indicator = _maemo_show_progress_indicator +except AttributeError: + show_progress_indicator = _null_show_progress_indicator + + +def _null_mark_numbers_preferred(widget): + pass + + +def _newqt_mark_numbers_preferred(widget): + widget.setInputMethodHints(QtCore.Qt.ImhPreferNumbers) + + +try: + QtCore.Qt.ImhPreferNumbers + mark_numbers_preferred = _newqt_mark_numbers_preferred +except AttributeError: + mark_numbers_preferred = _null_mark_numbers_preferred + + +def _null_get_theme_icon(iconNames, fallback = None): + icon = fallback if fallback is not None else QtGui.QIcon() + return icon + + +def _newqt_get_theme_icon(iconNames, fallback = None): + for iconName in iconNames: + if QtGui.QIcon.hasThemeIcon(iconName): + icon = QtGui.QIcon.fromTheme(iconName) + break + else: + icon = fallback if fallback is not None else QtGui.QIcon() + return icon + + +try: + QtGui.QIcon.fromTheme + get_theme_icon = _newqt_get_theme_icon +except AttributeError: + get_theme_icon = _null_get_theme_icon +