Source code for cgl.apps.cookbook.cookbook

# Test of a commit
from __future__ import annotations

from dataclasses import dataclass
import logging
from pathlib import Path
import importlib
from PySide6 import QtGui, QtWidgets, QtCore

import cgl.apps.cookbook.widgets.footer as footer
import cgl.apps.cookbook.widgets.header as header
import cgl.apps.cookbook.widgets_new.left_menu as leftMenu
from cgl.apps.cookbook.widgets_new import recipe_details
from cgl.core.config.query import AlchemyConfigManager
from cgl.apps.cookbook.widgets.tools import ToolBrowserWidget
from cgl.core.utils.general import apply_theme


CFG = AlchemyConfigManager()

logger = logging.getLogger(__name__)


# -------------------------------
# Settings keys + facade
# -------------------------------
[docs] @dataclass(frozen=True) class Keys: COMPANY = "Context/company_short_name" PROJECT = "Context/project_short_name" SOFTWARE = "LeftMenu/lastSoftware" TYPE = "LeftMenu/lastRecipeType" RECIPE = "LeftMenu/lastRecipeLabel" # NEW: remember last selected recipe label
[docs] class Settings: def __init__(self): self._s = QtCore.QSettings()
[docs] def get(self, key: str, default: str = "") -> str: return self._s.value(key, default, str)
[docs] def set(self, key: str, value: str) -> None: self._s.setValue(key, value)
# convenience
[docs] def company(self, default="CGL"): return self.get(Keys.COMPANY, default)
[docs] def set_company(self, v: str): self.set(Keys.COMPANY, v)
[docs] def project(self, default="default"): return self.get(Keys.PROJECT, default)
[docs] def set_project(self, v: str): self.set(Keys.PROJECT, v)
[docs] def last_software(self, default="Blender"): return self.get(Keys.SOFTWARE, default)
[docs] def set_last_software(self, v: str): self.set(Keys.SOFTWARE, v)
[docs] def last_recipe_type(self, default="Tools"): return self.get(Keys.TYPE, default)
[docs] def set_last_recipe_type(self, v: str): self.set(Keys.TYPE, v)
[docs] def last_recipe_label(self, default=""): return self.get(Keys.RECIPE, default)
[docs] def set_last_recipe_label(self, v: str): self.set(Keys.RECIPE, v)
# ------------------------------- # Main window # -------------------------------
[docs] class Cookbook(QtWidgets.QWidget): """ Main Cookbook widget: - header - left menu - steps panel (details) - footer """ MARGIN = 5 def __init__(self, company: str, parent=None): super().__init__(parent) self.company = company self._settings = Settings() apply_theme(self) # window chrome icon_path = ( Path(CFG.get_code_root()) / "resources" / "icons" / "alchemy_green512px.ico" ) self.setWindowTitle("Alchemist's Cookbook") self.setWindowIcon(QtGui.QIcon(str(icon_path))) self.setObjectName("cb") # widgets self.header = header.CookbookHeader(parent=self) self.left_menu = leftMenu.LeftMenu(company=self.company, parent=self) # right-hand stack self.right_stack = QtWidgets.QStackedWidget() self.steps_panel = recipe_details.StepsPanel( company=self.company, project="default", parent=self ) self.tools_panel = ToolBrowserWidget(parent=self) self.right_stack.addWidget(self.steps_panel) # index 0 self.right_stack.addWidget(self.tools_panel) # index 1 # self.steps_panel = MenuEditorWindow(recipe_list_widget=self.left_menu.recipe_list) self.footer = footer.PathWidget(parent=self) # layout root = QtWidgets.QVBoxLayout() root.setContentsMargins(2, 2, 2, 2) root.setSpacing(0) row = QtWidgets.QHBoxLayout() row.setContentsMargins(0, 0, 0, 0) row.setSpacing(1) root.addWidget(self.header) row.addWidget(self.left_menu) row.addWidget(self.right_stack) row.setStretch(0, 0) row.setStretch(1, 1) root.addLayout(row) root.addWidget(self.footer) root.setStretch(0, 0) self.setLayout(root) self.header.setObjectName("cb_light") # signal wiring self.header.cookbook_root_changed.connect(self.left_menu.load_recipes) self.header.cookbook_root_changed.connect(self.footer.set_path) self.header.company_combo.currentIndexChanged.connect(self.set_repo_path) # keep StepsPanel context in sync with combo changes self.left_menu.software_combo.currentTextChanged.connect( lambda sw: self.steps_panel.update_context( sw, self.left_menu.recipe_type_combo.currentText() ) ) self.left_menu.recipe_type_combo.currentTextChanged.connect( lambda rt: self.steps_panel.update_context( self.left_menu.software_combo.currentText(), rt ) ) self.left_menu.recipe_type_combo.currentTextChanged.connect( self.on_recipe_type_changed ) # recipe selection → details + remember self.left_menu.recipe_changed.connect(self.steps_panel.on_recipe_changed) self.left_menu.recipe_changed.connect(self._remember_last_recipe_label) self.left_menu.tool_changed.connect(self.tools_panel.load_tool) self.left_menu.tool_changed.connect(self.footer.set_path) # code tab path → footer self.steps_panel.tab_changed.connect(self.footer.set_path) # initial state self.set_repo_path() self._initial_load_and_restore() # ------------------------------- # Bootstrapping # -------------------------------
[docs] def on_recipe_type_changed(self): recipe_type = self.left_menu.recipe_type_combo.currentText() if recipe_type.lower() == "tools": self.right_stack.setCurrentWidget(self.tools_panel) # instead of self.tools_panel.model.setRootPath("") self.tools_panel.load_tool("", "") # reset to empty state else: self.right_stack.setCurrentWidget(self.steps_panel)
def _initial_load_and_restore(self): """ Populate the recipe list, sync context, and restore last selected recipe. """ # Ensure left menu has items self.left_menu.load_recipes() # Sync StepsPanel context once on startup sw = self.left_menu.software_combo.currentText() rt = self.left_menu.recipe_type_combo.currentText() self.steps_panel.update_context(sw, rt) self.on_recipe_type_changed() # Ensure StepsPanel is in correct mode # Restore last selected recipe label, if present QtCore.QTimer.singleShot(0, self._restore_last_recipe_selection) def _restore_last_recipe_selection(self): want = self._settings.last_recipe_label("") if not want: return lw = self.left_menu.recipe_list for i in range(lw.count()): item = lw.item(i) if item and item.text() == want: lw.setCurrentItem(item) # Emit selection behavior manually so details panel updates self.left_menu.on_recipe_selected() break def _remember_last_recipe_label(self, item: QtWidgets.QListWidgetItem | None): label = item.text() if item else "" self._settings.set_last_recipe_label(label) # ------------------------------- # Header + footer glue # -------------------------------
[docs] def set_repo_path(self): """Set the footer's repo path based on the current company selection in the header.""" self.footer.set_git_repo(self.company)
# ------------------------------- # Persist UI prefs on close # -------------------------------
[docs] def closeEvent(self, event: QtGui.QCloseEvent): # Save UI prefs to QSettings (company/project fixed, but we can still record them) self._settings.set_company(self.header.get_company_short_name()) self._settings.set_project(self.header.get_project_short_name()) self._settings.set_last_software(self.left_menu.software_combo.currentText()) self._settings.set_last_recipe_type( self.left_menu.recipe_type_combo.currentText() ) cur = self.left_menu.recipe_list.currentItem() self._settings.set_last_recipe_label(cur.text() if cur else "") event.accept()
[docs] def get_cookbook_function(source_file, function_name): """ Get a function from cookbook first, falling back to default implementation. Args: module_path: Dot path to module (e.g., "cookbook.shotgrid.create") function_name: Name of function to import fallback_module: Fallback module if cookbook doesn't exist Returns: Callable function """ source_file = Path(source_file) software = source_file.parent.name software_file = source_file.stem module_path = f"cookbook.{software}.{software_file}" try: # Try cookbook first module = importlib.import_module(module_path) if hasattr(module, function_name): logger.info(f"Using {function_name} from {module_path}") return getattr(module, function_name) except ImportError as e: return None
[docs] def cookbook_override(source_file): """ Decorator that automatically looks for cookbook overrides """ def decorator(original_func): def wrapper(*args, **kwargs): function_name = original_func.__name__ cookbook_func = get_cookbook_function(source_file, function_name) if cookbook_func: return cookbook_func(*args, **kwargs) else: return original_func(*args, **kwargs) return wrapper return decorator
# ------------------------------- # Standalone run # ------------------------------- if __name__ == "__main__": from resources import resources_rc # noqa: F401 from cgl.core.config.query import AlchemyConfigManager import sys CFG = AlchemyConfigManager() cookbook_root = CFG.cookbook_root sys.path.insert(0, cookbook_root) app = QtWidgets.QApplication([]) # apply_theme(app, dev_mode=False) QtCore.QCoreApplication.setOrganizationName("CGLumberjack") QtCore.QCoreApplication.setApplicationName("Cookbook") window = Cookbook(company=CFG.company) window.show() app.exec()