Repository: biolab/orange3

Issue:
Widget errors are hard to notice
One of my colleagues commented that looking for red triangles is part of the fun in using Orange. 

If the widget is open, errors (in the status bar) are hard to notice. While that could be fine for information and (maybe) warnings, errors should be more noticeable. I do not know a good way to do it, but #3372  inspired me to write this issue.

Also, they could include hyperlinks (perhaps into documentation).


Discussion:
Test red background for errors.

Resolving patch:
diff --git a/Orange/canvas/canvas/items/nodeitem.py b/Orange/canvas/canvas/items/nodeitem.py
--- a/Orange/canvas/canvas/items/nodeitem.py
+++ b/Orange/canvas/canvas/items/nodeitem.py
@@ -17,8 +17,8 @@
     QPainterPathStroker
 )
 from AnyQt.QtCore import (
-    Qt, QEvent, QPointF, QRectF, QRect, QSize, QTime, QTimer, QPropertyAnimation
-)
+    Qt, QEvent, QPointF, QRectF, QRect, QSize, QTime, QTimer, QPropertyAnimation,
+    QEasingCurve)
 from AnyQt.QtCore import pyqtSignal as Signal, pyqtProperty as Property
 
 from .graphicspathobject import GraphicsPathObject
@@ -587,7 +587,7 @@ def standard_icon(standard_pixmap):
     return style.standardIcon(standard_pixmap)
 
 
-class GraphicsIconItem(QGraphicsItem):
+class GraphicsIconItem(QGraphicsWidget):
     """
     A graphics item displaying an :class:`QIcon`.
     """
@@ -608,6 +608,15 @@ def __init__(self, parent=None, icon=None, iconSize=None, **kwargs):
         self.__iconSize = QSize(iconSize)
         self.__icon = QIcon(icon)
 
+        self._opacity = 1
+        self.anim = QPropertyAnimation(self, b"opacity")
+        self.anim.setDuration(350)
+        self.anim.setStartValue(1)
+        self.anim.setKeyValueAt(0.5, 0)
+        self.anim.setEndValue(1)
+        self.anim.setEasingCurve(QEasingCurve.OutQuad)
+        self.anim.setLoopCount(5)
+
     def setIcon(self, icon):
         """
         Set the icon (:class:`QIcon`).
@@ -616,6 +625,15 @@ def setIcon(self, icon):
             self.__icon = QIcon(icon)
             self.update()
 
+    def getOpacity(self):
+        return self._opacity
+
+    def setOpacity(self, o):
+        self._opacity = o
+        self.update()
+
+    opacity = Property(float, fget=getOpacity, fset=setOpacity)
+
     def icon(self):
         """
         Return the icon (:class:`QIcon`).
@@ -673,6 +691,7 @@ def paint(self, painter, option, widget=None):
                 QPainter.SmoothPixmapTransform,
                 self.__transformationMode == Qt.SmoothTransformation
             )
+            painter.setOpacity(self._opacity)
             self.__icon.paint(painter, target, Qt.AlignCenter, mode)
 
 
@@ -1227,6 +1246,8 @@ def __updateMessages(self):
         messages = [self.__error, self.__warning, self.__info]
         for message, item in zip(messages, items):
             item.setVisible(bool(message))
+            if bool(message):
+                item.anim.start(QPropertyAnimation.KeepWhenStopped)
             item.setToolTip(message or "")
 
         shown = [item for item in items if item.isVisible()]
diff --git a/Orange/widgets/utils/messagewidget.py b/Orange/widgets/utils/messagewidget.py
--- a/Orange/widgets/utils/messagewidget.py
+++ b/Orange/widgets/utils/messagewidget.py
@@ -9,7 +9,7 @@
     NamedTuple, Tuple, List, Dict, Iterable, Union, Optional, Hashable
 )
 
-from AnyQt.QtCore import Qt, QSize, QBuffer
+from AnyQt.QtCore import Qt, QSize, QBuffer, QPropertyAnimation, QEasingCurve, Property
 from AnyQt.QtGui import (
     QIcon, QPixmap, QPainter, QPalette, QLinearGradient, QBrush, QPen
 )
