Source code for cgl.plugins.CustomMenu
import json
import logging
import os
import sys
from datetime import datetime
from cgl.msd.version import VersionMsd
from cgl.msd.services.event_service import EventService
from cgl.core.config.query import AlchemyConfigManager
CFG = AlchemyConfigManager()
[docs]
class CustomMenu(object):
"""
A Base Class to be used in conjunction with the "Menu Designer".
A user simply has to fill in the following functions:
get_scene_path()
set_menu_parent()
create_menu()
add_button()f
delete_menu()
find_menu_by_name()
Examples can be found here:
1) `plugins/nuke/custom_menu.py`
1) `plugins/maya/custom_menu.py`
"""
path_object = None
def __init__(self, software, type_):
self.software = software
self.type = type_
self.scene_path = self.get_scene_path()
self.menu_parent = self.set_menu_parent()
self.shelf_set_name = None
self.shelf_path = None
self.shelf_set = None
self.path_object = None
if not self.software == "unreal":
self.set_path_object()
# self.cfg = ProjectConfig.getLastConfig()
# cookbook_dir = os.path.dirname(self.cfg.cookbook_folder)
self.cookbook_dir = CFG.cookbook_dir
self.config_root = CFG.cookbook_root
print("config root", self.config_root)
print("Adding {} to PATH".format(self.config_root))
sys.path.insert(0, self.config_root)
self.menus_file = os.path.join(
self.cookbook_dir, software, "%s.cgl" % self.type
).replace("\\", "/")
self.menus = self.load_cgl()
self.menus_folder = os.path.join(
os.path.dirname(self.menus_file), type_
).replace("\\", "/")
self.menu_dict = {}
[docs]
def set_path_object(self):
from cgl.core.path.object import PathObject
if self.scene_path:
self.path_object = PathObject().from_path_string(str(self.scene_path))
[docs]
def load_cgl(self):
"""
Returns:
dict: all the shelves, menus, or publish from the json file
"""
logging.info("Menus file: {}".format(self.menus_file))
if os.path.exists(self.menus_file):
with open(self.menus_file, "r") as stream:
result = json.load(stream)
if result:
return result[self.software]
else:
return
else:
logging.info("No menu file found!")
[docs]
def order_buttons(self, menu):
"""Orders the buttons correctly within a menu.
Args:
menu (str): the name of the menu to order
Returns:
list: a sorted list of buttons
"""
buttons = self.menus[menu]
buttons.pop("order")
try:
# there is something weird about this - as soon as these are removed "shelves" never is reinitialized
buttons.pop("active")
except KeyError:
pass
for button in buttons:
if button:
buttons[button]["order"] = buttons[button].get("order", 10)
if buttons:
return sorted(buttons, key=lambda key: buttons[key]["order"])
else:
return {}
[docs]
@staticmethod
def remove_inactive_buttons(menus):
to_pop = []
for menu in menus:
if menus[menu]["active"] == 0:
to_pop.append(menu)
for each in to_pop:
menus.pop(each)
if menus:
return menus
else:
return {}
[docs]
def get_icon_path(self, shelf, button):
"""
returns the icon path within the current menu of the cgl_tools directory of the corresponding icon
Args:
shelf:
button:
Returns:
icon path string
"""
icon = self.menus[shelf][button]["icon"]
logging.info(icon)
if icon:
# TODO need to add the root path to this.
icon_path = CFG.icon_path
print("cookbook icon path", icon_path)
if icon_path:
logging.info("icon", icon, icon_path)
icon_file = os.path.join(icon_path, icon)
return icon_file
else:
return ""
else:
return ""
# When Starting a new shelf, simply copy all methods below and fill them in with software-specific functions
# See Nuke and Maya examples: plugins/nuke/custom_menu.py & plugins/maya/custom_menu.py
[docs]
def add_button(
self,
menu_label,
label="",
annotation="",
command="",
icon="",
image_overlay_label="",
hot_key="",
menu_name=None,
):
"""
Adds a button with telemetry wrapping.
Subclasses implement `_create_button` to attach to the actual UI.
"""
def wrapped_command():
self._log_button_event(menu_label, label, command)
self._execute_command(
command
) # i can add a parent event id - parent_id = event_data["id"]
return self._create_button(
menu=menu_label,
label=label,
annotation=annotation,
command=wrapped_command,
icon=icon,
image_overlay_label=image_overlay_label,
hot_key=hot_key,
)
# --- to be implemented by DCCs ---
def _create_button(
self, menu, label, annotation, command, icon, image_overlay_label, hot_key
):
parts = self._process_cookbook_command(command)
print(parts)
raise NotImplementedError("_create_button must be implemented by subclass")
# --- telemetry and execution helpers ---
def _log_button_event(self, menu, label, command):
"""
Log a button click event to the version.msd file.
"""
parent_event_id = None
action = "menu_button_clicked"
repo = VersionMsd(path=self.path_object)
svc = EventService(repo)
parts = self._process_cookbook_command(command)
details = {
"tool": label or "unknown",
"recipe": (
command
if isinstance(command, str)
else getattr(command, "__name__", "callable")
),
"software": parts["software"],
"recipe_type": parts["recipe_type"],
"recipe_name": parts["recipe"],
"step": parts["step"],
"menu": getattr(menu, "objectName", lambda: str(menu))(),
"timestamp": datetime.utcnow().isoformat() + "Z",
}
if parent_event_id:
details["parent_event_id"] = parent_event_id
try:
event_data = svc.log_event(
action=action,
app=parts["software"],
details=details,
)
logging.info(f"Logged menu button click: {label}")
return event_data
except Exception as e:
logging.error(f"Failed to log tool event: {e}")
details["traceback"] = str(e)
svc.log_bug(
bug_id="button_clicked_logging_failed",
status="error",
details=details,
persist=True,
recipe_type=parts["recipe_type"],
app=parts["software"],
notify_discord=True,
)
def _execute_command(self, command):
"""
Execute the wrapped tool command.
"""
try:
if callable(command):
command()
elif isinstance(command, str):
exec(command, globals(), locals())
except Exception as e:
logging.error(f"Error executing menu command: {e}")
def _process_cookbook_command(self, command: str) -> dict:
"""
Parse a cookbook command string (or import path) into its components:
software, recipe_type, recipe, step.
Examples:
"cookbook.maya.tools.shaderbuddy.ui" → maya / tools / shaderbuddy / ui
"cookbook.unreal.layout.build_scene" → unreal / layout / build_scene / None
"import cookbook.maya.tools.shaderbuddy.ui" → maya / tools / shaderbuddy / ui
"cookbook.maya.tools.shaderbuddy.ui:run()" → maya / tools / shaderbuddy / ui
"""
result = {
"software": "unknown",
"recipe_type": "unknown",
"recipe": "unknown",
"step": "unknown",
}
if not isinstance(command, str):
return result
# Strip syntax noise
clean = (
command.replace("import ", "")
.replace(";", "")
.replace("python(", "")
.replace(")", "")
)
clean = clean.split(":")[0] # handle 'module:func()'
parts = [p for p in clean.split(".") if p]
# Expect pattern: cookbook.<software>.<recipe_type>.<recipe>.<step?>
if len(parts) >= 3 and parts[0] == "cookbook":
result["software"] = parts[1]
result["recipe_type"] = parts[2]
if len(parts) > 3:
result["recipe"] = parts[3]
if len(parts) > 4:
result["step"] = parts[4]
return result