From bc57a46961b1ba1c16c9da1c6fe8199f2cbe5a67 Mon Sep 17 00:00:00 2001 From: Hasiel Alvarez Date: Thu, 27 Jul 2023 17:35:08 -0700 Subject: [PATCH 1/7] Add support for custom icons by id in blender --- unimenu/apps/blender.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/unimenu/apps/blender.py b/unimenu/apps/blender.py index 124fdfe..8cde8ed 100644 --- a/unimenu/apps/blender.py +++ b/unimenu/apps/blender.py @@ -70,7 +70,11 @@ def execute(self, context): def menu_draw(self, context): # self is the parent menu # todo check if icon exists, if not use NONE, for now dirty try except hack try: - self.layout.operator(id_name, icon=icon_name) + if isinstance(icon_name, int): + # if the icon is a number it might be a custom icon + self.layout.operator(id_name, icon_value=icon_name) + else: + self.layout.operator(id_name, icon=icon_name) except TypeError: # icon not found: self.layout.operator(id_name, icon="NONE") From 4162a7e08dda25a24ff40527d7638b40429077ec Mon Sep 17 00:00:00 2001 From: hannes Date: Wed, 2 Aug 2023 15:50:43 +0100 Subject: [PATCH 2/7] refactor blender icon setup, add logging --- unimenu/apps/blender.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/unimenu/apps/blender.py b/unimenu/apps/blender.py index 8cde8ed..383f303 100644 --- a/unimenu/apps/blender.py +++ b/unimenu/apps/blender.py @@ -4,6 +4,8 @@ # 3. register operators # 4. add operators to menu """ +import logging + import bpy from typing import Union, Callable from unimenu.apps._abstract import MenuNodeAbstract @@ -68,15 +70,34 @@ def execute(self, context): # add to menu def menu_draw(self, context): # self is the parent menu + + icon_ID = None + icon_name = None + + if isinstance(icon_name, int): # use icon ID for custom icons + logging.debug(f"icon_name is int: {icon_name}") + icon_ID = icon_name + + elif isinstance(icon_name, str): # if str, can be default icon name, or a string path + logging.debug(f"icon_name is str: {icon_name}") + icon_name=icon_name + else: + raise TypeError(f"icon_name '{icon_name}' isn't a valid type '{type(icon_name)}', " + f"expecting str, int, Path") + + if icon_name: + self.layout.operator(id_name, icon=icon_name) + elif icon_ID: + self.layout.operator(id_name, icon_value=icon_ID) + else: + self.layout.operator(id_name, icon="NONE") + + def try_menu_draw(self, context): # self is the parent menu # todo check if icon exists, if not use NONE, for now dirty try except hack try: - if isinstance(icon_name, int): - # if the icon is a number it might be a custom icon - self.layout.operator(id_name, icon_value=icon_name) - else: - self.layout.operator(id_name, icon=icon_name) + return menu_draw(self, context) except TypeError: # icon not found: - self.layout.operator(id_name, icon="NONE") + return self.layout.operator(id_name, icon="NONE") parent.append(menu_draw) From 53f7b04b801151ec893af112f99418d8261fa0c7 Mon Sep 17 00:00:00 2001 From: hannes Date: Wed, 2 Aug 2023 16:23:56 +0100 Subject: [PATCH 3/7] refactor, fix incorrect override variable, name clash icon_name --- unimenu/apps/blender.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/unimenu/apps/blender.py b/unimenu/apps/blender.py index 383f303..e8e0869 100644 --- a/unimenu/apps/blender.py +++ b/unimenu/apps/blender.py @@ -66,32 +66,33 @@ def execute(self, context): bpy.utils.register_class(OperatorWrapper) # ensure None was not accidentally passed - icon_name = icon_name or "NONE" + icon_value = icon_name or "NONE" # add to menu def menu_draw(self, context): # self is the parent menu + _icon_ID = None + _icon_name = None - icon_ID = None - icon_name = None + if isinstance(icon_value, int): # use icon ID for custom icons + logging.debug(f"icon_name is int: {icon_value}") + _icon_ID = icon_value - if isinstance(icon_name, int): # use icon ID for custom icons - logging.debug(f"icon_name is int: {icon_name}") - icon_ID = icon_name + elif isinstance(icon_value, str): # if str, can be default icon name, or a string path + logging.debug(f"icon_name is str: {icon_value}") + _icon_name=icon_value - elif isinstance(icon_name, str): # if str, can be default icon name, or a string path - logging.debug(f"icon_name is str: {icon_name}") - icon_name=icon_name else: - raise TypeError(f"icon_name '{icon_name}' isn't a valid type '{type(icon_name)}', " + raise TypeError(f"icon_name '{icon_value}' isn't a valid type '{type(icon_value)}', " f"expecting str, int, Path") - if icon_name: + if _icon_name: self.layout.operator(id_name, icon=icon_name) - elif icon_ID: - self.layout.operator(id_name, icon_value=icon_ID) + elif _icon_ID: + self.layout.operator(id_name, icon_value=_icon_ID) else: self.layout.operator(id_name, icon="NONE") + def try_menu_draw(self, context): # self is the parent menu # todo check if icon exists, if not use NONE, for now dirty try except hack try: @@ -104,6 +105,7 @@ def try_menu_draw(self, context): # self is the parent menu return OperatorWrapper + def menu_wrapper(parent: bpy.types.Operator, label: str) -> bpy.types.Menu: """ 1 make class From ef49d543fc5c1930810f811f37053ea1c3a37ea8 Mon Sep 17 00:00:00 2001 From: hannes Date: Wed, 2 Aug 2023 16:47:25 +0100 Subject: [PATCH 4/7] add support for custom icon paths --- unimenu/apps/blender.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/unimenu/apps/blender.py b/unimenu/apps/blender.py index e8e0869..f8014bd 100644 --- a/unimenu/apps/blender.py +++ b/unimenu/apps/blender.py @@ -5,8 +5,10 @@ # 4. add operators to menu """ import logging +import pathlib import bpy +import bpy.utils.previews from typing import Union, Callable from unimenu.apps._abstract import MenuNodeAbstract import unimenu.apps @@ -21,6 +23,26 @@ def unique_operator_name(name) -> str: return new_name +def create_custom_icon(path, name: str = None): + """ + helper method to create a custom icon + + path: path to png + + returns icon ID + """ + name = name or "icon_name" + + preview_collection = bpy.utils.previews.new() + icon = preview_collection.load(name=name, path=str(path), path_type='IMAGE') + + icon_ID = icon.icon_id + # icon = preview_collection["icon_name"] # icon can be loaded by name + + # bpy.utils.previews.remove(preview_collection) # delete preview to avoid warning + return icon_ID + + def operator_wrapper( parent: bpy.types.Operator, label: str, id: str, command: Union[str, Callable], icon_name=None, tooltip=None ) -> bpy.types.Operator: @@ -79,7 +101,18 @@ def menu_draw(self, context): # self is the parent menu elif isinstance(icon_value, str): # if str, can be default icon name, or a string path logging.debug(f"icon_name is str: {icon_value}") - _icon_name=icon_value + + # check if it's a path + if pathlib.Path(icon_value).exists(): + logging.debug(f"icon_name str path exists: {icon_value}") + _icon_ID = create_custom_icon(icon_value) + + else: # assume icon is a default icon name + logging.debug(f"icon_name str path doesn't exists: {icon_value}, assuming it's a default icon name") + _icon_name=icon_value + + elif isinstance(icon_name, pathlib.Path): # use icon path for custom icons + _icon_ID = create_custom_icon(icon_value) else: raise TypeError(f"icon_name '{icon_value}' isn't a valid type '{type(icon_value)}', " @@ -100,7 +133,7 @@ def try_menu_draw(self, context): # self is the parent menu except TypeError: # icon not found: return self.layout.operator(id_name, icon="NONE") - parent.append(menu_draw) + parent.append(try_menu_draw) return OperatorWrapper From 913da2a49f2ffb42dff5fab98ef45fbda8337799 Mon Sep 17 00:00:00 2001 From: hannes Date: Wed, 2 Aug 2023 16:55:13 +0100 Subject: [PATCH 5/7] refactor, fix some bugs, rename operator_wrapper kwarg icon_name to icon --- unimenu/apps/blender.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/unimenu/apps/blender.py b/unimenu/apps/blender.py index f8014bd..05f2769 100644 --- a/unimenu/apps/blender.py +++ b/unimenu/apps/blender.py @@ -40,11 +40,12 @@ def create_custom_icon(path, name: str = None): # icon = preview_collection["icon_name"] # icon can be loaded by name # bpy.utils.previews.remove(preview_collection) # delete preview to avoid warning + # deleting this also deletes the icon, todo how to handle this better? return icon_ID def operator_wrapper( - parent: bpy.types.Operator, label: str, id: str, command: Union[str, Callable], icon_name=None, tooltip=None + parent: bpy.types.Operator, label: str, id: str, command: Union[str, Callable], icon=None, tooltip=None ) -> bpy.types.Operator: """ Wrap a command in a Blender operator & add it to a parent menu operator. @@ -54,7 +55,7 @@ def operator_wrapper( 3 add to (sub)menu (parent operator) """ - icon_name = icon_name or "NONE" + icon = icon or "NONE" tooltip = tooltip or "" # handle name @@ -88,38 +89,38 @@ def execute(self, context): bpy.utils.register_class(OperatorWrapper) # ensure None was not accidentally passed - icon_value = icon_name or "NONE" + icon = icon or "NONE" # add to menu def menu_draw(self, context): # self is the parent menu _icon_ID = None _icon_name = None - if isinstance(icon_value, int): # use icon ID for custom icons - logging.debug(f"icon_name is int: {icon_value}") - _icon_ID = icon_value + if isinstance(icon, int): # use icon ID for custom icons + logging.debug(f"icon is int: {icon}") + _icon_ID = icon - elif isinstance(icon_value, str): # if str, can be default icon name, or a string path - logging.debug(f"icon_name is str: {icon_value}") + elif isinstance(icon, str): # if str, can be default icon name, or a string path + logging.debug(f"icon is str: {icon}") # check if it's a path - if pathlib.Path(icon_value).exists(): - logging.debug(f"icon_name str path exists: {icon_value}") - _icon_ID = create_custom_icon(icon_value) + if pathlib.Path(icon).exists(): + logging.debug(f"icon str path exists: {icon}") + _icon_ID = create_custom_icon(icon) else: # assume icon is a default icon name - logging.debug(f"icon_name str path doesn't exists: {icon_value}, assuming it's a default icon name") - _icon_name=icon_value + logging.debug(f"icon str path doesn't exists: {icon}, assuming it's a default icon name") + _icon_name=icon - elif isinstance(icon_name, pathlib.Path): # use icon path for custom icons - _icon_ID = create_custom_icon(icon_value) + elif isinstance(icon, pathlib.Path): # use icon path for custom icons + _icon_ID = create_custom_icon(icon) else: - raise TypeError(f"icon_name '{icon_value}' isn't a valid type '{type(icon_value)}', " + raise TypeError(f"icon '{icon}' isn't a valid type '{type(icon)}', " f"expecting str, int, Path") if _icon_name: - self.layout.operator(id_name, icon=icon_name) + self.layout.operator(id_name, icon=_icon_name) elif _icon_ID: self.layout.operator(id_name, icon_value=_icon_ID) else: @@ -209,7 +210,7 @@ def _setup_sub_menu(self, parent_app_node=None) -> bpy.types.Menu: def _setup_menu_item(self, parent_app_node=None) -> bpy.types.Operator: icon = self.icon or "NONE" tooltip = self.tooltip or "" - return operator_wrapper(parent=parent_app_node, label=self.label, id=self.id, command=self.run, icon_name=icon, tooltip=tooltip) + return operator_wrapper(parent=parent_app_node, label=self.label, id=self.id, command=self.run, icon=icon, tooltip=tooltip) def _setup_separator(self, parent_app_node=None): # todo return separator correctly From 623d440b13440b440dcbc574a27814397b74f5c5 Mon Sep 17 00:00:00 2001 From: hannes Date: Wed, 2 Aug 2023 17:03:27 +0100 Subject: [PATCH 6/7] add sample for custom icon --- dev/unimenu_samples/blender_custom_icon.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 dev/unimenu_samples/blender_custom_icon.py diff --git a/dev/unimenu_samples/blender_custom_icon.py b/dev/unimenu_samples/blender_custom_icon.py new file mode 100644 index 0000000..12ebde3 --- /dev/null +++ b/dev/unimenu_samples/blender_custom_icon.py @@ -0,0 +1,13 @@ +import unimenu +import pyblish_lite +import pathlib + + +icon_path = pathlib.Path(pyblish_lite.__file__).parent / "img" / "logo-extrasmall.png" +menu_node = unimenu.Node( + label="pyblish lite", + command="import pyblish_lite;pyblish_lite.show()", + tooltip="Open Pyblish Lite", + icon=str(icon_path), +) +menu_node.setup() From 6a4d42cc69818cc21e74a40aa943357e2dcd5665 Mon Sep 17 00:00:00 2001 From: hannes Date: Wed, 2 Aug 2023 17:13:20 +0100 Subject: [PATCH 7/7] fix to remove warning on custom icon --- unimenu/apps/blender.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/unimenu/apps/blender.py b/unimenu/apps/blender.py index 05f2769..66f3522 100644 --- a/unimenu/apps/blender.py +++ b/unimenu/apps/blender.py @@ -13,6 +13,11 @@ from unimenu.apps._abstract import MenuNodeAbstract import unimenu.apps + +preview_collections = {} # store custom icons here to prevent blender warnings +# todo cleanup on teardown / unload with bpy.utils.previews.remove(preview_collection) + + def unique_operator_name(name) -> str: """ensure unique name for blender operators, adds _number to the end if name not unique""" unique_counter = 1 # start count from 2 @@ -33,14 +38,15 @@ def create_custom_icon(path, name: str = None): """ name = name or "icon_name" + preview_collection = preview_collections.get(path) + if preview_collection: + return preview_collection[name].icon_id + preview_collection = bpy.utils.previews.new() + preview_collections[path] = preview_collection icon = preview_collection.load(name=name, path=str(path), path_type='IMAGE') - icon_ID = icon.icon_id - # icon = preview_collection["icon_name"] # icon can be loaded by name - # bpy.utils.previews.remove(preview_collection) # delete preview to avoid warning - # deleting this also deletes the icon, todo how to handle this better? return icon_ID