Add a template system that lets users create new items from pre-configured .kc template files and save existing documents as reusable templates. Template use (New Item form): - templates.py: discovery, filtering, TemplateInfo dataclass - schema_form.py: template combo picker filtered by type/category - silo_commands.py: SiloSync.create_document_from_template() copies template .kc, strips identity, stamps Silo properties Template creation (Save as Template): - SaveAsTemplateDialog: captures name, description, item types, categories, author, and tags - Silo_SaveAsTemplate command: copies doc, strips Silo identity, injects silo/template.json, optionally uploads to Silo - Registered in Silo menu via InitGui.py Template search paths (3-tier, later shadows earlier by name): 1. mods/silo/freecad/templates/ (system) 2. ~/.local/share/FreeCAD/Templates/ (personal, sister to Macro/) 3. ~/projects/templates/ (org-shared)
168 lines
5.4 KiB
Python
168 lines
5.4 KiB
Python
"""Template discovery and metadata for Kindred Create .kc templates.
|
|
|
|
A template is a normal ``.kc`` file that contains a ``silo/template.json``
|
|
descriptor inside the ZIP archive. This module scans known directories for
|
|
template files, parses their descriptors, and provides filtering helpers
|
|
used by the schema form UI.
|
|
|
|
Search paths (checked in order, later shadows earlier by name):
|
|
1. ``{silo_addon}/templates/`` — system templates shipped with the addon
|
|
2. ``{userAppData}/Templates/`` — personal templates (sister to Macro/)
|
|
3. ``~/projects/templates/`` — org-shared project templates
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
import zipfile
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
|
|
|
|
@dataclass
|
|
class TemplateInfo:
|
|
"""Parsed template descriptor from ``silo/template.json``."""
|
|
|
|
path: str # Absolute path to the .kc file
|
|
name: str = ""
|
|
description: str = ""
|
|
item_types: List[str] = field(default_factory=list)
|
|
categories: List[str] = field(default_factory=list)
|
|
icon: str = ""
|
|
author: str = ""
|
|
tags: List[str] = field(default_factory=list)
|
|
|
|
|
|
def read_template_info(kc_path: str) -> Optional[TemplateInfo]:
|
|
"""Read ``silo/template.json`` from a ``.kc`` file.
|
|
|
|
Returns a :class:`TemplateInfo` if the file is a valid template,
|
|
or ``None`` if the file does not contain a template descriptor.
|
|
"""
|
|
try:
|
|
with zipfile.ZipFile(kc_path, "r") as zf:
|
|
if "silo/template.json" not in zf.namelist():
|
|
return None
|
|
raw = zf.read("silo/template.json")
|
|
data = json.loads(raw)
|
|
return TemplateInfo(
|
|
path=str(kc_path),
|
|
name=data.get("name", Path(kc_path).stem),
|
|
description=data.get("description", ""),
|
|
item_types=data.get("item_types", []),
|
|
categories=data.get("categories", []),
|
|
icon=data.get("icon", ""),
|
|
author=data.get("author", ""),
|
|
tags=data.get("tags", []),
|
|
)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def inject_template_json(kc_path: str, template_info: dict) -> bool:
|
|
"""Inject ``silo/template.json`` into a ``.kc`` (ZIP) file.
|
|
|
|
Rewrites the ZIP to avoid duplicate entries. Returns ``True`` on
|
|
success, raises on I/O errors.
|
|
"""
|
|
if not os.path.isfile(kc_path):
|
|
return False
|
|
|
|
payload = json.dumps(template_info, indent=2).encode("utf-8")
|
|
|
|
tmp_fd, tmp_path = tempfile.mkstemp(suffix=".kc")
|
|
os.close(tmp_fd)
|
|
try:
|
|
with zipfile.ZipFile(kc_path, "r") as zin:
|
|
with zipfile.ZipFile(tmp_path, "w", zipfile.ZIP_DEFLATED) as zout:
|
|
for item in zin.infolist():
|
|
if item.filename == "silo/template.json":
|
|
continue
|
|
zout.writestr(item, zin.read(item.filename))
|
|
zout.writestr("silo/template.json", payload)
|
|
shutil.move(tmp_path, kc_path)
|
|
return True
|
|
except Exception:
|
|
if os.path.exists(tmp_path):
|
|
os.unlink(tmp_path)
|
|
raise
|
|
|
|
|
|
def get_default_template_dir() -> str:
|
|
"""Return the user templates directory, creating it if needed.
|
|
|
|
Located at ``{userAppData}/Templates/``, a sister to ``Macro/``.
|
|
"""
|
|
import FreeCAD
|
|
|
|
d = os.path.join(FreeCAD.getUserAppDataDir(), "Templates")
|
|
os.makedirs(d, exist_ok=True)
|
|
return d
|
|
|
|
|
|
def discover_templates(search_paths: List[str]) -> List[TemplateInfo]:
|
|
"""Scan search paths for ``.kc`` files containing ``silo/template.json``.
|
|
|
|
Later paths shadow earlier paths by template name.
|
|
"""
|
|
by_name = {}
|
|
for search_dir in search_paths:
|
|
if not os.path.isdir(search_dir):
|
|
continue
|
|
for filename in sorted(os.listdir(search_dir)):
|
|
if not filename.lower().endswith(".kc"):
|
|
continue
|
|
full_path = os.path.join(search_dir, filename)
|
|
info = read_template_info(full_path)
|
|
if info is not None:
|
|
by_name[info.name] = info
|
|
return sorted(by_name.values(), key=lambda t: t.name)
|
|
|
|
|
|
def get_search_paths() -> List[str]:
|
|
"""Return the ordered list of template search directories."""
|
|
paths = []
|
|
|
|
# 1. System templates (shipped with the silo addon)
|
|
system_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
|
|
paths.append(system_dir)
|
|
|
|
# 2. User app-data templates (personal, sister to Macro/)
|
|
try:
|
|
import FreeCAD
|
|
|
|
user_app_templates = os.path.join(FreeCAD.getUserAppDataDir(), "Templates")
|
|
paths.append(user_app_templates)
|
|
except Exception:
|
|
pass
|
|
|
|
# 3. Shared project templates
|
|
try:
|
|
from silo_commands import get_projects_dir
|
|
|
|
user_dir = str(get_projects_dir() / "templates")
|
|
paths.append(user_dir)
|
|
except Exception:
|
|
pass
|
|
|
|
return paths
|
|
|
|
|
|
def filter_templates(
|
|
templates: List[TemplateInfo],
|
|
item_type: str = "",
|
|
category: str = "",
|
|
) -> List[TemplateInfo]:
|
|
"""Filter templates by item type and category prefix."""
|
|
result = []
|
|
for t in templates:
|
|
if item_type and t.item_types and item_type not in t.item_types:
|
|
continue
|
|
if category and t.categories:
|
|
if not any(category.startswith(prefix) for prefix in t.categories):
|
|
continue
|
|
result.append(t)
|
|
return result
|