Cleanup: f-string use
[blender.git] / release / scripts / modules / bpy_extras / keyconfig_utils_experimental.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20
21 __all__ = (
22     "keyconfig_export_as_data",
23     "keyconfig_import_from_data",
24 )
25
26
27 def indent(levels):
28     return levels * "    "
29
30
31 def round_float_32(f):
32     from struct import pack, unpack
33     return unpack("f", pack("f", f))[0]
34
35
36 def repr_f32(f):
37     f_round = round_float_32(f)
38     f_str = repr(f)
39     f_str_frac = f_str.partition(".")[2]
40     if not f_str_frac:
41         return f_str
42     for i in range(1, len(f_str_frac)):
43         f_test = round(f, i)
44         f_test_round = round_float_32(f_test)
45         if f_test_round == f_round:
46             return "%.*f" % (i, f_test)
47     return f_str
48
49
50 def kmi_args_as_data(kmi):
51     s = [
52         f"\"type\": '{kmi.type}'",
53         f"\"value\": '{kmi.value}'"
54     ]
55
56     if kmi.any:
57         s.append("\"any\": True")
58     else:
59         if kmi.shift:
60             s.append("\"shift\": True")
61         if kmi.ctrl:
62             s.append("\"ctrl\": True")
63         if kmi.alt:
64             s.append("\"alt\": True")
65         if kmi.oskey:
66             s.append("\"oskey\": True")
67     if kmi.key_modifier and kmi.key_modifier != 'NONE':
68         s.append(f"\"key_modifier\": '{kmi.key_modifier}'")
69
70     return "{" + ", ".join(s) + "}"
71
72
73 def _kmi_properties_to_lines_recursive(level, properties, lines):
74     from bpy.types import OperatorProperties
75
76     def string_value(value):
77         if isinstance(value, (str, bool, int)):
78             return repr(value)
79         elif isinstance(value, float):
80             return repr_f32(value)
81         elif getattr(value, '__len__', False):
82             return repr(tuple(value))
83         raise Exception(f"Export key configuration: can't write {value!r}")
84
85     for pname in properties.bl_rna.properties.keys():
86         if pname != "rna_type":
87             value = getattr(properties, pname)
88             if isinstance(value, OperatorProperties):
89                 lines_test = []
90                 _kmi_properties_to_lines_recursive(level + 2, value, lines_test)
91                 if lines_test:
92                     lines.append(f"{indent(level)}(\n")
93                     lines.append(f"{indent(level + 1)}\"{pname}\",\n")
94                     lines.append(f"{indent(level + 1)}" "[\n")
95                     lines.extend(lines_test)
96                     lines.append(f"{indent(level + 1)}" "],\n")
97                     lines.append(f"{indent(level)}" "),\n")
98                 del lines_test
99             elif properties.is_property_set(pname):
100                 value = string_value(value)
101                 lines.append((f"{indent(level)}(\"{pname}\", {value:s}),\n"))
102
103
104 def _kmi_properties_to_lines(level, kmi_props, lines):
105     if kmi_props is None:
106         return
107
108     lines_test = [f"{indent(level)}\"properties\": " "[\n"]
109     _kmi_properties_to_lines_recursive(level + 1, kmi_props, lines_test)
110     if len(lines_test) > 1:
111         lines_test.append(f"{indent(level)}" "],\n")
112         lines.extend(lines_test)
113
114
115 def _kmi_attrs_or_none(level, kmi):
116     lines = []
117     _kmi_properties_to_lines(level + 1, kmi.properties, lines)
118     if kmi.active is False:
119         lines.append(f"{indent(level)}\"active\":" "False,\n")
120     if not lines:
121         return None
122     return "".join(lines)
123
124
125 def keyconfig_export_as_data(wm, kc, filepath):
126     # Alternate foramt
127
128     # Generate a list of keymaps to export:
129     #
130     # First add all user_modified keymaps (found in keyconfigs.user.keymaps list),
131     # then add all remaining keymaps from the currently active custom keyconfig.
132     #
133     # This will create a final list of keymaps that can be used as a "diff" against
134     # the default blender keyconfig, recreating the current setup from a fresh blender
135     # without needing to export keymaps which haven't been edited.
136
137     from .keyconfig_utils import keyconfig_merge
138
139     class FakeKeyConfig:
140         keymaps = []
141     edited_kc = FakeKeyConfig()
142     for km in wm.keyconfigs.user.keymaps:
143         if km.is_user_modified:
144             edited_kc.keymaps.append(km)
145     # merge edited keymaps with non-default keyconfig, if it exists
146     if kc != wm.keyconfigs.default:
147         export_keymaps = keyconfig_merge(edited_kc, kc)
148     else:
149         export_keymaps = keyconfig_merge(edited_kc, edited_kc)
150
151     with open(filepath, "w") as fh:
152         fw = fh.write
153         fw("keyconfig_data = [\n")
154
155         for km, kc_x in export_keymaps:
156             km = km.active()
157             fw(f"{indent(1)}" "(\n")
158             fw(f"{indent(2)}" f"\"{km.name:s}\",\n")
159             fw(f"{indent(2)}" "{")
160             fw(f"\"space_type\": '{km.space_type:s}'")
161             fw(f", \"region_type\": '{km.region_type:s}'")
162             # We can detect from the kind of items.
163             if km.is_modal:
164                 fw(", \"modal\": True")
165             fw("},\n")
166             fw(f"{indent(2)}" "{\n")
167             is_modal = km.is_modal
168             fw(f"{indent(3)}" "\"items\": [\n")
169             for kmi in km.keymap_items:
170                 if is_modal:
171                     kmi_id = kmi.propvalue
172                 else:
173                     kmi_id = kmi.idname
174
175                 fw(f"{indent(4)}" "(")
176                 kmi_args = kmi_args_as_data(kmi)
177                 kmi_data = _kmi_attrs_or_none(5, kmi)
178                 if kmi_data is not None:
179                     fw("\n" f"{indent(5)}")
180                 fw(f"\"{kmi_id:s}\"")
181                 if kmi_data is None:
182                     fw(f", ")
183                 else:
184                     fw(",\n" f"{indent(5)}")
185
186                 fw(kmi_args)
187                 if kmi_data is None:
188                     fw(", None),\n")
189                 else:
190                     fw(",\n")
191                     fw(f"{indent(5)}" "{\n")
192                     fw(kmi_data)
193                     fw(f"{indent(5)}" "}\n")
194                     fw(f"{indent(4)}" "),\n")
195
196             fw(f"{indent(3)}" "],\n")
197             fw(f"{indent(2)}" "},\n")
198             fw(f"{indent(1)}" "),\n")
199         fw("]\n")
200         fw("\n\n")
201         fw("if __name__ == \"__main__\":\n")
202         fw("    import os\n")
203         fw("    from bpy_extras.keyconfig_utils import keyconfig_import_from_data\n")
204         fw("    keyconfig_import_from_data(os.path.splitext(os.path.basename(__file__))[0], keyconfig_data)\n")
205
206
207 def keyconfig_import_from_data(name, keyconfig_data):
208     # Load data in the format defined above.
209     #
210     # Runs at load time, keep this fast!
211
212     def kmi_props_setattr(kmi_props, attr, value):
213         if type(value) is list:
214             kmi_subprop = getattr(kmi_props, attr)
215             for subattr, subvalue in value:
216                 kmi_props_setattr(kmi_subprop, subattr, subvalue)
217             return
218
219         try:
220             setattr(kmi_props, attr, value)
221         except AttributeError:
222             print(f"Warning: property '{attr}' not found in keymap item '{kmi_props.__class__.__name__}'")
223         except Exception as ex:
224             print(f"Warning: {ex!r}")
225
226     import bpy
227     wm = bpy.context.window_manager
228     kc = wm.keyconfigs.new(name)
229     del name
230
231     for (km_name, km_args, km_content) in keyconfig_data:
232         km = kc.keymaps.new(km_name, **km_args)
233         is_modal = km_args.get("modal", False)
234         new_fn = getattr(km.keymap_items, "new_modal" if is_modal else "new")
235         for (kmi_idname, kmi_args, kmi_data) in km_content["items"]:
236             kmi = new_fn(kmi_idname, **kmi_args)
237             if kmi_data is not None:
238                 if not kmi_data.get("active", True):
239                     kmi.active = False
240                 kmi_props_data = kmi_data.get("properties", None)
241                 if kmi_props_data is not None:
242                     kmi_props = kmi.properties
243                     for attr, value in kmi_props_data:
244                         kmi_props_setattr(kmi_props, attr, value)