Ejemplo n.º 1
0
class QtApplication(QApplication, Application):
    pluginsLoaded = Signal()
    applicationRunning = Signal()

    def __init__(self, tray_icon_name=None, **kwargs):
        plugin_path = ""
        if sys.platform == "win32":
            if hasattr(sys, "frozen"):
                plugin_path = os.path.join(
                    os.path.dirname(os.path.abspath(sys.executable)), "PyQt5",
                    "plugins")
                Logger.log("i", "Adding QT5 plugin path: %s" % (plugin_path))
                QCoreApplication.addLibraryPath(plugin_path)
            else:
                import site
                for sitepackage_dir in site.getsitepackages():
                    QCoreApplication.addLibraryPath(
                        os.path.join(sitepackage_dir, "PyQt5", "plugins"))
        elif sys.platform == "darwin":
            plugin_path = os.path.join(Application.getInstallPrefix(),
                                       "Resources", "plugins")

        if plugin_path:
            Logger.log("i", "Adding QT5 plugin path: %s" % (plugin_path))
            QCoreApplication.addLibraryPath(plugin_path)

        os.environ["QSG_RENDER_LOOP"] = "basic"

        super().__init__(sys.argv, **kwargs)

        self.setAttribute(Qt.AA_UseDesktopOpenGL)
        major_version, minor_version, profile = OpenGLContext.detectBestOpenGLVersion(
        )

        if major_version is None and minor_version is None and profile is None:
            Logger.log(
                "e",
                "Startup failed because OpenGL version probing has failed: tried to create a 2.0 and 4.1 context. Exiting"
            )
            QMessageBox.critical(
                None, "Failed to probe OpenGL",
                "Could not probe OpenGL. This program requires OpenGL 2.0 or higher. Please check your video card drivers."
            )
            sys.exit(1)
        else:
            Logger.log(
                "d", "Detected most suitable OpenGL context version: %s" %
                (OpenGLContext.versionAsText(major_version, minor_version,
                                             profile)))
        OpenGLContext.setDefaultFormat(major_version,
                                       minor_version,
                                       profile=profile)

        self._plugins_loaded = False  # Used to determine when it's safe to use the plug-ins.
        self._main_qml = "main.qml"
        self._engine = None
        self._renderer = None
        self._main_window = None
        self._theme = None

        self._shutting_down = False
        self._qml_import_paths = []
        self._qml_import_paths.append(
            os.path.join(os.path.dirname(sys.executable), "qml"))
        self._qml_import_paths.append(
            os.path.join(Application.getInstallPrefix(), "Resources", "qml"))

        self.parseCommandLine()
        Logger.log("i", "Command line arguments: %s",
                   self._parsed_command_line)

        signal.signal(signal.SIGINT, signal.SIG_DFL)
        # This is done here as a lot of plugins require a correct gl context. If you want to change the framework,
        # these checks need to be done in your <framework>Application.py class __init__().

        i18n_catalog = i18nCatalog("uranium")

        self.showSplashMessage(
            i18n_catalog.i18nc("@info:progress", "Loading plugins..."))
        self._loadPlugins()
        self._plugin_registry.checkRequiredPlugins(self.getRequiredPlugins())
        self.pluginsLoaded.emit()

        self.showSplashMessage(
            i18n_catalog.i18nc("@info:progress", "Updating configuration..."))
        upgraded = UM.VersionUpgradeManager.VersionUpgradeManager.getInstance(
        ).upgrade()
        if upgraded:
            # Preferences might have changed. Load them again.
            # Note that the language can't be updated, so that will always revert to English.
            preferences = Preferences.getInstance()
            try:
                preferences.readFromFile(
                    Resources.getPath(Resources.Preferences,
                                      self._application_name + ".cfg"))
            except FileNotFoundError:
                pass

        self.showSplashMessage(
            i18n_catalog.i18nc("@info:progress", "Loading preferences..."))
        try:
            file_name = Resources.getPath(Resources.Preferences,
                                          self.getApplicationName() + ".cfg")
            Preferences.getInstance().readFromFile(file_name)
        except FileNotFoundError:
            pass

        self.getApplicationName()

        Preferences.getInstance().addPreference(
            "%s/recent_files" % self.getApplicationName(), "")

        self._recent_files = []
        file_names = Preferences.getInstance().getValue(
            "%s/recent_files" % self.getApplicationName()).split(";")
        for file_name in file_names:
            if not os.path.isfile(file_name):
                continue

            self._recent_files.append(QUrl.fromLocalFile(file_name))

        JobQueue.getInstance().jobFinished.connect(self._onJobFinished)

        # Initialize System tray icon and make it invisible because it is used only to show pop up messages
        self._tray_icon = None
        self._tray_icon_widget = None
        if tray_icon_name:
            self._tray_icon = QIcon(
                Resources.getPath(Resources.Images, tray_icon_name))
            self._tray_icon_widget = QSystemTrayIcon(self._tray_icon)
            self._tray_icon_widget.setVisible(False)

    recentFilesChanged = pyqtSignal()

    @pyqtProperty("QVariantList", notify=recentFilesChanged)
    def recentFiles(self):
        return self._recent_files

    def _onJobFinished(self, job):
        if (not isinstance(job, ReadMeshJob)
                and not isinstance(job, ReadFileJob)) or not job.getResult():
            return

        f = QUrl.fromLocalFile(job.getFileName())
        if f in self._recent_files:
            self._recent_files.remove(f)

        self._recent_files.insert(0, f)
        if len(self._recent_files) > 10:
            del self._recent_files[10]

        pref = ""
        for path in self._recent_files:
            pref += path.toLocalFile() + ";"

        Preferences.getInstance().setValue(
            "%s/recent_files" % self.getApplicationName(), pref)
        self.recentFilesChanged.emit()

    def run(self):
        pass

    def hideMessage(self, message):
        with self._message_lock:
            if message in self._visible_messages:
                self._visible_messages.remove(message)
                self.visibleMessageRemoved.emit(message)

    def showMessage(self, message):
        with self._message_lock:
            if message not in self._visible_messages:
                self._visible_messages.append(message)
                message.setLifetimeTimer(QTimer())
                message.setInactivityTimer(QTimer())
                self.visibleMessageAdded.emit(message)

        # also show toast message when the main window is minimized
        self.showToastMessage(self._application_name, message.getText())

    def _onMainWindowStateChanged(self, window_state):
        if self._tray_icon:
            visible = window_state == Qt.WindowMinimized
            self._tray_icon_widget.setVisible(visible)

    # Show toast message using System tray widget.
    def showToastMessage(self, title: str, message: str):
        if self.checkWindowMinimizedState() and self._tray_icon_widget:
            # NOTE: Qt 5.8 don't support custom icon for the system tray messages, but Qt 5.9 does.
            #       We should use the custom icon when we switch to Qt 5.9
            self._tray_icon_widget.showMessage(title, message)

    def setMainQml(self, path):
        self._main_qml = path

    def initializeEngine(self):
        # TODO: Document native/qml import trickery
        Bindings.register()

        self._engine = QQmlApplicationEngine()
        self._engine.setOutputWarningsToStandardError(False)
        self._engine.warnings.connect(self.__onQmlWarning)

        for path in self._qml_import_paths:
            self._engine.addImportPath(path)

        if not hasattr(sys, "frozen"):
            self._engine.addImportPath(
                os.path.join(os.path.dirname(__file__), "qml"))

        self._engine.rootContext().setContextProperty("QT_VERSION_STR",
                                                      QT_VERSION_STR)
        self._engine.rootContext().setContextProperty(
            "screenScaleFactor", self._screenScaleFactor())

        self.registerObjects(self._engine)

        self._engine.load(self._main_qml)
        self.engineCreatedSignal.emit()

    def exec_(self, *args, **kwargs):
        self.applicationRunning.emit()
        super().exec_(*args, **kwargs)

    @pyqtSlot()
    def reloadQML(self):
        # only reload when it is a release build
        if not self.getIsDebugMode():
            return
        self._engine.clearComponentCache()
        self._theme.reload()
        self._engine.load(self._main_qml)
        # Hide the window. For some reason we can't close it yet. This needs to be done in the onComponentCompleted.
        for obj in self._engine.rootObjects():
            if obj != self._engine.rootObjects()[-1]:
                obj.hide()

    @pyqtSlot()
    def purgeWindows(self):
        # Close all root objects except the last one.
        # Should only be called by onComponentCompleted of the mainWindow.
        for obj in self._engine.rootObjects():
            if obj != self._engine.rootObjects()[-1]:
                obj.close()

    @pyqtSlot("QList<QQmlError>")
    def __onQmlWarning(self, warnings):
        for warning in warnings:
            Logger.log("w", warning.toString())

    engineCreatedSignal = Signal()

    def isShuttingDown(self):
        return self._shutting_down

    def registerObjects(self, engine):
        engine.rootContext().setContextProperty("PluginRegistry",
                                                PluginRegistry.getInstance())

    def getRenderer(self):
        if not self._renderer:
            self._renderer = QtRenderer()

        return self._renderer

    @classmethod
    def addCommandLineOptions(self, parser, parsed_command_line={}):
        super().addCommandLineOptions(parser,
                                      parsed_command_line=parsed_command_line)
        parser.add_argument(
            "--disable-textures",
            dest="disable-textures",
            action="store_true",
            default=False,
            help=
            "Disable Qt texture loading as a workaround for certain crashes.")
        parser.add_argument("-qmljsdebugger",
                            help="For Qt's QML debugger compatibility")

    mainWindowChanged = Signal()

    def getMainWindow(self):
        return self._main_window

    def setMainWindow(self, window):
        if window != self._main_window:
            if self._main_window is not None:
                self._main_window.windowStateChanged.disconnect(
                    self._onMainWindowStateChanged)

            self._main_window = window
            if self._main_window is not None:
                self._main_window.windowStateChanged.connect(
                    self._onMainWindowStateChanged)

            self.mainWindowChanged.emit()

    def setVisible(self, visible):
        if self._engine is None:
            self.initializeEngine()

        if self._main_window is not None:
            self._main_window.visible = visible

    @property
    def isVisible(self):
        if self._main_window is not None:
            return self._main_window.visible

    def getTheme(self):
        if self._theme is None:
            if self._engine is None:
                Logger.log(
                    "e",
                    "The theme cannot be accessed before the engine is initialised"
                )
                return None

            self._theme = UM.Qt.Bindings.Theme.Theme.getInstance(self._engine)
        return self._theme

    #   Handle a function that should be called later.
    def functionEvent(self, event):
        e = _QtFunctionEvent(event)
        QCoreApplication.postEvent(self, e)

    #   Handle Qt events
    def event(self, event):
        if event.type() == _QtFunctionEvent.QtFunctionEvent:
            event._function_event.call()
            return True

        return super().event(event)

    def windowClosed(self):
        Logger.log("d", "Shutting down %s", self.getApplicationName())
        self._shutting_down = True

        try:
            Preferences.getInstance().writeToFile(
                Resources.getStoragePath(Resources.Preferences,
                                         self.getApplicationName() + ".cfg"))
        except Exception as e:
            Logger.log("e", "Exception while saving preferences: %s", repr(e))

        try:
            self.applicationShuttingDown.emit()
        except Exception as e:
            Logger.log("e", "Exception while emitting shutdown signal: %s",
                       repr(e))

        try:
            self.getBackend().close()
        except Exception as e:
            Logger.log("e", "Exception while closing backend: %s", repr(e))

        self.quit()

    def checkWindowMinimizedState(self):
        if self._main_window is not None and self._main_window.windowState(
        ) == Qt.WindowMinimized:
            return True
        else:
            return False

    ##  Get the backend of the application (the program that does the heavy lifting).
    #   The backend is also a QObject, which can be used from qml.
    #   \returns Backend \type{Backend}
    @pyqtSlot(result="QObject*")
    def getBackend(self):
        return self._backend

    ##  Property used to expose the backend
    #   It is made static as the backend is not supposed to change during runtime.
    #   This makes the connection between backend and QML more reliable than the pyqtSlot above.
    #   \returns Backend \type{Backend}
    @pyqtProperty("QVariant", constant=True)
    def backend(self):
        return self.getBackend()

    ##  Load a Qt translation catalog.
    #
    #   This method will locate, load and install a Qt message catalog that can be used
    #   by Qt's translation system, like qsTr() in QML files.
    #
    #   \param file_name The file name to load, without extension. It will be searched for in
    #                    the i18nLocation Resources directory. If it can not be found a warning
    #                    will be logged but no error will be thrown.
    #   \param language The language to load translations for. This can be any valid language code
    #                   or 'default' in which case the language is looked up based on system locale.
    #                   If the specified language can not be found, this method will fall back to
    #                   loading the english translations file.
    #
    #   \note When `language` is `default`, the language to load can be changed with the
    #         environment variable "LANGUAGE".
    def loadQtTranslation(self, file_name, language="default"):
        # TODO Add support for specifying a language from preferences
        path = None
        if language == "default":
            path = self._getDefaultLanguage(file_name)
        else:
            path = Resources.getPath(Resources.i18n, language, "LC_MESSAGES",
                                     file_name + ".qm")

        # If all else fails, fall back to english.
        if not path:
            Logger.log(
                "w",
                "Could not find any translations matching {0} for file {1}, falling back to english"
                .format(language, file_name))
            try:
                path = Resources.getPath(Resources.i18n, "en_US",
                                         "LC_MESSAGES", file_name + ".qm")
            except FileNotFoundError:
                Logger.log(
                    "w",
                    "Could not find English translations for file {0}. Switching to developer english."
                    .format(file_name))
                return

        translator = QTranslator()
        if not translator.load(path):
            Logger.log("e", "Unable to load translations %s", file_name)
            return

        # Store a reference to the translator.
        # This prevents the translator from being destroyed before Qt has a chance to use it.
        self._translators[file_name] = translator

        # Finally, install the translator so Qt can use it.
        self.installTranslator(translator)

    ## Create a class variable so we can manage the splash in the CrashHandler dialog when the Application instance
    # is not yet created, e.g. when an error occurs during the initialization
    splash = None

    def createSplash(self):
        if not self.getCommandLineOption("headless"):
            try:
                QtApplication.splash = self._createSplashScreen()
            except FileNotFoundError:
                QtApplication.splash = None
            else:
                if QtApplication.splash:
                    QtApplication.splash.show()
                    self.processEvents()

    ##  Display text on the splash screen.
    def showSplashMessage(self, message):
        if not QtApplication.splash:
            self.createSplash()

        if QtApplication.splash:
            QtApplication.splash.showMessage(message,
                                             Qt.AlignHCenter | Qt.AlignVCenter)
            self.processEvents()
        elif self.getCommandLineOption("headless"):
            Logger.log("d", message)

    ##  Close the splash screen after the application has started.
    def closeSplash(self):
        if QtApplication.splash:
            QtApplication.splash.close()
            QtApplication.splash = None

    ## Create a QML component from a qml file.
    #  \param qml_file_path: The absolute file path to the root qml file.
    #  \param context_properties: Optional dictionary containing the properties that will be set on the context of the
    #                              qml instance before creation.
    #  \return None in case the creation failed (qml error), else it returns the qml instance.
    #  \note If the creation fails, this function will ensure any errors are logged to the logging service.
    def createQmlComponent(
        self,
        qml_file_path: str,
        context_properties: Dict[str,
                                 "QObject"] = None) -> Optional["QObject"]:
        path = QUrl.fromLocalFile(qml_file_path)
        component = QQmlComponent(self._engine, path)
        result_context = QQmlContext(self._engine.rootContext())
        if context_properties is not None:
            for name, value in context_properties.items():
                result_context.setContextProperty(name, value)
        result = component.create(result_context)
        for err in component.errors():
            Logger.log("e", str(err.toString()))
        if result is None:
            return None

        # We need to store the context with the qml object, else the context gets garbage collected and the qml objects
        # no longer function correctly/application crashes.
        result.attached_context = result_context
        return result

    def _createSplashScreen(self):
        return QSplashScreen(
            QPixmap(
                Resources.getPath(Resources.Images,
                                  self.getApplicationName() + ".png")))

    def _screenScaleFactor(self):
        # OSX handles sizes of dialogs behind our backs, but other platforms need
        # to know about the device pixel ratio
        if sys.platform == "darwin":
            return 1.0
        else:
            # determine a device pixel ratio from font metrics, using the same logic as UM.Theme
            fontPixelRatio = QFontMetrics(
                QCoreApplication.instance().font()).ascent() / 11
            # round the font pixel ratio to quarters
            fontPixelRatio = int(fontPixelRatio * 4) / 4
            return fontPixelRatio

    def _getDefaultLanguage(self, file_name):
        # If we have a language override set in the environment, try and use that.
        lang = os.getenv("URANIUM_LANGUAGE")
        if lang:
            try:
                return Resources.getPath(Resources.i18n, lang, "LC_MESSAGES",
                                         file_name + ".qm")
            except FileNotFoundError:
                pass

        # Else, try and get the current language from preferences
        lang = Preferences.getInstance().getValue("general/language")
        if lang:
            try:
                return Resources.getPath(Resources.i18n, lang, "LC_MESSAGES",
                                         file_name + ".qm")
            except FileNotFoundError:
                pass

        # If none of those are set, try to use the environment's LANGUAGE variable.
        lang = os.getenv("LANGUAGE")
        if lang:
            try:
                return Resources.getPath(Resources.i18n, lang, "LC_MESSAGES",
                                         file_name + ".qm")
            except FileNotFoundError:
                pass

        # If looking up the language from the enviroment or preferences fails, try and use Qt's system locale instead.
        locale = QLocale.system()

        # First, try and find a directory for any of the provided languages
        for lang in locale.uiLanguages():
            try:
                return Resources.getPath(Resources.i18n, lang, "LC_MESSAGES",
                                         file_name + ".qm")
            except FileNotFoundError:
                pass

        # If that fails, see if we can extract a language code from the
        # preferred language, regardless of the country code. This will turn
        # "en-GB" into "en" for example.
        lang = locale.uiLanguages()[0]
        lang = lang[0:lang.find("-")]
        for subdirectory in os.path.listdir(Resources.getPath(Resources.i18n)):
            if subdirectory == "en_7S":  #Never automatically go to Pirate.
                continue
            if not os.path.isdir(
                    Resources.getPath(Resources.i18n, subdirectory)):
                continue
            if subdirectory.startswith(
                    lang +
                    "_"):  #Only match the language code, not the country code.
                return Resources.getPath(Resources.i18n, lang, "LC_MESSAGES",
                                         file_name + ".qm")

        return None
