From e69a4369423aa57cd00d81a4b459fb6f4888c30b Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 6 Aug 2025 16:15:05 +0200 Subject: [PATCH] Fix sometimes wrongly displayed view CURA-12660 This required a refactoring of the management of the active view. The previous behavior was that anyone could set the active view, depending on certain conditions. But now we also have a view that is set by a tool, so sometimes the actually set view would be incorrect. Now each Stage requests an active view, and each tool CAN also request an active view. Then the Controller decides which view should actually be active depending on the active stage and tool. --- cura/CuraApplication.py | 5 +---- cura/Stages/CuraStage.py | 6 ++++-- plugins/PaintTool/PaintTool.py | 22 ++++++++++------------ plugins/PaintTool/PaintView.py | 22 ++++------------------ plugins/PreviewStage/PreviewStage.py | 19 ------------------- plugins/SimulationView/SimulationView.py | 16 +++++++++++++--- resources/qml/ViewsSelector.qml | 4 ++-- 7 files changed, 34 insertions(+), 60 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 5ce358080c..57d4773cb3 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1038,7 +1038,6 @@ class CuraApplication(QtApplication): # Initialize UI state controller.setActiveStage("PrepareStage") - controller.setActiveView("SolidView") controller.setCameraTool("CameraTool") controller.setSelectionTool("SelectionTool") @@ -2089,9 +2088,7 @@ class CuraApplication(QtApplication): is_non_sliceable = "." + file_extension in self._non_sliceable_extensions if is_non_sliceable: - # Need to switch first to the preview stage and then to layer view - self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"), - self.getController().setActiveView("SimulationView"))) + self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"))) block_slicing_decorator = BlockSlicingDecorator() node.addDecorator(block_slicing_decorator) diff --git a/cura/Stages/CuraStage.py b/cura/Stages/CuraStage.py index 869ed309dc..8c207db8ad 100644 --- a/cura/Stages/CuraStage.py +++ b/cura/Stages/CuraStage.py @@ -1,6 +1,8 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import Optional + from PyQt6.QtCore import pyqtProperty, QUrl from UM.Stage import Stage @@ -13,8 +15,8 @@ from UM.Stage import Stage # * The MainComponent is the component that will be drawn starting from the bottom of the stageBar and fills the rest # of the screen. class CuraStage(Stage): - def __init__(self, parent = None) -> None: - super().__init__(parent) + def __init__(self, parent = None, active_view: Optional[str] = "SolidView") -> None: + super().__init__(parent, active_view = active_view) @pyqtProperty(str, constant = True) def stageId(self) -> str: diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index eaeb2dc69b..7a1b25f331 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -58,7 +58,8 @@ class PaintTool(Tool): self.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape") - Selection.selectionChanged.connect(self._updateIgnoreUnselectedObjects) + Selection.selectionChanged.connect(self._updateActiveView) + self._controller.activeViewChanged.connect(self._updateIgnoreUnselectedObjects) def _createBrushPen(self) -> QPen: pen = QPen() @@ -298,14 +299,9 @@ class PaintTool(Tool): # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes if event.type == Event.ToolActivateEvent: - controller.setActiveView("PaintTool") # Because that's the plugin-name, and the view is registered to it. - self._updateIgnoreUnselectedObjects() return True if event.type == Event.ToolDeactivateEvent: - controller.setActiveView("SolidView") - CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(False) - CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(False) return True if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled(): @@ -397,6 +393,9 @@ class PaintTool(Tool): return False + def getRequiredExtraRenderingPasses(self) -> list[str]: + return ["selection_faces", "picking_selected"] + @staticmethod def _updateScene(node: SceneNode = None): if node is None: @@ -404,11 +403,10 @@ class PaintTool(Tool): if node is not None: Application.getInstance().getController().getScene().sceneChanged.emit(node) - def getRequiredExtraRenderingPasses(self) -> list[str]: - return ["selection_faces", "picking_selected"] + def _updateActiveView(self): + self.setActiveView("PaintTool" if len(Selection.getAllSelectedObjects()) == 1 else None) def _updateIgnoreUnselectedObjects(self): - if self._controller.getActiveTool() is self: - ignore_unselected_objects = len(Selection.getAllSelectedObjects()) == 1 - CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(ignore_unselected_objects) - CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(ignore_unselected_objects) \ No newline at end of file + ignore_unselected_objects = self._controller.getActiveView().name == "PaintTool" + CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(ignore_unselected_objects) + CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(ignore_unselected_objects) \ No newline at end of file diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 73e8a511a6..22629e340c 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -9,8 +9,8 @@ from PyQt6.QtGui import QImage, QColor, QPainter from cura.CuraApplication import CuraApplication from cura.BuildVolume import BuildVolume +from cura.CuraView import CuraView from UM.PluginRegistry import PluginRegistry -from UM.View.View import View from UM.View.GL.ShaderProgram import ShaderProgram from UM.View.GL.Texture import Texture from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator @@ -22,7 +22,7 @@ from UM.Math.Color import Color catalog = i18nCatalog("cura") -class PaintView(View): +class PaintView(CuraView): """View for model-painting.""" UNDO_STACK_SIZE = 1024 @@ -33,7 +33,7 @@ class PaintView(View): self.value: int = value def __init__(self) -> None: - super().__init__() + super().__init__(use_empty_menu_placeholder = True) self._paint_shader: Optional[ShaderProgram] = None self._current_paint_texture: Optional[Texture] = None self._current_bits_ranges: tuple[int, int] = (0, 0) @@ -50,8 +50,6 @@ class PaintView(View): application.engineCreatedSignal.connect(self._makePaintModes) self._scene = application.getController().getScene() - self._solid_view = None - def _makePaintModes(self): theme = CuraApplication.getInstance().getTheme() usual_types = {"none": self.PaintType(Color(*theme.getColor("paint_normal_area").getRgb()), 0), @@ -179,18 +177,6 @@ class PaintView(View): if self._current_paint_type not in self._paint_modes: return - if self._solid_view is None: - plugin_registry = PluginRegistry.getInstance() - solid_view = plugin_registry.getPluginObject("SolidView") - if isinstance(solid_view, View): - self._solid_view = solid_view - - display_objects = Selection.getAllSelectedObjects().copy() - if len(display_objects) != 1 and self._solid_view is not None: - # Display the classic view until a single object is selected - self._solid_view.beginRendering() - return - self._checkSetup() renderer = self.getRenderer() @@ -201,7 +187,7 @@ class PaintView(View): paint_batch = renderer.createRenderBatch(shader=self._paint_shader) renderer.addRenderBatch(paint_batch) - for node in display_objects: + for node in Selection.getAllSelectedObjects(): paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix()) self._current_paint_texture = node.callDecoration("getPaintTexture") self._paint_shader.setTexture(0, self._current_paint_texture) diff --git a/plugins/PreviewStage/PreviewStage.py b/plugins/PreviewStage/PreviewStage.py index 88f432ef9b..3f1a4423b2 100644 --- a/plugins/PreviewStage/PreviewStage.py +++ b/plugins/PreviewStage/PreviewStage.py @@ -24,25 +24,6 @@ class PreviewStage(CuraStage): super().__init__(parent) self._application = application self._application.engineCreatedSignal.connect(self._engineCreated) - self._previously_active_view = None # type: Optional[View] - - def onStageSelected(self) -> None: - """When selecting the stage, remember which was the previous view so that - - we can revert to that view when we go out of the stage later. - """ - self._previously_active_view = self._application.getController().getActiveView() - - def onStageDeselected(self) -> None: - """Called when going to a different stage (away from the Preview Stage). - - When going to a different stage, the view should be reverted to what it - was before. Normally, that just reverts it to solid view. - """ - - if self._previously_active_view is not None: - self._application.getController().setActiveView(self._previously_active_view.getPluginId()) - self._previously_active_view = None def _engineCreated(self) -> None: """Delayed load of the QML files. diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 083fc73bf1..5d339e7f74 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -172,13 +172,20 @@ class SimulationView(CuraView): self._updateSliceWarningVisibility() self.activityChanged.emit() - def getSimulationPass(self) -> SimulationPass: + def getSimulationPass(self) -> Optional[SimulationPass]: if not self._layer_pass: + renderer = self.getRenderer() + if renderer is None: + return None + # Currently the RenderPass constructor requires a size > 0 # This should be fixed in RenderPass's constructor. self._layer_pass = SimulationPass(1, 1) self._compatibility_mode = self._evaluateCompatibilityMode() self._layer_pass.setSimulationView(self) + self._layer_pass.setEnabled(False) + renderer.addRenderPass(self._layer_pass) + return self._layer_pass def getCurrentLayer(self) -> int: @@ -734,11 +741,14 @@ class SimulationView(CuraView): # Make sure the SimulationPass is created layer_pass = self.getSimulationPass() + if layer_pass is None: + return False + renderer = self.getRenderer() if renderer is None: return False - renderer.addRenderPass(layer_pass) + layer_pass.setEnabled(True) # Make sure the NozzleNode is add to the root nozzle = self.getNozzleNode() @@ -778,7 +788,7 @@ class SimulationView(CuraView): return False if self._layer_pass is not None: - renderer.removeRenderPass(self._layer_pass) + self._layer_pass.setEnabled(False) if self._composite_pass: self._composite_pass.setLayerBindings(cast(List[str], self._old_layer_bindings)) self._composite_pass.setCompositeShader(cast(ShaderProgram, self._old_composite_shader)) diff --git a/resources/qml/ViewsSelector.qml b/resources/qml/ViewsSelector.qml index e76e5dbb67..b0e31ac532 100644 --- a/resources/qml/ViewsSelector.qml +++ b/resources/qml/ViewsSelector.qml @@ -38,7 +38,7 @@ Cura.ExpandablePopup { if (activeView == null) { - UM.Controller.setActiveView(viewModel.getItem(0).id) + UM.Controller.activeStage.setActiveView(viewModel.getItem(0).id) } } @@ -110,7 +110,7 @@ Cura.ExpandablePopup onClicked: { toggleContent() - UM.Controller.setActiveView(id) + UM.Controller.activeStage.setActiveView(id) } } }