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:
parent
c0e4de8457
commit
9cb3a17352
3
scripts/modules/_bpy_internal/__init__.py
Normal file
3
scripts/modules/_bpy_internal/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2017-2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
573
scripts/modules/_bpy_internal/freedesktop.py
Normal file
573
scripts/modules/_bpy_internal/freedesktop.py
Normal file
@ -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` */
|
||||
|
||||
|
84
source/blender/windowmanager/intern/wm_platform.cc
Normal file
84
source/blender/windowmanager/intern/wm_platform.cc
Normal file
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user