diff --git a/src/bonsai/bonsai/bim/module/bcf/operator.py b/src/bonsai/bonsai/bim/module/bcf/operator.py index df07babe69..b8daa24e00 100644 --- a/src/bonsai/bonsai/bim/module/bcf/operator.py +++ b/src/bonsai/bonsai/bim/module/bcf/operator.py @@ -42,6 +42,7 @@ import ifcopenshell.util.unit import bonsai.tool as tool import bonsai.bim.module.bcf.prop as bcf_prop import bonsai.bim.module.bcf.bcfstore as bcfstore +from bpy_extras.io_utils import ImportHelper, ExportHelper from pathlib import Path from math import radians, degrees, atan, tan, cos, sin from mathutils import Vector, Matrix, Euler, geometry @@ -63,13 +64,14 @@ class NewBcfProject(bpy.types.Operator): return {"FINISHED"} -class LoadBcfProject(bpy.types.Operator): +class LoadBcfProject(bpy.types.Operator, ImportHelper): bl_idname = "bim.load_bcf_project" bl_label = "Load BCF Project" bl_description = "Load the BCF file." bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"}) filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"}) + filename_ext = ".bcf" def execute(self, context): # Operator is also used when new project is created by not yet saved. @@ -109,10 +111,6 @@ class LoadBcfProject(bpy.types.Operator): self.report({"INFO"}, f"BCF Project '{Path(self.filepath).name}' is loaded.") return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class UnloadBcfProject(bpy.types.Operator): bl_idname = "bim.unload_bcf_project" @@ -342,7 +340,7 @@ class EditBcfTopic(bpy.types.Operator): return {"FINISHED"} -class SaveBcfProject(bpy.types.Operator): +class SaveBcfProject(bpy.types.Operator, ExportHelper): bl_idname = "bim.save_bcf_project" bl_label = "Save BCF Project" bl_description = "Save active BCF project by the provided filepath." @@ -350,6 +348,7 @@ class SaveBcfProject(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"}) save_current_bcf: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) + filename_ext = ".bcf" def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() @@ -366,8 +365,7 @@ class SaveBcfProject(bpy.types.Operator): self.filepath = str(path) return self.execute(context) - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) class AddBcfTopic(bpy.types.Operator): @@ -1536,13 +1534,13 @@ class OpenBcfReferenceLink(bpy.types.Operator): return {"FINISHED"} -class SelectBcfHeaderFile(bpy.types.Operator): +class SelectBcfHeaderFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_bcf_header_file" bl_label = "Select BCF Header File" bl_description = "Select filepath for BCF header reference." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"}) + filename_ext = ".ifc" def execute(self, context): if self.filepath: @@ -1550,17 +1548,12 @@ class SelectBcfHeaderFile(bpy.types.Operator): props.file_reference = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class SelectBcfBimSnippetReference(bpy.types.Operator): +class SelectBcfBimSnippetReference(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_bcf_bim_snippet_reference" bl_label = "Select BCF BIM Snippet Reference" bl_description = "Select filepath for BCF snippet reference." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): if self.filepath: @@ -1568,17 +1561,12 @@ class SelectBcfBimSnippetReference(bpy.types.Operator): props.bim_snippet_reference = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class SelectBcfDocumentReference(bpy.types.Operator): +class SelectBcfDocumentReference(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_bcf_document_reference" bl_label = "Select BCF Document Reference" bl_description = "Select filepath for BCF document reference." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): if self.filepath: @@ -1586,10 +1574,6 @@ class SelectBcfDocumentReference(bpy.types.Operator): props.document_reference = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class LoadBcfHeaderIfcFile(bpy.types.Operator): bl_idname = "bim.load_bcf_header_ifc_file" diff --git a/src/bonsai/bonsai/bim/module/brick/operator.py b/src/bonsai/bonsai/bim/module/brick/operator.py index e9b20a215d..bb4f5f3277 100644 --- a/src/bonsai/bonsai/bim/module/brick/operator.py +++ b/src/bonsai/bonsai/bim/module/brick/operator.py @@ -22,17 +22,18 @@ import ifcopenshell.api import bonsai.tool as tool import bonsai.core.brick as core import bonsai.bim.handler +from bpy_extras.io_utils import ImportHelper, ExportHelper from bonsai.bim.ifc import IfcStore from bonsai.tool.brick import BrickStore -class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator): +class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.load_brick_project" bl_label = "Load Brickschema Project" bl_options = {"REGISTER", "UNDO"} bl_description = "Load in a Brick project from a file" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"}) + filename_ext = ".ttl" def _execute(self, context): if os.path.exists(self.filepath) and "ttl" in os.path.splitext(self.filepath)[1].lower(): @@ -41,10 +42,6 @@ class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator): else: self.report({"ERROR"}, f"Failed to load {self.filepath}") - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class ViewBrickClass(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.view_brick_class" @@ -240,7 +237,7 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator): ) -class SerializeBrick(bpy.types.Operator): +class SerializeBrick(bpy.types.Operator, ExportHelper): bl_idname = "bim.serialize_brick" bl_label = "Serialize Brick" # Prevents crash on Blender 4.4.0. @@ -251,9 +248,7 @@ class SerializeBrick(bpy.types.Operator): def invoke(self, context, event): if self.should_save_as or not BrickStore.path: - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) else: return self.execute(context) diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index 39621a3c4e..83fed588d2 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -24,26 +24,20 @@ import logging import numpy as np import ifcopenshell import bonsai.tool as tool +from bpy_extras.io_utils import ExportHelper, ImportHelper from math import radians from mathutils import Matrix, Vector from bonsai.bim.ifc import IfcStore from bonsai.bim.module.clash.decorator import ClashDecorator -class ExportClashSets(bpy.types.Operator): +class ExportClashSets(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_clash_sets" bl_label = "Export Clash Sets" bl_description = "Export clash sets to a selected file" filename_ext = ".json" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) - def invoke(self, context, event): - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} - def execute(self, context): self.filepath = bpy.path.ensure_ext(self.filepath, ".json") clash_sets = tool.Clash.export_clash_sets() @@ -52,20 +46,17 @@ class ExportClashSets(bpy.types.Operator): return {"FINISHED"} -class ImportClashSets(bpy.types.Operator): +class ImportClashSets(bpy.types.Operator, ImportHelper): bl_idname = "bim.import_clash_sets" bl_label = "Import Clash Sets" bl_options = {"REGISTER", "UNDO"} bl_description = "Import clash sets from a selected file" filename_ext = ".json" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) def invoke(self, context, event): self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) def execute(self, context): tool.Clash.load_clash_sets(self.filepath) @@ -155,15 +146,15 @@ class RemoveClashSource(bpy.types.Operator): return {"FINISHED"} -class SelectClashSource(bpy.types.Operator): +class SelectClashSource(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_clash_source" bl_label = "Select Clash Source" bl_options = {"REGISTER", "UNDO"} bl_description = "Select an IFC file to add as a clash source" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) index: bpy.props.IntProperty() group: bpy.props.StringProperty() + filename_ext = ".ifc" def execute(self, context): props = tool.Clash.get_clash_props() @@ -171,46 +162,32 @@ class SelectClashSource(bpy.types.Operator): getattr(clash_set, self.group)[self.index].name = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class SelectClashResults(bpy.types.Operator): +class SelectClashResults(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_clash_results" bl_label = "Select Clash Results" bl_description = "Select filepath for clash results." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): props = tool.Clash.get_clash_props() props.clash_results_path = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class SelectSmartGroupedClashesPath(bpy.types.Operator): +class SelectSmartGroupedClashesPath(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_smart_grouped_clashes_path" bl_label = "Select Smart-Grouped Clashes Path" bl_description = "Select filepath for smart-grouped clashes." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): props = tool.Clash.get_clash_props() props.smart_grouped_clashes_path = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class ExecuteIfcClash(bpy.types.Operator): +class ExecuteIfcClash(bpy.types.Operator, ExportHelper): bl_idname = "bim.execute_ifc_clash" bl_label = "Execute IFC Clash" bl_description = "Execute clash detection and save the information to a .bcf or .json file" @@ -220,8 +197,7 @@ class ExecuteIfcClash(bpy.types.Operator): def invoke(self, context, event): if self.filepath: return self.execute(context) - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) def execute(self, context): from ifcclash import ifcclash @@ -294,19 +270,17 @@ class ExecuteIfcClash(bpy.types.Operator): return {"FINISHED"} -class SelectIfcClashResults(bpy.types.Operator): +class SelectIfcClashResults(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_ifc_clash_results" bl_label = "Select IFC Clash Results" bl_options = {"REGISTER", "UNDO"} bl_description = "Select the clashing IFC geometry stored in a file" + filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) filename_ext = ".json" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def invoke(self, context, event): self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) def execute(self, context): # TODO refactor into new clash results system diff --git a/src/bonsai/bonsai/bim/module/classification/operator.py b/src/bonsai/bonsai/bim/module/classification/operator.py index a2b19fcae1..ba9f9d1928 100644 --- a/src/bonsai/bonsai/bim/module/classification/operator.py +++ b/src/bonsai/bonsai/bim/module/classification/operator.py @@ -25,20 +25,16 @@ import ifcopenshell.util.classification import ifcopenshell.util.element import bonsai.tool as tool import bonsai.bim.helper +from bpy_extras.io_utils import ImportHelper from bonsai.bim.ifc import IfcStore -class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator): +class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.load_classification_library" bl_label = "Load Classification Library" bl_description = "Load classification library from the provided filepath." filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} def _execute(self, context): IfcStore.classification_file = ifcopenshell.open(self.filepath) diff --git a/src/bonsai/bonsai/bim/module/cost/operator.py b/src/bonsai/bonsai/bim/module/cost/operator.py index ead79ba57a..3cbe9b4d68 100644 --- a/src/bonsai/bonsai/bim/module/cost/operator.py +++ b/src/bonsai/bonsai/bim/module/cost/operator.py @@ -21,7 +21,7 @@ import bpy import ifcopenshell.api import bonsai.tool as tool -from bpy_extras.io_utils import ImportHelper +from bpy_extras.io_utils import ImportHelper, ExportHelper import bonsai.tool as tool import bonsai.core.cost as core from typing import get_args, TYPE_CHECKING @@ -678,7 +678,7 @@ class CalculateCostItemResourceValue(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} -class ExportCostSchedules(bpy.types.Operator): +class ExportCostSchedules(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_cost_schedules" bl_label = "Export Cost Schedule" bl_options = {"REGISTER", "UNDO"} @@ -701,9 +701,7 @@ class ExportCostSchedules(bpy.types.Operator): return {"FINISHED"} def invoke(self, context, event): - wm = context.window_manager - wm.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) def draw(self, context): self.layout.label(text="Choose a format") diff --git a/src/bonsai/bonsai/bim/module/csv/operator.py b/src/bonsai/bonsai/bim/module/csv/operator.py index bbb9da89f7..34a296fe6f 100644 --- a/src/bonsai/bonsai/bim/module/csv/operator.py +++ b/src/bonsai/bonsai/bim/module/csv/operator.py @@ -27,6 +27,7 @@ import ifcopenshell import ifcopenshell.util.selector import bonsai.tool as tool import bonsai.bim.module.drawing.scheduler as scheduler +from bpy_extras.io_utils import ExportHelper, ImportHelper from bonsai.bim.handler import refresh_ui_data from typing import TYPE_CHECKING from collections import Counter @@ -90,13 +91,13 @@ class ReorderCsvAttribute(bpy.types.Operator): return {"FINISHED"} -class ImportCsvAttributes(bpy.types.Operator): +class ImportCsvAttributes(bpy.types.Operator, ImportHelper): bl_idname = "bim.import_csv_attributes" bl_label = "Load CSV Settings" bl_description = "Import a json template for CSV export" bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filename_ext = ".json" def execute(self, context): props = tool.Blender.get_csv_props() @@ -125,19 +126,14 @@ class ImportCsvAttributes(bpy.types.Operator): setattr(new, prop, attribute[prop]) return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class ExportCsvAttributes(bpy.types.Operator): +class ExportCsvAttributes(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_csv_attributes" bl_label = "Save CSV Settings" bl_options = {"REGISTER", "UNDO"} bl_description = "Save a json template for CSV export" filename_ext = ".json" filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): props = tool.Blender.get_csv_props() @@ -179,14 +175,8 @@ class ExportCsvAttributes(bpy.types.Operator): return {"FINISHED"} - def invoke(self, context, event): - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class ExportIfcCsv(bpy.types.Operator): +class ExportIfcCsv(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_ifccsv" bl_label = "Export IFC" bl_description = "Export IFC data as a spreadsheet." @@ -212,10 +202,7 @@ class ExportIfcCsv(bpy.types.Operator): props = tool.Blender.get_csv_props() if props.format == "web": return self.execute(context) - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) def execute(self, context): import ifccsv @@ -299,13 +286,12 @@ class ExportIfcCsv(bpy.types.Operator): ] -class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): +class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.import_ifccsv" bl_label = "Import to IFC" bl_description = "Import IFC data from a spreadsheet." bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") @classmethod def poll(cls, context): @@ -317,9 +303,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): def invoke(self, context, event): self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".csv") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) def _execute(self, context): import ifccsv @@ -350,20 +334,15 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} -class SelectCsvIfcFile(bpy.types.Operator): +class SelectCsvIfcFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_csv_ifc_file" bl_label = "Select CSV IFC File" bl_description = "Select IFC file for spreadsheet import/export." bl_options = {"REGISTER", "UNDO"} filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): props = tool.Blender.get_csv_props() props.csv_ifc_file = self.filepath return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index c68bedbeb0..0b1c59211a 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -36,6 +36,7 @@ import bonsai.core.profile import bonsai.core.type import bonsai.bim.handler import bonsai.bim.import_ifc as import_ifc +from bpy_extras.io_utils import ImportHelper, ExportHelper from pathlib import Path from bonsai import get_debug_info, format_debug_info from bonsai.bim.ifc import IfcStore @@ -446,12 +447,11 @@ class ParseExpress(bpy.types.Operator): return {"FINISHED"} -class SelectExpressFile(bpy.types.Operator): +class SelectExpressFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_express_file" bl_label = "Select Express File" bl_options = {"REGISTER", "UNDO"} bl_description = "Select an IFC EXPRESS definition" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.exp", options={"HIDDEN"}) def execute(self, context): @@ -460,10 +460,6 @@ class SelectExpressFile(bpy.types.Operator): props.express_file = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class PurgeHdf5Cache(bpy.types.Operator): bl_idname = "bim.purge_hdf5_cache" @@ -523,7 +519,7 @@ class PrintUnusedElementStats(bpy.types.Operator): return {"FINISHED"} -class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator): +class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator, ExportHelper): bl_idname = "bim.purge_unused_elements_by_class" bl_label = "Purge Unused Elements By Class" bl_description = ( @@ -537,7 +533,7 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator): def invoke(self, context, event): if event.type == "LEFTMOUSE" and event.alt: - context.window_manager.fileselect_add(self) + return ExportHelper.invoke(self, context, event) return self.execute(context) @classmethod diff --git a/src/bonsai/bonsai/bim/module/diff/operator.py b/src/bonsai/bonsai/bim/module/diff/operator.py index a5c097db51..954e43ac1e 100644 --- a/src/bonsai/bonsai/bim/module/diff/operator.py +++ b/src/bonsai/bonsai/bim/module/diff/operator.py @@ -24,26 +24,23 @@ import ifcopenshell import bonsai.bim.handler import bonsai.bim.import_ifc import bonsai.tool as tool +from bpy_extras.io_utils import ImportHelper, ExportHelper from bonsai.bim.ifc import IfcStore -class SelectDiffJsonFile(bpy.types.Operator): +class SelectDiffJsonFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_diff_json_file" bl_label = "Select Diff JSON File" bl_description = "Select filepath for IFC diff results." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) + filename_ext = ".json" def execute(self, context): props = tool.Blender.get_diff_props() props.diff_json_file = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class VisualiseDiff(bpy.types.Operator): bl_idname = "bim.visualise_diff" @@ -104,56 +101,41 @@ class VisualiseDiff(bpy.types.Operator): return {"FINISHED"} -class SelectDiffOldFile(bpy.types.Operator): +class SelectDiffOldFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_diff_old_file" bl_label = "Select Diff Old File" bl_description = "Select filepath for an old IFC file to compare." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) + filename_ext = ".ifc" def execute(self, context): props = tool.Blender.get_diff_props() props.old_file = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class SelectDiffNewFile(bpy.types.Operator): +class SelectDiffNewFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_diff_new_file" bl_label = "Select Diff New File" bl_description = "Select filepath for a new IFC file to compare." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) + filename_ext = ".ifc" def execute(self, context): props = tool.Blender.get_diff_props() props.new_file = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class ExecuteIfcDiff(bpy.types.Operator): +class ExecuteIfcDiff(bpy.types.Operator, ExportHelper): bl_idname = "bim.execute_ifc_diff" bl_label = "Execute IFC Diff" bl_description = "Compare two IFC files and save a json diff report by the provided filepath." filename_ext = ".json" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) - def invoke(self, context, event): - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} - def execute(self, context): import ifcdiff diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 0cc32e5d07..56a59506d0 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -43,6 +43,7 @@ import bonsai.bim.module.drawing.svgwriter as svgwriter import bonsai.bim.module.drawing.annotation as annotation import bonsai.bim.module.drawing.sheeter as sheeter import bonsai.bim.export_ifc +from bpy_extras.io_utils import ImportHelper from bonsai.bim.module.drawing.decoration import CutDecorator from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData from typing import NamedTuple, List, Union, Optional, Literal @@ -2217,12 +2218,12 @@ class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase): # TODO: not exposed to the UI. -class SelectDocIfcFile(bpy.types.Operator): +class SelectDocIfcFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_doc_ifc_file" bl_label = "Select Documentation IFC File" bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filename_ext = ".ifc" index: bpy.props.IntProperty() def execute(self, context): @@ -2230,10 +2231,6 @@ class SelectDocIfcFile(bpy.types.Operator): props.ifc_files[self.index].name = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class ResizeText(bpy.types.Operator): bl_idname = "bim.resize_text" @@ -2612,13 +2609,12 @@ class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator): core.remove_sheet(tool.Ifc, tool.Drawing, sheet=tool.Ifc.get().by_id(self.sheet)) -class AddSchedule(bpy.types.Operator, tool.Ifc.Operator): +class AddSchedule(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.add_schedule" bl_label = "Add Schedule" bl_options = {"REGISTER", "UNDO"} bl_description = "Add an .ods, .xls or .xlsx file as a schedule" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"}) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) @@ -2626,10 +2622,6 @@ class AddSchedule(bpy.types.Operator, tool.Ifc.Operator): filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) core.add_document(tool.Ifc, tool.Drawing, "SCHEDULE", uri=filepath) - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class RemoveSchedule(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_schedule" @@ -2799,24 +2791,20 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator): tool.Drawing.import_sheets() -class AddReference(bpy.types.Operator, tool.Ifc.Operator): +class AddReference(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.add_reference" bl_label = "Add Reference" bl_description = "Import a .svg file to the project as a reference" - bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"}) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) + filename_ext = ".svg" def _execute(self, context): filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath) - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class RemoveReference(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_reference" @@ -3445,7 +3433,7 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False) -class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator): +class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.add_reference_image" bl_label = "Add Reference Image" bl_description = "Add or import reference image to the IFC project" @@ -3453,9 +3441,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) - filepath: bpy.props.StringProperty( - name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH" - ) filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) @@ -3483,10 +3468,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator): layout.prop(self, "override_existing_image") layout.prop(self, "use_existing_object_by_name") - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - def _execute(self, context): abs_path = Path(self.filepath).absolute().resolve() image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)) diff --git a/src/bonsai/bonsai/bim/module/fm/operator.py b/src/bonsai/bonsai/bim/module/fm/operator.py index c78f70dee4..fac40439c2 100644 --- a/src/bonsai/bonsai/bim/module/fm/operator.py +++ b/src/bonsai/bonsai/bim/module/fm/operator.py @@ -24,15 +24,15 @@ import logging import tempfile import ifcopenshell import bonsai.tool as tool +from bpy_extras.io_utils import ExportHelper, ImportHelper -class ExecuteIfcFM(bpy.types.Operator): +class ExecuteIfcFM(bpy.types.Operator, ExportHelper): bl_idname = "bim.execute_ifcfm" bl_label = "Execute IfcFM" bl_description = "Export IfcFM data as a spreadsheet." file_format: bpy.props.StringProperty() filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") @classmethod def poll(cls, context): @@ -45,9 +45,7 @@ class ExecuteIfcFM(bpy.types.Operator): def invoke(self, context, event): props = context.scene.BIMFMProperties self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) def execute(self, context): props = context.scene.BIMFMProperties @@ -83,13 +81,12 @@ class ExecuteIfcFM(bpy.types.Operator): return {"FINISHED"} -class SelectFMSpreadsheetFiles(bpy.types.Operator): +class SelectFMSpreadsheetFiles(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_fm_spreadsheet_files" bl_label = "Select FM Spreadsheet Files" bl_description = "Select FM spreadsheets to merge." bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement) def execute(self, context): @@ -101,17 +98,12 @@ class SelectFMSpreadsheetFiles(bpy.types.Operator): new.name = os.path.join(dirname, f.name) return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class ExecuteIfcFMFederate(bpy.types.Operator): +class ExecuteIfcFMFederate(bpy.types.Operator, ExportHelper): bl_idname = "bim.execute_ifcfm_federate" bl_label = "Merge IfcFM SpreadSheets" bl_description = "Merge added IfcFM spreadsheets." filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") @classmethod def poll(cls, context): @@ -124,9 +116,7 @@ class ExecuteIfcFMFederate(bpy.types.Operator): def invoke(self, context, event): props = context.scene.BIMFMProperties self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) def execute(self, context): props = context.scene.BIMFMProperties diff --git a/src/bonsai/bonsai/bim/module/georeference/operator.py b/src/bonsai/bonsai/bim/module/georeference/operator.py index edfa9a9054..7d8f98dd6c 100644 --- a/src/bonsai/bonsai/bim/module/georeference/operator.py +++ b/src/bonsai/bonsai/bim/module/georeference/operator.py @@ -20,6 +20,7 @@ import bpy import bonsai.tool as tool import bonsai.core.georeference as core +from bpy_extras.io_utils import ImportHelper from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator @@ -87,22 +88,18 @@ class GetCursorLocation(bpy.types.Operator, tool.Ifc.Operator): core.get_cursor_location(tool.Georeference) -class ImportPlot(bpy.types.Operator, tool.Ifc.Operator): +class ImportPlot(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.import_plot" bl_label = "Import Plot" bl_options = {"REGISTER", "UNDO"} bl_description = "Import plot from a csv file." - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) + filename_ext = ".csv" - def execute(self, context): + def _execute(self, context): core.import_plot(tool.Georeference, filepath=self.filepath) return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class EnableEditingWCS(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.enable_editing_wcs" diff --git a/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py b/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py index 149a309baa..315b16a98c 100644 --- a/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py +++ b/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py @@ -25,6 +25,7 @@ import bonsai.tool as tool import json import zipfile import os.path +from bpy_extras.io_utils import ImportHelper, ExportHelper def update_sverchok_modifier(context): @@ -181,17 +182,14 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator): # used code from Sverchok's SvNodeTreeImporter licensed under GPL v3 # the code was changed to work with ifc sverchok modifier # removed the part that was relying on node graph to be opened at execution -class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): +class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.import_sverchok_graph" bl_label = "Import Sverchok Graph" bl_description = "Import Sverchok graph from a json file." bl_options = {"REGISTER"} - filepath: bpy.props.StringProperty( - name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH" - ) - filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) + filename_ext = ".json" def _execute(self, context): import sverchok @@ -210,10 +208,6 @@ class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): props.node_group = node_group return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - def draw(self, context): col = self.layout.column() col.label(text="Destination tree to import JSON:") @@ -224,17 +218,14 @@ class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): # used code from Sverchok's SvNodeTreeExporter licensed under GPL v3 # the code was changed to work with ifc sverchok modifier # removed the part that was relying on node graph to be opened at execution -class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): +class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator, ExportHelper): bl_idname = "bim.export_sverchok_graph" bl_label = "Export Sverchok Graph" bl_description = "Export Sverchok graph to a json file." bl_options = {"REGISTER"} - filepath: bpy.props.StringProperty( - name="File Path", description="Filepath used for exporting to", maxlen=1024, default="", subtype="FILE_PATH" - ) - filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) + filename_ext = ".json" compact: bpy.props.BoolProperty(default=True, description="Compact representation of the JSON file") compress: bpy.props.BoolProperty() @@ -281,10 +272,6 @@ class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - def draw(self, context): graph_name = context.active_object.BIMSverchokProperties.node_group.name self.layout.label(text=f'Save node tree "{graph_name}" into json:') diff --git a/src/bonsai/bonsai/bim/module/patch/operator.py b/src/bonsai/bonsai/bim/module/patch/operator.py index bbd521ffd4..690f66a079 100644 --- a/src/bonsai/bonsai/bim/module/patch/operator.py +++ b/src/bonsai/bonsai/bim/module/patch/operator.py @@ -24,6 +24,7 @@ import ifcpatch import bonsai.tool as tool import bonsai.core.patch as core import bonsai.bim.handler +from bpy_extras.io_utils import ImportHelper, ExportHelper from pathlib import Path from typing import cast, TYPE_CHECKING @@ -31,41 +32,33 @@ if TYPE_CHECKING: from bonsai.bim.prop import AttributeDataType -class SelectIfcPatchInput(bpy.types.Operator): +class SelectIfcPatchInput(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_ifc_patch_input" bl_label = "Select IFC Patch Input" bl_description = "Select filepath for IFC patch input." bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifcZIP;*.ifcXML", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filename_ext = ".ifc" def execute(self, context): props = tool.Patch.get_patch_props() props.ifc_patch_input = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class SelectIfcPatchOutput(bpy.types.Operator): +class SelectIfcPatchOutput(bpy.types.Operator, ExportHelper): bl_idname = "bim.select_ifc_patch_output" bl_label = "Select IFC Patch Output" bl_description = "Select filepath for IFC patch output." bl_options = {"REGISTER", "UNDO"} + filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifcZIP;*.ifcXML", options={"HIDDEN"}) filename_ext = ".ifc" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): props = tool.Patch.get_patch_props() props.ifc_patch_output = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class ExecuteIfcPatch(bpy.types.Operator): bl_idname = "bim.execute_ifc_patch" diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 936a532d1e..68180a5e21 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -47,6 +47,7 @@ import bonsai.bim.helper import bonsai.bim.schema import bonsai.tool as tool import bonsai.core.project as core +from bpy_extras.io_utils import ExportHelper, ImportHelper from bonsai.bim.ifc import IfcStore from bonsai.bim.ui import IFCFileSelector from bonsai.bim import import_ifc @@ -152,12 +153,11 @@ class CreateProject(bpy.types.Operator): IfcStore.file = data["file"] -class SelectLibraryFile(bpy.types.Operator, IFCFileSelector): +class SelectLibraryFile(bpy.types.Operator, IFCFileSelector, ImportHelper): bl_idname = "bim.select_library_file" bl_label = "Select Library File" bl_options = {"REGISTER", "UNDO"} bl_description = "Select an IFC file that can be used as a library" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) append_all: bpy.props.BoolProperty(default=False) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) @@ -192,10 +192,6 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector): ProjectLibraryData.load() return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - def rollback(self, data): if data["old_filepath"]: IfcStore.library_path = data["old_filepath"] @@ -872,7 +868,7 @@ class DisableEditingHeader(bpy.types.Operator): return {"FINISHED"} -class LoadProject(bpy.types.Operator, IFCFileSelector): +class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper): bl_idname = "bim.load_project" bl_label = "Load Project" bl_options = {"REGISTER", "UNDO"} @@ -903,6 +899,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): default=False, ) use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) + filename_ext = ".ifc" @classmethod def description(cls, context, properties): @@ -1014,8 +1011,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): def invoke(self, context, event): if self.filepath: return self.execute(context) - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) def draw(self, context): if self.use_relative_path: @@ -1175,12 +1171,12 @@ class ToggleFilterCategories(bpy.types.Operator): return {"FINISHED"} -class LinkIfc(bpy.types.Operator): +class LinkIfc(bpy.types.Operator, ImportHelper): bl_idname = "bim.link_ifc" bl_label = "Link IFC" bl_options = {"REGISTER", "UNDO"} bl_description = "Reference in a read-only IFC model in the background" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") + files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement) directory: bpy.props.StringProperty(subtype="DIR_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) @@ -1190,6 +1186,7 @@ class LinkIfc(bpy.types.Operator): default=False, ) use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) + filename_ext = ".ifc" if TYPE_CHECKING: filepath: str @@ -1235,10 +1232,6 @@ class LinkIfc(bpy.types.Operator): print(f"Finished linking {len(files)} IFCs", time.time() - start) return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class UnlinkIfc(bpy.types.Operator): bl_idname = "bim.unlink_ifc" @@ -1544,7 +1537,7 @@ class SelectLinkHandle(bpy.types.Operator): return {"FINISHED"} -class ExportIFC(bpy.types.Operator): +class ExportIFC(bpy.types.Operator, ExportHelper): bl_idname = "bim.save_project" bl_label = "Save IFC" # Prevents crash on Blender 4.4.0. @@ -1552,7 +1545,6 @@ class ExportIFC(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") json_version: bpy.props.EnumProperty(items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version") json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False) should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"}) @@ -1583,15 +1575,8 @@ class ExportIFC(bpy.types.Operator): if (filepath := props.ifc_file) and not self.should_save_as: self.filepath = str(tool.Blender.ensure_blender_path_is_abs(Path(filepath))) return self.execute(context) - if not self.filepath: - if bpy.data.is_saved: - self.filepath = Path(bpy.data.filepath).with_suffix(".ifc").__str__() - else: - self.filepath = "untitled.ifc" - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) def execute(self, context): project_props = tool.Project.get_project_props() @@ -1668,12 +1653,11 @@ class ExportIFC(bpy.types.Operator): return "Save the IFC file. Will save both .IFC/.BLEND files if synced together" -class LoadLinkedProject(bpy.types.Operator): +class LoadLinkedProject(bpy.types.Operator, ImportHelper): bl_idname = "bim.load_linked_project" bl_label = "Load Project For Viewing Only" bl_description = "Operator is used to load a project .cache.blend to then link it to the IFC file." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty() file: ifcopenshell.file meshes: dict[str, bpy.types.Mesh] @@ -1682,9 +1666,7 @@ class LoadLinkedProject(bpy.types.Operator): def invoke(self, context, event): # Invoke is for debugging purposes, users are not intended to use this method really. - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) def execute(self, context): import ifcpatch diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index 5eccfd04a8..98e7ae6bce 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -32,7 +32,7 @@ import ifcopenshell.util.sequence import ifcopenshell.util.selector from datetime import datetime from dateutil import parser, relativedelta -from bpy_extras.io_utils import ImportHelper +from bpy_extras.io_utils import ImportHelper, ExportHelper from typing import get_args, TYPE_CHECKING from typing_extensions import assert_never @@ -794,7 +794,7 @@ class ImportMSP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): self.report({"INFO"}, "Import finished in {:.2f} seconds".format(time.time() - start)) -class ExportMSP(bpy.types.Operator, ImportHelper): +class ExportMSP(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_msp" bl_label = "Export MSP" bl_options = {"REGISTER", "UNDO"} @@ -827,7 +827,7 @@ class ExportMSP(bpy.types.Operator, ImportHelper): return {"FINISHED"} -class ExportP6(bpy.types.Operator, ImportHelper): +class ExportP6(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_p6" bl_label = "Export P6" bl_options = {"REGISTER", "UNDO"} diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index ea20f25be0..1a9a43f141 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -25,6 +25,7 @@ import ifcopenshell.api import ifcopenshell.api.style import ifcopenshell.util.representation import ifcopenshell.util.unit +from bpy_extras.io_utils import ImportHelper from pathlib import Path from mathutils import Vector from typing import Any, Union @@ -273,16 +274,12 @@ class SetAssetMaterialToExternalStyle(bpy.types.Operator): return {"FINISHED"} -class BrowseExternalStyle(bpy.types.Operator): +class BrowseExternalStyle(bpy.types.Operator, ImportHelper): bl_idname = "bim.browse_external_style" bl_label = "Browse External Style" bl_description = "Select filepath for an external style." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty( - name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH" - ) - filter_glob: bpy.props.StringProperty( default="*.blend", options={"HIDDEN"}, @@ -357,8 +354,7 @@ class BrowseExternalStyle(bpy.types.Operator): if data_block in data_blocks: self.data_block = data_block - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) def draw(self, context): layout = self.layout @@ -538,7 +534,7 @@ class SelectByStyle(bpy.types.Operator): return {"FINISHED"} -class ChooseTextureMapPath(bpy.types.Operator): +class ChooseTextureMapPath(bpy.types.Operator, ImportHelper): bl_idname = "bim.choose_texture_map_path" bl_label = "Choose Texture Map Path" bl_description = "Select filepath for a texture map." @@ -548,9 +544,6 @@ class ChooseTextureMapPath(bpy.types.Operator): use_relative_path: bpy.props.BoolProperty( name="Use Relative Path", description="Save path relative to IFC file", default=True ) - filepath: bpy.props.StringProperty( - name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH" - ) filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) @@ -563,10 +556,6 @@ class ChooseTextureMapPath(bpy.types.Operator): layout.label(text="Save the .ifc file first ") layout.label(text="to use relative paths.") - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - def execute(self, context): if self.texture_map_index < 0: self.report({"ERROR"}, "Provide a texture map index") diff --git a/src/bonsai/bonsai/bim/module/tester/operator.py b/src/bonsai/bonsai/bim/module/tester/operator.py index f653404d78..f1341f0225 100644 --- a/src/bonsai/bonsai/bim/module/tester/operator.py +++ b/src/bonsai/bonsai/bim/module/tester/operator.py @@ -28,6 +28,7 @@ import ifctester.reporter import ifcopenshell import bonsai.tool as tool import bonsai.bim.handler +from bpy_extras.io_utils import ExportHelper from pathlib import Path from typing import Union @@ -186,13 +187,13 @@ class SelectFailedEntities(bpy.types.Operator): return {"FINISHED"} -class ExportBcf(bpy.types.Operator): +class ExportBcf(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_bcf" bl_label = "Export BCF" bl_description = "Save ifctester BCF report by the provided filepath." bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.bcf", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filename_ext = ".bcf" def execute(self, context): bcf_reporter = ifctester.reporter.Bcf(tool.Tester.specs) @@ -200,7 +201,3 @@ class ExportBcf(bpy.types.Operator): bcf_reporter.to_file(self.filepath) self.report({"INFO"}, "Finished exporting!") return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 54dc26617a..52bd2cf42c 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -31,6 +31,7 @@ import ifcopenshell import bonsai.bim import bonsai.tool as tool import bonsai.bim.handler +from bpy_extras.io_utils import ImportHelper from bonsai.bim import import_ifc from bonsai.bim.prop import StrProperty from bonsai.bim.ui import IFCFileSelector @@ -145,12 +146,11 @@ class CloseBlendWarning(bpy.types.Operator): return context.window_manager.invoke_props_dialog(self) -class SelectURIAttribute(bpy.types.Operator): +class SelectURIAttribute(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_uri_attribute" bl_label = "Select URI Attribute" bl_options = {"REGISTER", "UNDO"} bl_description = "Select a local file" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") data_path: bpy.props.StringProperty(name="Data Path") use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) @@ -174,18 +174,13 @@ class SelectURIAttribute(bpy.types.Operator): attribute.string_value = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class BIM_OT_multiple_file_selector(bpy.types.Operator): +class BIM_OT_multiple_file_selector(bpy.types.Operator, ImportHelper): """Open Blender's file explorer to select one or multiple files.""" bl_idname = "bim.multiple_file_selector" bl_label = "Select File(s)" bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement) filter_glob: bpy.props.StringProperty(default="*", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -206,18 +201,17 @@ class BIM_OT_multiple_file_selector(bpy.types.Operator): def invoke(self, context, event): self.file_props = context.file_props - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) -class SelectIfcFile(bpy.types.Operator, IFCFileSelector): +class SelectIfcFile(bpy.types.Operator, IFCFileSelector, ImportHelper): bl_idname = "bim.select_ifc_file" bl_label = "Select IFC File" bl_options = {"REGISTER", "UNDO"} bl_description = f"Select a different IFC file.\n{tool.Blender.operator_invoke_filepath_hotkeys_description}" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) + filename_ext = ".ifc" def execute(self, context): if self.is_existing_ifc_file(): @@ -233,17 +227,14 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector): res = tool.Blender.operator_invoke_filepath_hotkeys(self, context, event, filepath) if res is not None: return res - - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) -class SelectDir(bpy.types.Operator): +class SelectDir(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_dir" bl_label = "Select Directory" bl_options = {"REGISTER", "UNDO"} bl_description = "Open a file browser to choose the directory" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") data_path: bpy.props.StringProperty(name="Data Path") def execute(self, context): @@ -262,8 +253,7 @@ class SelectDir(bpy.types.Operator): return {"FINISHED"} def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) class FileAssociate(bpy.types.Operator): @@ -768,13 +758,13 @@ class BIM_OT_remove_section_plane(bpy.types.Operator): bpy.ops.object.delete() -class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator): +class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.reload_ifc_file" bl_label = "Reload IFC File" bl_options = {"REGISTER", "UNDO"} bl_description = "Reload an updated IFC file" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) + filename_ext = ".ifc" def _execute(self, context): import ifcdiff @@ -842,10 +832,6 @@ class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator): bim_props.ifc_file = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class AddIfcFile(bpy.types.Operator): bl_idname = "bim.add_ifc_file" diff --git a/src/ifcsverchok/__init__.py b/src/ifcsverchok/__init__.py index 8263c73576..97fe04c5e0 100644 --- a/src/ifcsverchok/__init__.py +++ b/src/ifcsverchok/__init__.py @@ -32,6 +32,7 @@ import importlib import logging import types import bpy +from bpy_extras.io_utils import ExportHelper logger = logging.getLogger("sverchok.ifc") @@ -180,15 +181,15 @@ class IFC_Sv_UpdateCurrent(bpy.types.Operator): return {"FINISHED"} -class IFC_Sv_write_file(bpy.types.Operator): +class IFC_Sv_write_file(bpy.types.Operator, ExportHelper): bl_idname = "ifc.write_file_panel" bl_label = "Write File" bl_options = {"REGISTER", "UNDO"} bl_description = "Save transient IFC file to the provided path." - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) node_group: bpy.props.StringProperty(default="") force_mode: bpy.props.BoolProperty(default=False) + filename_ext = ".ifc" @classmethod def poll(cls, context): @@ -264,10 +265,6 @@ class IFC_Sv_write_file(bpy.types.Operator): self.report({"INFO"}, f"File written to: {self.filepath}") return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class IFC_PT_write_file_panel(bpy.types.Panel): bl_idname = "IFC_PT_write_file_panel"