@@ -363,6 +363,14 @@ def __init__(self, parent=None, openExternalLinks=False,
         self.__textlabel.setAttribute(Qt.WA_MacSmallSize)
         self.__defaultStyleSheet = defaultStyleSheet
 
+        self.anim = QPropertyAnimation(self.__iconwidget, b"opacity")
+        self.anim.setDuration(350)
+        self.anim.setStartValue(1)
+        self.anim.setKeyValueAt(0.5, 0)
+        self.anim.setEndValue(1)
+        self.anim.setEasingCurve(QEasingCurve.OutQuad)
+        self.anim.setLoopCount(5)
+
     def sizeHint(self):
         sh = super().sizeHint()
         h = self.style().pixelMetric(QStyle.PM_SmallIconSize)
@@ -497,6 +505,12 @@ def summarize(self):
         else:
             return Message()
 
+    def flashIcon(self):
+        for message in self.messages():
+            if message.severity != Severity.Information:
+                self.anim.start(QPropertyAnimation.KeepWhenStopped)
+                break
+
     @staticmethod
     def __styled(css, html):
         # Prepend css style sheet before a html fragment.
@@ -514,6 +528,7 @@ def __update(self):
         icon = message_icon(summary)
         self.__iconwidget.setIcon(icon)
         self.__iconwidget.setVisible(not (summary.isEmpty() or icon.isNull()))
+        self.anim.start(QPropertyAnimation.KeepWhenStopped)
         self.__textlabel.setTextFormat(summary.textFormat)
         self.__textlabel.setText(summary.text)
         self.__textlabel.setVisible(bool(summary.text))
@@ -614,6 +629,7 @@ def __init__(self, parent=None, icon=QIcon(), iconSize=QSize(), **kwargs):
         sizePolicy = kwargs.pop("sizePolicy", QSizePolicy(QSizePolicy.Fixed,
                                                           QSizePolicy.Fixed))
         super().__init__(parent, **kwargs)
+        self._opacity = 1
         self.__icon = QIcon(icon)
         self.__iconSize = QSize(iconSize)
         self.setSizePolicy(sizePolicy)
@@ -625,6 +641,15 @@ def setIcon(self, icon):
             self.updateGeometry()
             self.update()
 
+    def getOpacity(self):
+        return self._opacity
+
+    def setOpacity(self, o):
+        self._opacity = o
+        self.update()
+
+    opacity = Property(float, fget=getOpacity, fset=setOpacity)
+
     def icon(self):
         # type: () -> QIcon
         return QIcon(self.__icon)
@@ -652,6 +677,7 @@ def sizeHint(self):
 
     def paintEvent(self, event):
         painter = QStylePainter(self)
+        painter.setOpacity(self._opacity)
         opt = QStyleOption()
         opt.initFrom(self)
         painter.drawPrimitive(QStyle.PE_Widget, opt)
diff --git a/Orange/widgets/widget.py b/Orange/widgets/widget.py
--- a/Orange/widgets/widget.py
+++ b/Orange/widgets/widget.py
@@ -17,8 +17,7 @@
 )
 from AnyQt.QtCore import (
     Qt, QObject, QEvent, QRect, QMargins, QByteArray, QDataStream, QBuffer,
-    QSettings, QUrl, QThread, pyqtSignal as Signal, QSize
-)
+    QSettings, QUrl, QThread, pyqtSignal as Signal, QSize)
 from AnyQt.QtGui import QIcon, QKeySequence, QDesktopServices, QPainter
 
 from Orange.data import FileFormat
@@ -290,7 +289,7 @@ def __init_subclass__(cls, **_):
     @classmethod
     def get_widget_description(cls):
         if not cls.name:
-            return
+            return None
         properties = {name: getattr(cls, name) for name in
                       ("name", "icon", "description", "priority", "keywords",
                        "help", "help_ref", "url",
@@ -298,8 +297,7 @@ def get_widget_description(cls):
         properties["id"] = cls.id or cls.__module__
         properties["inputs"] = cls.get_signals("inputs")
         properties["outputs"] = cls.get_signals("outputs")
-        properties["qualified_name"] = \
-            "{}.{}".format(cls.__module__, cls.__name__)
+        properties["qualified_name"] = f"{cls.__module__}.{cls.__name__}"
         return properties
 
     @classmethod
@@ -827,6 +825,11 @@ def closeEvent(self, event):
             self.__updateSavedGeometry()
         QDialog.closeEvent(self, event)
 
+    def mousePressEvent(self, event):
+        """ Flash message bar icon on mouse press """
+        self.message_bar.flashIcon()
+        event.ignore()
+
     def setVisible(self, visible):
         # type: (bool) -> None
         """Reimplemented from `QDialog.setVisible`."""
@@ -919,7 +922,6 @@ def retrieveSpecificSettings(self):
         attributes. See :obj:`Orange.widgets.data.owcolor.OWColor` for an
         example.
         """
-        pass
 
     def storeSpecificSettings(self):
         """
@@ -931,7 +933,6 @@ def storeSpecificSettings(self):
         attributes. See :obj:`Orange.widgets.data.owcolor.OWColor` for an
         example.
         """
-        pass
 
     def saveSettings(self):
         """
@@ -948,7 +949,6 @@ def onDeleteWidget(self):
         If possible, subclasses should gracefully cancel any currently
         executing tasks.
         """
-        pass
 
     def handleNewSignals(self):
         """
@@ -958,7 +958,6 @@ def handleNewSignals(self):
         Reimplement this method in order to coalesce updates from
         multiple updated inputs.
         """
-        pass
 
     #: Widget's status message has changed.
     statusMessageChanged = Signal(str)
@@ -1049,7 +1048,6 @@ def workflowEnvChanged(self, key, value, oldvalue):
 
         The default implementation does nothing.
         """
-        pass
 
     def saveGeometryAndLayoutState(self):
         # type: () -> QByteArray
@@ -1248,7 +1246,7 @@ def paintEvent(self, event):
         painter.end()
 
 
-class Message(object):
+class Message:
     """
     A user message.
 


Test patch: