예제 #1
0
파일: app.py 프로젝트: cboydNYC/meshroom
    def __init__(self, args):
        QtArgs = [args[0], '-style', 'fusion'] + args[1:]  # force Fusion style by default
        super(MeshroomApp, self).__init__(QtArgs)

        self.setOrganizationName('AliceVision')
        self.setApplicationName('Meshroom')
        self.setAttribute(Qt.AA_EnableHighDpiScaling)
        self.setApplicationVersion(meshroom.__version__)

        font = self.font()
        font.setPointSize(9)
        self.setFont(font)

        pwd = os.path.dirname(__file__)
        self.setWindowIcon(QIcon(os.path.join(pwd, "img/meshroom.svg")))

        # QML engine setup
        qmlDir = os.path.join(pwd, "qml")
        url = os.path.join(qmlDir, "main.qml")
        self.engine = QmlInstantEngine()
        self.engine.addFilesFromDirectory(qmlDir, recursive=True)
        self.engine.setWatching(os.environ.get("MESHROOM_INSTANT_CODING", False))
        # whether to output qml warnings to stderr (disable by default)
        self.engine.setOutputWarningsToStandardError(MessageHandler.outputQmlWarnings)
        qInstallMessageHandler(MessageHandler.handler)

        self.engine.addImportPath(qmlDir)
        components.registerTypes()

        # expose available node types that can be instantiated
        self.engine.rootContext().setContextProperty("_nodeTypes", sorted(nodesDesc.keys()))

        # instantiate Reconstruction object
        r = Reconstruction(parent=self)
        self.engine.rootContext().setContextProperty("_reconstruction", r)

        # those helpers should be available from QML Utils module as singletons, but:
        #  - qmlRegisterUncreatableType is not yet available in PySide2
        #  - declaring them as singleton in qmldir file causes random crash at exit
        # => expose them as context properties instead
        self.engine.rootContext().setContextProperty("Filepath", FilepathHelper(parent=self))
        self.engine.rootContext().setContextProperty("Scene3DHelper", Scene3DHelper(parent=self))
        self.engine.rootContext().setContextProperty("Clipboard", ClipboardHelper(parent=self))

        # additional context properties
        self.engine.rootContext().setContextProperty("_PaletteManager", PaletteManager(self.engine, parent=self))
        self.engine.rootContext().setContextProperty("MeshroomApp", self)

        # request any potential computation to stop on exit
        self.aboutToQuit.connect(r.stopChildThreads)

        parser = argparse.ArgumentParser(prog=args[0], description='Launch Meshroom UI.')
        parser.add_argument('--project', metavar='MESHROOM_FILE', type=str, required=False,
                            help='Meshroom project file (e.g. myProject.mg).')
        args = parser.parse_args(args[1:])
        if args.project:
            r.loadUrl(QUrl.fromLocalFile(args.project))

        self.engine.load(os.path.normpath(url))
예제 #2
0
    def __init__(self, args):
        args = [args[0], '-style', 'fusion'
                ] + args[1:]  # force Fusion style by default

        super(MeshroomApp, self).__init__(args)
        self.setOrganizationName('AliceVision')
        self.setApplicationName('Meshroom')
        self.setAttribute(Qt.AA_EnableHighDpiScaling)
        self.setApplicationVersion(meshroom.__version__)

        font = self.font()
        font.setPointSize(9)
        self.setFont(font)

        pwd = os.path.dirname(__file__)
        self.setWindowIcon(QIcon(os.path.join(pwd, "img/meshroom.svg")))

        # QML engine setup
        qmlDir = os.path.join(pwd, "qml")
        url = os.path.join(qmlDir, "main.qml")
        self.engine = QmlInstantEngine()
        self.engine.addFilesFromDirectory(qmlDir, recursive=True)
        self.engine.setWatching(
            os.environ.get("MESHROOM_INSTANT_CODING", False))
        # whether to output qml warnings to stderr (disable by default)
        self.engine.setOutputWarningsToStandardError(
            bool(os.environ.get("MESHROOM_OUTPUT_QML_WARNINGS", False)))
        self.engine.addImportPath(qmlDir)
        components.registerTypes()

        # expose available node types that can be instantiated
        self.engine.rootContext().setContextProperty("_nodeTypes",
                                                     sorted(nodesDesc.keys()))
        r = Reconstruction(parent=self)
        self.engine.rootContext().setContextProperty("_reconstruction", r)
        pm = PaletteManager(self.engine, parent=self)
        self.engine.rootContext().setContextProperty("_PaletteManager", pm)
        fpHelper = FilepathHelper(parent=self)
        self.engine.rootContext().setContextProperty("Filepath", fpHelper)
        self.engine.rootContext().setContextProperty("MeshroomApp", self)
        # Request any potential computation to stop on exit
        self.aboutToQuit.connect(r.stopExecution)

        self.engine.load(os.path.normpath(url))
