294 lines
9.4 KiB
Python
Executable File
294 lines
9.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-FileCopyrightText: 2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
"""
|
|
Generates 'userdef_default_theme.c' from a 'userpref.blend' file.
|
|
|
|
Pass your user preferences blend file to this script to update the C source file.
|
|
|
|
eg:
|
|
|
|
./tools/utils/blender_theme_as_c.py ~/.config/blender/2.80/config/userpref.blend
|
|
|
|
.. or find the latest:
|
|
|
|
./tools/utils/blender_theme_as_c.py $(find ~/.config/blender -name "userpref.blend" | sort | tail -1)
|
|
"""
|
|
|
|
C_SOURCE_HEADER = r'''/* SPDX-FileCopyrightText: 2018 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
/**
|
|
* Generated by 'tools/utils/blender_theme_as_c.py'
|
|
*
|
|
* Do not hand edit this file!
|
|
*/
|
|
|
|
#include "DNA_userdef_types.h"
|
|
|
|
#include "BLO_readfile.h"
|
|
|
|
/* clang-format off */
|
|
|
|
#ifdef __LITTLE_ENDIAN__
|
|
# define RGBA(c) {((c) >> 24) & 0xff, ((c) >> 16) & 0xff, ((c) >> 8) & 0xff, (c) & 0xff}
|
|
# define RGB(c) {((c) >> 16) & 0xff, ((c) >> 8) & 0xff, (c) & 0xff}
|
|
#else
|
|
# define RGBA(c) {(c) & 0xff, ((c) >> 8) & 0xff, ((c) >> 16) & 0xff, ((c) >> 24) & 0xff}
|
|
# define RGB(c) {(c) & 0xff, ((c) >> 8) & 0xff, ((c) >> 16) & 0xff}
|
|
#endif
|
|
|
|
'''
|
|
|
|
|
|
def round_float_32(f):
|
|
from struct import pack, unpack
|
|
return unpack("f", pack("f", f))[0]
|
|
|
|
|
|
def repr_f32(f):
|
|
f_round = round_float_32(f)
|
|
f_str = repr(f)
|
|
f_str_frac = f_str.partition(".")[2]
|
|
if not f_str_frac:
|
|
return f_str
|
|
for i in range(1, len(f_str_frac)):
|
|
f_test = round(f, i)
|
|
f_test_round = round_float_32(f_test)
|
|
if f_test_round == f_round:
|
|
return "%.*f" % (i, f_test)
|
|
return f_str
|
|
|
|
|
|
import os
|
|
|
|
# Avoid maintaining multiple blendfile modules
|
|
import sys
|
|
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "modules"))
|
|
del sys
|
|
|
|
source_dst = os.path.join(
|
|
os.path.dirname(__file__),
|
|
"..", "..",
|
|
"release", "datafiles", "userdef", "userdef_default_theme.c",
|
|
)
|
|
|
|
dna_rename_defs_h = os.path.join(
|
|
os.path.dirname(__file__),
|
|
"..", "..",
|
|
"source", "blender", "makesdna", "intern", "dna_rename_defs.h",
|
|
)
|
|
|
|
|
|
def dna_rename_defs(blend):
|
|
"""
|
|
"""
|
|
from blendfile import DNAName
|
|
import re
|
|
re_dna_struct_rename = re.compile(
|
|
r'DNA_STRUCT_RENAME+\('
|
|
r'([a-zA-Z0-9_]+)' r',\s*'
|
|
r'([a-zA-Z0-9_]+)' r'\)',
|
|
)
|
|
|
|
re_dna_struct_rename_elem = re.compile(
|
|
r'DNA_STRUCT_RENAME_ELEM+\('
|
|
r'([a-zA-Z0-9_]+)' r',\s*'
|
|
r'([a-zA-Z0-9_]+)' r',\s*'
|
|
r'([a-zA-Z0-9_]+)' r'\)',
|
|
)
|
|
with open(dna_rename_defs_h, 'r', encoding='utf-8') as fh:
|
|
data = fh.read()
|
|
|
|
struct_runtime_to_storage_map = {}
|
|
member_runtime_to_storage_map = {}
|
|
|
|
for line in data.split('\n'):
|
|
m = re_dna_struct_rename.match(line)
|
|
if m is not None:
|
|
struct_storage, struct_runtime = m.groups()
|
|
struct_runtime_to_storage_map[struct_runtime] = struct_storage
|
|
continue
|
|
|
|
m = re_dna_struct_rename_elem.match(line)
|
|
if m is not None:
|
|
struct_name_runtime, member_storage, member_runtime = m.groups()
|
|
if struct_name_runtime not in member_runtime_to_storage_map:
|
|
member_runtime_to_storage_map[struct_name_runtime] = []
|
|
member_runtime_to_storage_map[struct_name_runtime].append((member_storage, member_runtime))
|
|
continue
|
|
|
|
for struct_name_runtime, members in member_runtime_to_storage_map.items():
|
|
if len(members) > 1:
|
|
# Order renames that are themselves destinations to go first, so that the item is not removed.
|
|
# Needed for e.g.
|
|
# `DNA_STRUCT_RENAME_ELEM(Light, energy_new, energy);`
|
|
# `DNA_STRUCT_RENAME_ELEM(Light, energy, energy_deprecated)`
|
|
# ... in this case the order matters.
|
|
member_runtime_set = set(member_runtime for (_member_storage, member_runtime) in members)
|
|
members_ordered = ([], [])
|
|
for (member_storage, member_runtime) in members:
|
|
members_ordered[member_storage not in member_runtime_set].append((member_storage, member_runtime))
|
|
members = members_ordered[0] + members_ordered[1]
|
|
del member_runtime_set, members_ordered
|
|
|
|
for (member_storage, member_runtime) in members:
|
|
struct_name_storage = struct_runtime_to_storage_map.get(struct_name_runtime, struct_name_runtime)
|
|
struct_name_storage = struct_name_storage.encode('utf-8')
|
|
# The struct it's self may have been renamed.
|
|
member_storage = member_storage.encode('utf-8')
|
|
member_runtime = member_runtime.encode('utf-8')
|
|
dna_struct = blend.structs[blend.sdna_index_from_id[struct_name_storage]]
|
|
for field in dna_struct.fields:
|
|
dna_name = field.dna_name
|
|
if member_storage == dna_name.name_only:
|
|
field.dna_name = dna_name = DNAName(dna_name.name_full)
|
|
del dna_struct.field_from_name[dna_name.name_only]
|
|
dna_name.name_full = dna_name.name_full.replace(member_storage, member_runtime)
|
|
dna_name.name_only = member_runtime
|
|
dna_struct.field_from_name[dna_name.name_only] = field
|
|
|
|
|
|
def theme_data(userpref_filename):
|
|
import blendfile
|
|
blend = blendfile.open_blend(userpref_filename)
|
|
dna_rename_defs(blend)
|
|
u = next((c for c in blend.blocks if c.code == b'USER'), None)
|
|
# theme_type = b.sdna_index_from_id[b'bTheme']
|
|
t = u.get_pointer((b'themes', b'first'))
|
|
t.refine_type(b'bTheme')
|
|
return blend, t
|
|
|
|
|
|
def is_ignore_dna_name(name):
|
|
if name.startswith(b'_'):
|
|
return True
|
|
elif name in {
|
|
b'active_theme_area',
|
|
}:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def write_member(fw, indent, b, theme, ls):
|
|
path_old = ()
|
|
|
|
for key, value in ls:
|
|
key = key if type(key) is tuple else (key,)
|
|
path_new = key[:-1]
|
|
|
|
if tuple(path_new) != tuple(path_old):
|
|
if path_old:
|
|
p = len(path_old) - 1
|
|
while p >= 0 and (p >= len(path_new) or path_new[p] != path_old[p]):
|
|
indent = p + 1
|
|
fw('\t' * indent)
|
|
fw('},\n')
|
|
p -= 1
|
|
del p
|
|
|
|
p = 0
|
|
for p in range(min(len(path_old), len(path_new))):
|
|
if path_old[p] != key[p]:
|
|
break
|
|
else:
|
|
p = p + 1
|
|
|
|
for i, c in enumerate(path_new[p:]):
|
|
indent = p + i + 1
|
|
fw('\t' * indent)
|
|
if type(c) is bytes:
|
|
attr = c.decode('ascii')
|
|
fw(f'.{attr} = ')
|
|
fw('{\n')
|
|
|
|
if not is_ignore_dna_name(key[-1]):
|
|
indent = '\t' * (len(path_new) + 1)
|
|
attr = key[-1].decode('ascii')
|
|
if isinstance(value, float):
|
|
if value != 0.0:
|
|
value_repr = repr_f32(value)
|
|
fw(f'{indent}.{attr} = {value_repr}f,\n')
|
|
elif isinstance(value, int):
|
|
if value != 0:
|
|
fw(f'{indent}.{attr} = {value},\n')
|
|
elif isinstance(value, bytes):
|
|
if set(value) != {0}:
|
|
if len(value) == 3:
|
|
value_repr = "".join(f'{ub:02x}' for ub in value)
|
|
fw(f'{indent}.{attr} = RGB(0x{value_repr}),\n')
|
|
elif len(value) == 4:
|
|
value_repr = "".join(f'{ub:02x}' for ub in value)
|
|
fw(f'{indent}.{attr} = RGBA(0x{value_repr}),\n')
|
|
else:
|
|
value = value.rstrip(b'\x00')
|
|
is_ascii = True
|
|
for ub in value:
|
|
if not (ub >= 32 and ub < 127):
|
|
is_ascii = False
|
|
break
|
|
if is_ascii:
|
|
value_repr = value.decode('ascii')
|
|
fw(f'{indent}.{attr} = "{value_repr}",\n')
|
|
else:
|
|
value_repr = "".join(f'{ub:02x}' for ub in value)
|
|
fw(f'{indent}.{attr} = {{{value_repr}}},\n')
|
|
else:
|
|
fw(f'{indent}.{attr} = {value},\n')
|
|
path_old = path_new
|
|
|
|
|
|
def convert_data(blend, theme, f):
|
|
fw = f.write
|
|
fw(C_SOURCE_HEADER)
|
|
fw('const bTheme U_theme_default = {\n')
|
|
ls = list(theme.items_recursive_iter(use_nil=False))
|
|
write_member(fw, 1, blend, theme, ls)
|
|
|
|
fw('};\n')
|
|
fw('\n')
|
|
fw('/* clang-format on */\n')
|
|
|
|
|
|
def file_remove_empty_braces(source_dst):
|
|
with open(source_dst, 'r', encoding='utf-8') as fh:
|
|
data = fh.read()
|
|
# Remove:
|
|
# .foo = { }
|
|
import re
|
|
|
|
def key_replace(match):
|
|
return ""
|
|
data_prev = None
|
|
# Braces may become empty by removing nested
|
|
while data != data_prev:
|
|
data_prev = data
|
|
data = re.sub(
|
|
r'\s+\.[a-zA-Z_0-9]+\s+=\s+\{\s*\},',
|
|
key_replace, data, re.MULTILINE,
|
|
)
|
|
|
|
# Use two spaces instead of tabs.
|
|
data = data.replace('\t', ' ')
|
|
|
|
with open(source_dst, 'w', encoding='utf-8') as fh:
|
|
fh.write(data)
|
|
|
|
|
|
def main():
|
|
import sys
|
|
blend, theme = theme_data(sys.argv[-1])
|
|
with open(source_dst, 'w', encoding='utf-8') as fh:
|
|
convert_data(blend, theme, fh)
|
|
|
|
# Microsoft Visual Studio doesn't support empty braces.
|
|
file_remove_empty_braces(source_dst)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|