165 lines
5.7 KiB
Python
165 lines
5.7 KiB
Python
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import bpy
|
|
|
|
from . import __package__ as base_package
|
|
|
|
def get_addon_prefs(context=None):
|
|
if not context:
|
|
context = bpy.context
|
|
|
|
addons = context.preferences.addons
|
|
if base_package.startswith('bl_ext'):
|
|
# 4.2 and later
|
|
name = base_package
|
|
else:
|
|
# Pre-4.2
|
|
name = base_package.split(".")[0]
|
|
|
|
if name in addons:
|
|
prefs = addons[name].preferences
|
|
if prefs == None:
|
|
pass
|
|
# print("This happens when packaging the extension, due to the registration delay.")
|
|
return addons[name].preferences
|
|
|
|
|
|
def update_prefs_on_file(self=None, context=None) -> tuple[str, dict]:
|
|
prefs = get_addon_prefs(context)
|
|
if not type(prefs).loading:
|
|
prefs.save_prefs_to_file()
|
|
|
|
|
|
class PrefsFileSaveLoadMixin:
|
|
"""Mix-in class that can be used by any add-on to store their preferences in a file,
|
|
so that they don't get lost when the add-on is disabled.
|
|
To use it, copy this class and the function above it, and do this in your code:
|
|
|
|
```
|
|
import bpy, json
|
|
from pathlib import Path
|
|
|
|
class MyAddonPrefs(PrefsFileSaveLoadMixin, bpy.types.AddonPreferences):
|
|
some_prop: bpy.props.IntProperty(update=update_prefs_on_file)
|
|
|
|
def register():
|
|
bpy.utils.register_class(MyAddonPrefs)
|
|
MyAddonPrefs.register_autoload_from_file()
|
|
|
|
def unregister():
|
|
update_prefs_on_file()
|
|
```
|
|
|
|
"""
|
|
|
|
# List of property names to not write to disk.
|
|
omit_from_disk: list[str] = []
|
|
|
|
loading = False
|
|
|
|
@staticmethod
|
|
def register_autoload_from_file(delay=0.5):
|
|
def timer_func(_scene=None):
|
|
prefs = get_addon_prefs()
|
|
if not prefs:
|
|
return 1
|
|
prefs.load_prefs_from_file()
|
|
bpy.app.timers.register(timer_func, first_interval=delay)
|
|
|
|
def prefs_to_dict_recursive(self, propgroup: 'IDPropertyGroup') -> dict:
|
|
"""Recursively convert AddonPreferences to a dictionary.
|
|
Note that AddonPreferences don't support PointerProperties,
|
|
so this function doesn't either."""
|
|
from rna_prop_ui import IDPropertyGroup
|
|
|
|
ret = {}
|
|
|
|
rna_class = None
|
|
if isinstance(propgroup, bpy.types.AddonPreferences):
|
|
prop_dict = {key:getattr(propgroup, key) for key in propgroup.bl_rna.properties.keys() if key not in ('rna_type', 'bl_idname')}
|
|
else:
|
|
property_group_class_name = type(propgroup).__name__
|
|
rna_class = bpy.types.PropertyGroup.bl_rna_get_subclass_py(
|
|
property_group_class_name
|
|
)
|
|
if not hasattr(rna_class, 'properties'):
|
|
rna_class = None
|
|
prop_dict = {key:getattr(propgroup, key) for key in propgroup.bl_rna.properties.keys() if key not in ('rna_type')}
|
|
|
|
for key, value in prop_dict.items():
|
|
if key in type(self).omit_from_disk:
|
|
continue
|
|
if type(value) in (list, bpy.types.bpy_prop_collection_idprop):
|
|
ret[key] = [self.prefs_to_dict_recursive(elem) for elem in value]
|
|
elif type(value) == IDPropertyGroup:
|
|
ret[key] = self.prefs_to_dict_recursive(value)
|
|
else:
|
|
if (
|
|
rna_class
|
|
and key in rna_class.properties
|
|
and hasattr(rna_class.properties[key], 'enum_items')
|
|
):
|
|
# Save enum values as string, not int.
|
|
ret[key] = rna_class.properties[key].enum_items[value].identifier
|
|
else:
|
|
ret[key] = value
|
|
return ret
|
|
|
|
def apply_prefs_from_dict_recursive(self, propgroup, data):
|
|
for key, value in data.items():
|
|
if not hasattr(propgroup, key):
|
|
# Property got removed or renamed in the implementation.
|
|
continue
|
|
if type(value) == list:
|
|
for elem in value:
|
|
collprop = getattr(propgroup, key)
|
|
entry = collprop.get(elem['name'])
|
|
if not entry:
|
|
entry = collprop.add()
|
|
self.apply_prefs_from_dict_recursive(entry, elem)
|
|
elif type(value) == dict:
|
|
self.apply_prefs_from_dict_recursive(getattr(propgroup, key), value)
|
|
else:
|
|
setattr(propgroup, key, value)
|
|
|
|
@staticmethod
|
|
def get_prefs_filepath() -> Path:
|
|
addon_name = __package__.split(".")[-1]
|
|
return Path(bpy.utils.user_resource('CONFIG')) / Path(addon_name + ".json")
|
|
|
|
def save_prefs_to_file(self, _context=None):
|
|
data_dict = self.prefs_to_dict_recursive(propgroup=self)
|
|
|
|
filepath = self.get_prefs_filepath()
|
|
|
|
with open(filepath, "w") as f:
|
|
json.dump(data_dict, f, indent=4)
|
|
|
|
return filepath, json.dumps(data_dict, indent=4)
|
|
|
|
def load_prefs_from_file(self) -> bool:
|
|
filepath = self.get_prefs_filepath()
|
|
if not filepath.exists():
|
|
return False
|
|
|
|
success = True
|
|
with open(filepath, "r") as f:
|
|
addon_data = json.load(f)
|
|
type(self).loading = True
|
|
try:
|
|
self.apply_prefs_from_dict_recursive(self, addon_data)
|
|
except Exception as exc:
|
|
# If we get an error raised here, and it isn't handled,
|
|
# the add-on seems to break.
|
|
print(
|
|
f"{base_package}: Failed to load {__package__} preferences from file."
|
|
)
|
|
raise exc
|
|
type(self).loading = False
|
|
return success
|