예제 #3
0
class MeshroomApp(QApplication):
    """ Meshroom UI Application. """
    def __init__(self, args):
        args = [args[0], '-style', 'fusion'] + args[1:]  # force Fusion style by default

        super(MeshroomApp, self).__init__(args)
        self.setOrganizationName('AliceVision')
        self.setApplicationName('Meshroom')
        self.setAttribute(Qt.AA_EnableHighDpiScaling)
        self.setApplicationVersion(meshroom.__version__)

        font = self.font()
        font.setPointSize(9)
        self.setFont(font)

        pwd = os.path.dirname(__file__)
        self.setWindowIcon(QIcon(os.path.join(pwd, "img/meshroom.svg")))

        # QML engine setup
        qmlDir = os.path.join(pwd, "qml")
        url = os.path.join(qmlDir, "main.qml")
        self.engine = QmlInstantEngine()
        self.engine.addFilesFromDirectory(qmlDir, recursive=True)
        self.engine.setWatching(os.environ.get("MESHROOM_INSTANT_CODING", False))
        # whether to output qml warnings to stderr (disable by default)
        self.engine.setOutputWarningsToStandardError(bool(os.environ.get("MESHROOM_OUTPUT_QML_WARNINGS", False)))
        self.engine.addImportPath(qmlDir)
        components.registerTypes()

        # expose available node types that can be instantiated
        self.engine.rootContext().setContextProperty("_nodeTypes", sorted(nodesDesc.keys()))
        r = Reconstruction(parent=self)
        self.engine.rootContext().setContextProperty("_reconstruction", r)
        pm = PaletteManager(self.engine, parent=self)
        self.engine.rootContext().setContextProperty("_PaletteManager", pm)
        fpHelper = FilepathHelper(parent=self)
        self.engine.rootContext().setContextProperty("Filepath", fpHelper)
        self.engine.rootContext().setContextProperty("MeshroomApp", self)
        # Request any potential computation to stop on exit
        self.aboutToQuit.connect(r.stopExecution)

        self.engine.load(os.path.normpath(url))

    @Slot(str, result=str)
    def markdownToHtml(self, md):
        """
        Convert markdown to HTML.

        Args:
            md (str): the markdown text to convert

        Returns:
            str: the resulting HTML string
        """
        try:
            from markdown import markdown
        except ImportError:
            logging.warning("Can't import markdown module, returning source markdown text.")
            return md
        return markdown(md)

    @Property(QJsonValue, constant=True)
    def systemInfo(self):
        import platform
        import sys
        return {
            'platform': '{} {}'.format(platform.system(), platform.release()),
            'python': 'Python {}'.format(sys.version.split(" ")[0])
        }

    @Property("QVariantList", constant=True)
    def licensesModel(self):
        """
        Get info about open-source licenses for the application.
        Model provides:
            title: the name of the project
            localUrl: the local path to COPYING.md
            onlineUrl: the remote path to COPYING.md
        """
        rootDir = os.environ.get("MESHROOM_INSTALL_DIR", os.getcwd())
        return [
            {
                "title": "Meshroom",
                "localUrl": os.path.join(rootDir, "COPYING.md"),
                "onlineUrl": "https://raw.githubusercontent.com/alicevision/meshroom/develop/COPYING.md"
            },
            {
                "title": "AliceVision",
                "localUrl": os.path.join(rootDir, "aliceVision", "share", "aliceVision", "COPYING.md"),
                "onlineUrl": "https://raw.githubusercontent.com/alicevision/AliceVision/develop/COPYING.md"
            }
        ]
