Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 66 additions & 40 deletions meshroom/ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from meshroom.core.desc import NodeVersionTypeEnum
from meshroom.core.submitter import BaseSubmitter
from meshroom.core.taskManager import TaskManager
from meshroom.common import Property, Variant, Signal, Slot
from meshroom.common import Property, Variant, Signal, Slot, strtobool

from meshroom.env import EnvVar, EnvVarHelpAction

Expand All @@ -36,6 +36,9 @@
from meshroom.ui import commands


QML_DIR = os.path.join(os.path.dirname(__file__), "qml")


class FileStatus(Enum):
MISSING=0
EXISTS=1
Expand Down Expand Up @@ -233,7 +236,8 @@ def __init__(self, inputArgs):
self.debugger = QQmlDebuggingEnabler(printWarning=True)
qtArgs = [f"-qmljsdebugger={debuggerParams}"]

logging.getLogger().setLevel(meshroom.logStringToPython[args.verbose])
verbosity = meshroom.logStringToPython[args.verbose]
logging.getLogger().setLevel(verbosity)

super().__init__(inputArgs[:1] + qtArgs)

Expand Down Expand Up @@ -271,53 +275,33 @@ def __init__(self, inputArgs):
components.registerTypes()

# 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)
if QtCore.__version_info__ < (5, 14, 2):
# After 5.14.1, it gets stuck during logging
qInstallMessageHandler(MessageHandler.handler)
url = os.path.normpath(os.path.join(QML_DIR, "main.qml"))

self.engine.addImportPath(qmlDir)
# Add PySide6's bundled QML modules path (Qt3D, QtQuick.Scene3D, etc.)
pyside6QmlPath = os.path.join(os.path.dirname(QtCore.__file__), "Qt", "qml")
if os.path.isdir(pyside6QmlPath):
self.engine.addImportPath(pyside6QmlPath)

# expose available node types that can be instantiated
self.engine.rootContext().setContextProperty("_nodeTypes", {n: {"category": pluginManager.getRegisteredNodePlugins()[n].nodeDescriptor.category} for n in sorted(pluginManager.getRegisteredNodePlugins().keys())})
watching = bool(strtobool(os.environ.get("MESHROOM_INSTANT_CODING", "0")))
self.engine = QmlInstantEngine(url, self.setupEngine, watching=watching)
if verbosity < logging.INFO:
self.engine.setVerbose(True)
self.engine.addFilesFromDirectory(QML_DIR, recursive=True)

# instantiate the 3D Scene object
self._undoStack = commands.UndoStack(self)
self._defaultSubmitterName = os.environ.get('MESHROOM_DEFAULT_SUBMITTER', '')
self._taskManager = TaskManager(self)
self._activeProject = Scene(undoStack=self._undoStack, taskManager=self._taskManager, defaultPipeline=args.pipeline, parent=self)
self._activeProject.setSubmitLabel(args.submitLabel)
self.engine.rootContext().setContextProperty("_currentScene", self._activeProject)

# 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))
self.engine.rootContext().setContextProperty("ThumbnailCache", ThumbnailCache(parent=self))
self.engine.rootContext().setContextProperty("ShapeFilesHelper", ShapeFilesHelper(self.activeProject, parent=self))
self.engine.rootContext().setContextProperty("ShapeViewerHelper", ShapeViewerHelper(parent=self))

# additional context properties
# helper objects
self._filepathHelper = FilepathHelper(parent=self)
self._scene3DHelper = Scene3DHelper(parent=self)
self._transformations3DHelper = Transformations3DHelper(parent=self)
self._clipboardHelper = ClipboardHelper(parent=self)
self._thumbnailCache = ThumbnailCache(parent=self)
self._shapeFilesHelper = ShapeFilesHelper(self.activeProject, parent=self)
self._shapeViewerHelper = ShapeViewerHelper(parent=self)
self._messageController = MessageController(parent=self)
self.engine.rootContext().setContextProperty("_messageController", self._messageController)
self.engine.rootContext().setContextProperty("_PaletteManager", PaletteManager(self.engine, parent=self))
self.engine.rootContext().setContextProperty("ScriptEditorManager", ScriptEditorManager(parent=self))
self.engine.rootContext().setContextProperty("MeshroomApp", self)
self.engine.rootContext().setContextProperty("NodeVersionType", NodeVersionTypeEnum(parent=self))
self._paletteManager = PaletteManager(self.engine, parent=self)
self._scriptEditorManager = ScriptEditorManager(parent=self)
self._nodeVersionType = NodeVersionTypeEnum(parent=self)