Ejemplo n.º 2
0
class QtApplication(QApplication, Application):
    pluginsLoaded = Signal()
    applicationRunning = Signal()
    
    def __init__(self, tray_icon_name: str = None, **kwargs) -> None:
        plugin_path = ""
        if sys.platform == "win32":
            if hasattr(sys, "frozen"):
                plugin_path = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "PyQt5", "plugins")
                Logger.log("i", "Adding QT5 plugin path: %s", plugin_path)
                QCoreApplication.addLibraryPath(plugin_path)
            else:
                import site
                for sitepackage_dir in site.getsitepackages():
                    QCoreApplication.addLibraryPath(os.path.join(sitepackage_dir, "PyQt5", "plugins"))
        elif sys.platform == "darwin":
            plugin_path = os.path.join(self.getInstallPrefix(), "Resources", "plugins")

        if plugin_path:
            Logger.log("i", "Adding QT5 plugin path: %s", plugin_path)
            QCoreApplication.addLibraryPath(plugin_path)

        # use Qt Quick Scene Graph "basic" render loop
        os.environ["QSG_RENDER_LOOP"] = "basic"

        super().__init__(sys.argv, **kwargs) # type: ignore

        self._qml_import_paths = [] #type: List[str]
        self._main_qml = "main.qml" #type: str
        self._qml_engine = None #type: Optional[QQmlApplicationEngine]
        self._main_window = None #type: Optional[MainWindow]
        self._tray_icon_name = tray_icon_name #type: Optional[str]
        self._tray_icon = None #type: Optional[str]
        self._tray_icon_widget = None #type: Optional[QSystemTrayIcon]
        self._theme = None #type: Optional[Theme]
        self._renderer = None #type: Optional[QtRenderer]

        self._job_queue = None #type: Optional[JobQueue]
        self._version_upgrade_manager = None #type: Optional[VersionUpgradeManager]

        self._is_shutting_down = False #type: bool

        self._recent_files = [] #type: List[QUrl]

        self._configuration_error_message = None #type: Optional[ConfigurationErrorMessage]

    def addCommandLineOptions(self) -> None:
        super().addCommandLineOptions()
        # This flag is used by QApplication. We don't process it.
        self._cli_parser.add_argument("-qmljsdebugger",
                                      help = "For Qt's QML debugger compatibility")

    def initialize(self) -> None:
        super().initialize()
        # Initialize the package manager to remove and install scheduled packages.
        self._package_manager = self._package_manager_class(self, parent = self)

        self._mesh_file_handler = MeshFileHandler(self) #type: MeshFileHandler
        self._workspace_file_handler = WorkspaceFileHandler(self) #type: WorkspaceFileHandler

        # Remove this and you will get Windows 95 style for all widgets if you are using Qt 5.10+
        self.setStyle("fusion")

        self.setAttribute(Qt.AA_UseDesktopOpenGL)
        major_version, minor_version, profile = OpenGLContext.detectBestOpenGLVersion()

        if major_version is None and minor_version is None and profile is None and not self.getIsHeadLess():
            Logger.log("e", "Startup failed because OpenGL version probing has failed: tried to create a 2.0 and 4.1 context. Exiting")
            QMessageBox.critical(None, "Failed to probe OpenGL",
                                 "Could not probe OpenGL. This program requires OpenGL 2.0 or higher. Please check your video card drivers.")
            sys.exit(1)
        else:
            opengl_version_str = OpenGLContext.versionAsText(major_version, minor_version, profile)
            Logger.log("d", "Detected most suitable OpenGL context version: %s", opengl_version_str)
        if not self.getIsHeadLess():
            OpenGLContext.setDefaultFormat(major_version, minor_version, profile = profile)

        self._qml_import_paths.append(os.path.join(os.path.dirname(sys.executable), "qml"))
        self._qml_import_paths.append(os.path.join(self.getInstallPrefix(), "Resources", "qml"))

        Logger.log("i", "Initializing job queue ...")
        self._job_queue = JobQueue()
        self._job_queue.jobFinished.connect(self._onJobFinished)

        Logger.log("i", "Initializing version upgrade manager ...")
        self._version_upgrade_manager = VersionUpgradeManager(self)

    def startSplashWindowPhase(self) -> None:
        super().startSplashWindowPhase()

        self._package_manager.initialize()

        # Read preferences here (upgrade won't work) to get the language in use, so the splash window can be shown in
        # the correct language.
        try:
            preferences_filename = Resources.getPath(Resources.Preferences, self._app_name + ".cfg")
            self._preferences.readFromFile(preferences_filename)
        except FileNotFoundError:
            Logger.log("i", "Preferences file not found, ignore and use default language '%s'", self._default_language)

        signal.signal(signal.SIGINT, signal.SIG_DFL)
        # This is done here as a lot of plugins require a correct gl context. If you want to change the framework,
        # these checks need to be done in your <framework>Application.py class __init__().

        i18n_catalog = i18nCatalog("uranium")

        self._configuration_error_message = ConfigurationErrorMessage(self,
              i18n_catalog.i18nc("@info:status", "Your configuration seems to be corrupt."),
              lifetime = 0,
              title = i18n_catalog.i18nc("@info:title", "Configuration errors")
              )
        # Remove, install, and then loading plugins
        self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Loading plugins..."))
        # Remove and install the plugins that have been scheduled
        self._plugin_registry.initializeBeforePluginsAreLoaded()
        self._loadPlugins()
        self._plugin_registry.checkRequiredPlugins(self.getRequiredPlugins())
        self.pluginsLoaded.emit()

        self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Updating configuration..."))
        with self._container_registry.lockFile():
            VersionUpgradeManager.getInstance().upgrade()

        # Load preferences again because before we have loaded the plugins, we don't have the upgrade routine for
        # the preferences file. Now that we have, load the preferences file again so it can be upgraded and loaded.
        try:
            preferences_filename = Resources.getPath(Resources.Preferences, self._app_name + ".cfg")
            with open(preferences_filename, "r", encoding = "utf-8") as f:
                serialized = f.read()
            # This performs the upgrade for Preferences
            self._preferences.deserialize(serialized)
            self._preferences.setValue("general/plugins_to_remove", "")
            self._preferences.writeToFile(preferences_filename)
        except FileNotFoundError:
            Logger.log("i", "The preferences file cannot be found, will use default values")

        # Force the configuration file to be written again since the list of plugins to remove maybe changed
        self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Loading preferences..."))
        try:
            self._preferences_filename = Resources.getPath(Resources.Preferences, self._app_name + ".cfg")
            self._preferences.readFromFile(self._preferences_filename)
        except FileNotFoundError:
            Logger.log("i", "The preferences file '%s' cannot be found, will use default values",
                       self._preferences_filename)
            self._preferences_filename = Resources.getStoragePath(Resources.Preferences, self._app_name + ".cfg")

        # FIXME: This is done here because we now use "plugins.json" to manage plugins instead of the Preferences file,
        # but the PluginRegistry will still import data from the Preferences files if present, such as disabled plugins,
        # so we need to reset those values AFTER the Preferences file is loaded.
        self._plugin_registry.initializeAfterPluginsAreLoaded()

        # Check if we have just updated from an older version
        self._preferences.addPreference("general/last_run_version", "")
        last_run_version_str = self._preferences.getValue("general/last_run_version")
        if not last_run_version_str:
            last_run_version_str = self._version
        last_run_version = Version(last_run_version_str)
        current_version = Version(self._version)
        if last_run_version < current_version:
            self._just_updated_from_old_version = True
        self._preferences.setValue("general/last_run_version", str(current_version))
        self._preferences.writeToFile(self._preferences_filename)

        # Preferences: recent files
        self._preferences.addPreference("%s/recent_files" % self._app_name, "")
        file_names = self._preferences.getValue("%s/recent_files" % self._app_name).split(";")
        for file_name in file_names:
            if not os.path.isfile(file_name):
                continue
            self._recent_files.append(QUrl.fromLocalFile(file_name))

        if not self.getIsHeadLess():
            # Initialize System tray icon and make it invisible because it is used only to show pop up messages
            self._tray_icon = None
            if self._tray_icon_name:
                self._tray_icon = QIcon(Resources.getPath(Resources.Images, self._tray_icon_name))
                self._tray_icon_widget = QSystemTrayIcon(self._tray_icon)
                self._tray_icon_widget.setVisible(False)

    def initializeEngine(self) -> None:
        # TODO: Document native/qml import trickery
        self._qml_engine = QQmlApplicationEngine(self)
        self._qml_engine.setOutputWarningsToStandardError(False)
        self._qml_engine.warnings.connect(self.__onQmlWarning)

        for path in self._qml_import_paths:
            self._qml_engine.addImportPath(path)

        if not hasattr(sys, "frozen"):
            self._qml_engine.addImportPath(os.path.join(os.path.dirname(__file__), "qml"))

        self._qml_engine.rootContext().setContextProperty("QT_VERSION_STR", QT_VERSION_STR)
        self._qml_engine.rootContext().setContextProperty("screenScaleFactor", self._screenScaleFactor())

        self.registerObjects(self._qml_engine)

        Bindings.register()
        self._qml_engine.load(self._main_qml)
        self.engineCreatedSignal.emit()

    recentFilesChanged = pyqtSignal()

    @pyqtProperty("QVariantList", notify=recentFilesChanged)
    def recentFiles(self) -> List[QUrl]:
        return self._recent_files

    def _onJobFinished(self, job: Job) -> None:
        if isinstance(job, WriteFileJob):
            if not job.getResult() or not job.getAddToRecentFiles():
                # For a write file job, if it failed or it doesn't need to be added to the recent files list, we do not
                # add it.
                return
        elif (not isinstance(job, ReadMeshJob) and not isinstance(job, ReadFileJob)) or not job.getResult():
            return

        if isinstance(job, (ReadMeshJob, ReadFileJob, WriteFileJob)):
            self.addFileToRecentFiles(job.getFileName())

    def addFileToRecentFiles(self, file_name: str) -> None:
        file_path = QUrl.fromLocalFile(file_name)

        if file_path in self._recent_files:
            self._recent_files.remove(file_path)

        self._recent_files.insert(0, file_path)
        if len(self._recent_files) > 10:
            del self._recent_files[10]

        pref = ""
        for path in self._recent_files:
            pref += path.toLocalFile() + ";"

        self.getPreferences().setValue("%s/recent_files" % self.getApplicationName(), pref)
        self.recentFilesChanged.emit()

    def run(self) -> None:
        super().run()

    def hideMessage(self, message: Message) -> None:
        with self._message_lock:
            if message in self._visible_messages:
                message.hide(send_signal = False)  # we're in handling hideMessageSignal so we don't want to resend it
                self._visible_messages.remove(message)
                self.visibleMessageRemoved.emit(message)

    def showMessage(self, message: Message) -> None:
        with self._message_lock:
            if message not in self._visible_messages:
                self._visible_messages.append(message)
                message.setLifetimeTimer(QTimer())
                message.setInactivityTimer(QTimer())
                self.visibleMessageAdded.emit(message)

        # also show toast message when the main window is minimized
        self.showToastMessage(self._app_name, message.getText())

    def _onMainWindowStateChanged(self, window_state: int) -> None:
        if self._tray_icon and self._tray_icon_widget:
            visible = window_state == Qt.WindowMinimized
            self._tray_icon_widget.setVisible(visible)

    # Show toast message using System tray widget.
    def showToastMessage(self, title: str, message: str) -> None:
        if self.checkWindowMinimizedState() and self._tray_icon_widget:
            # NOTE: Qt 5.8 don't support custom icon for the system tray messages, but Qt 5.9 does.
            #       We should use the custom icon when we switch to Qt 5.9
            self._tray_icon_widget.showMessage(title, message)

    def setMainQml(self, path: str) -> None:
        self._main_qml = path

    def exec_(self, *args: Any, **kwargs: Any) -> None:
        self.applicationRunning.emit()
        super().exec_(*args, **kwargs)
        
    @pyqtSlot()
    def reloadQML(self) -> None:
        # only reload when it is a release build
        if not self.getIsDebugMode():
            return
        if self._qml_engine and self._theme:
            self._qml_engine.clearComponentCache()
            self._theme.reload()
            self._qml_engine.load(self._main_qml)
            # Hide the window. For some reason we can't close it yet. This needs to be done in the onComponentCompleted.
            for obj in self._qml_engine.rootObjects():
                if obj != self._qml_engine.rootObjects()[-1]:
                    obj.hide()

    @pyqtSlot()
    def purgeWindows(self) -> None:
        # Close all root objects except the last one.
        # Should only be called by onComponentCompleted of the mainWindow.
        if self._qml_engine:
            for obj in self._qml_engine.rootObjects():
                if obj != self._qml_engine.rootObjects()[-1]:
                    obj.close()

    @pyqtSlot("QList<QQmlError>")
    def __onQmlWarning(self, warnings: List[QQmlError]) -> None:
        for warning in warnings:
            Logger.log("w", warning.toString())

    engineCreatedSignal = Signal()

    def isShuttingDown(self) -> bool:
        return self._is_shutting_down

    def registerObjects(self, engine) -> None: #type: ignore #Don't type engine, because the type depends on the platform you're running on so it always gives an error somewhere.
        engine.rootContext().setContextProperty("PluginRegistry", PluginRegistry.getInstance())

    def getRenderer(self) -> QtRenderer:
        if not self._renderer:
            self._renderer = QtRenderer()

        return cast(QtRenderer, self._renderer)

    mainWindowChanged = Signal()

    def getMainWindow(self) -> Optional[MainWindow]:
        return self._main_window

    def setMainWindow(self, window: MainWindow) -> None:
        if window != self._main_window:
            if self._main_window is not None:
                self._main_window.windowStateChanged.disconnect(self._onMainWindowStateChanged)

            self._main_window = window
            if self._main_window is not None:
                self._main_window.windowStateChanged.connect(self._onMainWindowStateChanged)

            self.mainWindowChanged.emit()

    def setVisible(self, visible: bool) -> None:
        if self._main_window is not None:
            self._main_window.visible = visible

    @property
    def isVisible(self) -> bool:
        if self._main_window is not None:
            return self._main_window.visible #type: ignore #MyPy doesn't realise that self._main_window cannot be None here.
        return False

    def getTheme(self) -> Optional[Theme]:
        if self._theme is None:
            if self._qml_engine is None:
                Logger.log("e", "The theme cannot be accessed before the engine is initialised")
                return None

            self._theme = UM.Qt.Bindings.Theme.Theme.getInstance(self._qml_engine)
        return self._theme

    #   Handle a function that should be called later.
    def functionEvent(self, event: QEvent) -> None:
        e = _QtFunctionEvent(event)
        QCoreApplication.postEvent(self, e)

    #   Handle Qt events
    def event(self, event: QEvent) -> bool:
        if event.type() == _QtFunctionEvent.QtFunctionEvent:
            event._function_event.call()
            return True

        return super().event(event)

    def windowClosed(self, save_data: bool = True) -> None:
        Logger.log("d", "Shutting down %s", self.getApplicationName())
        self._is_shutting_down = True

        # garbage collect tray icon so it gets properly closed before the application is closed
        self._tray_icon_widget = None

        if save_data:
            try:
                self.savePreferences()
            except Exception as e:
                Logger.log("e", "Exception while saving preferences: %s", repr(e))

        try:
            self.applicationShuttingDown.emit()
        except Exception as e:
            Logger.log("e", "Exception while emitting shutdown signal: %s", repr(e))

        try:
            self.getBackend().close()
        except Exception as e:
            Logger.log("e", "Exception while closing backend: %s", repr(e))

        if self._tray_icon_widget:
            self._tray_icon_widget.deleteLater()

        self.quit()

    def checkWindowMinimizedState(self) -> bool:
        if self._main_window is not None and self._main_window.windowState() == Qt.WindowMinimized:
            return True
        else:
            return False

    ##  Get the backend of the application (the program that does the heavy lifting).
    #   The backend is also a QObject, which can be used from qml.
    @pyqtSlot(result = "QObject*")
    def getBackend(self) -> Backend:
        return self._backend

    ##  Property used to expose the backend
    #   It is made static as the backend is not supposed to change during runtime.
    #   This makes the connection between backend and QML more reliable than the pyqtSlot above.
    #   \returns Backend \type{Backend}
    @pyqtProperty("QVariant", constant = True)
    def backend(self) -> Backend:
        return self.getBackend()

    ## Create a class variable so we can manage the splash in the CrashHandler dialog when the Application instance
    # is not yet created, e.g. when an error occurs during the initialization
    splash = None  # type: Optional[QSplashScreen]

    def createSplash(self) -> None:
        if not self.getIsHeadLess():
            try:
                QtApplication.splash = self._createSplashScreen()
            except FileNotFoundError:
                QtApplication.splash = None
            else:
                if QtApplication.splash:
                    QtApplication.splash.show()
                    self.processEvents()

    ##  Display text on the splash screen.
    def showSplashMessage(self, message: str) -> None:
        if not QtApplication.splash:
            self.createSplash()
        
        if QtApplication.splash:
            QtApplication.splash.showMessage(message, Qt.AlignHCenter | Qt.AlignVCenter)
            self.processEvents()
        elif self.getIsHeadLess():
            Logger.log("d", message)

    ##  Close the splash screen after the application has started.
    def closeSplash(self) -> None:
        if QtApplication.splash:
            QtApplication.splash.close()
            QtApplication.splash = None

    ## Create a QML component from a qml file.
    #  \param qml_file_path: The absolute file path to the root qml file.
    #  \param context_properties: Optional dictionary containing the properties that will be set on the context of the
    #                              qml instance before creation.
    #  \return None in case the creation failed (qml error), else it returns the qml instance.
    #  \note If the creation fails, this function will ensure any errors are logged to the logging service.
    def createQmlComponent(self, qml_file_path: str, context_properties: Dict[str, "QObject"] = None) -> Optional["QObject"]:
        if self._qml_engine is None: # Protect in case the engine was not initialized yet
            return None
        path = QUrl.fromLocalFile(qml_file_path)
        component = QQmlComponent(self._qml_engine, path)
        result_context = QQmlContext(self._qml_engine.rootContext()) #type: ignore #MyPy doens't realise that self._qml_engine can't be None here.
        if context_properties is not None:
            for name, value in context_properties.items():
                result_context.setContextProperty(name, value)
        result = component.create(result_context)
        for err in component.errors():
            Logger.log("e", str(err.toString()))
        if result is None:
            return None

        # We need to store the context with the qml object, else the context gets garbage collected and the qml objects
        # no longer function correctly/application crashes.
        result.attached_context = result_context
        return result

    ##  Delete all nodes containing mesh data in the scene.
    #   \param only_selectable. Set this to False to delete objects from all build plates
    @pyqtSlot()
    def deleteAll(self, only_selectable = True) -> None:
        Logger.log("i", "Clearing scene")
        if not self.getController().getToolsEnabled():
            return

        nodes = []
        for node in DepthFirstIterator(self.getController().getScene().getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
            if not isinstance(node, SceneNode):
                continue
            if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
                continue  # Node that doesnt have a mesh and is not a group.
            if only_selectable and not node.isSelectable():
                continue
            if not node.callDecoration("isSliceable") and not node.callDecoration("getLayerData") and not node.callDecoration("isGroup"):
                continue  # Only remove nodes that are selectable.
            if node.getParent() and cast(SceneNode, node.getParent()).callDecoration("isGroup"):
                continue  # Grouped nodes don't need resetting as their parent (the group) is resetted)
            nodes.append(node)
        if nodes:
            op = GroupedOperation()

            for node in nodes:
                op.addOperation(RemoveSceneNodeOperation(node))

                # Reset the print information
                self.getController().getScene().sceneChanged.emit(node)

            op.push()
            Selection.clear()

    ##  Get the MeshFileHandler of this application.
    def getMeshFileHandler(self) -> MeshFileHandler:
        return self._mesh_file_handler

    def getWorkspaceFileHandler(self) -> WorkspaceFileHandler:
        return self._workspace_file_handler

    @pyqtSlot(result = QObject)
    def getPackageManager(self) -> PackageManager:
        return self._package_manager

    ##  Gets the instance of this application.
    #
    #   This is just to further specify the type of Application.getInstance().
    #   \return The instance of this application.
    @classmethod
    def getInstance(cls, *args, **kwargs) -> "QtApplication":
        return cast(QtApplication, super().getInstance(**kwargs))

    def _createSplashScreen(self) -> QSplashScreen:
        return QSplashScreen(QPixmap(Resources.getPath(Resources.Images, self.getApplicationName() + ".png")))

    def _screenScaleFactor(self) -> float:
        # OSX handles sizes of dialogs behind our backs, but other platforms need
        # to know about the device pixel ratio
        if sys.platform == "darwin":
            return 1.0
        else:
            # determine a device pixel ratio from font metrics, using the same logic as UM.Theme
            fontPixelRatio = QFontMetrics(QCoreApplication.instance().font()).ascent() / 11
            # round the font pixel ratio to quarters
            fontPixelRatio = int(fontPixelRatio * 4) / 4
            return fontPixelRatio
Ejemplo n.º 3
0
class QtApplication(QApplication, Application):
    pluginsLoaded = Signal()
    applicationRunning = Signal()

    def __init__(self, tray_icon_name: str = None, **kwargs) -> None:
        plugin_path = ""
        if sys.platform == "win32":
            if hasattr(sys, "frozen"):
                plugin_path = os.path.join(
                    os.path.dirname(os.path.abspath(sys.executable)), "PyQt5",
                    "plugins")
                Logger.log("i", "Adding QT5 plugin path: %s", plugin_path)
                QCoreApplication.addLibraryPath(plugin_path)
            else:
                import site
                for sitepackage_dir in site.getsitepackages():
                    QCoreApplication.addLibraryPath(
                        os.path.join(sitepackage_dir, "PyQt5", "plugins"))
        elif sys.platform == "darwin":
            plugin_path = os.path.join(self.getInstallPrefix(), "Resources",
                                       "plugins")

        if plugin_path:
            Logger.log("i", "Adding QT5 plugin path: %s", plugin_path)
            QCoreApplication.addLibraryPath(plugin_path)

        # use Qt Quick Scene Graph "basic" render loop
        os.environ["QSG_RENDER_LOOP"] = "basic"

        super().__init__(sys.argv, **kwargs)  # type: ignore

        self._qml_import_paths = []  #type: List[str]
        self._main_qml = "main.qml"  #type: str
        self._qml_engine = None  #type: Optional[QQmlApplicationEngine]
        self._main_window = None  #type: Optional[MainWindow]
        self._tray_icon_name = tray_icon_name  #type: Optional[str]
        self._tray_icon = None  #type: Optional[str]
        self._tray_icon_widget = None  #type: Optional[QSystemTrayIcon]
        self._theme = None  #type: Optional[Theme]
        self._renderer = None  #type: Optional[QtRenderer]

        self._job_queue = None  #type: Optional[JobQueue]
        self._version_upgrade_manager = None  #type: Optional[VersionUpgradeManager]

        self._is_shutting_down = False  #type: bool

        self._recent_files = []  #type: List[QUrl]

        self._configuration_error_message = None  #type: Optional[ConfigurationErrorMessage]

    def addCommandLineOptions(self) -> None:
        super().addCommandLineOptions()
        # This flag is used by QApplication. We don't process it.
        self._cli_parser.add_argument(
            "-qmljsdebugger", help="For Qt's QML debugger compatibility")

    def initialize(self) -> None:
        super().initialize()
        # Initialize the package manager to remove and install scheduled packages.
        self._package_manager = self._package_manager_class(self, parent=self)

        self._mesh_file_handler = MeshFileHandler(self)  #type: MeshFileHandler
        self._workspace_file_handler = WorkspaceFileHandler(
            self)  #type: WorkspaceFileHandler

        # Remove this and you will get Windows 95 style for all widgets if you are using Qt 5.10+
        self.setStyle("fusion")

        self.setAttribute(Qt.AA_UseDesktopOpenGL)
        major_version, minor_version, profile = OpenGLContext.detectBestOpenGLVersion(
        )

        if major_version is None or minor_version is None or profile is None:
            Logger.log(
                "e",
                "Startup failed because OpenGL version probing has failed: tried to create a 2.0 and 4.1 context. Exiting"
            )
            if not self.getIsHeadLess():
                QMessageBox.critical(
                    None, "Failed to probe OpenGL",
                    "Could not probe OpenGL. This program requires OpenGL 2.0 or higher. Please check your video card drivers."
                )
            sys.exit(1)
        else:
            opengl_version_str = OpenGLContext.versionAsText(
                major_version, minor_version, profile)
            Logger.log("d",
                       "Detected most suitable OpenGL context version: %s",
                       opengl_version_str)
        if not self.getIsHeadLess():
            OpenGLContext.setDefaultFormat(major_version,
                                           minor_version,
                                           profile=profile)

        self._qml_import_paths.append(
            os.path.join(os.path.dirname(sys.executable), "qml"))
        self._qml_import_paths.append(
            os.path.join(self.getInstallPrefix(), "Resources", "qml"))

        Logger.log("i", "Initializing job queue ...")
        self._job_queue = JobQueue()
        self._job_queue.jobFinished.connect(self._onJobFinished)

        Logger.log("i", "Initializing version upgrade manager ...")
        self._version_upgrade_manager = VersionUpgradeManager(self)

    def startSplashWindowPhase(self) -> None:
        super().startSplashWindowPhase()
        i18n_catalog = i18nCatalog("uranium")
        self.showSplashMessage(
            i18n_catalog.i18nc("@info:progress",
                               "Initializing package manager..."))
        self._package_manager.initialize()

        # Read preferences here (upgrade won't work) to get the language in use, so the splash window can be shown in
        # the correct language.
        try:
            preferences_filename = Resources.getPath(Resources.Preferences,
                                                     self._app_name + ".cfg")
            self._preferences.readFromFile(preferences_filename)
        except FileNotFoundError:
            Logger.log(
                "i",
                "Preferences file not found, ignore and use default language '%s'",
                self._default_language)

        signal.signal(signal.SIGINT, signal.SIG_DFL)
        # This is done here as a lot of plugins require a correct gl context. If you want to change the framework,
        # these checks need to be done in your <framework>Application.py class __init__().

        self._configuration_error_message = ConfigurationErrorMessage(
            self,
            i18n_catalog.i18nc("@info:status",
                               "Your configuration seems to be corrupt."),
            lifetime=0,
            title=i18n_catalog.i18nc("@info:title", "Configuration errors"))
        # Remove, install, and then loading plugins
        self.showSplashMessage(
            i18n_catalog.i18nc("@info:progress", "Loading plugins..."))
        # Remove and install the plugins that have been scheduled
        self._plugin_registry.initializeBeforePluginsAreLoaded()
        self._loadPlugins()
        self._plugin_registry.checkRequiredPlugins(self.getRequiredPlugins())
        self.pluginsLoaded.emit()

        self.showSplashMessage(
            i18n_catalog.i18nc("@info:progress", "Updating configuration..."))
        with self._container_registry.lockFile():
            VersionUpgradeManager.getInstance().upgrade()

        # Load preferences again because before we have loaded the plugins, we don't have the upgrade routine for
        # the preferences file. Now that we have, load the preferences file again so it can be upgraded and loaded.
        self.showSplashMessage(
            i18n_catalog.i18nc("@info:progress", "Loading preferences..."))
        try:
            preferences_filename = Resources.getPath(Resources.Preferences,
                                                     self._app_name + ".cfg")
            with open(preferences_filename, "r", encoding="utf-8") as f:
                serialized = f.read()
            # This performs the upgrade for Preferences
            self._preferences.deserialize(serialized)
            self._preferences.setValue("general/plugins_to_remove", "")
            self._preferences.writeToFile(preferences_filename)
        except (FileNotFoundError, UnicodeDecodeError):
            Logger.log(
                "i",
                "The preferences file cannot be found or it is corrupted, so we will use default values"
            )

        self.processEvents()
        # Force the configuration file to be written again since the list of plugins to remove maybe changed
        try:
            self._preferences_filename = Resources.getPath(
                Resources.Preferences, self._app_name + ".cfg")
            self._preferences.readFromFile(self._preferences_filename)
        except FileNotFoundError:
            Logger.log(
                "i",
                "The preferences file '%s' cannot be found, will use default values",
                self._preferences_filename)
            self._preferences_filename = Resources.getStoragePath(
                Resources.Preferences, self._app_name + ".cfg")

        # FIXME: This is done here because we now use "plugins.json" to manage plugins instead of the Preferences file,
        # but the PluginRegistry will still import data from the Preferences files if present, such as disabled plugins,
        # so we need to reset those values AFTER the Preferences file is loaded.
        self._plugin_registry.initializeAfterPluginsAreLoaded()

        # Check if we have just updated from an older version
        self._preferences.addPreference("general/last_run_version", "")
        last_run_version_str = self._preferences.getValue(
            "general/last_run_version")
        if not last_run_version_str:
            last_run_version_str = self._version
        last_run_version = Version(last_run_version_str)
        current_version = Version(self._version)
        if last_run_version < current_version:
            self._just_updated_from_old_version = True
        self._preferences.setValue("general/last_run_version",
                                   str(current_version))
        self._preferences.writeToFile(self._preferences_filename)

        # Preferences: recent files
        self._preferences.addPreference("%s/recent_files" % self._app_name, "")
        file_names = self._preferences.getValue("%s/recent_files" %
                                                self._app_name).split(";")
        for file_name in file_names:
            if not os.path.isfile(file_name):
                continue
            self._recent_files.append(QUrl.fromLocalFile(file_name))

        if not self.getIsHeadLess():
            # Initialize System tray icon and make it invisible because it is used only to show pop up messages
            self._tray_icon = None
            if self._tray_icon_name:
                self._tray_icon = QIcon(
                    Resources.getPath(Resources.Images, self._tray_icon_name))
                self._tray_icon_widget = QSystemTrayIcon(self._tray_icon)
                self._tray_icon_widget.setVisible(False)

    def initializeEngine(self) -> None:
        # TODO: Document native/qml import trickery
        self._qml_engine = QQmlApplicationEngine(self)
        self.processEvents()
        self._qml_engine.setOutputWarningsToStandardError(False)
        self._qml_engine.warnings.connect(self.__onQmlWarning)

        for path in self._qml_import_paths:
            self._qml_engine.addImportPath(path)

        if not hasattr(sys, "frozen"):
            self._qml_engine.addImportPath(
                os.path.join(os.path.dirname(__file__), "qml"))

        self._qml_engine.rootContext().setContextProperty(
            "QT_VERSION_STR", QT_VERSION_STR)
        self.processEvents()
        self._qml_engine.rootContext().setContextProperty(
            "screenScaleFactor", self._screenScaleFactor())

        self.registerObjects(self._qml_engine)

        Bindings.register()

        # Preload theme. The theme will be loaded on first use, which will incur a ~0.1s freeze on the MainThread.
        # Do it here, while the splash screen is shown. Also makes this freeze explicit and traceable.
        self.getTheme()
        self.processEvents()

        self.showSplashMessage(
            self._i18n_catalog.i18nc("@info:progress", "Loading UI..."))
        self._qml_engine.load(self._main_qml)
        self.engineCreatedSignal.emit()

    recentFilesChanged = pyqtSignal()

    @pyqtProperty("QVariantList", notify=recentFilesChanged)
    def recentFiles(self) -> List[QUrl]:
        return self._recent_files

    def _onJobFinished(self, job: Job) -> None:
        if isinstance(job, WriteFileJob):
            if not job.getResult() or not job.getAddToRecentFiles():
                # For a write file job, if it failed or it doesn't need to be added to the recent files list, we do not
                # add it.
                return
        elif (not isinstance(job, ReadMeshJob)
              and not isinstance(job, ReadFileJob)) or not job.getResult():
            return

        if isinstance(job, (ReadMeshJob, ReadFileJob, WriteFileJob)):
            self.addFileToRecentFiles(job.getFileName())

    def addFileToRecentFiles(self, file_name: str) -> None:
        file_path = QUrl.fromLocalFile(file_name)

        if file_path in self._recent_files:
            self._recent_files.remove(file_path)

        self._recent_files.insert(0, file_path)
        if len(self._recent_files) > 10:
            del self._recent_files[10]

        pref = ""
        for path in self._recent_files:
            pref += path.toLocalFile() + ";"

        self.getPreferences().setValue(
            "%s/recent_files" % self.getApplicationName(), pref)
        self.recentFilesChanged.emit()

    def run(self) -> None:
        super().run()

    def hideMessage(self, message: Message) -> None:
        with self._message_lock:
            if message in self._visible_messages:
                message.hide(
                    send_signal=False
                )  # we're in handling hideMessageSignal so we don't want to resend it
                self._visible_messages.remove(message)
                self.visibleMessageRemoved.emit(message)

    def showMessage(self, message: Message) -> None:
        with self._message_lock:
            if message not in self._visible_messages:
                self._visible_messages.append(message)
                message.setLifetimeTimer(QTimer())
                message.setInactivityTimer(QTimer())
                self.visibleMessageAdded.emit(message)

        # also show toast message when the main window is minimized
        self.showToastMessage(self._app_name, message.getText())

    def _onMainWindowStateChanged(self, window_state: int) -> None:
        if self._tray_icon and self._tray_icon_widget:
            visible = window_state == Qt.WindowMinimized
            self._tray_icon_widget.setVisible(visible)

    # Show toast message using System tray widget.
    def showToastMessage(self, title: str, message: str) -> None:
        if self.checkWindowMinimizedState() and self._tray_icon_widget:
            # NOTE: Qt 5.8 don't support custom icon for the system tray messages, but Qt 5.9 does.
            #       We should use the custom icon when we switch to Qt 5.9
            self._tray_icon_widget.showMessage(title, message)

    def setMainQml(self, path: str) -> None:
        self._main_qml = path

    def exec_(self, *args: Any, **kwargs: Any) -> None:
        self.applicationRunning.emit()
        super().exec_(*args, **kwargs)

    @pyqtSlot()
    def reloadQML(self) -> None:
        # only reload when it is a release build
        if not self.getIsDebugMode():
            return
        if self._qml_engine and self._theme:
            self._qml_engine.clearComponentCache()
            self._theme.reload()
            self._qml_engine.load(self._main_qml)
            # Hide the window. For some reason we can't close it yet. This needs to be done in the onComponentCompleted.
            for obj in self._qml_engine.rootObjects():
                if obj != self._qml_engine.rootObjects()[-1]:
                    obj.hide()

    @pyqtSlot()
    def purgeWindows(self) -> None:
        # Close all root objects except the last one.
        # Should only be called by onComponentCompleted of the mainWindow.
        if self._qml_engine:
            for obj in self._qml_engine.rootObjects():
                if obj != self._qml_engine.rootObjects()[-1]:
                    obj.close()

    @pyqtSlot("QList<QQmlError>")
    def __onQmlWarning(self, warnings: List[QQmlError]) -> None:
        for warning in warnings:
            Logger.log("w", warning.toString())

    engineCreatedSignal = Signal()

    def isShuttingDown(self) -> bool:
        return self._is_shutting_down

    def registerObjects(
        self, engine
    ) -> None:  #type: ignore #Don't type engine, because the type depends on the platform you're running on so it always gives an error somewhere.
        engine.rootContext().setContextProperty("PluginRegistry",
                                                PluginRegistry.getInstance())

    def getRenderer(self) -> QtRenderer:
        if not self._renderer:
            self._renderer = QtRenderer()

        return cast(QtRenderer, self._renderer)

    mainWindowChanged = Signal()

    def getMainWindow(self) -> Optional[MainWindow]:
        return self._main_window

    def setMainWindow(self, window: MainWindow) -> None:
        if window != self._main_window:
            if self._main_window is not None:
                self._main_window.windowStateChanged.disconnect(
                    self._onMainWindowStateChanged)

            self._main_window = window
            if self._main_window is not None:
                self._main_window.windowStateChanged.connect(
                    self._onMainWindowStateChanged)
            self.mainWindowChanged.emit()

    def setVisible(self, visible: bool) -> None:
        if self._main_window is not None:
            self._main_window.visible = visible

    @property
    def isVisible(self) -> bool:
        if self._main_window is not None:
            return self._main_window.visible  #type: ignore #MyPy doesn't realise that self._main_window cannot be None here.
        return False

    def getTheme(self) -> Optional[Theme]:
        if self._theme is None:
            if self._qml_engine is None:
                Logger.log(
                    "e",
                    "The theme cannot be accessed before the engine is initialised"
                )
                return None

            self._theme = UM.Qt.Bindings.Theme.Theme.getInstance(
                self._qml_engine)
        return self._theme

    #   Handle a function that should be called later.
    def functionEvent(self, event: QEvent) -> None:
        e = _QtFunctionEvent(event)
        QCoreApplication.postEvent(self, e)

    #   Handle Qt events
    def event(self, event: QEvent) -> bool:
        if event.type() == _QtFunctionEvent.QtFunctionEvent:
            event._function_event.call()
            return True

        return super().event(event)

    def windowClosed(self, save_data: bool = True) -> None:
        Logger.log("d", "Shutting down %s", self.getApplicationName())
        self._is_shutting_down = True

        # garbage collect tray icon so it gets properly closed before the application is closed
        self._tray_icon_widget = None

        if save_data:
            try:
                self.savePreferences()
            except Exception as e:
                Logger.log("e", "Exception while saving preferences: %s",
                           repr(e))

        try:
            self.applicationShuttingDown.emit()
        except Exception as e:
            Logger.log("e", "Exception while emitting shutdown signal: %s",
                       repr(e))

        try:
            self.getBackend().close()
        except Exception as e:
            Logger.log("e", "Exception while closing backend: %s", repr(e))

        if self._tray_icon_widget:
            self._tray_icon_widget.deleteLater()

        self.quit()

    def checkWindowMinimizedState(self) -> bool:
        if self._main_window is not None and self._main_window.windowState(
        ) == Qt.WindowMinimized:
            return True
        else:
            return False

    ##  Get the backend of the application (the program that does the heavy lifting).
    #   The backend is also a QObject, which can be used from qml.
    @pyqtSlot(result="QObject*")
    def getBackend(self) -> Backend:
        return self._backend

    ##  Property used to expose the backend
    #   It is made static as the backend is not supposed to change during runtime.
    #   This makes the connection between backend and QML more reliable than the pyqtSlot above.
    #   \returns Backend \type{Backend}
    @pyqtProperty("QVariant", constant=True)
    def backend(self) -> Backend:
        return self.getBackend()

    ## Create a class variable so we can manage the splash in the CrashHandler dialog when the Application instance
    # is not yet created, e.g. when an error occurs during the initialization
    splash = None  # type: Optional[QSplashScreen]

    def createSplash(self) -> None:
        if not self.getIsHeadLess():
            try:
                QtApplication.splash = self._createSplashScreen()
            except FileNotFoundError:
                QtApplication.splash = None
            else:
                if QtApplication.splash:
                    QtApplication.splash.show()
                    self.processEvents()

    ##  Display text on the splash screen.
    def showSplashMessage(self, message: str) -> None:
        if not QtApplication.splash:
            self.createSplash()

        if QtApplication.splash:
            self.processEvents(
            )  # Process events from previous loading phase before updating the message
            QtApplication.splash.showMessage(
                message,
                Qt.AlignHCenter | Qt.AlignVCenter)  # Now update the message
            self.processEvents()  # And make sure it is immediately visible
        elif self.getIsHeadLess():
            Logger.log("d", message)

    ##  Close the splash screen after the application has started.
    def closeSplash(self) -> None:
        if QtApplication.splash:
            QtApplication.splash.close()
            QtApplication.splash = None

    ## Create a QML component from a qml file.
    #  \param qml_file_path: The absolute file path to the root qml file.
    #  \param context_properties: Optional dictionary containing the properties that will be set on the context of the
    #                              qml instance before creation.
    #  \return None in case the creation failed (qml error), else it returns the qml instance.
    #  \note If the creation fails, this function will ensure any errors are logged to the logging service.
    def createQmlComponent(
        self,
        qml_file_path: str,
        context_properties: Dict[str,
                                 "QObject"] = None) -> Optional["QObject"]:
        if self._qml_engine is None:  # Protect in case the engine was not initialized yet
            return None
        path = QUrl.fromLocalFile(qml_file_path)
        component = QQmlComponent(self._qml_engine, path)
        result_context = QQmlContext(
            self._qml_engine.rootContext()
        )  #type: ignore #MyPy doens't realise that self._qml_engine can't be None here.
        if context_properties is not None:
            for name, value in context_properties.items():
                result_context.setContextProperty(name, value)
        result = component.create(result_context)
        for err in component.errors():
            Logger.log("e", str(err.toString()))
        if result is None:
            return None

        # We need to store the context with the qml object, else the context gets garbage collected and the qml objects
        # no longer function correctly/application crashes.
        result.attached_context = result_context
        return result

    ##  Delete all nodes containing mesh data in the scene.
    #   \param only_selectable. Set this to False to delete objects from all build plates
    @pyqtSlot()
    def deleteAll(self, only_selectable=True) -> None:
        self.getController().deleteAllNodesWithMeshData(only_selectable)

    ##  Get the MeshFileHandler of this application.
    def getMeshFileHandler(self) -> MeshFileHandler:
        return self._mesh_file_handler

    def getWorkspaceFileHandler(self) -> WorkspaceFileHandler:
        return self._workspace_file_handler

    @pyqtSlot(result=QObject)
    def getPackageManager(self) -> PackageManager:
        return self._package_manager

    ##  Gets the instance of this application.
    #
    #   This is just to further specify the type of Application.getInstance().
    #   \return The instance of this application.
    @classmethod
    def getInstance(cls, *args, **kwargs) -> "QtApplication":
        return cast(QtApplication, super().getInstance(**kwargs))

    def _createSplashScreen(self) -> QSplashScreen:
        return QSplashScreen(
            QPixmap(
                Resources.getPath(Resources.Images,
                                  self.getApplicationName() + ".png")))

    def _screenScaleFactor(self) -> float:
        # OSX handles sizes of dialogs behind our backs, but other platforms need
        # to know about the device pixel ratio
        if sys.platform == "darwin":
            return 1.0
        else:
            # determine a device pixel ratio from font metrics, using the same logic as UM.Theme
            fontPixelRatio = QFontMetrics(
                QCoreApplication.instance().font()).ascent() / 11
            # round the font pixel ratio to quarters
            fontPixelRatio = int(fontPixelRatio * 4) / 4
            return fontPixelRatio

    @pyqtProperty(str, constant=True)
    def applicationDisplayName(self) -> str:
        return self.getApplicationDisplayName()
Ejemplo n.º 4
0
class Application(QObjectBase):
    _instance = None  # type: Optional["Application"]

    machine_list = QProperty[QObjectList[Machine]](
        QObjectList[Machine]("PLACEHOLDER"))
    active_machine = QProperty[Machine](Machine())

    @classmethod
    def getInstance(cls, *args: Any) -> "Application":
        assert cls._instance is not None
        return cls._instance

    def __init__(self) -> None:
        super().__init__()
        assert Application._instance is None
        Application._instance = self

        PluginRegistry.getInstance().findPlugins(
            os.path.join(os.getcwd(), "plugins"))

        self.__app = QGuiApplication(sys.argv)
        self.__qml_engine = QQmlApplicationEngine(self.__app)
        self.__qml_engine.warnings.connect(self.__onWarning)
        self.__qml_engine.setOutputWarningsToStandardError(False)

        self.__view = View(self)

        qmlRegisterType(MainWindow, "NK3", 1, 0, "MainWindow")
        qmlRegisterType(MouseHandler, "NK3", 1, 0, "MouseHandler")
        qmlRegisterSingletonType(Application, "NK3", 1, 0, "Application",
                                 Application.getInstance)

        self.__process_result = Result()

        self.machine_list = QObjectList[Machine]("machine")

        self.__document_list = QObjectList[DocumentNode]("node")
        self.__document_list.rowsInserted.connect(
            lambda parent, first, last: self.__view.home())

        self.__dispatcher = Dispatcher(self.__document_list)
        self.__dispatcher.onResultData = self.__onResultData
        self.active_machineChanged.connect(self.__onActiveMachineChanged)

        self.__last_file = ""

        s = Storage()
        if s.load():
            s.loadMachines(self.machine_list)
            self.__last_file = s.getGeneralSetting("last_file")
            if os.path.isfile(self.__last_file):
                self._loadFile(self.__last_file)
        if self.machine_list.size() == 0:
            machine_type = PluginRegistry.getInstance().getClass(
                Machine, "RouterMachine")
            assert machine_type is not None
            self.machine_list.append(machine_type())
            output_method_type = PluginRegistry.getInstance().getClass(
                OutputMethod, "GCodeOutputMethod")
            if output_method_type is not None:
                self.machine_list[0].output_method = output_method_type()
            self.machine_list[0].addTool(self.machine_list[0].tool_types[0])
        self.active_machine = self.machine_list[0]

        self.__qml_engine.rootContext().setContextProperty(
            "document_list", self.__document_list)
        self.__qml_engine.load(QUrl("resources/qml/Main.qml"))

        self.__app.aboutToQuit.connect(self._onQuit)

    def __onWarning(self, warnings: List[QQmlError]) -> None:
        for warning in warnings:
            logging.info("%s:%d %s",
                         warning.url().toLocalFile(), warning.line(),
                         warning.description())

    @property
    def document_list(self) -> QObjectList[DocumentNode]:
        return self.__document_list

    @property
    def result_data(self) -> Result:
        return self.__process_result

    def __onActiveMachineChanged(self) -> None:
        for node in DepthFirstIterator(self.__document_list):
            node.tool_index = -1
            node.operation_index = -1
        for machine in self.machine_list:
            if machine != self.active_machine:
                machine.output_method.release()
        self.__dispatcher.setActiveMachine(self.active_machine)
        self.active_machine.output_method.activate()
        self.__qml_engine.rootContext().setContextProperty(
            "output_method", self.active_machine.output_method)

    def __onResultData(self, result: Result) -> None:
        self.__process_result = result
        self.repaint()

    def getView(self) -> View:
        return self.__view

    def repaint(self) -> None:
        root_objects = self.__qml_engine.rootObjects()
        if len(root_objects) > 0:
            root_objects[0].requestRepaint.emit()

    def start(self) -> int:
        if not self.__qml_engine.rootObjects():
            return -1
        return self.__app.exec_()

    @qtSlot
    def reloadQML(self) -> None:
        for window in self.__qml_engine.rootObjects():
            window.close()
        self.__qml_engine.clearComponentCache()
        logging.info("Reloading QML")
        self.__qml_engine.load(QUrl("resources/qml/Main.qml"))

    @qtSlot
    def loadFile(self, filename: QUrl) -> None:
        self._loadFile(filename.toLocalFile())

    def _loadFile(self, filename: str) -> None:
        logging.info("Going to load: %s", filename)
        reader = FileReader.getFileTypes()[os.path.splitext(filename)[1]
                                           [1:].lower()]
        document_node = reader().load(filename)
        while len(self.__document_list) > 0:
            self.__document_list.remove(0)
        self.__document_list.append(document_node)
        self.__last_file = filename
        self.repaint()

    @qtSlot
    def getLoadFileTypes(self) -> List[str]:
        types = FileReader.getFileTypes()
        result = [
            "Vector files (%s)" %
            (" ".join(["*.%s" % ext for ext in types.keys()]))
        ]
        for ext in types.keys():
            result.append("%s (*.%s)" % (ext, ext))
        return result

    def _onQuit(self) -> None:
        s = Storage()
        s.setGeneralSetting("last_file", self.__last_file)
        s.storeMachines(self.machine_list)
        s.save()