예제 #4
0
파일: app.py 프로젝트: cboydNYC/meshroom
class MeshroomApp(QApplication):
    """ Meshroom UI Application. """
    def __init__(self, args):
        QtArgs = [args[0], '-style', 'fusion'] + args[1:]  # force Fusion style by default
        super(MeshroomApp, self).__init__(QtArgs)

        self.setOrganizationName('AliceVision')
        self.setApplicationName('Meshroom')
        self.setAttribute(Qt.AA_EnableHighDpiScaling)
        self.setApplicationVersion(meshroom.__version__)

        font = self.font()
        font.setPointSize(9)
        self.setFont(font)

        pwd = os.path.dirname(__file__)
        self.setWindowIcon(QIcon(os.path.join(pwd, "img/meshroom.svg")))

        # QML engine setup
        qmlDir = os.path.join(pwd, "qml")
        url = os.path.join(qmlDir, "main.qml")
        self.engine = QmlInstantEngine()
        self.engine.addFilesFromDirectory(qmlDir, recursive=True)
        self.engine.setWatching(os.environ.get("MESHROOM_INSTANT_CODING", False))
        # whether to output qml warnings to stderr (disable by default)
        self.engine.setOutputWarningsToStandardError(MessageHandler.outputQmlWarnings)
        qInstallMessageHandler(MessageHandler.handler)

        self.engine.addImportPath(qmlDir)
        components.registerTypes()

        # expose available node types that can be instantiated
        self.engine.rootContext().setContextProperty("_nodeTypes", sorted(nodesDesc.keys()))

        # instantiate Reconstruction object
        r = Reconstruction(parent=self)
        self.engine.rootContext().setContextProperty("_reconstruction", r)

        # those helpers should be available from QML Utils module as singletons, but:
        #  - qmlRegisterUncreatableType is not yet available in PySide2
        #  - declaring them as singleton in qmldir file causes random crash at exit
        # => expose them as context properties instead
        self.engine.rootContext().setContextProperty("Filepath", FilepathHelper(parent=self))
        self.engine.rootContext().setContextProperty("Scene3DHelper", Scene3DHelper(parent=self))
        self.engine.rootContext().setContextProperty("Clipboard", ClipboardHelper(parent=self))

        # additional context properties
        self.engine.rootContext().setContextProperty("_PaletteManager", PaletteManager(self.engine, parent=self))
        self.engine.rootContext().setContextProperty("MeshroomApp", self)

        # request any potential computation to stop on exit
        self.aboutToQuit.connect(r.stopChildThreads)

        parser = argparse.ArgumentParser(prog=args[0], description='Launch Meshroom UI.')
        parser.add_argument('--project', metavar='MESHROOM_FILE', type=str, required=False,
                            help='Meshroom project file (e.g. myProject.mg).')
        args = parser.parse_args(args[1:])
        if args.project:
            r.loadUrl(QUrl.fromLocalFile(args.project))

        self.engine.load(os.path.normpath(url))

    @Slot(str, result=str)
    def markdownToHtml(self, md):
        """
        Convert markdown to HTML.

        Args:
            md (str): the markdown text to convert

        Returns:
            str: the resulting HTML string
        """
        try:
            from markdown import markdown
        except ImportError:
            logging.warning("Can't import markdown module, returning source markdown text.")
            return md
        return markdown(md)

    @Property(QJsonValue, constant=True)
    def systemInfo(self):
        import platform
        import sys
        return {
            'platform': '{} {}'.format(platform.system(), platform.release()),
            'python': 'Python {}'.format(sys.version.split(" ")[0])
        }

    @Property("QVariantList", constant=True)
    def licensesModel(self):
        """
        Get info about open-source licenses for the application.
        Model provides:
            title: the name of the project
            localUrl: the local path to COPYING.md
            onlineUrl: the remote path to COPYING.md
        """
        rootDir = os.environ.get("MESHROOM_INSTALL_DIR", os.getcwd())
        return [
            {
                "title": "Meshroom",
                "localUrl": os.path.join(rootDir, "COPYING.md"),
                "onlineUrl": "https://raw.githubusercontent.com/alicevision/meshroom/develop/COPYING.md"
            },
            {
                "title": "AliceVision",
                "localUrl": os.path.join(rootDir, "aliceVision", "share", "aliceVision", "COPYING.md"),
                "onlineUrl": "https://raw.githubusercontent.com/alicevision/AliceVision/develop/COPYING.md"
            }
        ]
예제 #5
0
파일: app.py 프로젝트: cs210/Worldsight
    def __init__(self, args):
        QtArgs = [args[0], '-style', 'fusion'
                  ] + args[1:]  # force Fusion style by default

        parser = argparse.ArgumentParser(prog=args[0],
                                         description='Launch Meshroom UI.',
                                         add_help=True)

        parser.add_argument(
            'project',
            metavar='PROJECT',
            type=str,
            nargs='?',
            help=
            'Meshroom project file (e.g. myProject.mg) or folder with images to reconstruct.'
        )
        parser.add_argument(
            '-i',
            '--import',
            metavar='IMAGES/FOLDERS',
            type=str,
            nargs='*',
            help='Import images or folder with images to reconstruct.')
        parser.add_argument(
            '-I',
            '--importRecursive',
            metavar='FOLDERS',
            type=str,
            nargs='*',
            help=
            'Import images to reconstruct from specified folder and sub-folders.'
        )
        parser.add_argument('-s',
                            '--save',
                            metavar='PROJECT.mg',
                            type=str,
                            default='',
                            help='Save the created scene.')
        parser.add_argument(
            '-p',
            '--pipeline',
            metavar='MESHROOM_FILE/photogrammetry/hdri',
            type=str,
            default=os.environ.get("MESHROOM_DEFAULT_PIPELINE",
                                   "photogrammetry"),
            help=
            'Override the default Meshroom pipeline with this external graph.')
        parser.add_argument(
            "--verbose",
            help="Verbosity level",
            default='warning',
            choices=['fatal', 'error', 'warning', 'info', 'debug', 'trace'],
        )

        args = parser.parse_args(args[1:])

        logStringToPython = {
            'fatal': logging.FATAL,
            'error': logging.ERROR,
            'warning': logging.WARNING,
            'info': logging.INFO,
            'debug': logging.DEBUG,
            'trace': logging.DEBUG,
        }
        logging.getLogger().setLevel(logStringToPython[args.verbose])

        QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)

        super(MeshroomApp, self).__init__(QtArgs)

        self.setOrganizationName('AliceVision')
        self.setApplicationName('Meshroom')
        self.setApplicationVersion(meshroom.__version_name__)

        font = self.font()
        font.setPointSize(9)
        self.setFont(font)

        pwd = os.path.dirname(__file__)
        self.setWindowIcon(QIcon(os.path.join(pwd, "img/meshroom.svg")))

        # QML engine setup
        qmlDir = os.path.join(pwd, "qml")
        url = os.path.join(qmlDir, "main.qml")
        self.engine = QmlInstantEngine()
        self.engine.addFilesFromDirectory(qmlDir, recursive=True)
        self.engine.setWatching(
            os.environ.get("MESHROOM_INSTANT_CODING", False))
        # whether to output qml warnings to stderr (disable by default)
        self.engine.setOutputWarningsToStandardError(
            MessageHandler.outputQmlWarnings)
        qInstallMessageHandler(MessageHandler.handler)

        self.engine.addImportPath(qmlDir)
        components.registerTypes()

        # expose available node types that can be instantiated
        self.engine.rootContext().setContextProperty("_nodeTypes",
                                                     sorted(nodesDesc.keys()))

        # instantiate Reconstruction object
        r = Reconstruction(defaultPipeline=args.pipeline, parent=self)
        self.engine.rootContext().setContextProperty("_reconstruction", r)

        # those helpers should be available from QML Utils module as singletons, but:
        #  - qmlRegisterUncreatableType is not yet available in PySide2
        #  - declaring them as singleton in qmldir file causes random crash at exit
        # => expose them as context properties instead
        self.engine.rootContext().setContextProperty(
            "Filepath", FilepathHelper(parent=self))
        self.engine.rootContext().setContextProperty(
            "Scene3DHelper", Scene3DHelper(parent=self))
        self.engine.rootContext().setContextProperty(
            "Clipboard", ClipboardHelper(parent=self))

        # additional context properties
        self.engine.rootContext().setContextProperty(
            "_PaletteManager", PaletteManager(self.engine, parent=self))
        self.engine.rootContext().setContextProperty("MeshroomApp", self)

        # request any potential computation to stop on exit
        self.aboutToQuit.connect(r.stopChildThreads)

        if args.project and not os.path.isfile(args.project):
            raise RuntimeError(
                "Meshroom Command Line Error: 'PROJECT' argument should be a Meshroom project file (.mg).\n"
                "Invalid value: '{}'".format(args.project))

        if args.project:
            r.load(args.project)
        else:
            r.new()

        # import is a python keyword, so we have to access the attribute by a string
        if getattr(args, "import", None):
            r.importImagesFromFolder(getattr(args, "import"), recursive=False)

        if args.importRecursive:
            r.importImagesFromFolder(args.importRecursive, recursive=True)

        if args.save:
            if os.path.isfile(args.save):
                raise RuntimeError(
                    "Meshroom Command Line Error: Cannot save the new Meshroom project as the file (.mg) already exists.\n"
                    "Invalid value: '{}'".format(args.save))
            projectFolder = os.path.dirname(args.save)
            if not os.path.isdir(projectFolder):
                if not os.path.isdir(os.path.dirname(projectFolder)):
                    raise RuntimeError(
                        "Meshroom Command Line Error: Cannot save the new Meshroom project file (.mg) as the parent of the folder does not exists.\n"
                        "Invalid value: '{}'".format(args.save))
                os.mkdir(projectFolder)
            r.saveAs(args.save)

        self.engine.load(os.path.normpath(url))
