From 9cb3a17352bd7ed57df82f35a56dcf0af85fbf03 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Fri, 5 Apr 2024 11:38:21 +1100 Subject: [PATCH] Linux: freedesktop support for file type association Support freedesktop file association on Linux/Unix via the command line arguments: `--register{-allusers}` `--unregister{-allusers}` as well registration actions from the user preferences. Once registered, the "Blender" application is available from launchers and `*.blend` files are assoisated with the blender binary used for registration. The following operations are performed: - Setup the desktop file. - Setup the file association & make it default. - Copy the icon. - Setup the thumbnailer (`--register-allusers` only). Notes: - Registering/unregistering for all users manipulates files under `/usr/local` and requires running Blender as root. From the command line this can be done using `sudo`, e.g. `sudo ./blender --register-allusers`. From the GUI, the `pkexec` command is used. - Recent versions of GNOME execute the thumbnailer in a restricted environment (`bwrap`) requiring `blender-thumbnailer` to be copied into `/usr/local/bin` (synlinks don't work). So thumbnailing copies the binary rather than linking and only works when registering for all users. Ref !120283 --- scripts/modules/_bpy_internal/__init__.py | 3 + scripts/modules/_bpy_internal/freedesktop.py | 573 ++++++++++++++++++ scripts/startup/bl_ui/space_userpref.py | 21 +- .../editors/space_userpref/CMakeLists.txt | 1 + .../editors/space_userpref/userpref_ops.cc | 104 ++-- source/blender/windowmanager/CMakeLists.txt | 1 + source/blender/windowmanager/WM_api.hh | 7 + .../windowmanager/intern/wm_platform.cc | 84 +++ source/creator/creator_args.cc | 90 ++- 9 files changed, 807 insertions(+), 77 deletions(-) create mode 100644 scripts/modules/_bpy_internal/__init__.py create mode 100644 scripts/modules/_bpy_internal/freedesktop.py create mode 100644 source/blender/windowmanager/intern/wm_platform.cc diff --git a/scripts/modules/_bpy_internal/__init__.py b/scripts/modules/_bpy_internal/__init__.py new file mode 100644 index 00000000000..d8c375b607e --- /dev/null +++ b/scripts/modules/_bpy_internal/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2017-2023 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later diff --git a/scripts/modules/_bpy_internal/freedesktop.py b/scripts/modules/_bpy_internal/freedesktop.py new file mode 100644 index 00000000000..2b930e07c8e --- /dev/null +++ b/scripts/modules/_bpy_internal/freedesktop.py @@ -0,0 +1,573 @@ +# SPDX-FileCopyrightText: 2017-2023 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later + +# TODO: file-type icons are currently not setup. +# Currently `xdg-icon-resource` doesn't support SVG's, so we would need to generate PNG's. +# Or wait until SVG's are supported, see: https://gitlab.freedesktop.org/xdg/xdg-utils/-/merge_requests/41 +# +# NOTE: Typically this will run from Blender, you may also run this directly from Python +# which can be useful for testing. + +__all__ = ( + "register", + "unregister", +) + +import argparse +import os +import shlex +import shutil +import subprocess +import sys +import tempfile + +from typing import ( + Callable, + Optional, +) + +VERBOSE = True + + +# ----------------------------------------------------------------------------- +# Environment + +HOME_DIR = os.path.normpath(os.path.expanduser("~")) + +# https://wiki.archlinux.org/title/XDG_Base_Directory +# Typically: `~/.local/share`. +XDG_DATA_HOME = os.environ.get("XDG_DATA_HOME") or os.path.join(HOME_DIR, ".local", "share") + +HOMEDIR_LOCAL_BIN = os.path.join(HOME_DIR, ".local", "bin") + +BLENDER_ENV = "bpy" in sys.modules + +# ----------------------------------------------------------------------------- +# Programs + +# The command `xdg-mime` handles most of the file assosiation actions. +XDG_MIME_PROG = shutil.which("xdg-mime") or "" + +# Initialize by `bpy` or command line arguments. +BLENDER_BIN = "" +# Set to `os.path.dirname(BLENDER_BIN)`. +BLENDER_DIR = "" + + +# ----------------------------------------------------------------------------- +# Path Constants + +# These files are included along side a portable Blender installation. +BLENDER_DESKTOP = "blender.desktop" +# The target binary. +BLENDER_FILENAME = "blender" +# The target binary (thumbnailer). +BLENDER_THUMBNAILER_FILENAME = "blender-thumbnailer" + + +# ----------------------------------------------------------------------------- +# Other Constants + +# The mime type Blender users. +BLENDER_MIME = "application/x-blender" +# Use `/usr/local` because this is not managed by the systems package manager. +SYSTEM_PREFIX = "/usr/local" + + +# ----------------------------------------------------------------------------- +# Utility Functions + + +# Display a short path, for nicer display only. +def filepath_repr(filepath: str) -> str: + if filepath.startswith(HOME_DIR): + return "~" + filepath[len(HOME_DIR):] + return filepath + + +def system_path_contains(dirpath: str) -> bool: + dirpath = os.path.normpath(dirpath) + for path in os.environ.get("PATH", "").split(os.pathsep): + # `$PATH` can include relative locations. + path = os.path.normpath(os.path.abspath(path)) + if path == dirpath: + return True + return False + + +# When removing files to make way for newly copied file an `os.path.exists` +# check isn't sufficient as the path may be a broken symbolic-link. +def path_exists_or_is_link(path: str) -> bool: + return os.path.exists(path) or os.path.islink(path) + + +def filepath_ensure_removed(path: str) -> bool: + if path_exists_or_is_link(path): + os.remove(path) + return True + return False + + +# ----------------------------------------------------------------------------- +# Handle Associations +# +# On registration when handlers return False this causes registration to fail and unregister to be called. +# Non fatal errors should print a message and return True instead. + +def handle_bin(do_register: bool, all_users: bool) -> Optional[str]: + if all_users: + dirpath_dst = os.path.join(SYSTEM_PREFIX, "bin") + else: + dirpath_dst = HOMEDIR_LOCAL_BIN + + if VERBOSE: + sys.stdout.write("- {:s} symbolic-links in: {:s}\n".format( + ("Setup" if do_register else "Remove"), + filepath_repr(dirpath_dst), + )) + + if do_register: + if not all_users: + if not system_path_contains(dirpath_dst): + sys.stdout.write( + "The PATH environment variable doesn't contain \"{:s}\", not creating symlinks\n".format( + dirpath_dst, + )) + # NOTE: this is not an error, don't consider it a failure. + return None + + os.makedirs(dirpath_dst, exist_ok=True) + + # Full path, then name to create at the destination. + files_to_link = [ + (BLENDER_BIN, BLENDER_FILENAME, False), + ] + + blender_thumbnailer_src = os.path.join(BLENDER_DIR, BLENDER_THUMBNAILER_FILENAME) + if os.path.exists(blender_thumbnailer_src): + # Unfortunately the thumbnailer must be copied for `bwrap` to find it. + files_to_link.append((blender_thumbnailer_src, BLENDER_THUMBNAILER_FILENAME, True)) + else: + sys.stdout.write(" Thumbnailer not found, skipping: \"{:s}\"\n".format(blender_thumbnailer_src)) + + for filepath_src, filename, do_full_copy in files_to_link: + filepath_dst = os.path.join(dirpath_dst, filename) + filepath_ensure_removed(filepath_dst) + if not do_register: + continue + + if not os.path.exists(filepath_src): + sys.stderr.write("File not found, skipping link: \"{:s}\" -> \"{:s}\"\n".format( + filepath_src, filepath_dst, + )) + if do_full_copy: + shutil.copyfile(filepath_src, filepath_dst) + os.chmod(filepath_dst, 0o755) + else: + os.symlink(filepath_src, filepath_dst) + return None + + +def handle_desktop_file(do_register: bool, all_users: bool) -> Optional[str]: + # `cp ./blender.desktop ~/.local/share/applications/` + + filename = BLENDER_DESKTOP + + if all_users: + base_dir = os.path.join(SYSTEM_PREFIX, "share") + else: + base_dir = XDG_DATA_HOME + + dirpath_dst = os.path.join(base_dir, "applications") + + filepath_desktop_src = os.path.join(BLENDER_DIR, filename) + filepath_desktop_dst = os.path.join(dirpath_dst, filename) + + if VERBOSE: + sys.stdout.write("- {:s} desktop-file: {:s}\n".format( + ("Setup" if do_register else "Remove"), + filepath_repr(filepath_desktop_dst), + )) + + filepath_ensure_removed(filepath_desktop_dst) + if not do_register: + return None + + if not os.path.exists(filepath_desktop_src): + # Unlike other missing things, this must be an error otherwise + # the MIME association fails which is the main purpose of registering types. + return "Error: desktop file not found: {:s}".format(filepath_desktop_src) + + os.makedirs(dirpath_dst, exist_ok=True) + + with open(filepath_desktop_src, "r", encoding="utf-8") as fh: + data = fh.read() + + data = data.replace("\nExec=blender %f\n", "\nExec={:s} %f\n".format(BLENDER_BIN)) + + with open(filepath_desktop_dst, "w", encoding="utf-8") as fh: + fh.write(data) + return None + + +def handle_thumbnailer(do_register: bool, all_users: bool) -> Optional[str]: + filename = "blender.thumbnailer" + + if all_users: + base_dir = os.path.join(SYSTEM_PREFIX, "share") + else: + base_dir = XDG_DATA_HOME + + dirpath_dst = os.path.join(base_dir, "thumbnailers") + filepath_thumbnailer_dst = os.path.join(dirpath_dst, filename) + + if VERBOSE: + sys.stdout.write("- {:s} thumbnailer: {:s}\n".format( + ("Setup" if do_register else "Remove"), + filepath_repr(filepath_thumbnailer_dst), + )) + + filepath_ensure_removed(filepath_thumbnailer_dst) + if not do_register: + return None + + blender_thumbnailer_bin = os.path.join(BLENDER_DIR, BLENDER_THUMBNAILER_FILENAME) + if not os.path.exists(blender_thumbnailer_bin): + sys.stderr.write("Thumbnailer not found, this may not be a portable installation: {:s}\n".format( + blender_thumbnailer_bin, + )) + return None + + os.makedirs(dirpath_dst, exist_ok=True) + + # NOTE: unfortunately this can't be `blender_thumbnailer_bin` because GNOME calls the command + # with wrapper that means the command *must* be in the users `$PATH`. + # and it cannot be a SYMLINK. + if shutil.which("bwrap") is not None: + command = BLENDER_THUMBNAILER_FILENAME + else: + command = blender_thumbnailer_bin + + with open(filepath_thumbnailer_dst, "w", encoding="utf-8") as fh: + fh.write("[Thumbnailer Entry]\n") + fh.write("TryExec={:s}\n".format(command)) + fh.write("Exec={:s} %i %o\n".format(command)) + fh.write("MimeType={:s};\n".format(BLENDER_MIME)) + return None + + +def handle_mime_association_xml(do_register: bool, all_users: bool) -> Optional[str]: + # `xdg-mime install x-blender.xml` + filename = "x-blender.xml" + + if all_users: + base_dir = os.path.join(SYSTEM_PREFIX, "share") + else: + base_dir = XDG_DATA_HOME + + # Ensure directories exist `xdg-mime` will fail with an error if these don't exist. + for dirpath_dst in ( + os.path.join(base_dir, "mime", "application"), + os.path.join(base_dir, "mime", "packages") + ): + os.makedirs(dirpath_dst, exist_ok=True) + del dirpath_dst + + # Unfortunately there doesn't seem to be a way to know the installed location. + # Use hard-coded location. + package_xml_dst = os.path.join(base_dir, "mime", "application", filename) + + if VERBOSE: + sys.stdout.write("- {:s} mime type: {:s}\n".format( + ("Setup" if do_register else "Remove"), + filepath_repr(package_xml_dst), + )) + + env = { + **os.environ, + "XDG_DATA_DIRS": os.path.join(SYSTEM_PREFIX, "share") + } + + if not do_register: + if not os.path.exists(package_xml_dst): + return None + # NOTE: `xdg-mime query default application/x-blender` could be used to check + # if the XML is installed, however there is some slim chance the XML is installed + # but the default doesn't point to Blender, just uninstall as it's harmless. + cmd = ( + XDG_MIME_PROG, + "uninstall", + "--mode", "system" if all_users else "user", + package_xml_dst, + ) + subprocess.check_output(cmd, env=env) + return None + + with tempfile.TemporaryDirectory() as tempdir: + package_xml_src = os.path.join(tempdir, filename) + with open(package_xml_src, mode="w", encoding="utf-8") as fh: + fh.write("""\n""") + fh.write("""\n""") + fh.write(""" \n""".format(BLENDER_MIME)) + # NOTE: not using a trailing full-stop seems to be the convention here. + fh.write(""" Blender scene\n""") + fh.write(""" \n""") + # TODO: this doesn't seem to work, GNOME's Nautilus & KDE's Dolphin + # already have a file-type icon for this so we might consider this low priority. + if False: + fh.write(""" \n""") + fh.write(""" \n""") + fh.write("""\n""") + + cmd = ( + XDG_MIME_PROG, + "install", + "--mode", "system" if all_users else "user", + package_xml_src, + ) + subprocess.check_output(cmd, env=env) + return None + + +def handle_mime_association_default(do_register: bool, all_users: bool) -> Optional[str]: + # `xdg-mime default blender.desktop application/x-blender` + + if VERBOSE: + sys.stdout.write("- {:s} mime type as default\n".format( + ("Setup" if do_register else "Remove"), + )) + + # NOTE: there doesn't seem to be a way to reverse this action. + if not do_register: + return None + + cmd = ( + XDG_MIME_PROG, + "default", + BLENDER_DESKTOP, + BLENDER_MIME, + ) + subprocess.check_output(cmd) + return None + + +def handle_icon(do_register: bool, all_users: bool) -> Optional[str]: + filename = "blender.svg" + if all_users: + base_dir = os.path.join(SYSTEM_PREFIX, "share") + else: + base_dir = XDG_DATA_HOME + + dirpath_dst = os.path.join(base_dir, "icons", "hicolor", "scalable", "apps") + + filepath_desktop_src = os.path.join(BLENDER_DIR, filename) + filepath_desktop_dst = os.path.join(dirpath_dst, filename) + + if VERBOSE: + sys.stdout.write("- {:s} icon: {:s}\n".format( + ("Setup" if do_register else "Remove"), + filepath_repr(filepath_desktop_dst), + )) + + filepath_ensure_removed(filepath_desktop_dst) + if not do_register: + return None + + if not os.path.exists(filepath_desktop_src): + sys.stderr.write(" Icon file not found, skipping: \"{:s}\"\n".format(filepath_desktop_src)) + # Not an error. + return None + + os.makedirs(dirpath_dst, exist_ok=True) + + with open(filepath_desktop_src, "rb") as fh: + data = fh.read() + + with open(filepath_desktop_dst, "wb") as fh: + fh.write(data) + + return None + + +# ----------------------------------------------------------------------------- +# Escalate Privileges + +def main_run_as_root(do_register: bool) -> Optional[str]: + # If the system prefix doesn't exist, fail with an error because it's highly likely that the + # system won't use this when it has not been created. + if not os.path.exists(SYSTEM_PREFIX): + return "Error: system path does not exist {!r}".format(SYSTEM_PREFIX) + + prog: Optional[str] = shutil.which("pkexec") + if prog is None: + return "Error: command \"pkexec\" not found" + + cmd = [ + prog, + sys.executable, + # Skips users `site-packages`. + "-s", + __file__, + BLENDER_BIN, + "--action={:s}".format("register-allusers" if do_register else "unregister-allusers"), + ] + if VERBOSE: + sys.stdout.write("Executing: {:s}\n".format(shlex.join(cmd))) + + proc = subprocess.run(cmd, stderr=subprocess.PIPE) + if proc.returncode != 0: + if proc.stderr: + return proc.stderr.decode("utf-8", errors="surrogateescape") + return "Error: pkexec returned non-zero returncode" + + return None + + +# ----------------------------------------------------------------------------- +# Checked Call +# +# While exceptions should not happen, we can't entirely prevent this as it's always possible +# a file write fails or a command doesn't work as expected anymore. +# Handle these cases gracefully. + +def call_handle_checked( + fn: Callable[[bool, bool], Optional[str]], + *, + do_register: bool, + all_users: bool +) -> Optional[str]: + try: + result = fn(do_register, all_users) + except BaseException as ex: + # This should never happen. + result = "Internal Error: {!r}".format(ex) + return result + + +# ----------------------------------------------------------------------------- +# Main Registration Functions + +def register_impl(do_register: bool, all_users: bool) -> Optional[str]: + # A non-empty string indicates an error (which is forwarded to the user), otherwise None for success. + + global BLENDER_BIN + global BLENDER_DIR + + if BLENDER_ENV: + # Only use of `bpy`. + BLENDER_BIN = os.path.normpath(__import__("bpy").app.binary_path) + + # Running inside Blender, detect the need for privilege escalation (which will run outside of Blender). + if all_users: + if os.geteuid() != 0: + # Run this script with escalated privileges. + return main_run_as_root(do_register) + else: + assert BLENDER_BIN != "" + + BLENDER_DIR = os.path.dirname(BLENDER_BIN) + + if all_users: + if not os.access(SYSTEM_PREFIX, os.W_OK): + return "Error: {:s} not writable, this command may need to run as a superuser!".format(SYSTEM_PREFIX) + + if VERBOSE: + sys.stdout.write("{:s}: {:s}\n".format("Register" if do_register else "Unregister", BLENDER_BIN)) + + if XDG_MIME_PROG == "": + return "Could not find \"xdg-mime\", unable to associate mime-types" + + handlers = ( + handle_bin, + handle_icon, + handle_desktop_file, + handle_mime_association_xml, + # This only makes sense for users, although there may be a way to do this for all users. + *(() if all_users else (handle_mime_association_default,)), + # The thumbnailer only works when installed for all users. + *((handle_thumbnailer,) if all_users else ()), + ) + + error_or_none = None + for i, fn in enumerate(handlers): + if (error_or_none := call_handle_checked(fn, do_register=do_register, all_users=all_users)) is not None: + break + + if error_or_none is not None: + # Roll back registration on failure. + if do_register: + for fn in reversed(handlers[:i + 1]): + error_or_none_reverse = call_handle_checked(fn, do_register=False, all_users=all_users) + if error_or_none_reverse is not None: + sys.stdout.write("Error reverting action: {:s}\n".format(error_or_none_reverse)) + + # Print to the `stderr`, in case the user has a console open, it can be helpful + # especially if it's multi-line. + sys.stdout.write("{:s}\n".format(error_or_none)) + + return error_or_none + + +def register(all_users: bool = False) -> Optional[str]: + # Return an empty string for success. + return register_impl(True, all_users) + + +def unregister(all_users: bool = False) -> Optional[str]: + # Return an empty string for success. + return register_impl(False, all_users) + + +# ----------------------------------------------------------------------------- +# Running directly (Escalated Privileges) +# +# Needed when running as an administer. + +register_actions = { + "register": (True, False), + "unregister": (False, False), + "register-allusers": (True, True), + "unregister-allusers": (False, True), +} + + +def argparse_create() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + parser.add_argument( + "blender_bin", + metavar="BLENDER_BIN", + type=str, + help="The location of Blender's binary", + ) + + parser.add_argument( + "--action", + choices=register_actions.keys(), + dest="register_action", + required=True, + ) + + return parser + + +def main() -> int: + global BLENDER_BIN + assert BLENDER_BIN == "" + args = argparse_create().parse_args() + BLENDER_BIN = args.blender_bin + do_register, all_users = register_actions[args.register_action] + + if do_register: + result = register(all_users=all_users) + else: + result = unregister(all_users=all_users) + + if result: + sys.stderr.write("{:s}\n".format(result)) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/startup/bl_ui/space_userpref.py b/scripts/startup/bl_ui/space_userpref.py index f19e73ee97f..de44d09d96c 100644 --- a/scripts/startup/bl_ui/space_userpref.py +++ b/scripts/startup/bl_ui/space_userpref.py @@ -667,15 +667,22 @@ class USERPREF_PT_system_os_settings(SystemPanel, CenterAlignMixIn, Panel): @classmethod def poll(cls, _context): - # Only for Windows so far - import sys - return sys.platform[:3] == "win" + # macOS isn't supported. + from sys import platform + if platform == "darwin": + return False + return True def draw_centered(self, context, layout): - if context.preferences.system.is_microsoft_store_install: - layout.label(text="Microsoft Store installation") - layout.label(text="Use Windows 'Default Apps' to associate with blend files") - else: + from sys import platform + associate_supported = True + if platform[:3] == "win": + if context.preferences.system.is_microsoft_store_install: + layout.label(text="Microsoft Store installation") + layout.label(text="Use Windows 'Default Apps' to associate with blend files") + associate_supported = False + + if associate_supported: layout.label(text="Open blend files with this Blender version") split = layout.split(factor=0.5) split.alignment = 'LEFT' diff --git a/source/blender/editors/space_userpref/CMakeLists.txt b/source/blender/editors/space_userpref/CMakeLists.txt index b7446ab09bf..6e8b7aff86a 100644 --- a/source/blender/editors/space_userpref/CMakeLists.txt +++ b/source/blender/editors/space_userpref/CMakeLists.txt @@ -30,6 +30,7 @@ set(LIB PRIVATE bf::dna PRIVATE bf::extern::fmtlib PRIVATE bf::intern::guardedalloc + bf_windowmanager ) blender_add_lib(bf_editor_space_userpref "${SRC}" "${INC}" "${INC_SYS}" "${LIB}") diff --git a/source/blender/editors/space_userpref/userpref_ops.cc b/source/blender/editors/space_userpref/userpref_ops.cc index fb90aa72f4a..abcf7c8051e 100644 --- a/source/blender/editors/space_userpref/userpref_ops.cc +++ b/source/blender/editors/space_userpref/userpref_ops.cc @@ -776,46 +776,66 @@ static bool associate_blend_poll(bContext *C) return false; } return true; -#else - CTX_wm_operator_poll_msg_set(C, "Windows-only operator"); +#elif defined(__APPLE__) + CTX_wm_operator_poll_msg_set(C, "Windows & Linux only operator"); return false; +#else + UNUSED_VARS(C); + return true; #endif } +static bool assosiate_blend(bool do_register, bool all_users, char **error_msg) +{ + const bool result = WM_platform_assosiate_set(do_register, all_users, error_msg); +#ifdef WIN32 + if ((result == false) && + /* For some reason the message box isn't shown in this case. */ + (all_users == false)) + { + const char *msg = do_register ? "Unable to register file association" : + "Unable to unregister file association"; + MessageBox(0, msg, "Blender", MB_OK | MB_ICONERROR); + } +#endif /* !WIN32 */ + return result; +} + static int associate_blend_exec(bContext * /*C*/, wmOperator *op) { -#ifdef WIN32 +#ifdef __APPLE__ + UNUSED_VARS(op); + BLI_assert_unreachable(); + return OPERATOR_CANCELLED; +#else + +# ifdef WIN32 if (BLI_windows_is_store_install()) { BKE_report( op->reports, RPT_ERROR, "Registration not possible from Microsoft Store installations"); return OPERATOR_CANCELLED; } +# endif const bool all_users = (U.uiflag & USER_REGISTER_ALL_USERS); + char *error_msg = nullptr; WM_cursor_wait(true); + const bool success = assosiate_blend(true, all_users, &error_msg); + WM_cursor_wait(false); - if (all_users && BLI_windows_execute_self("--register-allusers", true, true, true)) { - BKE_report(op->reports, RPT_INFO, "File association registered"); - WM_cursor_wait(false); - return OPERATOR_FINISHED; - } - else if (!all_users && BLI_windows_register_blend_extension(false)) { - BKE_report(op->reports, RPT_INFO, "File association registered"); - WM_cursor_wait(false); - return OPERATOR_FINISHED; - } - else { - BKE_report(op->reports, RPT_ERROR, "Unable to register file association"); - WM_cursor_wait(false); - MessageBox(0, "Unable to register file association", "Blender", MB_OK | MB_ICONERROR); + if (!success) { + BKE_report( + op->reports, RPT_ERROR, error_msg ? error_msg : "Unable to register file association"); + if (error_msg) { + MEM_freeN(error_msg); + } return OPERATOR_CANCELLED; } -#else - UNUSED_VARS(op); - BLI_assert_unreachable(); - return OPERATOR_CANCELLED; -#endif + BLI_assert(error_msg == nullptr); + BKE_report(op->reports, RPT_INFO, "File association registered"); + return OPERATOR_FINISHED; +#endif /* !__APPLE__ */ } static void PREFERENCES_OT_associate_blend(wmOperatorType *ot) @@ -832,38 +852,38 @@ static void PREFERENCES_OT_associate_blend(wmOperatorType *ot) static int unassociate_blend_exec(bContext * /*C*/, wmOperator *op) { -#ifdef WIN32 +#ifdef __APPLE__ + UNUSED_VARS(op); + BLI_assert_unreachable(); + return OPERATOR_CANCELLED; +#else +# ifdef WIN32 if (BLI_windows_is_store_install()) { BKE_report( op->reports, RPT_ERROR, "Unregistration not possible from Microsoft Store installations"); return OPERATOR_CANCELLED; } +# endif const bool all_users = (U.uiflag & USER_REGISTER_ALL_USERS); + char *error_msg = nullptr; WM_cursor_wait(true); + bool success = assosiate_blend(false, all_users, &error_msg); + WM_cursor_wait(false); - if (all_users && BLI_windows_execute_self("--unregister-allusers", true, true, true)) { - BKE_report(op->reports, RPT_INFO, "File association unregistered"); - WM_cursor_wait(false); - return OPERATOR_FINISHED; - } - else if (!all_users && BLI_windows_unregister_blend_extension(false)) { - BKE_report(op->reports, RPT_INFO, "File association unregistered"); - WM_cursor_wait(false); - return OPERATOR_FINISHED; - } - else { - BKE_report(op->reports, RPT_ERROR, "Unable to unregister file association"); - WM_cursor_wait(false); - MessageBox(0, "Unable to unregister file association", "Blender", MB_OK | MB_ICONERROR); + if (!success) { + BKE_report( + op->reports, RPT_ERROR, error_msg ? error_msg : "Unable to unregister file association"); + if (error_msg) { + MEM_freeN(error_msg); + } return OPERATOR_CANCELLED; } -#else - UNUSED_VARS(op); - BLI_assert_unreachable(); - return OPERATOR_CANCELLED; -#endif + BLI_assert(error_msg == nullptr); + BKE_report(op->reports, RPT_INFO, "File association unregistered"); + return OPERATOR_FINISHED; +#endif /* !__APPLE__ */ } static void PREFERENCES_OT_unassociate_blend(wmOperatorType *ot) diff --git a/source/blender/windowmanager/CMakeLists.txt b/source/blender/windowmanager/CMakeLists.txt index bb247962f7d..87dd11cc376 100644 --- a/source/blender/windowmanager/CMakeLists.txt +++ b/source/blender/windowmanager/CMakeLists.txt @@ -49,6 +49,7 @@ set(SRC intern/wm_operator_utils.cc intern/wm_operators.cc intern/wm_panel_type.cc + intern/wm_platform.cc intern/wm_platform_support.cc intern/wm_playanim.cc intern/wm_splash_screen.cc diff --git a/source/blender/windowmanager/WM_api.hh b/source/blender/windowmanager/WM_api.hh index 872b533f020..919d839a1a1 100644 --- a/source/blender/windowmanager/WM_api.hh +++ b/source/blender/windowmanager/WM_api.hh @@ -1874,6 +1874,13 @@ void WM_generic_user_data_free(wmGenericUserData *wm_userdata); bool WM_region_use_viewport(ScrArea *area, ARegion *region); +/* `wm_platform.cc` */ + +/** + * \return Success. + */ +bool WM_platform_assosiate_set(bool do_register, bool all_users, char **r_error_msg); + #ifdef WITH_XR_OPENXR /* `wm_xr_session.cc` */ diff --git a/source/blender/windowmanager/intern/wm_platform.cc b/source/blender/windowmanager/intern/wm_platform.cc new file mode 100644 index 00000000000..118c09bbf01 --- /dev/null +++ b/source/blender/windowmanager/intern/wm_platform.cc @@ -0,0 +1,84 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup wm + * + * Interactions with the underlying platform. + */ + +#include "BLI_string.h" + +#include "WM_api.hh" /* Own include. */ + +#ifdef WIN32 +# include "BLI_winstuff.h" +#elif defined(__APPLE__) +/* Pass. */ +#else +# include "BKE_context.hh" + +# include "BPY_extern_run.h" +#endif + +/* -------------------------------------------------------------------- */ +/** \name Register File Assosiation + * \{ */ + +bool WM_platform_assosiate_set(bool do_register, bool all_users, char **r_error_msg) +{ + bool result = false; + *r_error_msg = nullptr; +#ifdef WIN32 + { + if (all_users) { + if (do_register) { + result = BLI_windows_execute_self("--register-allusers", true, true, true); + } + else { + result = BLI_windows_execute_self("--unregister-allusers", true, true, true); + } + } + else { + if (do_register) { + result = BLI_windows_register_blend_extension(false); + } + else { + result = BLI_windows_unregister_blend_extension(false); + } + } + } +#elif defined(__APPLE__) + /* Pass. */ +#else + { + BPy_RunErrInfo err_info = {}; + err_info.use_single_line_error = true; + err_info.r_string = r_error_msg; + + const char *imports[] = {"_bpy_internal", "_bpy_internal.freedesktop", nullptr}; + char expr_buf[128]; + + SNPRINTF(expr_buf, + "_bpy_internal.freedesktop.%s(all_users=%d)", + do_register ? "register" : "unregister", + int(all_users)); + + /* NOTE: this could be null, however the running a script without `bpy.context` access + * is a rare enough situation that it's better to keep this a requirement of the API and + * pass in a temporary context instead of making an exception for this one case. */ + bContext *C_temp = CTX_create(); + char *value = nullptr; + if (BPY_run_string_as_string_or_none(C_temp, imports, expr_buf, &err_info, &value)) { + result = (value == nullptr); + *r_error_msg = value; + } + /* Else `r_error_msg` will be set to a single line exception. */ + CTX_free(C_temp); + } +#endif + return result; +} + +/** \} */ diff --git a/source/creator/creator_args.cc b/source/creator/creator_args.cc index 07ffa0c7d13..1c658034c31 100644 --- a/source/creator/creator_args.cc +++ b/source/creator/creator_args.cc @@ -1646,57 +1646,91 @@ static int arg_handle_start_with_console(int /*argc*/, const char ** /*argv*/, v return 0; } -static const char arg_handle_register_extension_doc[] = - "\n\t" - "Register blend-file extension for current user, then exit (Windows only)."; -static int arg_handle_register_extension(int /*argc*/, const char ** /*argv*/, void * /*data*/) +static bool arg_handle_extension_registration(const bool do_register, const bool all_users) { + /* Logic runs in #main_args_handle_registration. */ + char *error_msg = nullptr; + bool result = WM_platform_assosiate_set(do_register, all_users, &error_msg); + if (error_msg) { + fprintf(stderr, "Error: %s\n", error_msg); + MEM_freeN(error_msg); + } + # ifdef WIN32 - G.background = 1; - BLI_windows_register_blend_extension(false); TerminateProcess(GetCurrentProcess(), 0); # endif - return 0; + return result; +} + +static const char arg_handle_register_extension_doc[] = + "\n\t" + "Register blend-file extension for current user, then exit (Windows & Linux only)."; +static int arg_handle_register_extension(int argc, const char **argv, void *data) +{ + G.quiet = true; + background_mode_set(); + +# if !(defined(WIN32) && defined(__APPLE__)) + if (!main_arg_deferred_is_set()) { + main_arg_deferred_setup(arg_handle_register_extension, argc, argv, data); + return argc - 1; + } +# endif + arg_handle_extension_registration(true, false); + return argc - 1; } static const char arg_handle_register_extension_all_doc[] = "\n\t" - "Register blend-file extension for all users, then exit (Windows only)."; -static int arg_handle_register_extension_all(int /*argc*/, const char ** /*argv*/, void * /*data*/) + "Register blend-file extension for all users, then exit (Windows & Linux only)."; +static int arg_handle_register_extension_all(int argc, const char **argv, void *data) { -# ifdef WIN32 - G.background = 1; - BLI_windows_register_blend_extension(true); - TerminateProcess(GetCurrentProcess(), 0); + G.quiet = true; + background_mode_set(); + +# if !(defined(WIN32) && defined(__APPLE__)) + if (!main_arg_deferred_is_set()) { + main_arg_deferred_setup(arg_handle_register_extension_all, argc, argv, data); + return argc - 1; + } # endif - return 0; + arg_handle_extension_registration(true, true); + return argc - 1; } static const char arg_handle_unregister_extension_doc[] = "\n\t" - "Unregister blend-file extension for current user, then exit (Windows only)."; -static int arg_handle_unregister_extension(int /*argc*/, const char ** /*argv*/, void * /*data*/) + "Unregister blend-file extension for current user, then exit (Windows & Linux only)."; +static int arg_handle_unregister_extension(int argc, const char **argv, void *data) { -# ifdef WIN32 - G.background = 1; - BLI_windows_unregister_blend_extension(false); - TerminateProcess(GetCurrentProcess(), 0); + G.quiet = true; + background_mode_set(); + +# if !(defined(WIN32) && defined(__APPLE__)) + if (!main_arg_deferred_is_set()) { + main_arg_deferred_setup(arg_handle_unregister_extension, argc, argv, data); + return argc - 1; + } # endif + arg_handle_extension_registration(false, false); return 0; } static const char arg_handle_unregister_extension_all_doc[] = "\n\t" - "Unregister blend-file extension for all users, then exit (Windows only)."; -static int arg_handle_unregister_extension_all(int /*argc*/, - const char ** /*argv*/, - void * /*data*/) + "Unregister blend-file extension for all users, then exit (Windows & Linux only)."; +static int arg_handle_unregister_extension_all(int argc, const char **argv, void *data) { -# ifdef WIN32 - G.background = 1; - BLI_windows_unregister_blend_extension(true); - TerminateProcess(GetCurrentProcess(), 0); + G.quiet = true; + background_mode_set(); + +# if !(defined(WIN32) && defined(__APPLE__)) + if (!main_arg_deferred_is_set()) { + main_arg_deferred_setup(arg_handle_unregister_extension_all, argc, argv, data); + return argc - 1; + } # endif + arg_handle_extension_registration(false, true); return 0; }