From a14819082102e9b42b573eb8dbd285680c20a5e6 Mon Sep 17 00:00:00 2001 From: Rafael Irgolic Date: Mon, 8 Apr 2019 12:32:51 +0200 Subject: [PATCH 1/6] overlay.py: Implement non-intrusive notifications --- Orange/canvas/__main__.py | 32 +- Orange/canvas/application/canvasmain.py | 78 +++- Orange/canvas/config.py | 2 + Orange/canvas/gui/stackedwidget.py | 17 +- Orange/canvas/utils/overlay.py | 580 ++++++++++++++++++++++++ Orange/widgets/utils/overlay.py | 26 +- 6 files changed, 666 insertions(+), 69 deletions(-) create mode 100644 Orange/canvas/utils/overlay.py diff --git a/Orange/canvas/__main__.py b/Orange/canvas/__main__.py index dfdfdafee70..4bcf2a7c410 100644 --- a/Orange/canvas/__main__.py +++ b/Orange/canvas/__main__.py @@ -180,34 +180,6 @@ def make_sql_logger(level=logging.INFO): sql_log.addHandler(handler) -def show_survey(): - # If run for the first time, open a browser tab with a survey - settings = QSettings() - show_survey = settings.value("startup/show-survey", True, type=bool) - if show_survey: - question = QMessageBox( - QMessageBox.Question, - 'Orange Survey', - 'We would like to know more about how our software is used.\n\n' - 'Would you care to fill our short 1-minute survey?', - QMessageBox.Yes | QMessageBox.No) - question.setDefaultButton(QMessageBox.Yes) - later = question.addButton('Ask again later', QMessageBox.NoRole) - question.setEscapeButton(later) - - def handle_response(result): - if result == QMessageBox.Yes: - success = QDesktopServices.openUrl( - QUrl("https://orange.biolab.si/survey/short.html")) - settings.setValue("startup/show-survey", not success) - else: - settings.setValue("startup/show-survey", result != QMessageBox.No) - - question.finished.connect(handle_response) - question.show() - return question - - def check_for_updates(): settings = QSettings() check_updates = settings.value('startup/check-updates', True, type=bool) @@ -433,6 +405,8 @@ def onrequest(url): settings = QSettings() + settings.setValue('startup/launch-count', settings.value('startup/launch-count', 0, int) + 1) + stylesheet = options.stylesheet or defaultstylesheet stylesheet_string = None @@ -558,7 +532,6 @@ def show_message(message): canvas_window.load_scheme(open_requests[-1].toLocalFile()) # local references prevent destruction - survey = show_survey() update_check = check_for_updates() send_stat = send_usage_statistics() @@ -591,7 +564,6 @@ def show_message(message): status = 42 del canvas_window - del survey del update_check del send_stat diff --git a/Orange/canvas/application/canvasmain.py b/Orange/canvas/application/canvasmain.py index 096a4e958ce..874e3142fbb 100644 --- a/Orange/canvas/application/canvasmain.py +++ b/Orange/canvas/application/canvasmain.py @@ -53,14 +53,14 @@ def user_documents_path(): return QDesktopServices.storageLocation( QDesktopServices.DocumentsLocation) -from ...widgets.utils.overlay import MessageOverlayWidget - from ..gui.dropshadow import DropShadowFrame from ..gui.dock import CollapsibleDockWidget from ..gui.quickhelp import QuickHelpTipEvent from ..gui.utils import message_critical, message_question, \ message_warning, message_information +from ..utils.overlay import NotificationWidget, NotificationOverlay + from ..document.usagestatistics import UsageStatistics from ..help import HelpManager @@ -384,15 +384,48 @@ def touch(): self.setMinimumSize(600, 500) - # ask for anonymous data collection permission - def requestDataCollectionPermission(): - permDialogButtons = MessageOverlayWidget.AcceptRole | MessageOverlayWidget.RejectRole - permDialog = MessageOverlayWidget(parent=w, - text="Do you wish to share anonymous usage " - "statistics to help improve Orange?", + self.notification_overlay = NotificationOverlay(self.scheme_widget) + + self.setup_notifications() + + def setup_notifications(self): + settings = config.settings() + + # If run for the first time, prompt short survey + show_survey = settings["startup/show-survey"] + if show_survey: + surveyDialogButtons = NotificationWidget.Ok | NotificationWidget.Close + surveyDialog = NotificationWidget(parent=self.scheme_widget, + icon=QIcon("Orange/Dlg_down3.png"), + title="Survey", + text="We want to get to know our users better.\n" + "Would you care to fill out a " + "1-minute survey?", wordWrap=True, - standardButtons=permDialogButtons) - btnOK = permDialog.button(MessageOverlayWidget.AcceptRole) + standardButtons=surveyDialogButtons) + + def handle_response(button): + if surveyDialog.buttonRole(button) == NotificationWidget.AcceptRole: + success = QDesktopServices.openUrl( + QUrl("https://orange.biolab.si/survey/short.html")) + settings["startup/show-survey"] = not success + elif surveyDialog.buttonRole(button) == NotificationWidget.RejectRole: + settings["startup/show-survey"] = False + + surveyDialog.clicked.connect(handle_response) + + surveyDialog.setWidget(self.scheme_widget) + surveyDialog.show() + + # data collection permission + if not settings["error-reporting/permission-requested"]: + permDialogButtons = NotificationWidget.AcceptRole | NotificationWidget.RejectRole + permDialog = NotificationWidget(parent=self.scheme_widget, + text="Do you wish to share anonymous usage " + "statistics to help improve Orange?", + wordWrap=True, + standardButtons=permDialogButtons) + btnOK = permDialog.button(NotificationWidget.AcceptRole) btnOK.setText("Allow") def respondToRequest(): @@ -405,22 +438,18 @@ def shareData(): permDialog.accepted.connect(shareData) permDialog.setStyleSheet(""" - MessageOverlayWidget { - background: qlineargradient( - x1: 0, y1: 0, x2: 0, y2: 1, - stop:0 #666, stop:0.3 #6D6D6D, stop:1 #666) - } - MessageOverlayWidget QLabel#text-label { - color: white; - }""") - - permDialog.setWidget(w) + MessageOverlayWidget { + background: qlineargradient( + x1: 0, y1: 0, x2: 0, y2: 1, + stop:0 #666, stop:0.3 #6D6D6D, stop:1 #666) + } + MessageOverlayWidget QLabel#text-label { + color: white; + }""") + + permDialog.setWidget(self.scheme_widget) permDialog.show() - settings = config.settings() - if not settings["error-reporting/permission-requested"]: - requestDataCollectionPermission() - def setup_actions(self): """Initialize main window actions. """ @@ -1912,6 +1941,7 @@ def closeEvent(self, event): settings.endGroup() self.help_dock.close() self.log_dock.close() + self.notification_overlay.close() super().closeEvent(event) __did_restore = False diff --git a/Orange/canvas/config.py b/Orange/canvas/config.py index 6604f2d8ba1..49775e54559 100644 --- a/Orange/canvas/config.py +++ b/Orange/canvas/config.py @@ -56,6 +56,8 @@ def init(): ("startup/check-updates", bool, True, "Check for updates"), + ("startup/launch-count", int, 0, ""), + ("stylesheet", str, "orange", "QSS stylesheet to use"), diff --git a/Orange/canvas/gui/stackedwidget.py b/Orange/canvas/gui/stackedwidget.py index c7d8f4fb57a..f934b429e91 100644 --- a/Orange/canvas/gui/stackedwidget.py +++ b/Orange/canvas/gui/stackedwidget.py @@ -86,12 +86,25 @@ def maximumSize(self): else: return QStackedLayout.maximumSize(self) + def hasHeightForWidth(self) -> bool: + current = self.currentWidget() + if current is not None: + return current.hasHeightForWidth() + else: + return False + + def heightForWidth(self, width: int) -> int: + current = self.currentWidget() + if current is not None: + return current.heightForWidth(width) + else: + return -1 + def geometry(self): # Reimplemented due to QTBUG-47107. return QRect(self.__rect) - def setGeometry(self, rect): - # type: (QRect) -> None + def setGeometry(self, rect: QRect) -> None: if rect == self.__rect: return self.__rect = QRect(rect) diff --git a/Orange/canvas/utils/overlay.py b/Orange/canvas/utils/overlay.py new file mode 100644 index 00000000000..e4ad9787f1b --- /dev/null +++ b/Orange/canvas/utils/overlay.py @@ -0,0 +1,580 @@ +import enum +import functools +import operator +import sys +from collections import namedtuple + +from AnyQt.QtCore import Signal, Qt, QSize +from AnyQt.QtGui import QIcon, QPixmap, QPainter +from AnyQt.QtWidgets import QAbstractButton, QHBoxLayout, QPushButton, QStyle, QWidget, \ + QVBoxLayout, QLabel, QSizePolicy, QStyleOption + +from Orange.canvas.gui.stackedwidget import StackLayout +from Orange.widgets.utils.buttons import SimpleButton +from Orange.widgets.utils.overlay import OverlayWidget + + +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.AutoText, 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.setTextFormat(Qt.RichText) + 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 len(rolearg) != 0: + 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 + else: + 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() + + +def proxydoc(func): + return functools.wraps(func, assigned=["__doc__"], updated=[]) + + +class NotificationWidget(QWidget): + #: Emitted when a button with an Accept role is clicked + accepted = Signal() + #: Emitted when a button with a Reject role is clicked + rejected = Signal() + #: Emitted when a button with a Dismiss role is clicked + dismissed = Signal() + #: Emitted when a button is clicked + clicked = Signal(QAbstractButton) + + NoButton, Ok, Close = list(NotificationMessageWidget.StandardButton) + InvalidRole, AcceptRole, RejectRole, DismissRole = \ + list(NotificationMessageWidget.ButtonRole) + + def __init__(self, parent=None, title="", text="", textFormat=Qt.AutoText, icon=QIcon(), + wordWrap=True, standardButtons=NoButton, acceptLabel="Ok", rejectLabel="No", + **kwargs): + super().__init__(parent, **kwargs) + self._margin = 10 # used in stylesheet and for dismiss button + + layout = QHBoxLayout() + if sys.platform == "darwin": + layout.setContentsMargins(6, 6, 6, 6) + else: + layout.setContentsMargins(9, 9, 9, 9) + + self.setStyleSheet(""" + NotificationWidget { + margin: """ + str(self._margin) + """px; + background: #626262; + border: 1px solid #999999; + border-radius: 8px; + } + NotificationWidget QLabel#text-label { + color: white; + } + NotificationWidget QLabel#title-label { + color: white; + font-weight: bold; + }""") + + self._msgwidget = NotificationMessageWidget( + parent=self, title=title, text=text, textFormat=textFormat, icon=icon, + wordWrap=wordWrap, standardButtons=standardButtons, acceptLabel=acceptLabel, + rejectLabel=rejectLabel + ) + self._msgwidget.accepted.connect(self.accepted) + self._msgwidget.rejected.connect(self.rejected) + self._msgwidget.clicked.connect(self.clicked) + + self._dismiss_button = SimpleButton(parent=self, + icon=QIcon(self.style().standardIcon( + QStyle.SP_TitleBarCloseButton))) + self._dismiss_button.setFixedSize(18, 18) + self._dismiss_button.clicked.connect(self.dismissed) + + def dismiss_handler(): + self.clicked.emit(self._dismiss_button) + self._dismiss_button.clicked.connect(dismiss_handler) + + layout.addWidget(self._msgwidget) + self.setLayout(layout) + + self.setFixedWidth(400) + + def resizeEvent(self, event): + super().resizeEvent(event) + if sys.platform == "darwin": + corner_margin = 6 + else: + corner_margin = 7 + x = self.width() - self._dismiss_button.width() - self._margin - corner_margin + y = self._margin + corner_margin + self._dismiss_button.move(x, y) + + def clone(self): + cloned = NotificationWidget(parent=self.parent(), + title=self.title(), + text=self.text(), + textFormat=self._msgwidget.textFormat(), + icon=self.icon(), + standardButtons=self._msgwidget.standardButtons(), + acceptLabel=self._msgwidget._acceptLabel, + rejectLabel=self._msgwidget._rejectLabel) + cloned.accepted.connect(self.accepted) + cloned.rejected.connect(self.rejected) + cloned.dismissed.connect(self.dismissed) + + button_map = dict(zip( + [b.button for b in cloned._msgwidget._buttons] + [cloned._dismiss_button], + [b.button for b in self._msgwidget._buttons] + [self._dismiss_button])) + cloned.clicked.connect(lambda b: + self.clicked.emit(button_map[b])) + + return cloned + + def paintEvent(self, event): + opt = QStyleOption() + opt.initFrom(self) + painter = QPainter(self) + self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) + + @proxydoc(NotificationMessageWidget.setText) + def setText(self, text): + self._msgwidget.setText(text) + + @proxydoc(NotificationMessageWidget.text) + def text(self): + return self._msgwidget.text() + + @proxydoc(NotificationMessageWidget.setTitle) + def setTitle(self, title): + self._msgwidget.setTitle(title) + + @proxydoc(NotificationMessageWidget.title) + def title(self): + return self._msgwidget.title() + + @proxydoc(NotificationMessageWidget.setIcon) + def setIcon(self, icon): + self._msgwidget.setIcon(icon) + + @proxydoc(NotificationMessageWidget.icon) + def icon(self): + return self._msgwidget.icon() + + @proxydoc(NotificationMessageWidget.textFormat) + def textFormat(self): + return self._msgwidget.textFormat() + + @proxydoc(NotificationMessageWidget.setTextFormat) + def setTextFormat(self, textFormat): + self._msgwidget.setTextFormat(textFormat) + + @proxydoc(NotificationMessageWidget.setStandardButtons) + def setStandardButtons(self, buttons): + self._msgwidget.setStandardButtons(buttons) + + @proxydoc(NotificationMessageWidget.addButton) + def addButton(self, *args): + return self._msgwidget.addButton(*args) + + @proxydoc(NotificationMessageWidget.removeButton) + def removeButton(self, button): + self._msgwidget.removeButton(button) + + @proxydoc(NotificationMessageWidget.buttonRole) + def buttonRole(self, button): + if button is self._dismiss_button: + return NotificationWidget.DismissRole + return self._msgwidget.buttonRole(button) + + @proxydoc(NotificationMessageWidget.button) + def button(self, standardButton): + return self._msgwidget.button(standardButton) + + +class NotificationOverlay(OverlayWidget): + # each canvas instance has its own overlay instance + overlayInstances = [] + + # list of queued notifications, overlay instances retain a clone of each notification + notifQueue = [] + + def __init__(self, parent=None, alignment=Qt.AlignRight | Qt.AlignBottom, **kwargs): + """ + Registers a new canvas instance to simultaneously display notifications in. + The parent parameter should be the canvas' scheme widget. + """ + super().__init__(parent, alignment=alignment, **kwargs) + + layout = StackLayout() + self.setLayout(layout) + + self._widgets = [] + + for notif in self.notifQueue: + cloned = notif.clone() + cloned.setParent(self) + cloned.clicked.connect(NotificationOverlay.nextNotification) + self.addWidget(cloned) + + self.overlayInstances.append(self) + + self.setWidget(parent) + + @staticmethod + def registerNotification(notif): + """ + Queues notification in all canvas instances (shows it if no other notifications present). + """ + notif.hide() + NotificationOverlay.notifQueue.append(notif) + + overlays = NotificationOverlay.overlayInstances + for overlay in overlays: + # each canvas requires its own instance of each notification + cloned = notif.clone() + cloned.setParent(overlay) + cloned.clicked.connect(NotificationOverlay.nextNotification) + overlay.addWidget(cloned) + + @staticmethod + def nextNotification(): + NotificationOverlay.notifQueue.pop(0) + + overlays = NotificationOverlay.overlayInstances + for overlay in overlays: + overlay.nextWidget() + + def addWidget(self, widget): + """ + Append the widget to the stack. + """ + self._widgets.append(widget) + self.layout().addWidget(widget) + + def nextWidget(self): + """ + Removes first widget from the stack. + + .. note:: The widget is hidden but is not deleted. + + """ + widget = self._widgets[0] + self._widgets.pop(0) + self.layout().removeWidget(widget) + + def currentWidget(self): + """ + Return the currently displayed widget. + """ + if not self._widgets: + return None + return self._widgets[0] + + def close(self): + self.overlayInstances.remove(self) + super().close() diff --git a/Orange/widgets/utils/overlay.py b/Orange/widgets/utils/overlay.py index 02711d8a21f..c3d19fe6d58 100644 --- a/Orange/widgets/utils/overlay.py +++ b/Orange/widgets/utils/overlay.py @@ -236,7 +236,7 @@ def __init__(self, parent=None, icon=QIcon(), text="", wordWrap=False, self.__icon = QIcon() self.__wordWrap = wordWrap self.__standardButtons = MessageWidget.NoButton - self.__buttons = [] + self._buttons = [] layout = QHBoxLayout() layout.setContentsMargins(8, 0, 8, 0) @@ -356,7 +356,7 @@ def setStandardButtons(self, buttons): def standardButtons(self): return functools.reduce( operator.ior, - (slot.stdbutton for slot in self.__buttons + (slot.stdbutton for slot in self._buttons if slot.stdbutton is not None), MessageWidget.NoButton) @@ -404,9 +404,9 @@ def addButton(self, button, *rolearg): 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() + self._buttons.append(MessageWidget._Button(button, role, stdbutton)) + button.clicked.connect(self._button_clicked) + self._relayout() return button @@ -416,10 +416,10 @@ def removeButton(self, button): :type button: QAbstractButton """ - slot = [s for s in self.__buttons if s.button is button] + slot = [s for s in self._buttons if s.button is button] if slot: slot = slot[0] - self.__buttons.remove(slot) + self._buttons.remove(slot) self.layout().removeWidget(slot.button) slot.button.setParent(None) @@ -429,7 +429,7 @@ def buttonRole(self, button): :type button: QAbstractButton """ - for slot in self.__buttons: + for slot in self._buttons: if slot.button is button: return slot.role else: @@ -441,13 +441,13 @@ def button(self, standardButton): :type standardButton: StandardButton """ - for slot in self.__buttons: + for slot in self._buttons: if slot.stdbutton == standardButton: return slot.button else: return None - def __button_clicked(self): + def _button_clicked(self): button = self.sender() role = self.buttonRole(button) self.clicked.emit(button) @@ -461,15 +461,15 @@ def __button_clicked(self): elif role == MessageWidget.HelpRole: self.helpRequested.emit() - def __relayout(self): - for slot in self.__buttons: + 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, + orderd = sorted(self._buttons, key=lambda slot: order.get(slot.role, -1)) prev = self.__textlabel From 105aa7e81ea783a0bb4e4242d13b98abec74de6b Mon Sep 17 00:00:00 2001 From: Rafael Irgolic Date: Thu, 6 Jun 2019 13:29:29 +0200 Subject: [PATCH 2/6] canvasmain -> __main__.py: Reimplement survey and statistics request as notifications --- Orange/canvas/__main__.py | 60 ++++++++++++++++++++++- Orange/canvas/application/canvasmain.py | 64 ------------------------- Orange/canvas/config.py | 3 ++ 3 files changed, 61 insertions(+), 66 deletions(-) diff --git a/Orange/canvas/__main__.py b/Orange/canvas/__main__.py index 4bcf2a7c410..49fafc1192f 100644 --- a/Orange/canvas/__main__.py +++ b/Orange/canvas/__main__.py @@ -19,7 +19,7 @@ import pkg_resources -from AnyQt.QtGui import QFont, QColor, QPalette, QDesktopServices +from AnyQt.QtGui import QFont, QColor, QPalette, QDesktopServices, QIcon from AnyQt.QtWidgets import QMessageBox from AnyQt.QtCore import ( Qt, QDir, QUrl, QSettings, QThread, pyqtSignal, QT_VERSION @@ -39,6 +39,7 @@ from Orange.canvas.registry import qt from Orange.canvas.registry import WidgetRegistry, set_global_registry from Orange.canvas.registry import cache +from Orange.canvas.utils.overlay import NotificationWidget, NotificationOverlay log = logging.getLogger(__name__) @@ -180,6 +181,57 @@ def make_sql_logger(level=logging.INFO): sql_log.addHandler(handler) +def setup_notifications(): + settings = config.settings() + + # If run for the fifth time, prompt short survey + show_survey = settings["startup/show-short-survey"] and \ + settings["startup/launch-count"] >= 5 + if show_survey: + surveyDialogButtons = NotificationWidget.Ok | NotificationWidget.Close + surveyDialog = NotificationWidget(icon=QIcon("Orange/widgets/icons/information.png"), + title="Survey", + text="We want to understand our users better.\n" + "Would you like to take a short survey?", + standardButtons=surveyDialogButtons) + + def handle_response(button): + if surveyDialog.buttonRole(button) == NotificationWidget.AcceptRole: + success = QDesktopServices.openUrl( + QUrl("https://orange.biolab.si/survey/short.html")) + settings["startup/show-short-survey"] = not success + elif surveyDialog.buttonRole(button) == NotificationWidget.RejectRole: + settings["startup/show-short-survey"] = False + + surveyDialog.clicked.connect(handle_response) + + NotificationOverlay.registerNotification(surveyDialog) + + # data collection permission + if not settings["error-reporting/permission-requested"]: + permDialogButtons = NotificationWidget.Ok | NotificationWidget.Close + permDialog = NotificationWidget(icon=QIcon("distribute/icon-48.png"), + title="Anonymous Usage Statistics", + text="Do you wish to opt-in to sharing " + "statistics about how you use Orange?\n" + "All data is anonymized and used " + "exclusively for understanding how users " + "interact with Orange.", + standardButtons=permDialogButtons) + btnOK = permDialog.button(NotificationWidget.AcceptRole) + btnOK.setText("Allow") + + def handle_response(button): + if permDialog.buttonRole(button) != permDialog.DismissRole: + settings["error-reporting/permission-requested"] = True + if permDialog.buttonRole(button) == permDialog.AcceptRole: + settings["error-reporting/send-statistics"] = True + + permDialog.clicked.connect(handle_response) + + NotificationOverlay.registerNotification(permDialog) + + def check_for_updates(): settings = QSettings() check_updates = settings.value('startup/check-updates', True, type=bool) @@ -244,8 +296,8 @@ def compare_versions(latest): thread.start() return thread -def send_usage_statistics(): +def send_usage_statistics(): class SendUsageStatistics(QThread): def run(self): try: @@ -259,6 +311,7 @@ def run(self): thread.start() return thread + def main(argv=None): if argv is None: argv = sys.argv @@ -531,6 +584,9 @@ def show_message(message): open_requests[-1]) canvas_window.load_scheme(open_requests[-1].toLocalFile()) + # initialize notifications + setup_notifications() + # local references prevent destruction update_check = check_for_updates() send_stat = send_usage_statistics() diff --git a/Orange/canvas/application/canvasmain.py b/Orange/canvas/application/canvasmain.py index 874e3142fbb..90a4e8859ee 100644 --- a/Orange/canvas/application/canvasmain.py +++ b/Orange/canvas/application/canvasmain.py @@ -386,70 +386,6 @@ def touch(): self.notification_overlay = NotificationOverlay(self.scheme_widget) - self.setup_notifications() - - def setup_notifications(self): - settings = config.settings() - - # If run for the first time, prompt short survey - show_survey = settings["startup/show-survey"] - if show_survey: - surveyDialogButtons = NotificationWidget.Ok | NotificationWidget.Close - surveyDialog = NotificationWidget(parent=self.scheme_widget, - icon=QIcon("Orange/Dlg_down3.png"), - title="Survey", - text="We want to get to know our users better.\n" - "Would you care to fill out a " - "1-minute survey?", - wordWrap=True, - standardButtons=surveyDialogButtons) - - def handle_response(button): - if surveyDialog.buttonRole(button) == NotificationWidget.AcceptRole: - success = QDesktopServices.openUrl( - QUrl("https://orange.biolab.si/survey/short.html")) - settings["startup/show-survey"] = not success - elif surveyDialog.buttonRole(button) == NotificationWidget.RejectRole: - settings["startup/show-survey"] = False - - surveyDialog.clicked.connect(handle_response) - - surveyDialog.setWidget(self.scheme_widget) - surveyDialog.show() - - # data collection permission - if not settings["error-reporting/permission-requested"]: - permDialogButtons = NotificationWidget.AcceptRole | NotificationWidget.RejectRole - permDialog = NotificationWidget(parent=self.scheme_widget, - text="Do you wish to share anonymous usage " - "statistics to help improve Orange?", - wordWrap=True, - standardButtons=permDialogButtons) - btnOK = permDialog.button(NotificationWidget.AcceptRole) - btnOK.setText("Allow") - - def respondToRequest(): - settings["error-reporting/permission-requested"] = True - - def shareData(): - settings["error-reporting/send-statistics"] = True - - permDialog.clicked.connect(respondToRequest) - permDialog.accepted.connect(shareData) - - permDialog.setStyleSheet(""" - MessageOverlayWidget { - background: qlineargradient( - x1: 0, y1: 0, x2: 0, y2: 1, - stop:0 #666, stop:0.3 #6D6D6D, stop:1 #666) - } - MessageOverlayWidget QLabel#text-label { - color: white; - }""") - - permDialog.setWidget(self.scheme_widget) - permDialog.show() - def setup_actions(self): """Initialize main window actions. """ diff --git a/Orange/canvas/config.py b/Orange/canvas/config.py index 49775e54559..806d5f8ebaf 100644 --- a/Orange/canvas/config.py +++ b/Orange/canvas/config.py @@ -58,6 +58,9 @@ def init(): ("startup/launch-count", int, 0, ""), + ("startup/show-short-survey", bool, True, + "Has the user not been asked to take a short survey yet"), + ("stylesheet", str, "orange", "QSS stylesheet to use"), From ba8a6382b188cc99b183c43d99a166e4e8c4ce45 Mon Sep 17 00:00:00 2001 From: Rafael Irgolic Date: Thu, 6 Jun 2019 13:08:49 +0200 Subject: [PATCH 3/6] __main__: Reimplement update prompt as notification --- Orange/canvas/__main__.py | 41 +++++++++++++++++++++------------- Orange/canvas/config.py | 3 ++- Orange/canvas/utils/overlay.py | 3 +-- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/Orange/canvas/__main__.py b/Orange/canvas/__main__.py index 49fafc1192f..c8d48c834a6 100644 --- a/Orange/canvas/__main__.py +++ b/Orange/canvas/__main__.py @@ -273,23 +273,32 @@ def ua_string(): def compare_versions(latest): version = pkg_resources.parse_version - if version(latest) <= version(current): + skipped = settings.value('startup/latest-skipped-version', "", type=str) + if version(latest) <= version(current) or \ + latest == skipped: return - question = QMessageBox( - QMessageBox.Information, - 'Orange Update Available', - 'A newer version of Orange is available.