예제 #6
0
class MeshroomApp(QApplication):
    """ Meshroom UI Application. """
    def __init__(self, args):
        QtArgs = [args[0], '-style', 'fusion'
                  ] + args[1:]  # force Fusion style by default

        parser = argparse.ArgumentParser(prog=args[0],
                                         description='Launch Meshroom UI.',
                                         add_help=True)

        parser.add_argument(
            'project',
            metavar='PROJECT',
            type=str,
            nargs='?',
            help=
            'Meshroom project file (e.g. myProject.mg) or folder with images to reconstruct.'
        )
        parser.add_argument(
            '-i',
            '--import',
            metavar='IMAGES/FOLDERS',
            type=str,
            nargs='*',
            help='Import images or folder with images to reconstruct.')
        parser.add_argument(
            '-I',
            '--importRecursive',
            metavar='FOLDERS',
            type=str,
            nargs='*',
            help=
            'Import images to reconstruct from specified folder and sub-folders.'
        )
        parser.add_argument('-s',
                            '--save',
                            metavar='PROJECT.mg',
                            type=str,
                            default='',
                            help='Save the created scene.')
        parser.add_argument(
            '-p',
            '--pipeline',
            metavar=
            'MESHROOM_FILE/photogrammetry/panoramaHdr/panoramaFisheyeHdr',
            type=str,
            default=os.environ.get("MESHROOM_DEFAULT_PIPELINE",
                                   "photogrammetry"),
            help=
            'Override the default Meshroom pipeline with this external graph.')
        parser.add_argument(
            "--verbose",
            help="Verbosity level",
            default='warning',
            choices=['fatal', 'error', 'warning', 'info', 'debug', 'trace'],
        )

        args = parser.parse_args(args[1:])

        logStringToPython = {
            'fatal': logging.FATAL,
            'error': logging.ERROR,
            'warning': logging.WARNING,
            'info': logging.INFO,
            'debug': logging.DEBUG,
            'trace': logging.DEBUG,
        }
        logging.getLogger().setLevel(logStringToPython[args.verbose])

        QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)

        super(MeshroomApp, self).__init__(QtArgs)

        self.setOrganizationName('AliceVision')
        self.setApplicationName('Meshroom')
        self.setApplicationVersion(meshroom.__version_name__)

        font = self.font()
        font.setPointSize(9)
        self.setFont(font)

        pwd = os.path.dirname(__file__)
        self.setWindowIcon(QIcon(os.path.join(pwd, "img/meshroom.svg")))

        # QML engine setup
        qmlDir = os.path.join(pwd, "qml")
        url = os.path.join(qmlDir, "main.qml")
        self.engine = QmlInstantEngine()
        self.engine.addFilesFromDirectory(qmlDir, recursive=True)
        self.engine.setWatching(
            os.environ.get("MESHROOM_INSTANT_CODING", False))
        # whether to output qml warnings to stderr (disable by default)
        self.engine.setOutputWarningsToStandardError(
            MessageHandler.outputQmlWarnings)
        qInstallMessageHandler(MessageHandler.handler)

        self.engine.addImportPath(qmlDir)
        components.registerTypes()

        # expose available node types that can be instantiated
        self.engine.rootContext().setContextProperty("_nodeTypes",
                                                     sorted(nodesDesc.keys()))

        # instantiate Reconstruction object
        self._undoStack = commands.UndoStack(self)
        self._taskManager = TaskManager(self)
        r = Reconstruction(undoStack=self._undoStack,
                           taskManager=self._taskManager,
                           defaultPipeline=args.pipeline,
                           parent=self)
        self.engine.rootContext().setContextProperty("_reconstruction", r)

        # those helpers should be available from QML Utils module as singletons, but:
        #  - qmlRegisterUncreatableType is not yet available in PySide2
        #  - declaring them as singleton in qmldir file causes random crash at exit
        # => expose them as context properties instead
        self.engine.rootContext().setContextProperty(
            "Filepath", FilepathHelper(parent=self))
        self.engine.rootContext().setContextProperty(
            "Scene3DHelper", Scene3DHelper(parent=self))
        self.engine.rootContext().setContextProperty(
            "Transformations3DHelper", Transformations3DHelper(parent=self))
        self.engine.rootContext().setContextProperty(
            "Clipboard", ClipboardHelper(parent=self))

        # additional context properties
        self.engine.rootContext().setContextProperty(
            "_PaletteManager", PaletteManager(self.engine, parent=self))
        self.engine.rootContext().setContextProperty("MeshroomApp", self)

        # request any potential computation to stop on exit
        self.aboutToQuit.connect(r.stopChildThreads)

        if args.project and not os.path.isfile(args.project):
            raise RuntimeError(
                "Meshroom Command Line Error: 'PROJECT' argument should be a Meshroom project file (.mg).\n"
                "Invalid value: '{}'".format(args.project))

        if args.project:
            r.load(args.project)
            self.addRecentProjectFile(args.project)
        else:
            r.new()

        # import is a python keyword, so we have to access the attribute by a string
        if getattr(args, "import", None):
            r.importImagesFromFolder(getattr(args, "import"), recursive=False)

        if args.importRecursive:
            r.importImagesFromFolder(args.importRecursive, recursive=True)

        if args.save:
            if os.path.isfile(args.save):
                raise RuntimeError(
                    "Meshroom Command Line Error: Cannot save the new Meshroom project as the file (.mg) already exists.\n"
                    "Invalid value: '{}'".format(args.save))
            projectFolder = os.path.dirname(args.save)
            if not os.path.isdir(projectFolder):
                if not os.path.isdir(os.path.dirname(projectFolder)):
                    raise RuntimeError(
                        "Meshroom Command Line Error: Cannot save the new Meshroom project file (.mg) as the parent of the folder does not exists.\n"
                        "Invalid value: '{}'".format(args.save))
                os.mkdir(projectFolder)
            r.saveAs(args.save)
            self.addRecentProjectFile(args.save)

        self.engine.load(os.path.normpath(url))

    def _recentProjectFiles(self):
        projects = []
        settings = QSettings()
        settings.beginGroup("RecentFiles")
        size = settings.beginReadArray("Projects")
        for i in range(size):
            settings.setArrayIndex(i)
            p = settings.value("filepath")
            if p:
                projects.append(p)
        settings.endArray()
        return projects

    @Slot(str)
    @Slot(QUrl)
    def addRecentProjectFile(self, projectFile):
        if not isinstance(projectFile, (QUrl, pyCompatibility.basestring)):
            raise TypeError("Unexpected data type: {}".format(
                projectFile.__class__))
        if isinstance(projectFile, QUrl):
            projectFileNorm = projectFile.toLocalFile()
            if not projectFileNorm:
                projectFileNorm = projectFile.toString()
        else:
            projectFileNorm = QUrl(projectFile).toLocalFile()
            if not projectFileNorm:
                projectFileNorm = QUrl.fromLocalFile(projectFile).toLocalFile()

        projects = self._recentProjectFiles()

        # remove duplicates while preserving order
        from collections import OrderedDict
        uniqueProjects = OrderedDict.fromkeys(projects)
        projects = list(uniqueProjects)
        # remove previous usage of the value
        if projectFileNorm in uniqueProjects:
            projects.remove(projectFileNorm)
        # add the new value in the first place
        projects.insert(0, projectFileNorm)

        # keep only the 10 first elements
        projects = projects[0:20]

        settings = QSettings()
        settings.beginGroup("RecentFiles")
        size = settings.beginWriteArray("Projects")
        for i, p in enumerate(projects):
            settings.setArrayIndex(i)
            settings.setValue("filepath", p)
        settings.endArray()
        settings.sync()

        self.recentProjectFilesChanged.emit()

    @Slot(str)
    @Slot(QUrl)
    def removeRecentProjectFile(self, projectFile):
        if not isinstance(projectFile, (QUrl, pyCompatibility.basestring)):
            raise TypeError("Unexpected data type: {}".format(
                projectFile.__class__))
        if isinstance(projectFile, QUrl):
            projectFileNorm = projectFile.toLocalFile()
            if not projectFileNorm:
                projectFileNorm = projectFile.toString()
        else:
            projectFileNorm = QUrl(projectFile).toLocalFile()
            if not projectFileNorm:
                projectFileNorm = QUrl.fromLocalFile(projectFile).toLocalFile()

        projects = self._recentProjectFiles()

        # remove duplicates while preserving order
        from collections import OrderedDict
        uniqueProjects = OrderedDict.fromkeys(projects)
        projects = list(uniqueProjects)
        # remove previous usage of the value
        if projectFileNorm not in uniqueProjects:
            return

        projects.remove(projectFileNorm)

        settings = QSettings()
        settings.beginGroup("RecentFiles")
        size = settings.beginWriteArray("Projects")
        for i, p in enumerate(projects):
            settings.setArrayIndex(i)
            settings.setValue("filepath", p)
        settings.endArray()
        settings.sync()

        self.recentProjectFilesChanged.emit()

    @Slot(str, result=str)
    def markdownToHtml(self, md):
        """
        Convert markdown to HTML.

        Args:
            md (str): the markdown text to convert

        Returns:
            str: the resulting HTML string
        """
        try:
            from markdown import markdown
        except ImportError:
            logging.warning(
                "Can't import markdown module, returning source markdown text."
            )
            return md
        return markdown(md)

    @Property(QJsonValue, constant=True)
    def systemInfo(self):
        import platform
        import sys
        return {
            'platform': '{} {}'.format(platform.system(), platform.release()),
            'python': 'Python {}'.format(sys.version.split(" ")[0])
        }

    @Property("QVariantList", constant=True)
    def licensesModel(self):
        """
        Get info about open-source licenses for the application.
        Model provides:
            title: the name of the project
            localUrl: the local path to COPYING.md
            onlineUrl: the remote path to COPYING.md
        """
        rootDir = os.environ.get("MESHROOM_INSTALL_DIR", os.getcwd())
        return [{
            "title":
            "Meshroom",
            "localUrl":
            os.path.join(rootDir, "COPYING.md"),
            "onlineUrl":
            "https://raw.githubusercontent.com/alicevision/meshroom/develop/COPYING.md"
        }, {
            "title":
            "AliceVision",
            "localUrl":
            os.path.join(rootDir, "aliceVision", "share", "aliceVision",
                         "COPYING.md"),
            "onlineUrl":
            "https://raw.githubusercontent.com/alicevision/AliceVision/develop/COPYING.md"
        }]

    recentProjectFilesChanged = Signal()
    recentProjectFiles = Property("QVariantList",
                                  _recentProjectFiles,
                                  notify=recentProjectFilesChanged)