From 0ce6e94352193512455855cf69a152e0bb0c3f4c Mon Sep 17 00:00:00 2001 From: Stephen Boddy Date: Sat, 11 Jul 2026 06:40:28 +0100 Subject: [PATCH] Make autosave recovery prompt properly modal The recovery popup used invoke_popup, which is dismissed the instant the mouse leaves its bounds - closing the prompt without loading either file, and with no visible feedback that anything happened. Switches to invoke_props_dialog, which blocks the rest of the UI and is only dismissed by an explicit action. Since Blender always renders both a fixed "Cancel" button and one labelled by confirm_text on that dialog type, the prompt is reframed as a direct Yes/Cancel question ("Do you want to load the autosaved version instead?") instead of adding separate Load Original/Load Autosave buttons on top of those. Folds the load logic directly into the popup's execute()/cancel(), so the now-redundant LoadAutosavedRecovery operator is removed. Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Sonnet 5 --- .../bonsai/bim/module/project/__init__.py | 1 - .../bonsai/bim/module/project/operator.py | 74 ++++++------------- 2 files changed, 22 insertions(+), 53 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index caa9458471..f915e294a0 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -61,7 +61,6 @@ classes = ( operator.LoadLink, operator.AutosavePrompt, operator.LoadAutosavedRecoveryPopup, - operator.LoadAutosavedRecovery, operator.LoadLinkedProject, operator.LoadProject, operator.LoadProjectElements, diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index d289df44c0..41e7d49c5d 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2169,71 +2169,41 @@ class LoadAutosavedRecoveryPopup(bpy.types.Operator): layout.label(text="A newer autosaved copy was found:", icon="INFO") layout.label(text=os.path.basename(self.autosaved_filepath)) layout.separator() - layout.label(text=f"Original: {os.path.basename(self.original_filepath)}") - layout.label(text="Which one do you want to load?") - - row = layout.row(align=True) - op = row.operator("bim.load_autosaved_recovery", text="Load Original", icon="LOOP_BACK") - op.file_type = "ORIGINAL" - self._pass_props(op) - - op = row.operator("bim.load_autosaved_recovery", text="Load Autosave", icon="LOOP_FORWARDS") - op.file_type = "AUTOSAVE" - self._pass_props(op) - - def _pass_props(self, op): - op.original_filepath = self.original_filepath - op.autosaved_filepath = self.autosaved_filepath - op.is_advanced = self.is_advanced - op.use_relative_path = self.use_relative_path - op.should_start_fresh_session = self.should_start_fresh_session - op.import_without_ifc_data = self.import_without_ifc_data + layout.label(text="Do you want to load the autosaved version instead?") + layout.label(text="(Cancel will load the original)") def invoke(self, context, event): - return context.window_manager.invoke_popup(self, width=420) + # invoke_props_dialog is modal - unlike invoke_popup/popup_menu, it + # isn't dismissed by the mouse simply leaving its bounds. It always + # renders both a fixed "Cancel" button and this confirm_text one, so + # the question is framed as Yes/Cancel rather than adding separate + # Load buttons on top. + return context.window_manager.invoke_props_dialog( + self, width=420, title="Recover Autosaved File", confirm_text="Yes" + ) - def execute(self, context): - # This should almost never run - self.report({"INFO"}, "Popup closed without choosing") - return {"FINISHED"} - - -class LoadAutosavedRecovery(bpy.types.Operator): - bl_idname = "bim.load_autosaved_recovery" - bl_label = "Recover Autosaved File" - bl_options = {"REGISTER", "UNDO"} - - file_type: bpy.props.StringProperty(default="AUTOSAVE") - original_filepath: bpy.props.StringProperty(options={"SKIP_SAVE"}) - autosaved_filepath: bpy.props.StringProperty(options={"SKIP_SAVE"}) - is_advanced: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) - use_relative_path: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) - should_start_fresh_session: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"}) - import_without_ifc_data: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) - - def execute(self, context): - if self.file_type == "ORIGINAL": - filepath = self.original_filepath - else: - filepath = self.autosaved_filepath - - # Call the real loader - result = bpy.ops.bim.load_project( + def _load(self, filepath: str, skip_recent: bool) -> set["rna_enums.OperatorReturnItems"]: + return bpy.ops.bim.load_project( filepath=filepath, skip_autosave_recovery=True, # Prevent infinite loop is_advanced=self.is_advanced, use_relative_path=self.use_relative_path, should_start_fresh_session=self.should_start_fresh_session, import_without_ifc_data=self.import_without_ifc_data, - skip_recent=(self.file_type == "AUTOSAVE") + skip_recent=skip_recent, ) - # If user chose autosave, override the stored path - if self.file_type == "AUTOSAVE": - tool.Ifc.set_path(self.original_filepath) - + def execute(self, context): + result = self._load(self.autosaved_filepath, skip_recent=True) + # Re-point tracking at the original path so future saves write back + # to it, not "_autosaved.ifc". + tool.Ifc.set_path(self.original_filepath) return result + def cancel(self, context): + # Also reached via Escape or a click outside the dialog, not just Cancel. + self._load(self.original_filepath, skip_recent=False) + class AutosavePrompt(bpy.types.Operator): bl_idname = "bim.autosave_prompt"