class MessagesWidget(QWidget): """ An iconified multiple message display area. `MessagesWidget` displays a short message along with an icon. If there are multiple messages they are summarized. The user can click on the widget to display the full message text in a popup view. """ #: Signal emitted when an embedded html link is clicked #: (if `openExternalLinks` is `False`). linkActivated = Signal(str) #: Signal emitted when an embedded html link is hovered. linkHovered = Signal(str) Severity = Severity #: General informative message. Information = Severity.Information #: A warning message severity. Warning = Severity.Warning #: An error message severity. Error = Severity.Error Message = Message def __init__(self, parent=None, openExternalLinks=False, **kwargs): kwargs.setdefault( "sizePolicy", QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)) super().__init__(parent, **kwargs) self.__openExternalLinks = openExternalLinks # type: bool self.__messages = OrderedDict() # type: Dict[Hashable, Message] #: The full (joined all messages text - rendered as html), displayed #: in a tooltip. self.__fulltext = "" #: The full text displayed in a popup. Is empty if the message is #: short self.__popuptext = "" #: Leading icon self.__iconwidget = IconWidget( sizePolicy=QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) #: Inline message text self.__textlabel = QLabel( wordWrap=False, textInteractionFlags=Qt.LinksAccessibleByMouse, openExternalLinks=self.__openExternalLinks, sizePolicy=QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum), ) #: Indicator that extended contents are accessible with a click on the #: widget. self.__popupicon = QLabel( sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum), text="\N{VERTICAL ELLIPSIS}", visible=False, ) self.__textlabel.linkActivated.connect(self.linkActivated) self.__textlabel.linkHovered.connect(self.linkHovered) self.setLayout(QHBoxLayout()) self.layout().setContentsMargins(2, 1, 2, 1) self.layout().setSpacing(0) self.layout().addWidget(self.__iconwidget) self.layout().addSpacing(4) self.layout().addWidget(self.__textlabel) self.layout().addWidget(self.__popupicon) self.__textlabel.setAttribute(Qt.WA_MacSmallSize) def sizeHint(self): sh = super().sizeHint() h = self.style().pixelMetric(QStyle.PM_SmallIconSize) return sh.expandedTo(QSize(0, h + 2)) def openExternalLinks(self): # type: () -> bool """ If True then linkActivated signal will be emitted when the user clicks on an html link in a message, otherwise links are opened using `QDesktopServices.openUrl` """ return self.__openExternalLinks def setOpenExternalLinks(self, state): # type: (bool) -> None """ """ # TODO: update popup if open self.__openExternalLinks = state self.__textlabel.setOpenExternalLinks(state) def setMessage(self, message_id, message): # type: (Hashable, Message) -> None """ Add a `message` for `message_id` to the current display. Note ---- Set an empty `Message` instance to clear the message display but retain the relative ordering in the display should a message for `message_id` reactivate. """ self.__messages[message_id] = message self.__update() def removeMessage(self, message_id): # type: (Hashable) -> None """ Remove message for `message_id` from the display. Note ---- Setting an empty `Message` instance will also clear the display, however the relative ordering of the messages will be retained, should the `message_id` 'reactivate'. """ del self.__messages[message_id] self.__update() def setMessages(self, messages): # type: (Union[Iterable[Tuple[Hashable, Message]], Dict[Hashable, Message]]) -> None """ Set multiple messages in a single call. """ messages = OrderedDict(messages) self.__messages.update(messages) self.__update() def clear(self): # type: () -> None """ Clear all messages. """ self.__messages.clear() self.__update() def messages(self): # type: () -> List[Message] return list(self.__messages.values()) def summarize(self): # type: () -> Message """ Summarize all the messages into a single message. """ messages = [m for m in self.__messages.values() if not m.isEmpty()] if messages: return summarize(messages) else: return Message() def __update(self): """ Update the current display state. """ self.ensurePolished() summary = self.summarize() icon = message_icon(summary) self.__iconwidget.setIcon(icon) self.__iconwidget.setVisible(not (summary.isEmpty() or icon.isNull())) self.__textlabel.setTextFormat(summary.textFormat) self.__textlabel.setText(summary.text) messages = [m for m in self.__messages.values() if not m.isEmpty()] if messages: messages = sorted(messages, key=attrgetter("severity"), reverse=True) fulltext = "<hr/>".join(m.asHtml() for m in messages) else: fulltext = "" self.__fulltext = fulltext self.setToolTip(fulltext) def is_short(m): return not (m.informativeText or m.detailedText) if not messages or len(messages) == 1 and is_short(messages[0]): self.__popuptext = "" else: self.__popuptext = fulltext self.__popupicon.setVisible(bool(self.__popuptext)) self.layout().activate() def mousePressEvent(self, event): if event.button() == Qt.LeftButton: if self.__popuptext: popup = QMenu(self) label = QLabel( self, textInteractionFlags=Qt.TextBrowserInteraction, openExternalLinks=self.__openExternalLinks, text=self.__popuptext, ) label.linkActivated.connect(self.linkActivated) label.linkHovered.connect(self.linkHovered) action = QWidgetAction(popup) action.setDefaultWidget(label) popup.addAction(action) popup.popup(event.globalPos(), action) event.accept() return else: super().mousePressEvent(event) def enterEvent(self, event): super().enterEvent(event) self.update() def leaveEvent(self, event): super().leaveEvent(event) self.update() def changeEvent(self, event): super().changeEvent(event) self.update() def paintEvent(self, event): opt = QStyleOption() opt.initFrom(self) if not self.__popupicon.isVisible(): return if not (opt.state & QStyle.State_MouseOver or opt.state & QStyle.State_HasFocus): return palette = opt.palette # type: QPalette if opt.state & QStyle.State_HasFocus: pen = QPen(palette.color(QPalette.Highlight)) else: pen = QPen(palette.color(QPalette.Dark)) if (self.__fulltext and opt.state & QStyle.State_MouseOver and opt.state & QStyle.State_Active): g = QLinearGradient() g.setCoordinateMode(QLinearGradient.ObjectBoundingMode) base = palette.color(QPalette.Window) base.setAlpha(90) g.setColorAt(0, base.lighter(200)) g.setColorAt(0.6, base) g.setColorAt(1.0, base.lighter(200)) brush = QBrush(g) else: brush = QBrush(Qt.NoBrush) p = QPainter(self) p.setBrush(brush) p.setPen(pen) p.drawRect(opt.rect.adjusted(0, 0, -1, -1))
class MessageWidget(QWidget): """ A widget displaying a simple message to the user. This is an alternative to a full QMessageBox intended for inline modeless messages. [[icon] {Message text} (Ok) (Cancel)] """ #: Emitted when a button with the AcceptRole is clicked accepted = Signal() #: Emitted when a button with the RejectRole is clicked rejected = Signal() #: Emitted when a button with the HelpRole is clicked helpRequested = Signal() #: Emitted when a button is clicked clicked = Signal(QAbstractButton) class StandardButton(enum.IntEnum): NoButton, Ok, Close, Help = 0x0, 0x1, 0x2, 0x4 NoButton, Ok, Close, Help = list(StandardButton) class ButtonRole(enum.IntEnum): InvalidRole, AcceptRole, RejectRole, HelpRole = 0, 1, 2, 3 InvalidRole, AcceptRole, RejectRole, HelpRole = list(ButtonRole) _Button = namedtuple("_Button", ["button", "role", "stdbutton"]) def __init__(self, parent=None, icon=QIcon(), text="", wordWrap=False, textFormat=Qt.AutoText, standardButtons=NoButton, **kwargs): super().__init__(parent, **kwargs) self.__text = text self.__icon = QIcon() self.__wordWrap = wordWrap self.__standardButtons = MessageWidget.NoButton self._buttons = [] layout = QHBoxLayout() layout.setContentsMargins(8, 0, 8, 0) self.__iconlabel = QLabel(objectName="icon-label") self.__iconlabel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.__textlabel = QLabel(objectName="text-label", text=text, wordWrap=wordWrap, textFormat=textFormat) if sys.platform == "darwin": self.__textlabel.setAttribute(Qt.WA_MacSmallSize) layout.addWidget(self.__iconlabel) layout.addWidget(self.__textlabel) self.setLayout(layout) self.setIcon(icon) self.setStandardButtons(standardButtons) def setText(self, text): """ Set the current message text. :type message: str """ if self.__text != text: self.__text = text self.__textlabel.setText(text) def text(self): """ Return the current message text. :rtype: str """ return self.__text def setIcon(self, icon): """ Set the message icon. :type icon: QIcon | QPixmap | QString | QStyle.StandardPixmap """ if isinstance(icon, QStyle.StandardPixmap): icon = self.style().standardIcon(icon) else: icon = QIcon(icon) if self.__icon != icon: self.__icon = QIcon(icon) if not self.__icon.isNull(): size = self.style().pixelMetric(QStyle.PM_SmallIconSize, None, self) pm = self.__icon.pixmap(QSize(size, size)) else: pm = QPixmap() self.__iconlabel.setPixmap(pm) self.__iconlabel.setVisible(not pm.isNull()) def icon(self): """ Return the current icon. :rtype: QIcon """ return QIcon(self.__icon) def setWordWrap(self, wordWrap): """ Set the message text wrap property :type wordWrap: bool """ if self.__wordWrap != wordWrap: self.__wordWrap = wordWrap self.__textlabel.setWordWrap(wordWrap) def wordWrap(self): """ Return the message text wrap property. :rtype: bool """ return self.__wordWrap def setTextFormat(self, textFormat): """ Set message text format :type textFormat: Qt.TextFormat """ self.__textlabel.setTextFormat(textFormat) def textFormat(self): """ Return the message text format. :rtype: Qt.TextFormat """ return self.__textlabel.textFormat() def changeEvent(self, event): # reimplemented if event.type() == 177: # QEvent.MacSizeChange: ... super().changeEvent(event) def setStandardButtons(self, buttons): for button in MessageWidget.StandardButton: existing = self.button(button) if button & buttons and existing is None: self.addButton(button) elif existing is not None: self.removeButton(existing) def standardButtons(self): return functools.reduce( operator.ior, (slot.stdbutton for slot in self._buttons if slot.stdbutton is not None), MessageWidget.NoButton) def addButton(self, button, *rolearg): """ addButton(QAbstractButton, ButtonRole) addButton(str, ButtonRole) addButton(StandardButton) Add and return a button """ stdbutton = None if isinstance(button, QAbstractButton): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(QAbstractButton, role)") role = rolearg[0] elif isinstance(button, MessageWidget.StandardButton): if len(rolearg) != 0: raise TypeError("Wrong number of arguments for " "addButton(StandardButton)") stdbutton = button if button == MessageWidget.Ok: role = MessageWidget.AcceptRole button = QPushButton("Ok", default=False, autoDefault=False) elif button == MessageWidget.Close: role = MessageWidget.RejectRole # button = QPushButton( # default=False, autoDefault=False, flat=True, # icon=QIcon(self.style().standardIcon( # QStyle.SP_TitleBarCloseButton))) button = SimpleButton(icon=QIcon(self.style().standardIcon( QStyle.SP_TitleBarCloseButton))) elif button == MessageWidget.Help: role = MessageWidget.HelpRole button = QPushButton("Help", default=False, autoDefault=False) elif isinstance(button, str): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(str, ButtonRole)") role = rolearg[0] button = QPushButton(button, default=False, autoDefault=False) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) self._buttons.append(MessageWidget._Button(button, role, stdbutton)) button.clicked.connect(self._button_clicked) self._relayout() return button def removeButton(self, button): """ Remove a `button`. :type button: QAbstractButton """ slot = [s for s in self._buttons if s.button is button] if slot: slot = slot[0] self._buttons.remove(slot) self.layout().removeWidget(slot.button) slot.button.setParent(None) def buttonRole(self, button): """ Return the ButtonRole for button :type button: QAbstractButton """ for slot in self._buttons: if slot.button is button: return slot.role else: return MessageWidget.InvalidRole def button(self, standardButton): """ Return the button for the StandardButton. :type standardButton: StandardButton """ for slot in self._buttons: if slot.stdbutton == standardButton: return slot.button else: return None def _button_clicked(self): button = self.sender() role = self.buttonRole(button) self.clicked.emit(button) if role == MessageWidget.AcceptRole: self.accepted.emit() self.close() elif role == MessageWidget.RejectRole: self.rejected.emit() self.close() elif role == MessageWidget.HelpRole: self.helpRequested.emit() def _relayout(self): for slot in self._buttons: self.layout().removeWidget(slot.button) order = { MessageOverlayWidget.HelpRole: 0, MessageOverlayWidget.AcceptRole: 2, MessageOverlayWidget.RejectRole: 3, } orderd = sorted(self._buttons, key=lambda slot: order.get(slot.role, -1)) prev = self.__textlabel for slot in orderd: self.layout().addWidget(slot.button) QWidget.setTabOrder(prev, slot.button)
class MessageWidget(QWidget): """ A widget displaying a simple message to the user. This is an alternative to a full QMessageBox intended for inline modeless messages. [[icon] {Message text} (Ok) (Cancel)] """ #: Emitted when a button with the AcceptRole is clicked accepted = Signal() #: Emitted when a button with the RejectRole is clicked rejected = Signal() #: Emitted when a button with the HelpRole is clicked helpRequested = Signal() #: Emitted when a button is clicked clicked = Signal(QAbstractButton) class StandardButton(enum.IntEnum): NoButton, Ok, Close, Help = 0x0, 0x1, 0x2, 0x4 NoButton, Ok, Close, Help = list(StandardButton) class ButtonRole(enum.IntEnum): InvalidRole, AcceptRole, RejectRole, HelpRole = 0, 1, 2, 3 InvalidRole, AcceptRole, RejectRole, HelpRole = list(ButtonRole) _Button = namedtuple("_Button", ["button", "role", "stdbutton"]) def __init__(self, parent=None, icon=QIcon(), text="", wordWrap=False, textFormat=Qt.AutoText, standardButtons=NoButton, **kwargs): super().__init__(parent, **kwargs) self.__text = text self.__icon = QIcon() self.__wordWrap = wordWrap self.__standardButtons = MessageWidget.NoButton self.__buttons = [] layout = QHBoxLayout() layout.setContentsMargins(8, 0, 8, 0) self.__iconlabel = QLabel(objectName="icon-label") self.__iconlabel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.__textlabel = QLabel(objectName="text-label", text=text, wordWrap=wordWrap, textFormat=textFormat) if sys.platform == "darwin": self.__textlabel.setAttribute(Qt.WA_MacSmallSize) layout.addWidget(self.__iconlabel) layout.addWidget(self.__textlabel) self.setLayout(layout) self.setIcon(icon) self.setStandardButtons(standardButtons) def setText(self, text): """ Set the current message text. :type message: str """ if self.__text != text: self.__text = text self.__textlabel.setText(text) def text(self): """ Return the current message text. :rtype: str """ return self.__text def setIcon(self, icon): """ Set the message icon. :type icon: QIcon | QPixmap | QString | QStyle.StandardPixmap """ if isinstance(icon, QStyle.StandardPixmap): icon = self.style().standardIcon(icon) else: icon = QIcon(icon) if self.__icon != icon: self.__icon = QIcon(icon) if not self.__icon.isNull(): size = self.style().pixelMetric( QStyle.PM_SmallIconSize, None, self) pm = self.__icon.pixmap(QSize(size, size)) else: pm = QPixmap() self.__iconlabel.setPixmap(pm) self.__iconlabel.setVisible(not pm.isNull()) def icon(self): """ Return the current icon. :rtype: QIcon """ return QIcon(self.__icon) def setWordWrap(self, wordWrap): """ Set the message text wrap property :type wordWrap: bool """ if self.__wordWrap != wordWrap: self.__wordWrap = wordWrap self.__textlabel.setWordWrap(wordWrap) def wordWrap(self): """ Return the message text wrap property. :rtype: bool """ return self.__wordWrap def setTextFormat(self, textFormat): """ Set message text format :type textFormat: Qt.TextFormat """ self.__textlabel.setTextFormat(textFormat) def textFormat(self): """ Return the message text format. :rtype: Qt.TextFormat """ return self.__textlabel.textFormat() def changeEvent(self, event): # reimplemented if event.type() == 177: # QEvent.MacSizeChange: ... super().changeEvent(event) def setStandardButtons(self, buttons): for button in MessageWidget.StandardButton: existing = self.button(button) if button & buttons and existing is None: self.addButton(button) elif existing is not None: self.removeButton(existing) def standardButtons(self): return functools.reduce( operator.ior, (slot.stdbutton for slot in self.__buttons if slot.stdbutton is not None), MessageWidget.NoButton) def addButton(self, button, *rolearg): """ addButton(QAbstractButton, ButtonRole) addButton(str, ButtonRole) addButton(StandardButton) Add and return a button """ stdbutton = None if isinstance(button, QAbstractButton): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(QAbstractButton, role)") role = rolearg[0] elif isinstance(button, MessageWidget.StandardButton): if len(rolearg) != 0: raise TypeError("Wrong number of arguments for " "addButton(StandardButton)") stdbutton = button if button == MessageWidget.Ok: role = MessageWidget.AcceptRole button = QPushButton("Ok", default=False, autoDefault=False) elif button == MessageWidget.Close: role = MessageWidget.RejectRole # button = QPushButton( # default=False, autoDefault=False, flat=True, # icon=QIcon(self.style().standardIcon( # QStyle.SP_TitleBarCloseButton))) button = SimpleButton( icon=QIcon(self.style().standardIcon( QStyle.SP_TitleBarCloseButton))) elif button == MessageWidget.Help: role = MessageWidget.HelpRole button = QPushButton("Help", default=False, autoDefault=False) elif isinstance(button, str): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(str, ButtonRole)") role = rolearg[0] button = QPushButton(button, default=False, autoDefault=False) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) self.__buttons.append(MessageWidget._Button(button, role, stdbutton)) button.clicked.connect(self.__button_clicked) self.__relayout() return button def removeButton(self, button): """ Remove a `button`. :type button: QAbstractButton """ slot = [s for s in self.__buttons if s.button is button] if slot: slot = slot[0] self.__buttons.remove(slot) self.layout().removeWidget(slot.button) slot.button.setParent(None) def buttonRole(self, button): """ Return the ButtonRole for button :type button: QAbsstractButton """ for slot in self.__buttons: if slot.button is button: return slot.role else: return MessageWidget.InvalidRole def button(self, standardButton): """ Return the button for the StandardButton. :type standardButton: StandardButton """ for slot in self.__buttons: if slot.stdbutton == standardButton: return slot.button else: return None def __button_clicked(self): button = self.sender() role = self.buttonRole(button) self.clicked.emit(button) if role == MessageWidget.AcceptRole: self.accepted.emit() self.close() elif role == MessageWidget.RejectRole: self.rejected.emit() self.close() elif role == MessageWidget.HelpRole: self.helpRequested.emit() def __relayout(self): for slot in self.__buttons: self.layout().removeWidget(slot.button) order = { MessageOverlayWidget.HelpRole: 0, MessageOverlayWidget.AcceptRole: 2, MessageOverlayWidget.RejectRole: 3, } orderd = sorted(self.__buttons, key=lambda slot: order.get(slot.role, -1)) prev = self.__textlabel for slot in orderd: self.layout().addWidget(slot.button) QWidget.setTabOrder(prev, slot.button)
class MessagesWidget(QWidget): """ An iconified multiple message display area. `MessagesWidget` displays a short message along with an icon. If there are multiple messages they are summarized. The user can click on the widget to display the full message text in a popup view. """ #: Signal emitted when an embedded html link is clicked #: (if `openExternalLinks` is `False`). linkActivated = Signal(str) #: Signal emitted when an embedded html link is hovered. linkHovered = Signal(str) Severity = Severity #: General informative message. Information = Severity.Information #: A warning message severity. Warning = Severity.Warning #: An error message severity. Error = Severity.Error Message = Message def __init__(self, parent=None, openExternalLinks=False, defaultStyleSheet="", **kwargs): kwargs.setdefault( "sizePolicy", QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) ) super().__init__(parent, **kwargs) self.__openExternalLinks = openExternalLinks # type: bool self.__messages = OrderedDict() # type: Dict[Hashable, Message] #: The full (joined all messages text - rendered as html), displayed #: in a tooltip. self.__fulltext = "" #: The full text displayed in a popup. Is empty if the message is #: short self.__popuptext = "" #: Leading icon self.__iconwidget = IconWidget( sizePolicy=QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) ) #: Inline message text self.__textlabel = QLabel( wordWrap=False, textInteractionFlags=Qt.LinksAccessibleByMouse, openExternalLinks=self.__openExternalLinks, sizePolicy=QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum) ) #: Indicator that extended contents are accessible with a click on the #: widget. self.__popupicon = QLabel( sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum), text="\N{VERTICAL ELLIPSIS}", visible=False, ) self.__textlabel.linkActivated.connect(self.linkActivated) self.__textlabel.linkHovered.connect(self.linkHovered) self.setLayout(QHBoxLayout()) self.layout().setContentsMargins(2, 1, 2, 1) self.layout().setSpacing(0) self.layout().addWidget(self.__iconwidget) self.layout().addSpacing(4) self.layout().addWidget(self.__textlabel) self.layout().addWidget(self.__popupicon) self.__textlabel.setAttribute(Qt.WA_MacSmallSize) self.__defaultStyleSheet = defaultStyleSheet def sizeHint(self): sh = super().sizeHint() h = self.style().pixelMetric(QStyle.PM_SmallIconSize) if all(m.isEmpty() for m in self.messages()): sh.setWidth(0) return sh.expandedTo(QSize(0, h + 2)) def minimumSizeHint(self): msh = super().minimumSizeHint() h = self.style().pixelMetric(QStyle.PM_SmallIconSize) if all(m.isEmpty() for m in self.messages()): msh.setWidth(0) else: msh.setWidth(h + 2) return msh.expandedTo(QSize(0, h + 2)) def setOpenExternalLinks(self, state): # type: (bool) -> None """ If `True` then `linkActivated` signal will be emitted when the user clicks on an html link in a message, otherwise links are opened using `QDesktopServices.openUrl` """ # TODO: update popup if open self.__openExternalLinks = state self.__textlabel.setOpenExternalLinks(state) def openExternalLinks(self): # type: () -> bool """ """ return self.__openExternalLinks def setDefaultStyleSheet(self, css): # type: (str) -> None """ Set a default css to apply to the rendered text. Parameters ---------- css : str A css style sheet as supported by Qt's Rich Text support. Note ---- Not to be confused with `QWidget.styleSheet` See Also -------- `Supported HTML Subset`_ .. _`Supported HTML Subset`: http://doc.qt.io/qt-5/richtext-html-subset.html """ if self.__defaultStyleSheet != css: self.__defaultStyleSheet = css self.__update() def defaultStyleSheet(self): """ Returns ------- css : str The current style sheet """ return self.__defaultStyleSheet def setMessage(self, message_id, message): # type: (Hashable, Message) -> None """ Add a `message` for `message_id` to the current display. Note ---- Set an empty `Message` instance to clear the message display but retain the relative ordering in the display should a message for `message_id` reactivate. """ self.__messages[message_id] = message self.__update() def removeMessage(self, message_id): # type: (Hashable) -> None """ Remove message for `message_id` from the display. Note ---- Setting an empty `Message` instance will also clear the display, however the relative ordering of the messages will be retained, should the `message_id` 'reactivate'. """ del self.__messages[message_id] self.__update() def setMessages(self, messages): # type: (Union[Iterable[Tuple[Hashable, Message]], Dict[Hashable, Message]]) -> None """ Set multiple messages in a single call. """ messages = OrderedDict(messages) self.__messages.update(messages) self.__update() def clear(self): # type: () -> None """ Clear all messages. """ self.__messages.clear() self.__update() def messages(self): # type: () -> List[Message] """ Return all set messages. Returns ------- messages: `List[Message]` """ return list(self.__messages.values()) def summarize(self): # type: () -> Message """ Summarize all the messages into a single message. """ messages = [m for m in self.__messages.values() if not m.isEmpty()] if messages: return summarize(messages) else: return Message() @staticmethod def __styled(css, html): # Prepend css style sheet before a html fragment. if css.strip(): return "<style>\n" + escape(css) + "\n</style>\n" + html else: return html def __update(self): """ Update the current display state. """ self.ensurePolished() summary = self.summarize() icon = message_icon(summary) self.__iconwidget.setIcon(icon) self.__iconwidget.setVisible(not (summary.isEmpty() or icon.isNull())) self.__textlabel.setTextFormat(summary.textFormat) self.__textlabel.setText(summary.text) self.__textlabel.setVisible(bool(summary.text)) messages = [m for m in self.__messages.values() if not m.isEmpty()] if messages: messages = sorted(messages, key=attrgetter("severity"), reverse=True) fulltext = "<hr/>".join(m.asHtml() for m in messages) else: fulltext = "" self.__fulltext = fulltext self.setToolTip(self.__styled(self.__defaultStyleSheet, fulltext)) def is_short(m): return not (m.informativeText or m.detailedText) if not messages or len(messages) == 1 and is_short(messages[0]): self.__popuptext = "" else: self.__popuptext = fulltext self.__popupicon.setVisible(bool(self.__popuptext)) self.layout().activate() def mousePressEvent(self, event): if event.button() == Qt.LeftButton: if self.__popuptext: popup = QMenu(self) label = QLabel( self, textInteractionFlags=Qt.TextBrowserInteraction, openExternalLinks=self.__openExternalLinks, ) label.setText(self.__styled(self.__defaultStyleSheet, self.__popuptext)) label.linkActivated.connect(self.linkActivated) label.linkHovered.connect(self.linkHovered) action = QWidgetAction(popup) action.setDefaultWidget(label) popup.addAction(action) popup.popup(event.globalPos(), action) event.accept() return else: super().mousePressEvent(event) def enterEvent(self, event): super().enterEvent(event) self.update() def leaveEvent(self, event): super().leaveEvent(event) self.update() def changeEvent(self, event): super().changeEvent(event) self.update() def paintEvent(self, event): opt = QStyleOption() opt.initFrom(self) if not self.__popupicon.isVisible(): return if not (opt.state & QStyle.State_MouseOver or opt.state & QStyle.State_HasFocus): return palette = opt.palette # type: QPalette if opt.state & QStyle.State_HasFocus: pen = QPen(palette.color(QPalette.Highlight)) else: pen = QPen(palette.color(QPalette.Dark)) if self.__fulltext and \ opt.state & QStyle.State_MouseOver and \ opt.state & QStyle.State_Active: g = QLinearGradient() g.setCoordinateMode(QLinearGradient.ObjectBoundingMode) base = palette.color(QPalette.Window) base.setAlpha(90) g.setColorAt(0, base.lighter(200)) g.setColorAt(0.6, base) g.setColorAt(1.0, base.lighter(200)) brush = QBrush(g) else: brush = QBrush(Qt.NoBrush) p = QPainter(self) p.setBrush(brush) p.setPen(pen) p.drawRect(opt.rect.adjusted(0, 0, -1, -1))
class NotificationMessageWidget(QWidget): #: Emitted when a button with the AcceptRole is clicked accepted = Signal() #: Emitted when a button with the RejectRole is clicked rejected = Signal() #: Emitted when a button is clicked clicked = Signal(QAbstractButton) class StandardButton(enum.IntEnum): NoButton, Ok, Close = 0x0, 0x1, 0x2 NoButton, Ok, Close = list(StandardButton) class ButtonRole(enum.IntEnum): InvalidRole, AcceptRole, RejectRole, DismissRole = 0, 1, 2, 3 InvalidRole, AcceptRole, RejectRole, DismissRole = list(ButtonRole) _Button = namedtuple("_Button", ["button", "role", "stdbutton"]) def __init__(self, parent=None, icon=QIcon(), title="", text="", wordWrap=False, textFormat=Qt.PlainText, standardButtons=NoButton, acceptLabel="Ok", rejectLabel="No", **kwargs): super().__init__(parent, **kwargs) self._title = title self._text = text self._icon = QIcon() self._wordWrap = wordWrap self._standardButtons = NotificationMessageWidget.NoButton self._buttons = [] self._acceptLabel = acceptLabel self._rejectLabel = rejectLabel self._iconlabel = QLabel(objectName="icon-label") self._titlelabel = QLabel(objectName="title-label", text=title, wordWrap=wordWrap, textFormat=textFormat) self._textlabel = QLabel(objectName="text-label", text=text, wordWrap=wordWrap, textFormat=textFormat) self._textlabel.setTextInteractionFlags(Qt.TextBrowserInteraction) self._textlabel.setOpenExternalLinks(True) if sys.platform == "darwin": self._titlelabel.setAttribute(Qt.WA_MacSmallSize) self._textlabel.setAttribute(Qt.WA_MacSmallSize) layout = QHBoxLayout() self._iconlabel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) layout.addWidget(self._iconlabel) layout.setAlignment(self._iconlabel, Qt.AlignTop) message_layout = QVBoxLayout() self._titlelabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) if sys.platform == "darwin": self._titlelabel.setContentsMargins(0, 1, 0, 0) else: self._titlelabel.setContentsMargins(0, 0, 0, 0) message_layout.addWidget(self._titlelabel) self._textlabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) message_layout.addWidget(self._textlabel) self.button_layout = QHBoxLayout() self.button_layout.setAlignment(Qt.AlignLeft) message_layout.addLayout(self.button_layout) layout.addLayout(message_layout) layout.setSpacing(7) self.setLayout(layout) self.setIcon(icon) self.setStandardButtons(standardButtons) def setText(self, text): """ Set the current message text. :type message: str """ if self._text != text: self._text = text self._textlabel.setText(text) def text(self): """ Return the current message text. :rtype: str """ return self._text def setTitle(self, title): """ Set the current title text. :type title: str """ if self._title != title: self._title = title self._titleLabel.setText(title) def title(self): """ Return the current title text. :rtype: str """ return self._title def setIcon(self, icon): """ Set the message icon. :type icon: QIcon | QPixmap | QString | QStyle.StandardPixmap """ if isinstance(icon, QStyle.StandardPixmap): icon = self.style().standardIcon(icon) else: icon = QIcon(icon) if self._icon != icon: self._icon = QIcon(icon) if not self._icon.isNull(): size = self.style().pixelMetric(QStyle.PM_SmallIconSize, None, self) pm = self._icon.pixmap(QSize(size, size)) else: pm = QPixmap() self._iconlabel.setPixmap(pm) self._iconlabel.setVisible(not pm.isNull()) def icon(self): """ Return the current icon. :rtype: QIcon """ return QIcon(self._icon) def setWordWrap(self, wordWrap): """ Set the message text wrap property :type wordWrap: bool """ if self._wordWrap != wordWrap: self._wordWrap = wordWrap self._textlabel.setWordWrap(wordWrap) def wordWrap(self): """ Return the message text wrap property. :rtype: bool """ return self._wordWrap def setTextFormat(self, textFormat): """ Set message text format :type textFormat: Qt.TextFormat """ self._textlabel.setTextFormat(textFormat) def textFormat(self): """ Return the message text format. :rtype: Qt.TextFormat """ return self._textlabel.textFormat() def setAcceptLabel(self, label): """ Set the accept button label. :type label: str """ self._acceptLabel = label def acceptLabel(self): """ Return the accept button label. :rtype str """ return self._acceptLabel def setRejectLabel(self, label): """ Set the reject button label. :type label: str """ self._rejectLabel = label def rejectLabel(self): """ Return the reject button label. :rtype str """ return self._rejectLabel def setStandardButtons(self, buttons): for button in NotificationMessageWidget.StandardButton: existing = self.button(button) if button & buttons and existing is None: self.addButton(button) elif existing is not None: self.removeButton(existing) def standardButtons(self): return functools.reduce( operator.ior, (slot.stdbutton for slot in self._buttons if slot.stdbutton is not None), NotificationMessageWidget.NoButton) def addButton(self, button, *rolearg): """ addButton(QAbstractButton, ButtonRole) addButton(str, ButtonRole) addButton(StandardButton) Add and return a button """ stdbutton = None if isinstance(button, QAbstractButton): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(QAbstractButton, role)") role = rolearg[0] elif isinstance(button, NotificationMessageWidget.StandardButton): if rolearg: raise TypeError("Wrong number of arguments for " "addButton(StandardButton)") stdbutton = button if button == NotificationMessageWidget.Ok: role = NotificationMessageWidget.AcceptRole button = QPushButton(self._acceptLabel, default=False, autoDefault=False) elif button == NotificationMessageWidget.Close: role = NotificationMessageWidget.RejectRole button = QPushButton(self._rejectLabel, default=False, autoDefault=False) elif isinstance(button, str): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(str, ButtonRole)") role = rolearg[0] button = QPushButton(button, default=False, autoDefault=False) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) self._buttons.append( NotificationMessageWidget._Button(button, role, stdbutton)) button.clicked.connect(self._button_clicked) self._relayout() return button def _relayout(self): for slot in self._buttons: self.button_layout.removeWidget(slot.button) order = { NotificationWidget.AcceptRole: 0, NotificationWidget.RejectRole: 1, } ordered = sorted([ b for b in self._buttons if self.buttonRole(b.button) != NotificationMessageWidget.DismissRole ], key=lambda slot: order.get(slot.role, -1)) prev = self._textlabel for slot in ordered: self.button_layout.addWidget(slot.button) QWidget.setTabOrder(prev, slot.button) def removeButton(self, button): """ Remove a `button`. :type button: QAbstractButton """ slot = [s for s in self._buttons if s.button is button] if slot: slot = slot[0] self._buttons.remove(slot) self.layout().removeWidget(slot.button) slot.button.setParent(None) def buttonRole(self, button): """ Return the ButtonRole for button :type button: QAbstractButton """ for slot in self._buttons: if slot.button is button: return slot.role return NotificationMessageWidget.InvalidRole def button(self, standardButton): """ Return the button for the StandardButton. :type standardButton: StandardButton """ for slot in self._buttons: if slot.stdbutton == standardButton: return slot.button return None def _button_clicked(self): button = self.sender() role = self.buttonRole(button) self.clicked.emit(button) if role == NotificationMessageWidget.AcceptRole: self.accepted.emit() self.close() elif role == NotificationMessageWidget.RejectRole: self.rejected.emit() self.close()