# request any potential computation to stop on exit
self.aboutToQuit.connect(self._activeProject.stopChildThreads)
Expand Down Expand Up @@ -374,7 +358,49 @@ def __init__(self, inputArgs):
self._activeProject.saveAs(args.save)
self.addRecentProjectFile(args.save)

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

def setupEngine(self, engine):
"""Setup context properties and QML paths on the engine setup.
Runs on every build of the engine (first load + hot reloads).
"""

# whether to output qml warnings to stderr (disable by default)
engine.setOutputWarningsToStandardError(MessageHandler.outputQmlWarnings)
if QtCore.__version_info__ < (5, 14, 2):
# After 5.14.1, it gets stuck during logging
qInstallMessageHandler(MessageHandler.handler)

engine.addImportPath(QML_DIR)
# Add PySide6's bundled QML modules path (Qt3D, QtQuick.Scene3D, etc.)
pyside6QmlPath = os.path.join(os.path.dirname(QtCore.__file__), "Qt", "qml")
if os.path.isdir(pyside6QmlPath):
engine.addImportPath(pyside6QmlPath)

# expose available node types that can be instantiated
engine.rootContext().setContextProperty("_nodeTypes", {n: {"category": pluginManager.getRegisteredNodePlugins()[n].nodeDescriptor.category} for n in sorted(pluginManager.getRegisteredNodePlugins().keys())})
# expose the Scene object to access the graph
engine.rootContext().setContextProperty("_currentScene", self._activeProject)

# those helpers should be available from QML Utils module as singletons, but:
# - qmlRegisterUncreatableType is not yet available in PySide2
# - declaring them as singleton in QML_DIR file causes random crash at exit
# => expose them as context properties instead
engine.rootContext().setContextProperty("Filepath", self._filepathHelper)
engine.rootContext().setContextProperty("Scene3DHelper", self._scene3DHelper)
engine.rootContext().setContextProperty("Transformations3DHelper", self._transformations3DHelper)
engine.rootContext().setContextProperty("Clipboard", self._clipboardHelper)
engine.rootContext().setContextProperty("ThumbnailCache", self._thumbnailCache)
engine.rootContext().setContextProperty("ShapeFilesHelper", self._shapeFilesHelper)
engine.rootContext().setContextProperty("ShapeViewerHelper", self._shapeViewerHelper)


# additional context properties
engine.rootContext().setContextProperty("_messageController", self._messageController)
engine.rootContext().setContextProperty("_PaletteManager", self._paletteManager)
engine.rootContext().setContextProperty("ScriptEditorManager", self._scriptEditorManager)
engine.rootContext().setContextProperty("NodeVersionType", self._nodeVersionType)
engine.rootContext().setContextProperty("MeshroomApp", self)

def terminateManual(self):
self.engine.clearComponentCache()
Expand Down
6 changes: 3 additions & 3 deletions meshroom/ui/palette.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from PySide6.QtCore import QObject, Qt, Slot, Property, Signal
from PySide6.QtCore import QObject, Qt, Slot, Property, Signal, QTimer
from PySide6.QtGui import QPalette, QColor
from PySide6.QtWidgets import QApplication

Expand Down Expand Up @@ -53,9 +53,9 @@ def togglePalette(self):
app.setPalette(self.defaultPalette)
else:
app.setPalette(self.darkPalette)
if self.qmlEngine.rootObjects():
self.qmlEngine.reload()
self.paletteChanged.emit()
if self.qmlEngine.rootObjects():
QTimer.singleShot(0, self.qmlEngine.reload)

paletteChanged = Signal()
palette = Property(QPalette, lambda self: QApplication.instance().palette(), notify=paletteChanged)
Expand Down
129 changes: 87 additions & 42 deletions meshroom/ui/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,49 +9,46 @@
import shiboken6


class QmlInstantEngine(QQmlApplicationEngine):
class QmlInstantEngine(QObject):
"""
QmlInstantEngine is a utility class helping to develop QML applications.
It reloads itself whenever one of the watched source files is modified.
As it consumes resources, make sure to disable file watching in production mode.
"""

def __init__(self, sourceFile="", watching=True, verbose=False, parent=None):
def __init__(self, sourceFile, setupEngine, watching=True, verbose=False, parent=None):
"""
watching -- Defines whether the watcher is active (default: True)
verbose -- if True, output log information (default: False)
sourceFile -- Main QML file.
setupEngine -- Callback to call on reload after creating the engine (for hot-reload).
watching -- Defines whether the watcher is active (default: True)
verbose -- if True, output log information (default: False)
"""
super().__init__(parent)