' - 'Current version: {}
' - 'Latest version: {}'.format(current, latest), - textFormat=Qt.RichText) - ok = question.addButton('Download Now', question.AcceptRole) - question.setDefaultButton(ok) - question.addButton('Remind Later', question.RejectRole) - question.finished.connect( - lambda: - question.clickedButton() == ok and - QDesktopServices.openUrl(QUrl("https://orange.biolab.si/download/"))) - question.show() + + from Orange.canvas.utils.overlay import NotificationWidget + + questionButtons = NotificationWidget.Ok | NotificationWidget.Close + question = NotificationWidget(icon=QIcon('Orange/widgets/icons/Dlg_down3.png'), + title='Orange Update Available', + text='Current version: {}
' + 'Latest version: {}'.format(current, latest), + textFormat=Qt.RichText, + standardButtons=questionButtons, + acceptLabel="Download", + rejectLabel="Skip this Version") + + def handle_click(b): + if question.buttonRole(b) != question.DismissRole: + settings.setValue('startup/latest-skipped-version', latest) + if question.buttonRole(b) == question.AcceptRole: + QDesktopServices.openUrl(QUrl("https://orange.biolab.si/download/")) + + question.clicked.connect(handle_click) + + NotificationOverlay.registerNotification(question) thread = GetLatestVersion() thread.resultReady.connect(compare_versions) diff --git a/Orange/canvas/config.py b/Orange/canvas/config.py index 806d5f8ebaf..8f8413d407e 100644 --- a/Orange/canvas/config.py +++ b/Orange/canvas/config.py @@ -56,7 +56,8 @@ def init(): ("startup/check-updates", bool, True, "Check for updates"), - ("startup/launch-count", int, 0, ""), + ("startup/launch-count", int, 0, + ""), ("startup/show-short-survey", bool, True, "Has the user not been asked to take a short survey yet"), diff --git a/Orange/canvas/utils/overlay.py b/Orange/canvas/utils/overlay.py index e4ad9787f1b..ccd1587bcb2 100644 --- a/Orange/canvas/utils/overlay.py +++ b/Orange/canvas/utils/overlay.py @@ -35,7 +35,7 @@ class ButtonRole(enum.IntEnum): _Button = namedtuple("_Button", ["button", "role", "stdbutton"]) def __init__(self, parent=None, icon=QIcon(), title="", text="", wordWrap=False, - textFormat=Qt.AutoText, standardButtons=NoButton, acceptLabel="Ok", + textFormat=Qt.PlainText, standardButtons=NoButton, acceptLabel="Ok", rejectLabel="No", **kwargs): super().__init__(parent, **kwargs) self._title = title @@ -52,7 +52,6 @@ def __init__(self, parent=None, icon=QIcon(), title="", text="", wordWrap=False, wordWrap=wordWrap, textFormat=textFormat) self._textlabel = QLabel(objectName="text-label", text=text, wordWrap=wordWrap, textFormat=textFormat) - self._textlabel.setTextFormat(Qt.RichText) self._textlabel.setTextInteractionFlags(Qt.TextBrowserInteraction) self._textlabel.setOpenExternalLinks(True) From d165e8134055dde5b45cb64ecc9e539ab63da6ad Mon Sep 17 00:00:00 2001 From: Rafael Irgolic Date: Mon, 10 Jun 2019 16:25:47 +0200 Subject: [PATCH 4/6] test_overlay: Add tests --- Orange/canvas/utils/tests/test_overlay.py | 94 +++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 Orange/canvas/utils/tests/test_overlay.py diff --git a/Orange/canvas/utils/tests/test_overlay.py b/Orange/canvas/utils/tests/test_overlay.py new file mode 100644 index 00000000000..e0c4f75a10e --- /dev/null +++ b/Orange/canvas/utils/tests/test_overlay.py @@ -0,0 +1,94 @@ +import unittest.mock + +from AnyQt.QtCore import Qt, QEvent +from AnyQt.QtTest import QTest +from AnyQt.QtWidgets import QWidget, QApplication + +from Orange.canvas.utils.overlay import NotificationWidget, NotificationOverlay +from Orange.widgets.tests.base import GuiTest + + +class TestOverlay(GuiTest): + def setUp(self) -> None: + self.container = QWidget() + stdb = NotificationWidget.Ok | NotificationWidget.Close + self.overlay = NotificationOverlay(self.container) + self.notif = NotificationWidget(title="lol", + text="hihi", + standardButtons=stdb) + self.container.show() + QTest.qWaitForWindowExposed(self.container) + self.overlay.show() + QTest.qWaitForWindowExposed(self.overlay) + + def tearDown(self) -> None: + NotificationOverlay.overlayInstances = [] + NotificationOverlay.notifQueue = [] + self.container = None + self.overlay = None + self.notif = None + + def test_notification_dismiss(self): + mock = unittest.mock.MagicMock() + self.notif.clicked.connect(mock) + NotificationOverlay.registerNotification(self.notif) + QTest.mouseClick(self.notif._dismiss_button, Qt.LeftButton) + mock.assert_called_once_with(self.notif._dismiss_button) + + def test_notification_message(self): + self.notif.setText("Hello world! It's so nice here") + QApplication.sendPostedEvents(self.notif, QEvent.LayoutRequest) + self.assertTrue(self.notif.geometry().isValid()) + + button_ok = self.notif.button(NotificationWidget.Ok) + button_close = self.notif.button(NotificationWidget.Close) + + self.assertTrue(all([button_ok, button_close])) + self.assertIs(self.notif.button(NotificationWidget.Ok), button_ok) + self.assertIs(self.notif.button(NotificationWidget.Close), button_close) + + button = self.notif.button(NotificationWidget.Ok) + self.assertIsNot(button, None) + self.assertTrue(self.notif.buttonRole(button), + NotificationWidget.AcceptRole) + + mock = unittest.mock.MagicMock() + self.notif.accepted.connect(mock) + + NotificationOverlay.registerNotification(self.notif) + + cloned = NotificationOverlay.overlayInstances[0].currentWidget() + self.assertTrue(cloned.isVisible()) + button = cloned._msgwidget.button(NotificationWidget.Ok) + QTest.mouseClick(button, Qt.LeftButton) + self.assertFalse(cloned.isVisible()) + + mock.assert_called_once() + + def test_queued_notifications(self): + surveyDialogButtons = NotificationWidget.Ok | NotificationWidget.Close + surveyDialog = NotificationWidget(title="Survey", + text="We want to understand our users better.\n" + "Would you like to take a short survey?", + standardButtons=surveyDialogButtons) + + def handle_survey_response(b): + self.assertEquals(self.notif.buttonRole(b), NotificationWidget.DismissRole) + + self.notif.clicked.connect(handle_survey_response) + + NotificationOverlay.registerNotification(self.notif) + notif1 = NotificationOverlay.overlayInstances[0]._widgets[0] + button = notif1._dismiss_button + + NotificationOverlay.registerNotification(surveyDialog) + notif2 = NotificationOverlay.overlayInstances[0]._widgets[1] + + self.assertTrue(notif1.isVisible()) + self.assertFalse(notif2.isVisible()) + + QTest.mouseClick(button, Qt.LeftButton) + + self.assertFalse(notif1.isVisible()) + self.assertTrue(notif2.isVisible()) + From d81a9d084bc12e84df4ab4707ebbceb12f995936 Mon Sep 17 00:00:00 2001 From: Rafael Irgolic Date: Wed, 12 Jun 2019 11:12:51 +0200 Subject: [PATCH 5/6] pylint --- Orange/canvas/__main__.py | 12 +++++------- Orange/canvas/application/canvasmain.py | 2 +- Orange/canvas/config.py | 15 ++++++++++----- Orange/canvas/utils/overlay.py | 12 +++++++----- Orange/canvas/utils/tests/test_overlay.py | 5 +++-- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/Orange/canvas/__main__.py b/Orange/canvas/__main__.py index c8d48c834a6..437cb4cbf4a 100644 --- a/Orange/canvas/__main__.py +++ b/Orange/canvas/__main__.py @@ -20,7 +20,6 @@ import pkg_resources from AnyQt.QtGui import QFont, QColor, QPalette, QDesktopServices, QIcon -from AnyQt.QtWidgets import QMessageBox from AnyQt.QtCore import ( Qt, QDir, QUrl, QSettings, QThread, pyqtSignal, QT_VERSION ) @@ -195,7 +194,7 @@ def setup_notifications(): "Would you like to take a short survey?", standardButtons=surveyDialogButtons) - def handle_response(button): + def handle_survey_response(button): if surveyDialog.buttonRole(button) == NotificationWidget.AcceptRole: success = QDesktopServices.openUrl( QUrl("https://orange.biolab.si/survey/short.html")) @@ -203,7 +202,7 @@ def handle_response(button): elif surveyDialog.buttonRole(button) == NotificationWidget.RejectRole: settings["startup/show-short-survey"] = False - surveyDialog.clicked.connect(handle_response) + surveyDialog.clicked.connect(handle_survey_response) NotificationOverlay.registerNotification(surveyDialog) @@ -221,13 +220,13 @@ def handle_response(button): btnOK = permDialog.button(NotificationWidget.AcceptRole) btnOK.setText("Allow") - def handle_response(button): + def handle_permission_response(button): if permDialog.buttonRole(button) != permDialog.DismissRole: settings["error-reporting/permission-requested"] = True if permDialog.buttonRole(button) == permDialog.AcceptRole: settings["error-reporting/send-statistics"] = True - permDialog.clicked.connect(handle_response) + permDialog.clicked.connect(handle_permission_response) NotificationOverlay.registerNotification(permDialog) @@ -278,8 +277,6 @@ def compare_versions(latest): latest == skipped: return - from Orange.canvas.utils.overlay import NotificationWidget - questionButtons = NotificationWidget.Ok | NotificationWidget.Close question = NotificationWidget(icon=QIcon('Orange/widgets/icons/Dlg_down3.png'), title='Orange Update Available', @@ -304,6 +301,7 @@ def handle_click(b): thread.resultReady.connect(compare_versions) thread.start() return thread + return None def send_usage_statistics(): diff --git a/Orange/canvas/application/canvasmain.py b/Orange/canvas/application/canvasmain.py index 90a4e8859ee..79f09d567c2 100644 --- a/Orange/canvas/application/canvasmain.py +++ b/Orange/canvas/application/canvasmain.py @@ -59,7 +59,7 @@ def user_documents_path(): from ..gui.utils import message_critical, message_question, \ message_warning, message_information -from ..utils.overlay import NotificationWidget, NotificationOverlay +from ..utils.overlay import NotificationOverlay from ..document.usagestatistics import UsageStatistics diff --git a/Orange/canvas/config.py b/Orange/canvas/config.py index 8f8413d407e..8606bccb10e 100644 --- a/Orange/canvas/config.py +++ b/Orange/canvas/config.py @@ -117,11 +117,14 @@ def init(): ("quickmenu/trigger-on-any-key", bool, False, "Show quick menu on double click."), - ("logging/level", int, 1, "Logging level"), + ("logging/level", int, 1, + "Logging level"), - ("logging/show-on-error", bool, True, "Show log window on error"), + ("logging/show-on-error", bool, True, + "Show log window on error"), - ("logging/dockable", bool, True, "Allow log window to be docked"), + ("logging/dockable", bool, True, + "Allow log window to be docked"), ("help/open-in-external-browser", bool, False, "Open help in an external browser"), @@ -141,9 +144,11 @@ def init(): ("add-ons/pip-install-arguments", str, '', 'Arguments to pass to "pip install" when installing add-ons.'), - ("network/http-proxy", str, '', 'HTTP proxy.'), + ("network/http-proxy", str, '', + 'HTTP proxy.'), - ("network/https-proxy", str, '', 'HTTPS proxy.'), + ("network/https-proxy", str, '', + 'HTTPS proxy.'), ] spec = [config_slot(*t) for t in spec] diff --git a/Orange/canvas/utils/overlay.py b/Orange/canvas/utils/overlay.py index ccd1587bcb2..afbbd2b78bf 100644 --- a/Orange/canvas/utils/overlay.py +++ b/Orange/canvas/utils/overlay.py @@ -243,7 +243,7 @@ def addButton(self, button, *rolearg): "addButton(QAbstractButton, role)") role = rolearg[0] elif isinstance(button, NotificationMessageWidget.StandardButton): - if len(rolearg) != 0: + if rolearg: raise TypeError("Wrong number of arguments for " "addButton(StandardButton)") stdbutton = button @@ -318,8 +318,7 @@ def button(self, standardButton): for slot in self._buttons: if slot.stdbutton == standardButton: return slot.button - else: - return None + return None def _button_clicked(self): button = self.sender() @@ -420,12 +419,15 @@ def clone(self): textFormat=self._msgwidget.textFormat(), icon=self.icon(), standardButtons=self._msgwidget.standardButtons(), - acceptLabel=self._msgwidget._acceptLabel, - rejectLabel=self._msgwidget._rejectLabel) + acceptLabel=self._msgwidget.acceptLabel(), + rejectLabel=self._msgwidget.rejectLabel()) cloned.accepted.connect(self.accepted) cloned.rejected.connect(self.rejected) cloned.dismissed.connect(self.dismissed) + # each canvas displays a clone of the original notification, + # therefore the cloned buttons' events are connected to the original's + # pylint: disable=protected-access button_map = dict(zip( [b.button for b in cloned._msgwidget._buttons] + [cloned._dismiss_button], [b.button for b in self._msgwidget._buttons] + [self._dismiss_button])) diff --git a/Orange/canvas/utils/tests/test_overlay.py b/Orange/canvas/utils/tests/test_overlay.py index e0c4f75a10e..d29046cd9cc 100644 --- a/Orange/canvas/utils/tests/test_overlay.py +++ b/Orange/canvas/utils/tests/test_overlay.py @@ -1,3 +1,5 @@ +# pylint: disable=protected-access + import unittest.mock from AnyQt.QtCore import Qt, QEvent @@ -73,7 +75,7 @@ def test_queued_notifications(self): standardButtons=surveyDialogButtons) def handle_survey_response(b): - self.assertEquals(self.notif.buttonRole(b), NotificationWidget.DismissRole) + self.assertEqual(self.notif.buttonRole(b), NotificationWidget.DismissRole) self.notif.clicked.connect(handle_survey_response) @@ -91,4 +93,3 @@ def handle_survey_response(b): self.assertFalse(notif1.isVisible()) self.assertTrue(notif2.isVisible()) - From 2b216b4428bad23230a4d4a5cc851599d876819a Mon Sep 17 00:00:00 2001 From: Rafael Irgolic Date: Thu, 13 Jun 2019 13:26:02 +0200 Subject: [PATCH 6/6] __main__: Fix notification icon paths --- Orange/canvas/__main__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Orange/canvas/__main__.py b/Orange/canvas/__main__.py index 437cb4cbf4a..8b83cc72829 100644 --- a/Orange/canvas/__main__.py +++ b/Orange/canvas/__main__.py @@ -39,6 +39,7 @@ from Orange.canvas.registry import WidgetRegistry, set_global_registry from Orange.canvas.registry import cache from Orange.canvas.utils.overlay import NotificationWidget, NotificationOverlay +from Orange.widgets import gui log = logging.getLogger(__name__) @@ -188,7 +189,7 @@ def setup_notifications(): settings["startup/launch-count"] >= 5 if show_survey: surveyDialogButtons = NotificationWidget.Ok | NotificationWidget.Close - surveyDialog = NotificationWidget(icon=QIcon("Orange/widgets/icons/information.png"), + surveyDialog = NotificationWidget(icon=QIcon(gui.resource_filename("icons/information.png")), title="Survey", text="We want to understand our users better.\n" "Would you like to take a short survey?", @@ -209,7 +210,8 @@ def handle_survey_response(button): # data collection permission if not settings["error-reporting/permission-requested"]: permDialogButtons = NotificationWidget.Ok | NotificationWidget.Close - permDialog = NotificationWidget(icon=QIcon("distribute/icon-48.png"), + permDialog = NotificationWidget(icon=QIcon(gui.resource_filename( + "../../distribute/icon-48.png")), title="Anonymous Usage Statistics", text="Do you wish to opt-in to sharing " "statistics about how you use Orange?\n" @@ -278,7 +280,7 @@ def compare_versions(latest): return questionButtons = NotificationWidget.Ok | NotificationWidget.Close - question = NotificationWidget(icon=QIcon('Orange/widgets/icons/Dlg_down3.png'), + question = NotificationWidget(icon=QIcon(gui.resource_filename('icons/Dlg_down3.png')), title='Orange Update Available', text='Current version: {}
' 'Latest version: {}'.format(current, latest),