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
This commit is contained in:
Campbell Barton 2024-04-05 11:38:21 +11:00
parent c0e4de8457
commit 9cb3a17352
9 changed files with 807 additions and 77 deletions

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2017-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later

@ -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("""<?xml version="1.0" encoding="UTF-8"?>\n""")
fh.write("""<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">\n""")
fh.write(""" <mime-type type="{:s}">\n""".format(BLENDER_MIME))
# NOTE: not using a trailing full-stop seems to be the convention here.
fh.write(""" <comment>Blender scene</comment>\n""")
fh.write(""" <glob pattern="*.blend"/>\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(""" <icon name="application-x-blender"/>\n""")
fh.write(""" </mime-type>\n""")
fh.write("""</mime-info>\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())

@ -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'

@ -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}")

@ -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)

@ -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

@ -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` */

@ -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;
}
/** \} */

@ -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;
}