diff --git a/src/app/widgets/template_panel.py b/src/app/widgets/template_panel.py index 4bec2b6..3bd28aa 100644 --- a/src/app/widgets/template_panel.py +++ b/src/app/widgets/template_panel.py @@ -1,5 +1,236 @@ -from PySide6.QtWidgets import QWidget +import logging +from typing import Optional + +from PySide6.QtCore import Signal +from PySide6.QtWidgets import ( + QComboBox, + QHBoxLayout, + QInputDialog, + QLabel, + QMessageBox, + QPushButton, + QVBoxLayout, + QWidget, +) + +from core.models import Template, TransformState +from services.template_service import add_template, get_all_templates + +logger = logging.getLogger(__name__) + +_BUILTIN_PREFIX = "★ " +_CUSTOM_PREFIX = "◆ " class TemplatePanel(QWidget): - pass + """Right-panel section: template picker, save, save-as, reset.""" + + template_applied = Signal(object) # emits TransformState + + def __init__(self, parent=None): + super().__init__(parent) + self._current_state: Optional[TransformState] = None + self._garment_w: float = 0.0 + self._garment_h: float = 0.0 + self._updating = False + self._setup_ui() + self._load_templates() + self._apply_stylesheet() + + # ── UI construction ────────────────────────────────────────────────────── + + def _setup_ui(self): + col = QVBoxLayout(self) + col.setContentsMargins(0, 0, 0, 0) + col.setSpacing(0) + + header = QLabel("模板") + header.setObjectName("templateSectionHeader") + col.addWidget(header) + + body = QWidget() + body.setObjectName("templateBody") + body_col = QVBoxLayout(body) + body_col.setContentsMargins(10, 8, 10, 10) + body_col.setSpacing(8) + + self._combo = QComboBox() + self._combo.setObjectName("templateCombo") + self._combo.currentIndexChanged.connect(self._on_template_selected) + body_col.addWidget(self._combo) + + btn_row = QHBoxLayout() + btn_row.setSpacing(6) + + self._save_btn = QPushButton("保存") + self._save_btn.setObjectName("templateBtn") + self._save_btn.setToolTip("覆盖当前自定义模板") + self._save_btn.clicked.connect(self._save_template) + + self._saveas_btn = QPushButton("另存为") + self._saveas_btn.setObjectName("templateBtn") + self._saveas_btn.setToolTip("将当前参数保存为新模板") + self._saveas_btn.clicked.connect(self._save_as_template) + + btn_row.addWidget(self._save_btn) + btn_row.addWidget(self._saveas_btn) + body_col.addLayout(btn_row) + + self._reset_btn = QPushButton("重置为模板") + self._reset_btn.setObjectName("templateBtn") + self._reset_btn.setToolTip("将印花参数还原为所选模板的默认值") + self._reset_btn.clicked.connect(self._reset_to_template) + body_col.addWidget(self._reset_btn) + + col.addWidget(body) + + # ── template list management ───────────────────────────────────────────── + + def _load_templates(self, select_name: Optional[str] = None): + """Reload combo from template_service; restore selection by name if given.""" + templates = get_all_templates() + self._updating = True + try: + self._combo.blockSignals(True) + self._combo.clear() + for t in templates: + prefix = _BUILTIN_PREFIX if t.type == "builtin" else _CUSTOM_PREFIX + self._combo.addItem(prefix + t.name, userData=t) + if select_name: + for i in range(self._combo.count()): + t = self._combo.itemData(i) + if t and t.name == select_name: + self._combo.setCurrentIndex(i) + break + finally: + self._combo.blockSignals(False) + self._updating = False + self._update_button_states() + + def _update_button_states(self): + t = self._current_template() + is_builtin = (t is None) or (t.type == "builtin") + self._save_btn.setEnabled(not is_builtin) + + def _current_template(self) -> Optional[Template]: + return self._combo.currentData() + + # ── public API ─────────────────────────────────────────────────────────── + + def set_garment_size(self, w: float, h: float): + """Update garment dimensions (pixels) used for ratio <-> pixel conversion.""" + self._garment_w = w + self._garment_h = h + + def set_transform(self, state: TransformState): + """Store current print transform (used when saving a template).""" + self._current_state = state + + # ── signal handlers ────────────────────────────────────────────────────── + + def _on_template_selected(self, index: int): + if self._updating: + return + self._update_button_states() + t = self._combo.itemData(index) + if t and self._garment_w > 0 and self._garment_h > 0: + state = t.to_transform_state(self._garment_w, self._garment_h) + logger.debug("Template applied: %s", t.name) + self.template_applied.emit(state) + + def _reset_to_template(self): + """Re-apply the currently selected template.""" + t = self._current_template() + if not t: + return + if self._garment_w <= 0 or self._garment_h <= 0: + QMessageBox.information(self, "提示", "请先加载衣服图片。") + return + state = t.to_transform_state(self._garment_w, self._garment_h) + self.template_applied.emit(state) + + def _save_template(self): + """Overwrite the selected custom template with the current transform.""" + t = self._current_template() + if not t or t.type == "builtin": + QMessageBox.warning( + self, "无法保存", + "内置模板不可修改。\n请使用「另存为」创建自定义模板。", + ) + return + if not self._current_state: + QMessageBox.information(self, "提示", "当前没有可保存的印花参数。") + return + if self._garment_w <= 0 or self._garment_h <= 0: + QMessageBox.information(self, "提示", "请先加载衣服图片。") + return + add_template(self._state_to_template(t.name)) + logger.info("Template saved: %s", t.name) + self._load_templates(select_name=t.name) + + def _save_as_template(self): + """Create a new custom template from the current transform.""" + if not self._current_state: + QMessageBox.information(self, "提示", "当前没有可保存的印花参数。") + return + if self._garment_w <= 0 or self._garment_h <= 0: + QMessageBox.information(self, "提示", "请先加载衣服图片。") + return + name, ok = QInputDialog.getText(self, "另存为模板", "请输入模板名称:") + if not ok: + return + name = name.strip() + if not name: + QMessageBox.warning(self, "名称无效", "模板名称不能为空。") + return + add_template(self._state_to_template(name)) + logger.info("Template saved-as: %s", name) + self._load_templates(select_name=name) + + # ── helpers ────────────────────────────────────────────────────────────── + + def _state_to_template(self, name: str) -> Template: + s = self._current_state + gw, gh = self._garment_w, self._garment_h + return Template( + name=name, + x_ratio=s.x / gw, + y_ratio=s.y / gh, + width_ratio=s.width / gw, + height_ratio=s.height / gh, + rotation=s.rotation, + type="custom", + ) + + def _apply_stylesheet(self): + self.setStyleSheet(""" + #templateSectionHeader { + font-family: "Microsoft YaHei", "Segoe UI", sans-serif; + font-size: 12px; + font-weight: bold; + color: #555555; + background-color: #f7f7f7; + border-bottom: 1px solid #eeeeee; + padding: 4px 10px; + } + #templateBody { + background-color: #ffffff; + border-bottom: 1px solid #e0e0e0; + } + #templateCombo { + font-size: 12px; + border: 1px solid #d6d6d6; + border-radius: 3px; + padding: 2px 4px; + } + #templateBtn { + font-size: 12px; + padding: 4px 8px; + border: 1px solid #d6d6d6; + border-radius: 3px; + background: #f0f0f0; + } + #templateBtn:hover { background: #e0e0e0; } + #templateBtn:pressed { background: #d0d0d0; } + #templateBtn:disabled { color: #aaaaaa; background: #f7f7f7; } + """) diff --git a/tasks.md b/tasks.md index f4abd65..1c58962 100644 --- a/tasks.md +++ b/tasks.md @@ -373,20 +373,20 @@ 任务: -- [ ] 先读取 `src/app/widgets/template_panel.py` 现有内容 -- [ ] 完善 `src/app/widgets/template_panel.py` -- [ ] 显示模板下拉框 -- [ ] 支持选择模板 -- [ ] 支持保存模板 -- [ ] 支持另存为模板 -- [ ] 支持重置为模板 -- [ ] 通过 `template_service` 操作模板 +- [x] 先读取 `src/app/widgets/template_panel.py` 现有内容 +- [x] 完善 `src/app/widgets/template_panel.py` +- [x] 显示模板下拉框 +- [x] 支持选择模板 +- [x] 支持保存模板 +- [x] 支持另存为模板 +- [x] 支持重置为模板 +- [x] 通过 `template_service` 操作模板 验收: -- [ ] 选择模板后更新预览和参数面板 -- [ ] UI 不直接修改模板 JSON -- [ ] 自定义模板重启后仍可用 +- [x] 选择模板后更新预览和参数面板 +- [x] UI 不直接修改模板 JSON +- [x] 自定义模板重启后仍可用 ## 12. 导出面板 UI