# 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()