def setUp(self) -> None: self.container = QWidget() self.overlay = NotificationOverlay(self.container) self.server = NotificationServer() self.server.newNotification.connect(self.overlay.addNotification) self.server.nextNotification.connect(self.overlay.nextWidget) self.notif = Notification( title="Hello world!", text="Welcome to the testing grounds – this is where your resolve" "and stability will be tried and tested.", accept_button_label="Ok")
def setup_application(self): super().setup_application() clear_settings_flag = os.path.join(widget_settings_dir(), "DELETE_ON_START") # NOTE: No OWWidgetBase subclass should be imported before this options = self.options if options.clear_widget_settings or \ os.path.isfile(clear_settings_flag): self.clear_widget_settings() if options.clear_all: self.clear_widget_settings() self.clear_caches() self.clear_application_settings() notif_server = NotificationServer() canvas.notification_server_instance = notif_server self._update_check = check_for_updates() self._send_stat = send_usage_statistics() self._pull_notifs = pull_notifications() settings = QSettings() settings.setValue('startup/launch-count', settings.value('startup/launch-count', 0, int) + 1) if settings.value("reporting/send-statistics", False, type=bool) \ and is_release: UsageStatistics.set_enabled(True) app = self.application # set pyqtgraph colors def onPaletteChange(): p = app.palette() bg = p.base().color().name() fg = p.windowText().color().name() log.info('Setting pyqtgraph background to %s', bg) pyqtgraph.setConfigOption('background', bg) log.info('Setting pyqtgraph foreground to %s', fg) pyqtgraph.setConfigOption('foreground', fg) app.setProperty('darkMode', p.color(QPalette.Base).value() < 127) app.paletteChanged.connect(onPaletteChange) onPaletteChange()
def main(argv=None): # Allow termination with CTRL + C signal.signal(signal.SIGINT, signal.SIG_DFL) # Disable pyqtgraph's atexit and QApplication.aboutToQuit cleanup handlers. pyqtgraph.setConfigOption("exitCleanup", False) if argv is None: argv = sys.argv usage = "usage: %prog [options] [workflow_file]" parser = optparse.OptionParser(usage=usage) parser.add_option("--no-discovery", action="store_true", help="Don't run widget discovery " "(use full cache instead)") parser.add_option("--force-discovery", action="store_true", help="Force full widget discovery " "(invalidate cache)") parser.add_option("--clear-widget-settings", action="store_true", help="Remove stored widget setting") parser.add_option("--no-welcome", action="store_true", help="Don't show welcome dialog.") parser.add_option("--no-splash", action="store_true", help="Don't show splash screen.") parser.add_option("-l", "--log-level", help="Logging level (0, 1, 2, 3, 4)", type="int", default=1) parser.add_option("--style", help="QStyle to use", type="str", default=None) parser.add_option("--stylesheet", help="Application level CSS style sheet to use", type="str", default=None) parser.add_option("--qt", help="Additional arguments for QApplication", type="str", default=None) (options, args) = parser.parse_args(argv[1:]) levels = [ logging.CRITICAL, logging.ERROR, logging.WARN, logging.INFO, logging.DEBUG ] # Fix streams before configuring logging (otherwise it will store # and write to the old file descriptors) fix_win_pythonw_std_stream() # Try to fix macOS automatic window tabbing (Sierra and later) fix_macos_nswindow_tabbing() logging.basicConfig( level=levels[options.log_level], handlers=[make_stdout_handler(levels[options.log_level])]) # set default application configuration config_ = config.Config() canvasconfig.set_default(config_) log.info("Starting 'Orange Canvas' application.") qt_argv = argv[:1] style = options.style defaultstylesheet = "orange.qss" fusiontheme = None if style is not None: if style.startswith("fusion:"): qt_argv += ["-style", "fusion"] _, _, fusiontheme = style.partition(":") else: qt_argv += ["-style", style] if options.qt is not None: qt_argv += shlex.split(options.qt) qt_argv += args if QT_VERSION >= 0x50600: CanvasApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) log.debug("Starting CanvasApplicaiton with argv = %r.", qt_argv) app = CanvasApplication(qt_argv) config_.init() if app.style().metaObject().className() == "QFusionStyle": if fusiontheme == "breeze-dark": app.setPalette(breeze_dark()) defaultstylesheet = "darkorange.qss" palette = app.palette() if style is None and palette.color(QPalette.Window).value() < 127: log.info("Switching default stylesheet to darkorange") defaultstylesheet = "darkorange.qss" # Initialize SQL query and execution time logger (in SqlTable) sql_level = min(levels[options.log_level], logging.INFO) make_sql_logger(sql_level) clear_settings_flag = os.path.join(widget_settings_dir(), "DELETE_ON_START") if options.clear_widget_settings or \ os.path.isfile(clear_settings_flag): log.info("Clearing widget settings") shutil.rmtree(widget_settings_dir(), ignore_errors=True) # Set http_proxy environment variables, after (potentially) clearing settings fix_set_proxy_env() # Setup file log handler for the select logger list - this is always # at least INFO level = min(levels[options.log_level], logging.INFO) file_handler = logging.FileHandler(filename=os.path.join( config.log_dir(), "canvas.log"), mode="w") formatter = logging.Formatter( "%(asctime)s:%(levelname)s:%(name)s: %(message)s") file_handler.setFormatter(formatter) file_handler.setLevel(level) stream = TextStream() stream_handler = logging.StreamHandler(stream) stream_handler.setFormatter(formatter) stream_handler.setLevel(level) for namespace in ["orangecanvas", "orangewidget", "Orange"]: logger = logging.getLogger(namespace) logger.setLevel(level) logger.addHandler(file_handler) logger.addHandler(stream_handler) # intercept any QFileOpenEvent requests until the main window is # fully initialized. # NOTE: The QApplication must have the executable ($0) and filename # arguments passed in argv otherwise the FileOpen events are # triggered for them (this is done by Cocoa, but QApplicaiton filters # them out if passed in argv) open_requests = [] def onrequest(url): log.info("Received an file open request %s", url) path = url.path() exists = QFile(path).exists() if exists and \ ('pydevd.py' not in url.path() and # PyCharm debugger 'run_profiler.py' not in url.path()): # PyCharm profiler open_requests.append(url) app.fileOpenRequest.connect(onrequest) settings = QSettings() settings.setValue('startup/launch-count', settings.value('startup/launch-count', 0, int) + 1) if settings.value("reporting/send-statistics", False, type=bool) \ and is_release: UsageStatistics.set_enabled(True) stylesheet = options.stylesheet or defaultstylesheet stylesheet_string = None if stylesheet != "none": if os.path.isfile(stylesheet): with open(stylesheet, "r") as f: stylesheet_string = f.read() else: if not os.path.splitext(stylesheet)[1]: # no extension stylesheet = os.path.extsep.join([stylesheet, "qss"]) pkg_name = orangecanvas.__name__ resource = "styles/" + stylesheet if pkg_resources.resource_exists(pkg_name, resource): stylesheet_string = \ pkg_resources.resource_string(pkg_name, resource).decode() base = pkg_resources.resource_filename(pkg_name, "styles") pattern = re.compile( r"^\s@([a-zA-Z0-9_]+?)\s*:\s*([a-zA-Z0-9_/]+?);\s*$", flags=re.MULTILINE) matches = pattern.findall(stylesheet_string) for prefix, search_path in matches: QDir.addSearchPath(prefix, os.path.join(base, search_path)) log.info("Adding search path %r for prefix, %r", search_path, prefix) stylesheet_string = pattern.sub("", stylesheet_string) else: log.info("%r style sheet not found.", stylesheet) # Add the default canvas_icons search path dirpath = os.path.abspath(os.path.dirname(orangecanvas.__file__)) QDir.addSearchPath("canvas_icons", os.path.join(dirpath, "icons")) canvas_window = MainWindow() canvas_window.setAttribute(Qt.WA_DeleteOnClose) canvas_window.setWindowIcon(config.application_icon()) canvas_window.connect_output_stream(stream) # initialize notification server, set to initial canvas notif_server = NotificationServer() canvas.notification_server_instance = notif_server canvas_window.set_notification_server(notif_server) if stylesheet_string is not None: canvas_window.setStyleSheet(stylesheet_string) if not options.force_discovery: reg_cache = cache.registry_cache() else: reg_cache = None widget_registry = qt.QtWidgetRegistry() widget_discovery = config_.widget_discovery(widget_registry, cached_descriptions=reg_cache) want_splash = \ settings.value("startup/show-splash-screen", True, type=bool) and \ not options.no_splash if want_splash: pm, rect = config.splash_screen() splash_screen = SplashScreen(pixmap=pm, textRect=rect) splash_screen.setFont(QFont("Helvetica", 12)) color = QColor("#FFD39F") def show_message(message): splash_screen.showMessage(message, color=color) widget_registry.category_added.connect(show_message) log.info("Running widget discovery process.") cache_filename = os.path.join(config.cache_dir(), "widget-registry.pck") if options.no_discovery: with open(cache_filename, "rb") as f: widget_registry = pickle.load(f) widget_registry = qt.QtWidgetRegistry(widget_registry) else: if want_splash: splash_screen.show() widget_discovery.run(config.widgets_entry_points()) if want_splash: splash_screen.hide() splash_screen.deleteLater() # Store cached descriptions cache.save_registry_cache(widget_discovery.cached_descriptions) with open(cache_filename, "wb") as f: pickle.dump(WidgetRegistry(widget_registry), f) set_global_registry(widget_registry) canvas_window.set_widget_registry(widget_registry) canvas_window.show() canvas_window.raise_() want_welcome = \ settings.value("startup/show-welcome-screen", True, type=bool) \ and not options.no_welcome # Process events to make sure the canvas_window layout has # a chance to activate (the welcome dialog is modal and will # block the event queue, plus we need a chance to receive open file # signals when running without a splash screen) app.processEvents() app.fileOpenRequest.connect(canvas_window.open_scheme_file) if args: log.info("Loading a scheme from the command line argument %r", args[0]) canvas_window.load_scheme(args[0]) elif open_requests: log.info("Loading a scheme from an `QFileOpenEvent` for %r", open_requests[-1]) canvas_window.load_scheme(open_requests[-1].toLocalFile()) else: swp_loaded = canvas_window.ask_load_swp_if_exists() if not swp_loaded and want_welcome: canvas_window.welcome_dialog() # local references prevent destruction update_check = check_for_updates() send_stat = send_usage_statistics() pull_notifs = pull_notifications() # Tee stdout and stderr into Output dock log_view = canvas_window.output_view() stdout = TextStream() stdout.stream.connect(log_view.write) if sys.stdout: stdout.stream.connect(sys.stdout.write) stdout.flushed.connect(sys.stdout.flush) stderr = TextStream() error_writer = log_view.formatted(color=Qt.red) stderr.stream.connect(error_writer.write) if sys.stderr: stderr.stream.connect(sys.stderr.write) stderr.flushed.connect(sys.stderr.flush) log.info("Entering main event loop.") excepthook = ExceptHook(stream=stderr) excepthook.handledException.connect(handle_exception) try: with closing(stdout),\ closing(stderr),\ closing(stream), \ patch('sys.excepthook', excepthook),\ patch('sys.stderr', stderr),\ patch('sys.stdout', stdout): status = app.exec_() except BaseException: log.error("Error in main event loop.", exc_info=True) status = 42 del canvas_window del update_check del send_stat del pull_notifs app.processEvents() app.flush() # Collect any cycles before deleting the QApplication instance gc.collect() del app if status == 96: log.info('Restarting via exit code 96.') run_after_exit([sys.executable, sys.argv[0]]) return status
class TestOverlay(QAppTestCase): def setUp(self) -> None: self.container = QWidget() self.overlay = NotificationOverlay(self.container) self.server = NotificationServer() self.server.newNotification.connect(self.overlay.addNotification) self.server.nextNotification.connect(self.overlay.nextWidget) self.notif = Notification( title="Hello world!", text="Welcome to the testing grounds – this is where your resolve" "and stability will be tried and tested.", accept_button_label="Ok") def tearDown(self) -> None: self.container = None self.overlay = None self.notif = None self.server = None def test_notification_widget(self): stdb = NotificationWidget.Ok | NotificationWidget.Close notifw = NotificationWidget(self.overlay, title="Titl", text="Tixt", standardButtons=stdb) QApplication.sendPostedEvents(notifw, QEvent.LayoutRequest) self.assertTrue(notifw.geometry().isValid()) button_ok = notifw.button(NotificationWidget.Ok) button_close = notifw.button(NotificationWidget.Close) self.assertTrue(all([button_ok, button_close])) button = notifw.button(NotificationWidget.Ok) self.assertIsNot(button, None) self.assertEqual(notifw.buttonRole(button), NotificationWidget.AcceptRole) def test_notification_dismiss(self): mock = unittest.mock.MagicMock() self.notif.clicked.connect(mock) self.server.registerNotification(self.notif) notifw = self.overlay.currentWidget() QTest.mouseClick(notifw.dismissButton, Qt.LeftButton) mock.assert_called_once_with(self.notif.DismissRole) def test_notification_accept(self): mock = unittest.mock.MagicMock() self.notif.clicked.connect(mock) self.server.registerNotification(self.notif) notifw = self.overlay.currentWidget() b = notifw._msgWidget.button(NotificationWidget.Ok) QTest.mouseClick(b, Qt.LeftButton) mock.assert_called_once_with(self.notif.AcceptRole) def test_two_overlays(self): container2 = QWidget() overlay2 = NotificationOverlay(container2) self.server.newNotification.connect(overlay2.addNotification) self.server.nextNotification.connect(overlay2.nextWidget) mock = unittest.mock.MagicMock() self.notif.accepted.connect(mock) self.server.registerNotification(self.notif) self.container.show() container2.show() w1 = self.overlay.currentWidget() w2 = overlay2.currentWidget() self.assertTrue(w1.isVisible()) self.assertTrue(w2.isVisible()) button = w2.button(NotificationWidget.Ok) QTest.mouseClick(button, Qt.LeftButton) mock.assert_called_once_with() self.assertFalse(w1.isVisible()) self.assertFalse(w2.isVisible()) def test_queued_notifications(self): notif2 = Notification( title="Hello universe!", text= "I'm another notif! I'm about to queue behind my older brother.") def handle_click(role): self.assertEqual(role, Notification.DismissRole) self.notif.clicked.connect(handle_click) self.server.registerNotification(self.notif) notif1 = self.overlay.currentWidget() button = notif1.dismissButton self.server.registerNotification(notif2) notif2 = self.overlay._widgets[1] self.container.show() self.assertTrue(notif1.isVisible()) self.assertFalse(notif2.isVisible()) QTest.mouseClick(button, Qt.LeftButton) self.assertFalse(notif1.isVisible()) self.assertTrue(notif2.isVisible())