From 625eaeacc7471abde6c30b79f9852b354a0dbca5 Mon Sep 17 00:00:00 2001 From: David Fries Date: Sat, 31 Aug 2024 09:02:11 -0500 Subject: [PATCH 1/3] Scene.py add _hideReloadMessage, connect to startReloadAll When all files are being reloaded, the file modification reload dialog is redundant. This allows pressing F5 to reload and dismiss the dialog. --- UM/Scene/Scene.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/UM/Scene/Scene.py b/UM/Scene/Scene.py index ca68ab144..3f6181927 100644 --- a/UM/Scene/Scene.py +++ b/UM/Scene/Scene.py @@ -52,6 +52,12 @@ def __init__(self) -> None: self._metadata: Dict[str, Any] = {} + # If available connect reloadAll to hide the file change reload message. + import UM.Application + app = UM.Application.Application.getInstance() + if getattr(app, "startReloadAll", None): + app.startReloadAll.connect(self._hideReloadMessage) + def setMetaDataEntry(self, key: str, entry: Any) -> None: self._metadata[key] = entry @@ -189,8 +195,7 @@ def _onFileChanged(self, file_path: str) -> None: if modified_nodes: # Hide the message if it was already visible # Todo: keep one message for each modified file, when multiple had been updated at same time - if self._reload_message is not None: - self._reload_message.hide() + self._hideReloadMessage() self._reload_message = Message(i18n_catalog.i18nc("@info", "Would you like to reload {filename}?").format( filename = os.path.basename(file_path)), @@ -212,8 +217,7 @@ def _reloadNodes(self, nodes: List["SceneNode"], file_path: str, message: str, a if action != "reload": return - if self._reload_message is not None: - self._reload_message.hide() + self._hideReloadMessage() if not file_path or not os.path.isfile(file_path): # File doesn't exist anymore. return @@ -265,3 +269,10 @@ def _reloadJobFinished(self, replaced_nodes: [SceneNode], job: ReadMeshJob) -> N # Current node is a new one in the file, or it's name has changed # TODO: Load this mesh into the scene. Also alter the "ReloadAll" action in CuraApplication. Logger.log("w", "Could not find matching node for object '{0}' in the scene.", node_name) + + def _hideReloadMessage(self) -> None: + """Hide file modification reload dialog if showing""" + if self._reload_message is not None: + self._reload_message.hide() + if self._reload_callback is not None: + self._reload_callback = None From d47d35b30f347b8906c86d8ad10610ec8ef42c10 Mon Sep 17 00:00:00 2001 From: David Fries Date: Mon, 2 Sep 2024 21:21:00 -0500 Subject: [PATCH 2/3] Document missing SceneNode.py signal parameters They all take the same parameter and it his the SceneNode object to be more specific. --- UM/Scene/SceneNode.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/UM/Scene/SceneNode.py b/UM/Scene/SceneNode.py index 0ec1af8a7..6d817de4d 100644 --- a/UM/Scene/SceneNode.py +++ b/UM/Scene/SceneNode.py @@ -429,7 +429,10 @@ def setMeshData(self, mesh_data: Optional[MeshData]) -> None: self.meshDataChanged.emit(self) meshDataChanged = Signal() - """Emitted whenever the attached mesh data object changes.""" + """Emitted whenever the attached mesh data object changes. + + :param object: The SceneNode that had the changed mesh data object. + """ def _onMeshDataChanged(self) -> None: self.meshDataChanged.emit(self) @@ -517,7 +520,7 @@ def getAllChildren(self) -> List["SceneNode"]: childrenChanged = Signal() """Emitted whenever the list of children of this object or any child object changes. - :param object: The object that triggered the change. + :param object: The SceneNode that triggered the change. """ def _updateCachedNormalMatrix(self) -> None: @@ -716,7 +719,8 @@ def setPosition(self, position: Vector, transform_space: int = TransformSpace.Lo transformationChanged = Signal() """Signal. Emitted whenever the transformation of this object or any child object changes. - :param object: The object that caused the change. + + :param object: The SceneNode that triggered the change. """ def lookAt(self, target: Vector, up: Vector = Vector.Unit_Y) -> None: @@ -885,4 +889,4 @@ def __str__(self) -> str: """String output for debugging.""" name = self._name if self._name != "" else hex(id(self)) - return "<" + self.__class__.__qualname__ + " object: '" + name + "'>" \ No newline at end of file + return "<" + self.__class__.__qualname__ + " object: '" + name + "'>" From b9e12e20889636c3a26405c744251c49f652f999 Mon Sep 17 00:00:00 2001 From: David Fries Date: Mon, 2 Sep 2024 10:52:57 -0500 Subject: [PATCH 3/3] Fix removing watching for file modifications If the file is no longer part of the scene graph no longer watch it for file modifications. This had been triggered from MeshData when it was deleted, but _application was no longer populated so it was not being called. _application could be replaced by querying the current instance. from UM.Application import Application Application.getInstance().getController().getScene().removeWatchedFile(self._file_name) The problem with calling removeWatchedFile from MeshData __del__, is Cura 5.8.1 is creating three MeshData objects with the file name populated, when the file is loaded, two are deleted, the last stays around. If it were to call from the first two objects being deleted, it would remove the watch even though it is loaded as part of the scene. Further I'm seeing that the last object isn't getting deleted, even when it is removed from the scene graph. I don't know if this is a memory leak or if there would still be an issue with undo/redo. Instead listen for the scene graph meshDataChanged signal, collect the files in the scene graph and add or remove based on that list. This does not listen to sceneChanged signal as that includes transform changes, which won't change the set of files listened for. --- UM/Mesh/MeshData.py | 11 ---------- UM/Mesh/MeshReader.py | 2 -- UM/Scene/Scene.py | 47 +++++++++++++++++++++++++++++-------------- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/UM/Mesh/MeshData.py b/UM/Mesh/MeshData.py index 59f566dc9..0a00dec21 100644 --- a/UM/Mesh/MeshData.py +++ b/UM/Mesh/MeshData.py @@ -49,7 +49,6 @@ class MeshData: def __init__(self, vertices=None, normals=None, indices=None, colors=None, uvs=None, file_name=None, center_position=None, zero_position=None, type = MeshType.faces, attributes=None) -> None: - self._application = None # Initialize this later otherwise unit tests break self._vertices = NumPyUtil.immutableNDArray(vertices) self._normals = NumPyUtil.immutableNDArray(normals) @@ -82,16 +81,6 @@ def __init__(self, vertices=None, normals=None, indices=None, colors=None, uvs=N new_value[attribute_key] = attribute_value self._attributes[key] = new_value - def __del__(self): - """Triggered when this file is deleted. - - The file will then no longer be watched for changes. - """ - - if self._file_name: - if self._application: - self._application.getController().getScene().removeWatchedFile(self._file_name) - def set(self, vertices=Reuse, normals=Reuse, indices=Reuse, colors=Reuse, uvs=Reuse, file_name=Reuse, center_position=Reuse, zero_position=Reuse, attributes=Reuse) -> "MeshData": """Create a new MeshData with specified changes diff --git a/UM/Mesh/MeshReader.py b/UM/Mesh/MeshReader.py index dcb302992..10b892461 100644 --- a/UM/Mesh/MeshReader.py +++ b/UM/Mesh/MeshReader.py @@ -3,7 +3,6 @@ from typing import Union, List -import UM.Application from UM.FileHandler.FileReader import FileReader from UM.FileHandler.FileHandler import resolveAnySymlink from UM.Logger import Logger @@ -24,7 +23,6 @@ def read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]: file_name = resolveAnySymlink(file_name) result = self._read(file_name) - UM.Application.Application.getInstance().getController().getScene().addWatchedFile(file_name) # The mesh reader may set a MIME type itself if it knows a more specific MIME type than just going by extension. # If not, automatically generate one from our MIME type database, going by the file extension. diff --git a/UM/Scene/Scene.py b/UM/Scene/Scene.py index 3f6181927..ddbe9c6d8 100644 --- a/UM/Scene/Scene.py +++ b/UM/Scene/Scene.py @@ -46,6 +46,7 @@ def __init__(self) -> None: self._file_watcher.fileChanged.connect(self._onFileChanged) self._reload_message: Optional[Message] = None + self._reload_callback: Optional[functools.partial] = None # Need to keep these in memory. This is a memory leak every time you refresh, but a tiny one. self._callbacks: Set[Callable] = set() @@ -71,11 +72,13 @@ def _connectSignalsRoot(self) -> None: self._root.transformationChanged.connect(self.sceneChanged) self._root.childrenChanged.connect(self.sceneChanged) self._root.meshDataChanged.connect(self.sceneChanged) + self._root.meshDataChanged.connect(self._updateWatchedFiles) def _disconnectSignalsRoot(self) -> None: self._root.transformationChanged.disconnect(self.sceneChanged) self._root.childrenChanged.disconnect(self.sceneChanged) self._root.meshDataChanged.disconnect(self.sceneChanged) + self._root.meshDataChanged.disconnect(self._updateWatchedFiles) def setIgnoreSceneChanges(self, ignore_scene_changes: bool) -> None: if self._ignore_scene_changes != ignore_scene_changes: @@ -155,25 +158,39 @@ def findCamera(self, name: str) -> Optional[Camera]: return node return None - def addWatchedFile(self, file_path: str) -> None: - """Add a file to be watched for changes. + def _updateWatchedFiles(self, node_changed: SceneNode) -> None: + """Update the list of watched files based on the currently loaded nodes. + This handles both add and remove for _file_watcher.""" - :param file_path: The path to the file that must be watched. - """ - - # File watcher causes cura to crash on windows if threaded from removable device (usb, ...). - # Create QEventLoop earlier to fix this. - if Platform.isWindows(): - QEventLoop() - self._file_watcher.addPath(file_path) + watching = set(self._file_watcher.files()) + active = set() - def removeWatchedFile(self, file_path: str) -> None: - """Remove a file so that it will no longer be watched for changes. + # Get the list of files that are currently loaded in the scene graph. + from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator + for node in DepthFirstIterator(self.getRoot()): + md = node.getMeshData() + if md and md.getFileName() is not None: + active.add(md.getFileName()) - :param file_path: The path to the file that must no longer be watched. - """ + if watching == active: + return - self._file_watcher.removePath(file_path) + stop_watching = watching - active + start_watching = active - watching + for file_path in stop_watching: + self._file_watcher.removePath(file_path) + if start_watching: + # File watcher causes cura to crash on windows if threaded from removable device (usb, ...). + # Create QEventLoop earlier to fix this. + if Platform.isWindows(): + QEventLoop() + + for file_path in start_watching: + self._file_watcher.addPath(file_path) + + # Remove modified reload prompt if the file is no longer watched. + if self._reload_callback is not None and self._reload_callback.args[1] in stop_watching: + self._hideReloadMessage() def _onFileChanged(self, file_path: str) -> None: """Triggered whenever a file is changed that we currently have loaded."""