space_toolsystem_common.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  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. import bpy
  20. from bpy.types import (
  21. Menu,
  22. )
  23. __all__ = (
  24. "ToolDef",
  25. "ToolSelectPanelHelper",
  26. "activate_by_id",
  27. "activate_by_id_or_cycle",
  28. "description_from_id",
  29. "keymap_from_id",
  30. )
  31. # Support reloading icons.
  32. if "_icon_cache" in locals():
  33. release = bpy.app.icons.release
  34. for icon_value in _icon_cache.values():
  35. if icon_value != 0:
  36. release(icon_value)
  37. del release
  38. # (filename -> icon_value) map
  39. _icon_cache = {}
  40. def _keymap_fn_from_seq(keymap_data):
  41. def keymap_fn(km):
  42. if keymap_fn.keymap_data:
  43. from bl_keymap_utils.io import keymap_init_from_data
  44. keymap_init_from_data(km, keymap_fn.keymap_data)
  45. keymap_fn.keymap_data = keymap_data
  46. return keymap_fn
  47. def _item_is_fn(item):
  48. return (not (type(item) is ToolDef) and callable(item))
  49. from collections import namedtuple
  50. ToolDef = namedtuple(
  51. "ToolDef",
  52. (
  53. # Unique tool name (withing space & mode context).
  54. "idname",
  55. # The name to display in the interface.
  56. "label",
  57. # Description (for tooltip), when not set, use the description of 'operator',
  58. # may be a string or a 'function(context, item, keymap) -> string'.
  59. "description",
  60. # The name of the icon to use (found in ``release/datafiles/icons``) or None for no icon.
  61. "icon",
  62. # An optional cursor to use when this tool is active.
  63. "cursor",
  64. # An optional gizmo group to activate when the tool is set or None for no gizmo.
  65. "widget",
  66. # Optional keymap for tool, either:
  67. # - A function that populates a keymaps passed in as an argument.
  68. # - A tuple filled with triple's of:
  69. # ``(operator_id, operator_properties, keymap_item_args)``.
  70. #
  71. # Warning: currently 'from_dict' this is a list of one item,
  72. # so internally we can swap the keymap function for the keymap it's self.
  73. # This isn't very nice and may change, tool definitions shouldn't care about this.
  74. "keymap",
  75. # Optional data-block assosiated with this tool.
  76. # (Typically brush name, usage depends on mode, we could use for non-brush ID's in other modes).
  77. "data_block",
  78. # Optional primary operator (for introspection only).
  79. "operator",
  80. # Optional draw settings (operator options, tool_settings).
  81. "draw_settings",
  82. # Optional draw cursor.
  83. "draw_cursor",
  84. )
  85. )
  86. del namedtuple
  87. def from_dict(kw_args):
  88. """
  89. Use so each tool can avoid defining all members of the named tuple.
  90. Also convert the keymap from a tuple into a function
  91. (since keymap is a callback).
  92. """
  93. kw = {
  94. "description": None,
  95. "icon": None,
  96. "cursor": None,
  97. "widget": None,
  98. "keymap": None,
  99. "data_block": None,
  100. "operator": None,
  101. "draw_settings": None,
  102. "draw_cursor": None,
  103. }
  104. kw.update(kw_args)
  105. keymap = kw["keymap"]
  106. if keymap is None:
  107. pass
  108. elif type(keymap) is tuple:
  109. keymap = [_keymap_fn_from_seq(keymap)]
  110. else:
  111. keymap = [keymap]
  112. kw["keymap"] = keymap
  113. return ToolDef(**kw)
  114. def from_fn(fn):
  115. """
  116. Use as decorator so we can define functions.
  117. """
  118. return ToolDef.from_dict(fn())
  119. def with_args(**kw):
  120. def from_fn(fn):
  121. return ToolDef.from_dict(fn(**kw))
  122. return from_fn
  123. from_fn.with_args = with_args
  124. ToolDef.from_dict = from_dict
  125. ToolDef.from_fn = from_fn
  126. del from_dict, from_fn, with_args
  127. class ToolActivePanelHelper:
  128. # Sub-class must define.
  129. # bl_space_type = 'VIEW_3D'
  130. # bl_region_type = 'UI'
  131. bl_label = "Active Tool"
  132. # bl_category = "Tool"
  133. def draw(self, context):
  134. layout = self.layout
  135. layout.use_property_split = True
  136. layout.use_property_decorate = False
  137. ToolSelectPanelHelper.draw_active_tool_header(
  138. context,
  139. layout,
  140. show_tool_name=True,
  141. tool_key=ToolSelectPanelHelper._tool_key_from_context(context, space_type=self.bl_space_type),
  142. )
  143. class ToolSelectPanelHelper:
  144. """
  145. Generic Class, can be used for any toolbar.
  146. - keymap_prefix:
  147. The text prefix for each key-map for this spaces tools.
  148. - tools_all():
  149. Returns (context_mode, tools) tuple pair for all tools defined.
  150. - tools_from_context(context, mode=None):
  151. Returns tools available in this context.
  152. Each tool is a 'ToolDef' or None for a separator in the toolbar, use ``None``.
  153. """
  154. @staticmethod
  155. def _tool_class_from_space_type(space_type):
  156. return next(
  157. (cls for cls in ToolSelectPanelHelper.__subclasses__()
  158. if cls.bl_space_type == space_type),
  159. None
  160. )
  161. @staticmethod
  162. def _icon_value_from_icon_handle(icon_name):
  163. import os
  164. if icon_name is not None:
  165. assert(type(icon_name) is str)
  166. icon_value = _icon_cache.get(icon_name)
  167. if icon_value is None:
  168. dirname = bpy.utils.resource_path('LOCAL')
  169. if not os.path.exists(dirname):
  170. # TODO(campbell): use a better way of finding datafiles.
  171. dirname = bpy.utils.resource_path('SYSTEM')
  172. filename = os.path.join(dirname, "datafiles", "icons", icon_name + ".dat")
  173. try:
  174. icon_value = bpy.app.icons.new_triangles_from_file(filename)
  175. except Exception as ex:
  176. if not os.path.exists(filename):
  177. print("Missing icons:", filename, ex)
  178. else:
  179. print("Corrupt icon:", filename, ex)
  180. # Use none as a fallback (avoids layout issues).
  181. if icon_name != "none":
  182. icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle("none")
  183. else:
  184. icon_value = 0
  185. _icon_cache[icon_name] = icon_value
  186. return icon_value
  187. else:
  188. return 0
  189. @staticmethod
  190. def _tools_flatten(tools):
  191. """
  192. Flattens, skips None and calls generators.
  193. """
  194. for item in tools:
  195. if item is None:
  196. yield None
  197. elif type(item) is tuple:
  198. for sub_item in item:
  199. if sub_item is None:
  200. yield None
  201. elif _item_is_fn(sub_item):
  202. yield from sub_item(context)
  203. else:
  204. yield sub_item
  205. else:
  206. if _item_is_fn(item):
  207. yield from item(context)
  208. else:
  209. yield item
  210. @staticmethod
  211. def _tools_flatten_with_tool_index(tools):
  212. for item in tools:
  213. if item is None:
  214. yield None, -1
  215. elif type(item) is tuple:
  216. i = 0
  217. for sub_item in item:
  218. if sub_item is None:
  219. yield None, -1
  220. elif _item_is_fn(sub_item):
  221. for item_dyn in sub_item(context):
  222. yield item_dyn, i
  223. i += 1
  224. else:
  225. yield sub_item, i
  226. i += 1
  227. else:
  228. if _item_is_fn(item):
  229. for item_dyn in item(context):
  230. yield item_dyn, -1
  231. else:
  232. yield item, -1
  233. @staticmethod
  234. def _tool_get_active(context, space_type, mode, with_icon=False):
  235. """
  236. Return the active Python tool definition and icon name.
  237. """
  238. cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
  239. if cls is not None:
  240. tool_active = ToolSelectPanelHelper._tool_active_from_context(context, space_type, mode)
  241. tool_active_id = getattr(tool_active, "idname", None)
  242. for item in ToolSelectPanelHelper._tools_flatten(cls.tools_from_context(context, mode)):
  243. if item is not None:
  244. if item.idname == tool_active_id:
  245. if with_icon:
  246. icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon)
  247. else:
  248. icon_value = 0
  249. return (item, tool_active, icon_value)
  250. return None, None, 0
  251. @staticmethod
  252. def _tool_get_by_id(context, space_type, idname):
  253. """
  254. Return the active Python tool definition and index (if in sub-group, else -1).
  255. """
  256. cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
  257. if cls is not None:
  258. for item, index in ToolSelectPanelHelper._tools_flatten_with_tool_index(cls.tools_from_context(context)):
  259. if item is not None:
  260. if item.idname == idname:
  261. return (cls, item, index)
  262. return None, None, -1
  263. @staticmethod
  264. def _tool_get_by_flat_index(context, space_type, tool_index):
  265. """
  266. Return the active Python tool definition and index (if in sub-group, else -1).
  267. Return the index of the expanded list.
  268. """
  269. cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
  270. if cls is not None:
  271. i = 0
  272. for item, index in ToolSelectPanelHelper._tools_flatten_with_tool_index(cls.tools_from_context(context)):
  273. if item is not None:
  274. if i == tool_index:
  275. return (cls, item, index)
  276. i += 1
  277. return None, None, -1
  278. @staticmethod
  279. def _tool_get_by_index(context, space_type, tool_index):
  280. """
  281. Return the active Python tool definition and index (if in sub-group, else -1).
  282. Return the index of the list without expanding.
  283. """
  284. cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
  285. if cls is not None:
  286. i = 0
  287. for item in cls.tools_from_context(context):
  288. if item is not None:
  289. if i == tool_index:
  290. if type(item) is tuple:
  291. index = cls._tool_group_active.get(item[0].idname, 0)
  292. item = item[index]
  293. else:
  294. index = -1
  295. return (cls, item, index)
  296. i += 1
  297. return None, None, -1
  298. @staticmethod
  299. def _tool_active_from_context(context, space_type, mode=None, create=False):
  300. if space_type == 'VIEW_3D':
  301. if mode is None:
  302. mode = context.mode
  303. tool = context.workspace.tools.from_space_view3d_mode(mode, create=create)
  304. if tool is not None:
  305. tool.refresh_from_context()
  306. return tool
  307. elif space_type == 'IMAGE_EDITOR':
  308. space_data = context.space_data
  309. if mode is None:
  310. if space_data is None:
  311. mode = 'VIEW'
  312. else:
  313. mode = space_data.mode
  314. tool = context.workspace.tools.from_space_image_mode(mode, create=create)
  315. if tool is not None:
  316. tool.refresh_from_context()
  317. return tool
  318. elif space_type == 'NODE_EDITOR':
  319. space_data = context.space_data
  320. tool = context.workspace.tools.from_space_node(create=create)
  321. if tool is not None:
  322. tool.refresh_from_context()
  323. return tool
  324. return None
  325. @staticmethod
  326. def _tool_identifier_from_button(context):
  327. return context.button_operator.name
  328. @classmethod
  329. def _km_action_simple(cls, kc, context_descr, label, keymap_fn):
  330. km_idname = f"{cls.keymap_prefix:s} {context_descr:s}, {label:s}"
  331. km = kc.keymaps.get(km_idname)
  332. if km is None:
  333. km = kc.keymaps.new(km_idname, space_type=cls.bl_space_type, region_type='WINDOW', tool=True)
  334. keymap_fn[0](km)
  335. keymap_fn[0] = km.name
  336. # Special internal function, gives use items that contain keymaps.
  337. @staticmethod
  338. def _tools_flatten_with_keymap(tools):
  339. for item_parent in tools:
  340. if item_parent is None:
  341. continue
  342. for item in item_parent if (type(item_parent) is tuple) else (item_parent,):
  343. # skip None or generator function
  344. if item is None or _item_is_fn(item):
  345. continue
  346. if item.keymap is not None:
  347. yield item
  348. @classmethod
  349. def register(cls):
  350. wm = bpy.context.window_manager
  351. # Write into defaults, users may modify in preferences.
  352. kc = wm.keyconfigs.default
  353. # Track which tool-group was last used for non-active groups.
  354. # Blender stores the active tool-group index.
  355. #
  356. # {tool_name_first: index_in_group, ...}
  357. cls._tool_group_active = {}
  358. # ignore in background mode
  359. if kc is None:
  360. return
  361. for context_mode, tools in cls.tools_all():
  362. if context_mode is None:
  363. context_descr = "All"
  364. else:
  365. context_descr = context_mode.replace("_", " ").title()
  366. for item in cls._tools_flatten_with_keymap(tools):
  367. keymap_data = item.keymap
  368. if callable(keymap_data[0]):
  369. cls._km_action_simple(kc, context_descr, item.label, keymap_data)
  370. @classmethod
  371. def keymap_ui_hierarchy(cls, context_mode):
  372. # See: bpy_extras.keyconfig_utils
  373. # Keymaps may be shared, don't show them twice.
  374. visited = set()
  375. for context_mode_test, tools in cls.tools_all():
  376. if context_mode_test == context_mode:
  377. for item in cls._tools_flatten_with_keymap(tools):
  378. km_name = item.keymap[0]
  379. # print((km.name, cls.bl_space_type, 'WINDOW', []))
  380. if km_name in visited:
  381. continue
  382. visited.add(km_name)
  383. yield (km_name, cls.bl_space_type, 'WINDOW', [])
  384. # -------------------------------------------------------------------------
  385. # Layout Generators
  386. #
  387. # Meaning of recieved values:
  388. # - Bool: True for a separator, otherwise False for regular tools.
  389. # - None: Signal to finish (complete any final operations, e.g. add padding).
  390. @staticmethod
  391. def _layout_generator_single_column(layout, scale_y):
  392. col = layout.column(align=True)
  393. col.scale_y = scale_y
  394. is_sep = False
  395. while True:
  396. if is_sep is True:
  397. col = layout.column(align=True)
  398. col.scale_y = scale_y
  399. elif is_sep is None:
  400. yield None
  401. return
  402. is_sep = yield col
  403. @staticmethod
  404. def _layout_generator_multi_columns(layout, column_count, scale_y):
  405. scale_x = scale_y * 1.1
  406. column_last = column_count - 1
  407. col = layout.column(align=True)
  408. row = col.row(align=True)
  409. row.scale_x = scale_x
  410. row.scale_y = scale_y
  411. is_sep = False
  412. column_index = 0
  413. while True:
  414. if is_sep is True:
  415. if column_index != column_last:
  416. row.label(text="")
  417. col = layout.column(align=True)
  418. row = col.row(align=True)
  419. row.scale_x = scale_x
  420. row.scale_y = scale_y
  421. column_index = 0
  422. is_sep = yield row
  423. if is_sep is None:
  424. if column_index == column_last:
  425. row.label(text="")
  426. yield None
  427. return
  428. if column_index == column_count:
  429. column_index = 0
  430. row = col.row(align=True)
  431. row.scale_x = scale_x
  432. row.scale_y = scale_y
  433. column_index += 1
  434. @staticmethod
  435. def _layout_generator_detect_from_region(layout, region, scale_y):
  436. """
  437. Choose an appropriate layout for the toolbar.
  438. """
  439. # Currently this just checks the width,
  440. # we could have different layouts as preferences too.
  441. system = bpy.context.preferences.system
  442. view2d = region.view2d
  443. view2d_scale = (
  444. view2d.region_to_view(1.0, 0.0)[0] -
  445. view2d.region_to_view(0.0, 0.0)[0]
  446. )
  447. width_scale = region.width * view2d_scale / system.ui_scale
  448. if width_scale > 120.0:
  449. show_text = True
  450. column_count = 1
  451. else:
  452. show_text = False
  453. # 2 column layout, disabled
  454. if width_scale > 80.0:
  455. column_count = 2
  456. else:
  457. column_count = 1
  458. if column_count == 1:
  459. ui_gen = ToolSelectPanelHelper._layout_generator_single_column(
  460. layout, scale_y=scale_y,
  461. )
  462. else:
  463. ui_gen = ToolSelectPanelHelper._layout_generator_multi_columns(
  464. layout, column_count=column_count, scale_y=scale_y,
  465. )
  466. return ui_gen, show_text
  467. @classmethod
  468. def draw_cls(cls, layout, context, detect_layout=True, scale_y=1.75):
  469. # Use a classmethod so it can be called outside of a panel context.
  470. # XXX, this UI isn't very nice.
  471. # We might need to create new button types for this.
  472. # Since we probably want:
  473. # - tool-tips that include multiple key shortcuts.
  474. # - ability to click and hold to expose sub-tools.
  475. space_type = context.space_data.type
  476. tool_active_id = getattr(
  477. ToolSelectPanelHelper._tool_active_from_context(context, space_type),
  478. "idname", None,
  479. )
  480. if detect_layout:
  481. ui_gen, show_text = cls._layout_generator_detect_from_region(layout, context.region, scale_y)
  482. else:
  483. ui_gen = ToolSelectPanelHelper._layout_generator_single_column(layout, scale_y)
  484. show_text = True
  485. # Start iteration
  486. ui_gen.send(None)
  487. for item in cls.tools_from_context(context):
  488. if item is None:
  489. ui_gen.send(True)
  490. continue
  491. if type(item) is tuple:
  492. is_active = False
  493. i = 0
  494. for i, sub_item in enumerate(item):
  495. if sub_item is None:
  496. continue
  497. is_active = (sub_item.idname == tool_active_id)
  498. if is_active:
  499. index = i
  500. break
  501. del i, sub_item
  502. if is_active:
  503. # not ideal, write this every time :S
  504. cls._tool_group_active[item[0].idname] = index
  505. else:
  506. index = cls._tool_group_active.get(item[0].idname, 0)
  507. item = item[index]
  508. use_menu = True
  509. else:
  510. index = -1
  511. use_menu = False
  512. is_active = (item.idname == tool_active_id)
  513. icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon)
  514. sub = ui_gen.send(False)
  515. if use_menu:
  516. sub.operator_menu_hold(
  517. "wm.tool_set_by_id",
  518. text=item.label if show_text else "",
  519. depress=is_active,
  520. menu="WM_MT_toolsystem_submenu",
  521. icon_value=icon_value,
  522. ).name = item.idname
  523. else:
  524. sub.operator(
  525. "wm.tool_set_by_id",
  526. text=item.label if show_text else "",
  527. depress=is_active,
  528. icon_value=icon_value,
  529. ).name = item.idname
  530. # Signal to finish any remaining layout edits.
  531. ui_gen.send(None)
  532. def draw(self, context):
  533. self.draw_cls(self.layout, context)
  534. @staticmethod
  535. def _tool_key_from_context(context, *, space_type=None):
  536. if space_type is None:
  537. space_data = context.space_data
  538. space_type = space_data.type
  539. else:
  540. space_data = None
  541. if space_type == 'VIEW_3D':
  542. return space_type, context.mode
  543. elif space_type == 'IMAGE_EDITOR':
  544. if space_data is None:
  545. space_data = context.space_data
  546. return space_type, space_data.mode
  547. elif space_type == 'NODE_EDITOR':
  548. return space_type, None
  549. else:
  550. return None, None
  551. @staticmethod
  552. def tool_active_from_context(context):
  553. space_type = context.space_data.type
  554. return ToolSelectPanelHelper._tool_active_from_context(context, space_type)
  555. @staticmethod
  556. def draw_active_tool_header(
  557. context, layout,
  558. *,
  559. show_tool_name=False,
  560. tool_key=None,
  561. ):
  562. if tool_key is None:
  563. space_type, mode = ToolSelectPanelHelper._tool_key_from_context(context)
  564. else:
  565. space_type, mode = tool_key
  566. if space_type is None:
  567. return None
  568. item, tool, icon_value = ToolSelectPanelHelper._tool_get_active(context, space_type, mode, with_icon=True)
  569. if item is None:
  570. return None
  571. # Note: we could show 'item.text' here but it makes the layout jitter when switching tools.
  572. # Add some spacing since the icon is currently assuming regular small icon size.
  573. layout.label(text=" " + item.label if show_tool_name else " ", icon_value=icon_value)
  574. if show_tool_name:
  575. layout.separator()
  576. draw_settings = item.draw_settings
  577. if draw_settings is not None:
  578. draw_settings(context, layout, tool)
  579. return tool
  580. # The purpose of this menu is to be a generic popup to select between tools
  581. # in cases when a single tool allows to select alternative tools.
  582. class WM_MT_toolsystem_submenu(Menu):
  583. bl_label = ""
  584. @staticmethod
  585. def _tool_group_from_button(context):
  586. # Lookup the tool definitions based on the space-type.
  587. cls = ToolSelectPanelHelper._tool_class_from_space_type(context.space_data.type)
  588. if cls is not None:
  589. button_identifier = ToolSelectPanelHelper._tool_identifier_from_button(context)
  590. for item_group in cls.tools_from_context(context):
  591. if type(item_group) is tuple:
  592. for sub_item in item_group:
  593. if (sub_item is not None) and (sub_item.idname == button_identifier):
  594. return cls, item_group
  595. return None, None
  596. def draw(self, context):
  597. layout = self.layout
  598. layout.scale_y = 2.0
  599. _cls, item_group = self._tool_group_from_button(context)
  600. if item_group is None:
  601. # Should never happen, just in case
  602. layout.label(text="Unable to find toolbar group")
  603. return
  604. for item in item_group:
  605. if item is None:
  606. layout.separator()
  607. continue
  608. icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon)
  609. layout.operator(
  610. "wm.tool_set_by_id",
  611. text=item.label,
  612. icon_value=icon_value,
  613. ).name = item.idname
  614. def _activate_by_item(context, space_type, item, index):
  615. tool = ToolSelectPanelHelper._tool_active_from_context(context, space_type, create=True)
  616. tool.setup(
  617. idname=item.idname,
  618. keymap=item.keymap[0] if item.keymap is not None else "",
  619. cursor=item.cursor or 'DEFAULT',
  620. gizmo_group=item.widget or "",
  621. data_block=item.data_block or "",
  622. operator=item.operator or "",
  623. index=index,
  624. )
  625. WindowManager = bpy.types.WindowManager
  626. handle_map = _activate_by_item._cursor_draw_handle
  627. handle = handle_map.pop(space_type, None)
  628. if (handle is not None):
  629. WindowManager.draw_cursor_remove(handle)
  630. if item.draw_cursor is not None:
  631. def handle_fn(context, item, tool, xy):
  632. item.draw_cursor(context, tool, xy)
  633. handle = WindowManager.draw_cursor_add(handle_fn, (context, item, tool), space_type)
  634. handle_map[space_type] = handle
  635. _activate_by_item._cursor_draw_handle = {}
  636. def activate_by_id(context, space_type, text):
  637. _cls, item, index = ToolSelectPanelHelper._tool_get_by_id(context, space_type, text)
  638. if item is None:
  639. return False
  640. _activate_by_item(context, space_type, item, index)
  641. return True
  642. def activate_by_id_or_cycle(context, space_type, idname, offset=1):
  643. # Only cycle when the active tool is activated again.
  644. cls, item, _index = ToolSelectPanelHelper._tool_get_by_id(context, space_type, idname)
  645. if item is None:
  646. return False
  647. tool_active = ToolSelectPanelHelper._tool_active_from_context(context, space_type)
  648. id_active = getattr(tool_active, "idname", None)
  649. id_current = ""
  650. for item_group in cls.tools_from_context(context):
  651. if type(item_group) is tuple:
  652. index_current = cls._tool_group_active.get(item_group[0].idname, 0)
  653. for sub_item in item_group:
  654. if sub_item.idname == idname:
  655. id_current = item_group[index_current].idname
  656. break
  657. if id_current:
  658. break
  659. if id_current == "":
  660. return activate_by_id(context, space_type, idname)
  661. if id_active != id_current:
  662. return activate_by_id(context, space_type, id_current)
  663. index_found = (tool_active.index + offset) % len(item_group)
  664. cls._tool_group_active[item_group[0].idname] = index_found
  665. item_found = item_group[index_found]
  666. _activate_by_item(context, space_type, item_found, index_found)
  667. return True
  668. def description_from_id(context, space_type, idname, *, use_operator=True):
  669. # Used directly for tooltips.
  670. _cls, item, _index = ToolSelectPanelHelper._tool_get_by_id(context, space_type, idname)
  671. if item is None:
  672. return False
  673. # Custom description.
  674. description = item.description
  675. if description is not None:
  676. if callable(description):
  677. km = _keymap_from_item(context, item)
  678. return description(context, item, km)
  679. return description
  680. # Extract from the operator.
  681. if use_operator:
  682. operator = item.operator
  683. if operator is None:
  684. if item.keymap is not None:
  685. km = _keymap_from_item(context, item)
  686. if km is not None:
  687. for kmi in km.keymap_items:
  688. if kmi.active:
  689. operator = kmi.idname
  690. break
  691. if operator is not None:
  692. import _bpy
  693. return _bpy.ops.get_rna_type(operator).description
  694. return ""
  695. def item_from_id(context, space_type, idname):
  696. # Used directly for tooltips.
  697. _cls, item, _index = ToolSelectPanelHelper._tool_get_by_id(context, space_type, idname)
  698. return item
  699. def item_from_flat_index(context, space_type, index):
  700. _cls, item, _index = ToolSelectPanelHelper._tool_get_by_flat_index(context, space_type, index)
  701. return item
  702. def item_from_index(context, space_type, index):
  703. _cls, item, _index = ToolSelectPanelHelper._tool_get_by_index(context, space_type, index)
  704. return item
  705. def keymap_from_id(context, space_type, idname):
  706. # Used directly for tooltips.
  707. _cls, item, _index = ToolSelectPanelHelper._tool_get_by_id(context, space_type, idname)
  708. if item is None:
  709. return False
  710. keymap = item.keymap
  711. # List container of one.
  712. if keymap:
  713. return keymap[0]
  714. return ""
  715. def _keymap_from_item(context, item):
  716. if item.keymap is not None:
  717. wm = context.window_manager
  718. keyconf = wm.keyconfigs.active
  719. return keyconf.keymaps.get(item.keymap[0])
  720. return None
  721. classes = (
  722. WM_MT_toolsystem_submenu,
  723. )
  724. if __name__ == "__main__": # only for live edit.
  725. from bpy.utils import register_class
  726. for cls in classes:
  727. register_class(cls)