From a43efffde271df41f9093c62250f535af78d427e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 29 Jul 2025 13:50:12 +0500 Subject: [PATCH] Bonsai Mac app to quickly open .ifc files --- src/bonsai/bonsai/bim/__init__.py | 1 + src/bonsai/bonsai/bim/operator.py | 98 ++++++++++++++++++- src/bonsai/bonsai/bim/ui.py | 8 +- src/bonsai/bonsai/libs/desktop/mac/Info.plist | 35 +++++++ .../bonsai/libs/desktop/mac/document.wflow | 47 +++++++++ 5 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 src/bonsai/bonsai/libs/desktop/mac/Info.plist create mode 100644 src/bonsai/bonsai/libs/desktop/mac/document.wflow diff --git a/src/bonsai/bonsai/bim/__init__.py b/src/bonsai/bonsai/bim/__init__.py index cc030819c0..79fc5fdd54 100644 --- a/src/bonsai/bonsai/bim/__init__.py +++ b/src/bonsai/bonsai/bim/__init__.py @@ -110,6 +110,7 @@ classes = [ operator.ClippingPlaneCutWithCappings, operator.CloseBlendWarning, operator.CloseError, + operator.CreateMacBonsaiApp, operator.CopyTextToClipboard, operator.EditBlenderCollection, operator.FileAssociate, diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 6a7ce88c93..f57c972eb1 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -272,6 +272,9 @@ class WinRegistryKeys(Enum): return cls.__bonsai_key +LIBS_DESKTOP = Path(__file__).parent.parent / "libs" / "desktop" + + class FileAssociate(bpy.types.Operator): bl_idname = "bim.file_associate" bl_label = "Associate Bonsai with *.ifc files" @@ -288,11 +291,10 @@ class FileAssociate(bpy.types.Operator): return False def execute(self, context): - src_dir = os.path.join(os.path.dirname(__file__), "../libs/desktop") binary_path = bpy.app.binary_path if platform.system() == "Linux": destdir = os.path.join(os.environ["HOME"], ".local") - self.install_desktop_linux(src_dir=src_dir, destdir=destdir, binary_path=binary_path) + self.install_desktop_linux(src_dir=LIBS_DESKTOP, destdir=destdir, binary_path=binary_path) elif platform.system() == "Windows": self.install_desktop_windows(binary_path) self.report({"INFO"}, "Associations established.") @@ -404,6 +406,7 @@ class FileUnassociate(bpy.types.Operator): self.uninstall_desktop_linux(destdir=destdir) elif platform.system() == "Windows": self.uninstall_desktop_windows() + return {"FINISHED"} def uninstall_desktop_windows(self) -> None: @@ -474,6 +477,97 @@ class FileUnassociate(bpy.types.Operator): pass +class CreateMacBonsaiApp(bpy.types.Operator): + bl_idname = "bim.create_mac_bonsai_app" + bl_label = "Create Bonsai App to Open .ifc Files." + bl_options = set() + bl_description = ( + "Create 'Bonsai' application on Mac.\n\n" + "To make Mac use Bonsai to open IFC files automatically:\n" + "- Open context menu on any .ifc file\n" + "- Open With\n" + "- Other\n" + "- Select Bonsai in 'Applications'\n" + "- Check 'Always Open With'\n" + "- 'Open'\n" + "\n" + "ALT+click to uninstall Bonsai app if it was installed previously." + ) + + uninstall: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + uninstall: bool + + @classmethod + def poll(cls, context): + if platform.system() == "Darwin": + return True + cls.poll_message_set("Mac Only.") + return False + + def invoke(self, context, event): + self.uninstall = event.alt + return self.execute(context) + + def execute(self, context): + # I've tried to create AppleScript that would create this kind of app using Automator, + # but using poorly document AppleScript was unbearable :( + # So we just try to create automator app ourselves from a template. + + # Couldn't find a way to automatically establish .ifc to app association, + # without installing some other app to handle it (e.g. 'duti'). + # So currently we rely on the final user action to select Bonsai in 'Open With'. + + # NOTE: shell script to execute is part of .wflow file. + app_path = Path("/Applications") / "Bonsai.app" + + if self.uninstall: + if app_path.exists(): + shutil.rmtree(app_path) + self.report({"INFO"}, "Bonsai app was successfully removed.") + else: + self.report({"WARNING"}, f"Couldn't remove Bonsai app as it's not found at '{app_path}'.") + return {"FINISHED"} + + if app_path.exists(): + self.report({"WARNING"}, f"Bonsai.app already exists at '{app_path}'.") + return {"FINISHED"} + + # 1. Create .app bundle structure + contents = app_path / "Contents" + macos = contents / "MacOS" + resources = contents / "Resources" + for p in (macos, resources): + p.mkdir(parents=True, exist_ok=True) + + # Copy Automator Application Stub. + stub_src = Path( + "/System/Library/CoreServices/Automator Application Stub.app/Contents/MacOS/Automator Application Stub" + ) + stub_dest = macos / "Automator Application Stub" + shutil.copy(stub_src, stub_dest) + stub_dest.chmod(0o755) + + # xml files. + shutil.copy2(LIBS_DESKTOP / "Mac" / "Info.plist", contents / "Info.plist") + wflow_template = (LIBS_DESKTOP / "Mac" / "document.wflow").read_text() + wflow_template = wflow_template.replace("{{BLENDER_BINARY}}", bpy.app.binary_path) + (contents / "document.wflow").write_text(wflow_template) + + # Convert PNG icon to ICNS. + png_icon = LIBS_DESKTOP / "bonsai.png" + icns_icon = app_path / "Contents/Resources/AppIcon.icns" + subprocess.run(["sips", "-s", "format", "icns", png_icon, "--out", icns_icon], check=True) + + self.report( + {"INFO"}, + "Bonsai app was successfully created. " + f"Follow instructions in '{self.bl_label}' description to use Bonsai for .ifc files automatically.", + ) + return {"FINISHED"} + + class OpenUpstream(bpy.types.Operator): bl_idname = "bim.open_upstream" bl_label = "Open Upstream Reference" diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 8ec26ccc52..27837833b0 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -521,9 +521,13 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): row = layout.row() row.operator("bim.open_upstream", text="Visit Wiki").page = "wiki" row.operator("bim.open_upstream", text="Visit Community").page = "community" + row = layout.row() - row.operator("bim.file_associate", icon="LOCKVIEW_ON") - row.operator("bim.file_unassociate", icon="LOCKVIEW_OFF") + if platform.system() == "Darwin": + row.operator("bim.create_mac_bonsai_app", icon="LOCKVIEW_ON") + else: + row.operator("bim.file_associate", icon="LOCKVIEW_ON") + row.operator("bim.file_unassociate", icon="LOCKVIEW_OFF") bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Interface", self.draw_interface_settings) bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Command Paths", self.draw_commands) diff --git a/src/bonsai/bonsai/libs/desktop/mac/Info.plist b/src/bonsai/bonsai/libs/desktop/mac/Info.plist new file mode 100644 index 0000000000..1b3d8da539 --- /dev/null +++ b/src/bonsai/bonsai/libs/desktop/mac/Info.plist @@ -0,0 +1,35 @@ + + + + + AMIsApplet + + AMStayOpen + + CFBundleExecutable + Automator Application Stub + CFBundleIconFile + AppIcon + CFBundleIdentifier + com.apple.automator.Bonsai + CFBundleName + Bonsai + CFBundlePackageType + APPL + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + ifc + + CFBundleTypeName + IFC File + CFBundleTypeRole + Editor + + + LSUIElement + + + diff --git a/src/bonsai/bonsai/libs/desktop/mac/document.wflow b/src/bonsai/bonsai/libs/desktop/mac/document.wflow new file mode 100644 index 0000000000..56523ae39f --- /dev/null +++ b/src/bonsai/bonsai/libs/desktop/mac/document.wflow @@ -0,0 +1,47 @@ + + + + + AMApplicationBuild + 526 + AMApplicationVersion + 2.10 + AMDocumentVersion + 2 + actions + + + action + + ActionBundlePath + /System/Library/Automator/Run Shell Script.action + ActionParameters + + COMMAND_STRING + "{{BLENDER_BINARY}}" --python-expr "import bpy; \ + import sys; \ + filepath = sys.argv[sys.argv.index('--') + 1]; \ + bpy.ops.bim.load_project(filepath=filepath)" -- "$1" + CheckedForUserDefaultShell + + inputMethod + 1 + shell + /bin/zsh + source + + + BundleIdentifier + com.apple.RunShellScript + Class Name + RunShellScriptAction + + + + workflowMetaData + + workflowTypeIdentifier + com.apple.Automator.application + + +