keymap_from_toolbar.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  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. # Dynamically create a keymap which is used by the popup toolbar
  20. # for accelerator key access.
  21. __all__ = (
  22. "generate",
  23. )
  24. def generate(context, space_type):
  25. """
  26. Keymap for popup toolbar, currently generated each time.
  27. """
  28. from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
  29. def modifier_keywords_from_item(kmi):
  30. kw = {}
  31. for (attr, default) in (
  32. ("any", False),
  33. ("shift", False),
  34. ("ctrl", False),
  35. ("alt", False),
  36. ("oskey", False),
  37. ("key_modifier", 'NONE'),
  38. ):
  39. val = getattr(kmi, attr)
  40. if val != default:
  41. kw[attr] = val
  42. return kw
  43. def dict_as_tuple(d):
  44. return tuple((k, v) for (k, v) in sorted(d.items()))
  45. cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
  46. items_all = [
  47. # 0: tool
  48. # 1: keymap item (direct access)
  49. # 2: keymap item (newly calculated for toolbar)
  50. [item, None, None]
  51. for item in ToolSelectPanelHelper._tools_flatten(cls.tools_from_context(context))
  52. if item is not None
  53. ]
  54. items_all_id = {item_container[0].idname for item_container in items_all}
  55. # Press the toolbar popup key again to set the default tool,
  56. # this is useful because the select box tool is useful as a way
  57. # to 'drop' currently active tools (it's basically a 'none' tool).
  58. # so this allows us to quickly go back to a state that allows
  59. # a shortcut based workflow (before the tool system was added).
  60. use_tap_reset = True
  61. # TODO: support other tools for modes which don't use this tool.
  62. tap_reset_tool = "builtin.cursor"
  63. # Check the tool is available in the current context.
  64. if tap_reset_tool not in items_all_id:
  65. use_tap_reset = False
  66. from bl_operators.wm import use_toolbar_release_hack
  67. # Pie-menu style release to activate.
  68. use_release_confirm = True
  69. # Generate items when no keys are mapped.
  70. use_auto_keymap_alpha = False # Map manially in the default keymap
  71. use_auto_keymap_num = True
  72. # Temporary, only create so we can pass 'properties' to find_item_from_operator.
  73. use_hack_properties = True
  74. km_name_default = "Toolbar Popup"
  75. km_name = km_name_default + " <temp>"
  76. wm = context.window_manager
  77. keyconf_user = wm.keyconfigs.user
  78. keyconf_active = wm.keyconfigs.active
  79. keymap = keyconf_active.keymaps.get(km_name)
  80. if keymap is None:
  81. keymap = keyconf_active.keymaps.new(km_name, space_type='EMPTY', region_type='TEMPORARY')
  82. for kmi in keymap.keymap_items:
  83. keymap.keymap_items.remove(kmi)
  84. keymap_src = keyconf_user.keymaps.get(km_name_default)
  85. if keymap_src is not None:
  86. for kmi_src in keymap_src.keymap_items:
  87. # Skip tools that aren't currently shown.
  88. if (
  89. (kmi_src.idname == "wm.tool_set_by_id") and
  90. (kmi_src.properties.name not in items_all_id)
  91. ):
  92. continue
  93. keymap.keymap_items.new_from_item(kmi_src)
  94. del keymap_src
  95. del items_all_id
  96. kmi_unique_args = set()
  97. def kmi_unique_or_pass(kmi_args):
  98. kmi_unique_len = len(kmi_unique_args)
  99. kmi_unique_args.add(dict_as_tuple(kmi_args))
  100. return kmi_unique_len != len(kmi_unique_args)
  101. cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
  102. if use_hack_properties:
  103. kmi_hack = keymap.keymap_items.new("wm.tool_set_by_id", 'NONE', 'PRESS')
  104. kmi_hack_properties = kmi_hack.properties
  105. kmi_hack.active = False
  106. kmi_hack_brush_select = keymap.keymap_items.new("paint.brush_select", 'NONE', 'PRESS')
  107. kmi_hack_brush_select_properties = kmi_hack_brush_select.properties
  108. kmi_hack_brush_select.active = False
  109. if use_release_confirm or use_tap_reset:
  110. kmi_toolbar = wm.keyconfigs.find_item_from_operator(
  111. idname="wm.toolbar",
  112. )[1]
  113. kmi_toolbar_type = None if not kmi_toolbar else kmi_toolbar.type
  114. if use_tap_reset and kmi_toolbar_type is not None:
  115. kmi_toolbar_args_type_only = {"type": kmi_toolbar_type}
  116. kmi_toolbar_args = {**kmi_toolbar_args_type_only, **modifier_keywords_from_item(kmi_toolbar)}
  117. else:
  118. use_tap_reset = False
  119. del kmi_toolbar
  120. if use_tap_reset:
  121. kmi_found = None
  122. if use_hack_properties:
  123. # First check for direct assignment, if this tool already has a key, no need to add a new one.
  124. kmi_hack_properties.name = tap_reset_tool
  125. kmi_found = wm.keyconfigs.find_item_from_operator(
  126. idname="wm.tool_set_by_id",
  127. context='INVOKE_REGION_WIN',
  128. # properties={"name": item.idname},
  129. properties=kmi_hack_properties,
  130. include={'KEYBOARD'},
  131. )[1]
  132. if kmi_found:
  133. use_tap_reset = False
  134. del kmi_found
  135. if use_tap_reset:
  136. use_tap_reset = kmi_unique_or_pass(kmi_toolbar_args)
  137. if use_tap_reset:
  138. items_all[:] = [
  139. item_container
  140. for item_container in items_all
  141. if item_container[0].idname != tap_reset_tool
  142. ]
  143. # -----------------------
  144. # Begin Keymap Generation
  145. # -------------------------------------------------------------------------
  146. # Direct Tool Assignment & Brushes
  147. for item_container in items_all:
  148. item = item_container[0]
  149. # Only check the first item in the tools key-map (a little arbitrary).
  150. if use_hack_properties:
  151. # First check for direct assignment.
  152. kmi_hack_properties.name = item.idname
  153. kmi_found = wm.keyconfigs.find_item_from_operator(
  154. idname="wm.tool_set_by_id",
  155. context='INVOKE_REGION_WIN',
  156. # properties={"name": item.idname},
  157. properties=kmi_hack_properties,
  158. include={'KEYBOARD'},
  159. )[1]
  160. if kmi_found is None:
  161. if item.data_block:
  162. # PAINT_OT_brush_select
  163. mode = context.active_object.mode
  164. # See: BKE_paint_get_tool_prop_id_from_paintmode
  165. attr = {
  166. 'SCULPT': "sculpt_tool",
  167. 'VERTEX_PAINT': "vertex_tool",
  168. 'WEIGHT_PAINT': "weight_tool",
  169. 'TEXTURE_PAINT': "image_tool",
  170. 'PAINT_GPENCIL': "gpencil_tool",
  171. }.get(mode, None)
  172. if attr is not None:
  173. setattr(kmi_hack_brush_select_properties, attr, item.data_block)
  174. kmi_found = wm.keyconfigs.find_item_from_operator(
  175. idname="paint.brush_select",
  176. context='INVOKE_REGION_WIN',
  177. properties=kmi_hack_brush_select_properties,
  178. include={'KEYBOARD'},
  179. )[1]
  180. elif mode in {'PARTICLE_EDIT', 'SCULPT_GPENCIL'}:
  181. # Doesn't use brushes
  182. pass
  183. else:
  184. print("Unsupported mode:", mode)
  185. del mode, attr
  186. else:
  187. kmi_found = None
  188. if kmi_found is not None:
  189. pass
  190. elif item.operator is not None:
  191. kmi_found = wm.keyconfigs.find_item_from_operator(
  192. idname=item.operator,
  193. context='INVOKE_REGION_WIN',
  194. include={'KEYBOARD'},
  195. )[1]
  196. elif item.keymap is not None:
  197. km = keyconf_user.keymaps.get(item.keymap[0])
  198. if km is None:
  199. print("Keymap", repr(item.keymap[0]), "not found for tool", item.idname)
  200. kmi_found = None
  201. else:
  202. kmi_first = km.keymap_items
  203. kmi_first = kmi_first[0] if kmi_first else None
  204. if kmi_first is not None:
  205. kmi_found = wm.keyconfigs.find_item_from_operator(
  206. idname=kmi_first.idname,
  207. # properties=kmi_first.properties, # prevents matches, don't use.
  208. context='INVOKE_REGION_WIN',
  209. include={'KEYBOARD'},
  210. )[1]
  211. if kmi_found is None:
  212. # We need non-keyboard events so keys with 'key_modifier' key is found.
  213. kmi_found = wm.keyconfigs.find_item_from_operator(
  214. idname=kmi_first.idname,
  215. # properties=kmi_first.properties, # prevents matches, don't use.
  216. context='INVOKE_REGION_WIN',
  217. exclude={'KEYBOARD'},
  218. )[1]
  219. if kmi_found is not None:
  220. if kmi_found.key_modifier == 'NONE':
  221. kmi_found = None
  222. else:
  223. kmi_found = None
  224. del kmi_first
  225. del km
  226. else:
  227. kmi_found = None
  228. item_container[1] = kmi_found
  229. # -------------------------------------------------------------------------
  230. # Single Key Access
  231. # More complex multi-pass test.
  232. for item_container in items_all:
  233. item, kmi_found = item_container[:2]
  234. if kmi_found is None:
  235. continue
  236. kmi_found_type = kmi_found.type
  237. # Only for single keys.
  238. if (
  239. (len(kmi_found_type) == 1) or
  240. # When a tool is being activated instead of running an operator, just copy the shortcut.
  241. (kmi_found.idname in {"wm.tool_set_by_id", "WM_OT_tool_set_by_id"})
  242. ):
  243. kmi_args = {"type": kmi_found_type, **modifier_keywords_from_item(kmi_found)}
  244. if kmi_unique_or_pass(kmi_args):
  245. kmi = keymap.keymap_items.new(idname="wm.tool_set_by_id", value='PRESS', **kmi_args)
  246. kmi.properties.name = item.idname
  247. item_container[2] = kmi
  248. # -------------------------------------------------------------------------
  249. # Single Key Modifier
  250. #
  251. #
  252. # Test for key_modifier, where alpha key is used as a 'key_modifier'
  253. # (grease pencil holding 'D' for example).
  254. for item_container in items_all:
  255. item, kmi_found, kmi_exist = item_container
  256. if kmi_found is None or kmi_exist:
  257. continue
  258. kmi_found_type = kmi_found.type
  259. if kmi_found_type in {
  260. 'LEFTMOUSE',
  261. 'RIGHTMOUSE',
  262. 'MIDDLEMOUSE',
  263. 'BUTTON4MOUSE',
  264. 'BUTTON5MOUSE',
  265. 'BUTTON6MOUSE',
  266. 'BUTTON7MOUSE',
  267. }:
  268. kmi_found_type = kmi_found.key_modifier
  269. # excludes 'NONE'
  270. if len(kmi_found_type) == 1:
  271. kmi_args = {"type": kmi_found_type, **modifier_keywords_from_item(kmi_found)}
  272. del kmi_args["key_modifier"]
  273. if kmi_unique_or_pass(kmi_args):
  274. kmi = keymap.keymap_items.new(idname="wm.tool_set_by_id", value='PRESS', **kmi_args)
  275. kmi.properties.name = item.idname
  276. item_container[2] = kmi
  277. # -------------------------------------------------------------------------
  278. # Assign A-Z to Keys
  279. #
  280. # When the keys are free.
  281. if use_auto_keymap_alpha:
  282. # Map all unmapped keys to numbers,
  283. # while this is a bit strange it means users will not confuse regular key bindings to ordered bindings.
  284. # First map A-Z.
  285. kmi_type_alpha_char = [chr(i) for i in range(65, 91)]
  286. kmi_type_alpha_args = {c: {"type": c} for c in kmi_type_alpha_char}
  287. kmi_type_alpha_args_tuple = {c: dict_as_tuple(kmi_type_alpha_args[c]) for c in kmi_type_alpha_char}
  288. for item_container in items_all:
  289. item, kmi_found, kmi_exist = item_container
  290. if kmi_exist:
  291. continue
  292. kmi_type = item.label[0].upper()
  293. kmi_tuple = kmi_type_alpha_args_tuple.get(kmi_type)
  294. if kmi_tuple and kmi_tuple not in kmi_unique_args:
  295. kmi_unique_args.add(kmi_tuple)
  296. kmi = keymap.keymap_items.new(
  297. idname="wm.tool_set_by_id",
  298. value='PRESS',
  299. **kmi_type_alpha_args[kmi_type],
  300. )
  301. kmi.properties.name = item.idname
  302. item_container[2] = kmi
  303. del kmi_type_alpha_char, kmi_type_alpha_args, kmi_type_alpha_args_tuple
  304. # -------------------------------------------------------------------------
  305. # Assign Numbers to Keys
  306. if use_auto_keymap_num:
  307. # Free events (last used first).
  308. kmi_type_auto = ('ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'ZERO')
  309. # Map both numbers and num-pad.
  310. kmi_type_dupe = {
  311. 'ONE': 'NUMPAD_1',
  312. 'TWO': 'NUMPAD_2',
  313. 'THREE': 'NUMPAD_3',
  314. 'FOUR': 'NUMPAD_4',
  315. 'FIVE': 'NUMPAD_5',
  316. 'SIX': 'NUMPAD_6',
  317. 'SEVEN': 'NUMPAD_7',
  318. 'EIGHT': 'NUMPAD_8',
  319. 'NINE': 'NUMPAD_9',
  320. 'ZERO': 'NUMPAD_0',
  321. }
  322. def iter_free_events():
  323. for mod in ({}, {"shift": True}, {"ctrl": True}, {"alt": True}):
  324. for e in kmi_type_auto:
  325. yield (e, mod)
  326. iter_events = iter(iter_free_events())
  327. for item_container in items_all:
  328. item, kmi_found, kmi_exist = item_container
  329. if kmi_exist:
  330. continue
  331. kmi_args = None
  332. while True:
  333. key, mod = next(iter_events, (None, None))
  334. if key is None:
  335. break
  336. kmi_args = {"type": key, **mod}
  337. kmi_tuple = dict_as_tuple(kmi_args)
  338. if kmi_tuple in kmi_unique_args:
  339. kmi_args = None
  340. else:
  341. break
  342. if kmi_args is not None:
  343. kmi = keymap.keymap_items.new(idname="wm.tool_set_by_id", value='PRESS', **kmi_args)
  344. kmi.properties.name = item.idname
  345. item_container[2] = kmi
  346. kmi_unique_args.add(kmi_tuple)
  347. key = kmi_type_dupe.get(kmi_args["type"])
  348. if key is not None:
  349. kmi_args["type"] = key
  350. kmi_tuple = dict_as_tuple(kmi_args)
  351. if not kmi_tuple in kmi_unique_args:
  352. kmi = keymap.keymap_items.new(idname="wm.tool_set_by_id", value='PRESS', **kmi_args)
  353. kmi.properties.name = item.idname
  354. kmi_unique_args.add(kmi_tuple)
  355. # ---------------------
  356. # End Keymap Generation
  357. if use_hack_properties:
  358. keymap.keymap_items.remove(kmi_hack)
  359. keymap.keymap_items.remove(kmi_hack_brush_select)
  360. # Keep last so we can try add a key without any modifiers
  361. # in the case this toolbar was activated with modifiers.
  362. if use_tap_reset:
  363. if len(kmi_toolbar_args_type_only) == len(kmi_toolbar_args):
  364. kmi_toolbar_args_available = kmi_toolbar_args
  365. else:
  366. # We have modifiers, see if we have a free key w/o modifiers.
  367. kmi_toolbar_tuple = dict_as_tuple(kmi_toolbar_args_type_only)
  368. if kmi_toolbar_tuple not in kmi_unique_args:
  369. kmi_toolbar_args_available = kmi_toolbar_args_type_only
  370. kmi_unique_args.add(kmi_toolbar_tuple)
  371. else:
  372. kmi_toolbar_args_available = kmi_toolbar_args
  373. del kmi_toolbar_tuple
  374. kmi = keymap.keymap_items.new(
  375. "wm.tool_set_by_id",
  376. value='PRESS' if use_toolbar_release_hack else 'DOUBLE_CLICK',
  377. **kmi_toolbar_args_available,
  378. )
  379. kmi.properties.name = tap_reset_tool
  380. if use_release_confirm:
  381. kmi = keymap.keymap_items.new(
  382. "ui.button_execute",
  383. type=kmi_toolbar_type,
  384. value='RELEASE',
  385. any=True,
  386. )
  387. kmi.properties.skip_depressed = True
  388. if use_toolbar_release_hack:
  389. # ... or pass through to let the toolbar know we're released.
  390. # Let the operator know we're released.
  391. kmi = keymap.keymap_items.new(
  392. "wm.tool_set_by_id",
  393. type=kmi_toolbar_type,
  394. value='RELEASE',
  395. any=True,
  396. )
  397. wm.keyconfigs.update()
  398. return keymap