io.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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. # <pep8 compliant>
  19. # -----------------------------------------------------------------------------
  20. # Export Functions
  21. __all__ = (
  22. "keyconfig_export_as_data",
  23. "keyconfig_import_from_data",
  24. "keyconfig_init_from_data",
  25. "keyconfig_merge",
  26. "keymap_init_from_data",
  27. )
  28. def indent(levels):
  29. return levels * " "
  30. def round_float_32(f):
  31. from struct import pack, unpack
  32. return unpack("f", pack("f", f))[0]
  33. def repr_f32(f):
  34. f_round = round_float_32(f)
  35. f_str = repr(f)
  36. f_str_frac = f_str.partition(".")[2]
  37. if not f_str_frac:
  38. return f_str
  39. for i in range(1, len(f_str_frac)):
  40. f_test = round(f, i)
  41. f_test_round = round_float_32(f_test)
  42. if f_test_round == f_round:
  43. return "%.*f" % (i, f_test)
  44. return f_str
  45. def kmi_args_as_data(kmi):
  46. s = [
  47. f"\"type\": '{kmi.type}'",
  48. f"\"value\": '{kmi.value}'"
  49. ]
  50. if kmi.any:
  51. s.append("\"any\": True")
  52. else:
  53. if kmi.shift:
  54. s.append("\"shift\": True")
  55. if kmi.ctrl:
  56. s.append("\"ctrl\": True")
  57. if kmi.alt:
  58. s.append("\"alt\": True")
  59. if kmi.oskey:
  60. s.append("\"oskey\": True")
  61. if kmi.key_modifier and kmi.key_modifier != 'NONE':
  62. s.append(f"\"key_modifier\": '{kmi.key_modifier}'")
  63. return "{" + ", ".join(s) + "}"
  64. def _kmi_properties_to_lines_recursive(level, properties, lines):
  65. from bpy.types import OperatorProperties
  66. def string_value(value):
  67. if isinstance(value, (str, bool, int)):
  68. return repr(value)
  69. elif isinstance(value, float):
  70. return repr_f32(value)
  71. elif getattr(value, '__len__', False):
  72. return repr(tuple(value))
  73. raise Exception(f"Export key configuration: can't write {value!r}")
  74. for pname in properties.bl_rna.properties.keys():
  75. if pname != "rna_type":
  76. value = getattr(properties, pname)
  77. if isinstance(value, OperatorProperties):
  78. lines_test = []
  79. _kmi_properties_to_lines_recursive(level + 2, value, lines_test)
  80. if lines_test:
  81. lines.append(f"(")
  82. lines.append(f"\"{pname}\",\n")
  83. lines.append(f"{indent(level + 3)}" "[")
  84. lines.extend(lines_test)
  85. lines.append("],\n")
  86. lines.append(f"{indent(level + 3)}" "),\n" f"{indent(level + 2)}")
  87. del lines_test
  88. elif properties.is_property_set(pname):
  89. value = string_value(value)
  90. lines.append((f"(\"{pname}\", {value:s}),\n" f"{indent(level + 2)}"))
  91. def _kmi_properties_to_lines(level, kmi_props, lines):
  92. if kmi_props is None:
  93. return
  94. lines_test = [f"\"properties\":\n" f"{indent(level + 1)}" "["]
  95. _kmi_properties_to_lines_recursive(level, kmi_props, lines_test)
  96. if len(lines_test) > 1:
  97. lines_test.append("],\n")
  98. lines.extend(lines_test)
  99. def _kmi_attrs_or_none(level, kmi):
  100. lines = []
  101. _kmi_properties_to_lines(level + 1, kmi.properties, lines)
  102. if kmi.active is False:
  103. lines.append(f"{indent(level)}\"active\":" "False,\n")
  104. if not lines:
  105. return None
  106. return "".join(lines)
  107. def keyconfig_export_as_data(wm, kc, filepath, *, all_keymaps=False):
  108. # Alternate foramt
  109. # Generate a list of keymaps to export:
  110. #
  111. # First add all user_modified keymaps (found in keyconfigs.user.keymaps list),
  112. # then add all remaining keymaps from the currently active custom keyconfig.
  113. #
  114. # This will create a final list of keymaps that can be used as a "diff" against
  115. # the default blender keyconfig, recreating the current setup from a fresh blender
  116. # without needing to export keymaps which haven't been edited.
  117. class FakeKeyConfig:
  118. keymaps = []
  119. edited_kc = FakeKeyConfig()
  120. for km in wm.keyconfigs.user.keymaps:
  121. if all_keymaps or km.is_user_modified:
  122. edited_kc.keymaps.append(km)
  123. # merge edited keymaps with non-default keyconfig, if it exists
  124. if kc != wm.keyconfigs.default:
  125. export_keymaps = keyconfig_merge(edited_kc, kc)
  126. else:
  127. export_keymaps = keyconfig_merge(edited_kc, edited_kc)
  128. with open(filepath, "w") as fh:
  129. fw = fh.write
  130. fw("keyconfig_data = \\\n[")
  131. for km, _kc_x in export_keymaps:
  132. km = km.active()
  133. fw("(")
  134. fw(f"\"{km.name:s}\",\n")
  135. fw(f"{indent(2)}" "{")
  136. fw(f"\"space_type\": '{km.space_type:s}'")
  137. fw(f", \"region_type\": '{km.region_type:s}'")
  138. # We can detect from the kind of items.
  139. if km.is_modal:
  140. fw(", \"modal\": True")
  141. fw("},\n")
  142. fw(f"{indent(2)}" "{")
  143. is_modal = km.is_modal
  144. fw(f"\"items\":\n")
  145. fw(f"{indent(3)}[")
  146. for kmi in km.keymap_items:
  147. if is_modal:
  148. kmi_id = kmi.propvalue
  149. else:
  150. kmi_id = kmi.idname
  151. fw(f"(")
  152. kmi_args = kmi_args_as_data(kmi)
  153. kmi_data = _kmi_attrs_or_none(4, kmi)
  154. fw(f"\"{kmi_id:s}\"")
  155. if kmi_data is None:
  156. fw(f", ")
  157. else:
  158. fw(",\n" f"{indent(5)}")
  159. fw(kmi_args)
  160. if kmi_data is None:
  161. fw(", None),\n")
  162. else:
  163. fw(",\n")
  164. fw(f"{indent(5)}" "{")
  165. fw(kmi_data)
  166. fw(f"{indent(6)}")
  167. fw("},\n" f"{indent(5)}")
  168. fw("),\n")
  169. fw(f"{indent(4)}")
  170. fw("],\n" f"{indent(3)}")
  171. fw("},\n" f"{indent(2)}")
  172. fw("),\n" f"{indent(1)}")
  173. fw("]\n")
  174. fw("\n\n")
  175. fw("if __name__ == \"__main__\":\n")
  176. fw(" import os\n")
  177. fw(" from bl_keymap_utils.io import keyconfig_import_from_data\n")
  178. fw(" keyconfig_import_from_data(os.path.splitext(os.path.basename(__file__))[0], keyconfig_data)\n")
  179. # -----------------------------------------------------------------------------
  180. # Import Functions
  181. def _kmi_props_setattr(kmi_props, attr, value):
  182. if type(value) is list:
  183. kmi_subprop = getattr(kmi_props, attr)
  184. for subattr, subvalue in value:
  185. _kmi_props_setattr(kmi_subprop, subattr, subvalue)
  186. return
  187. try:
  188. setattr(kmi_props, attr, value)
  189. except AttributeError:
  190. print(f"Warning: property '{attr}' not found in keymap item '{kmi_props.__class__.__name__}'")
  191. except Exception as ex:
  192. print(f"Warning: {ex!r}")
  193. def keymap_init_from_data(km, km_items, is_modal=False):
  194. new_fn = getattr(km.keymap_items, "new_modal" if is_modal else "new")
  195. for (kmi_idname, kmi_args, kmi_data) in km_items:
  196. kmi = new_fn(kmi_idname, **kmi_args)
  197. if kmi_data is not None:
  198. if not kmi_data.get("active", True):
  199. kmi.active = False
  200. kmi_props_data = kmi_data.get("properties", None)
  201. if kmi_props_data is not None:
  202. kmi_props = kmi.properties
  203. assert type(kmi_props_data) is list
  204. for attr, value in kmi_props_data:
  205. _kmi_props_setattr(kmi_props, attr, value)
  206. def keyconfig_init_from_data(kc, keyconfig_data):
  207. # Load data in the format defined above.
  208. #
  209. # Runs at load time, keep this fast!
  210. for (km_name, km_args, km_content) in keyconfig_data:
  211. km = kc.keymaps.new(km_name, **km_args)
  212. km_items = km_content["items"]
  213. # Check here instead of inside 'keymap_init_from_data'
  214. # because we want to allow both tuple & list types in that case.
  215. #
  216. # For full keymaps, ensure these are always lists to allow for extending them
  217. # in a generic way that doesn't have to check for the type each time.
  218. assert type(km_items) is list
  219. keymap_init_from_data(km, km_items, is_modal=km_args.get("modal", False))
  220. def keyconfig_import_from_data(name, keyconfig_data):
  221. # Load data in the format defined above.
  222. #
  223. # Runs at load time, keep this fast!
  224. import bpy
  225. wm = bpy.context.window_manager
  226. kc = wm.keyconfigs.new(name)
  227. keyconfig_init_from_data(kc, keyconfig_data)
  228. return kc
  229. # -----------------------------------------------------------------------------
  230. # Utility Functions
  231. def keyconfig_merge(kc1, kc2):
  232. """ note: kc1 takes priority over kc2
  233. """
  234. kc1_names = {km.name for km in kc1.keymaps}
  235. merged_keymaps = [(km, kc1) for km in kc1.keymaps]
  236. if kc1 != kc2:
  237. merged_keymaps.extend(
  238. (km, kc2)
  239. for km in kc2.keymaps
  240. if km.name not in kc1_names
  241. )
  242. return merged_keymaps