Compare commits

...

2 Commits

Author SHA1 Message Date
Andrej730 d129fe2863 Bonsai Mac app - allow to be called without arguments 2025-07-29 20:10:42 +05:00
Andrej730 a43efffde2 Bonsai Mac app to quickly open .ifc files 2025-07-29 15:01:27 +05:00
5 changed files with 195 additions and 4 deletions
+1
View File
@@ -110,6 +110,7 @@ classes = [
operator.ClippingPlaneCutWithCappings,
operator.CloseBlendWarning,
operator.CloseError,
operator.CreateMacBonsaiApp,
operator.CopyTextToClipboard,
operator.EditBlenderCollection,
operator.FileAssociate,
+96 -2
View File
@@ -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"
+6 -2
View File
@@ -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)
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AMIsApplet</key>
<true/>
<key>AMStayOpen</key>
<false/>
<key>CFBundleExecutable</key>
<string>Automator Application Stub</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>com.apple.automator.Bonsai</string>
<key>CFBundleName</key>
<string>Bonsai</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>ifc</string>
</array>
<key>CFBundleTypeName</key>
<string>IFC File</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
</array>
<key>LSUIElement</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AMApplicationBuild</key>
<string>526</string>
<key>AMApplicationVersion</key>
<string>2.10</string>
<key>AMDocumentVersion</key>
<string>2</string>
<key>actions</key>
<array>
<dict>
<key>action</key>
<dict>
<key>ActionBundlePath</key>
<string>/System/Library/Automator/Run Shell Script.action</string>
<key>ActionParameters</key>
<dict>
<key>COMMAND_STRING</key>
<string>
BLENDER="{{BLENDER_BINARY}}"
if [[ $# -eq 1 &amp;&amp; -f "$1" ]]; then
"$BLENDER" --python-expr "import bpy; \
import sys; \
filepath = sys.argv[sys.argv.index('--') + 1]; \
bpy.ops.bim.load_project(filepath=filepath) \
" -- "$1"
else
"$BLENDER" --python-expr "import bpy; \
bpy.ops.bim.create_project()\
"
fi
</string>
<key>CheckedForUserDefaultShell</key>
<true/>
<key>inputMethod</key>
<integer>1</integer>
<key>shell</key>
<string>/bin/zsh</string>
<key>source</key>
<string></string>
</dict>
<key>BundleIdentifier</key>
<string>com.apple.RunShellScript</string>
<key>Class Name</key>
<string>RunShellScriptAction</string>
</dict>
</dict>
</array>
<key>workflowMetaData</key>
<dict>
<key>workflowTypeIdentifier</key>
<string>com.apple.Automator.application</string>
</dict>
</dict>
</plist>