self._fileWatcher = QFileSystemWatcher() # Internal Qt File Watcher
self._sourceFile = ""
self._sourceFile = str(sourceFile) or ""
self._watchedFiles = [] # Internal watched files list
self._verbose = verbose # Verbose bool
self._watching = False #
self._extensions = ["qml", "js"] # File extensions that defines files to watch when adding a folder
self._verbose = verbose # Verbose bool
self._watching = False
self._extensions = ["qml", "js"]

# Callback to call for the engine setup (set the context properties, etc)
self._setupEngine = setupEngine
self._engine = QQmlApplicationEngine()
self._rootItem = None

def onObjectCreated(root, url):
if not root:
return
# Restore root item geometry
if self._rootItem:
root.setGeometry(self._rootItem.geometry())
self._rootItem.deleteLater()
self._rootItem = root

self.objectCreated.connect(onObjectCreated)
# Add a single shot timer to launch the reload after all events are processed
self._debounceTimer = QTimer(singleShot=True, interval=100)
self._debounceTimer.timeout.connect(self.reload)

# Update the watching status
self.setWatching(watching)

if sourceFile:
self.load(sourceFile)

def load(self, sourceFile):
self._sourceFile = sourceFile
super().load(sourceFile)
def __getattr__(self, name):
engine = self.__dict__.get("_engine")
if engine is not None and hasattr(engine, name):
return getattr(engine, name)
raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")

def setWatching(self, watchValue):
"""
Expand Down Expand Up @@ -145,8 +142,8 @@ def addFilesFromDirectory(self, dirname, recursive=False):
self.addFile(os.path.join(dirpath, filename))
else:
filenames = os.listdir(dirname)
filenames = [os.path.join(dirname, filename) for filename in filenames if
os.path.splitext(filename)[1][1:] in self._extensions]
filenames = [os.path.join(dirname, f) for f in filenames if
os.path.splitext(f)[1][1:] in self._extensions]
self.addFiles(filenames)

def removeFile(self, filename):
Expand All @@ -173,28 +170,76 @@ def onFileChanged(self, filepath):

if self._verbose:
print("Source file changed : ", filepath)
# Clear the QQuickEngine cache
self.clearComponentCache()
# Remove the modified file from the watched list
self.removeFile(filepath)
cptTry = 0
# Re-add file before debounce
if os.path.isfile(filepath):
self._fileWatcher.addPath(filepath)
self._debounceTimer.start()

# Make sure file is available before doing anything
# NOTE: useful to handle editors (Qt Creator) that deletes the source file and
# creates a new one when saving
while not os.path.exists(filepath) and cptTry < 10:
time.sleep(0.1)
cptTry += 1
def rootContext(self):
""" Context of the currently-live engine (None before the first reload). """
return self._engine.rootContext() if self._engine else None

self.reload()
@property
def engine(self):
return self._engine

# Finally, re-add the modified file to the watch system
# after a short cooldown to avoid multiple consecutive reloads
QTimer.singleShot(200, lambda: self.addFile(filepath))
@property
def rootItem(self):
return self._rootItem

def reload(self):
print(f"Reloading {self._sourceFile}")
self.load(self._sourceFile)

# Preserve window geometry across the swap.
oldPos, oldSize = None, None
if self._rootItem is not None and shiboken6.isValid(self._rootItem):
try:
oldPos = self._rootItem.position()
oldSize = self._rootItem.size()
except AttributeError:
Comment thread
Alxiice marked this conversation as resolved.
Dismissed
pass

# Destroy old root item and engine before building the new one.
if self._rootItem is not None and shiboken6.isValid(self._rootItem):
shiboken6.delete(self._rootItem)
self._rootItem = None

if self._engine is not None and shiboken6.isValid(self._engine):
shiboken6.delete(self._engine)
self._engine = None

# Build the new engine and load.
engine = QQmlApplicationEngine()
self._setupEngine(engine)

def onObjectCreated(root, url):
if root is None:
print(f"Failed to load {url.toString()} - check QML warnings above.")
return
self._rootItem = root
if oldPos is not None:
root.setPosition(oldPos)
if oldSize is not None:
root.resize(oldSize)

engine.objectCreated.connect(onObjectCreated)
engine.load(QUrl.fromLocalFile(self._sourceFile))
engine.objectCreated.disconnect(onObjectCreated)

self._engine = engine
Comment thread
Alxiice marked this conversation as resolved.

def clearComponentCache(self):
if self._engine:
self._engine.clearComponentCache()

def collectGarbage(self):
if self._engine:
self._engine.collectGarbage()

def deleteLater(self):
if self._engine:
self._engine.deleteLater()
super().deleteLater()


def makeProperty(T, attributeName, notify=None, resetOnDestroy=False):
Expand Down
Loading