feat: implement template panel UI with picker, save, save-as, reset
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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):
|
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; }
|
||||||
|
""")
|
||||||
|
|||||||
@@ -373,20 +373,20 @@
|
|||||||
|
|
||||||
任务:
|
任务:
|
||||||
|
|
||||||
- [ ] 先读取 `src/app/widgets/template_panel.py` 现有内容
|
- [x] 先读取 `src/app/widgets/template_panel.py` 现有内容
|
||||||
- [ ] 完善 `src/app/widgets/template_panel.py`
|
- [x] 完善 `src/app/widgets/template_panel.py`
|
||||||
- [ ] 显示模板下拉框
|
- [x] 显示模板下拉框
|
||||||
- [ ] 支持选择模板
|
- [x] 支持选择模板
|
||||||
- [ ] 支持保存模板
|
- [x] 支持保存模板
|
||||||
- [ ] 支持另存为模板
|
- [x] 支持另存为模板
|
||||||
- [ ] 支持重置为模板
|
- [x] 支持重置为模板
|
||||||
- [ ] 通过 `template_service` 操作模板
|
- [x] 通过 `template_service` 操作模板
|
||||||
|
|
||||||
验收:
|
验收:
|
||||||
|
|
||||||
- [ ] 选择模板后更新预览和参数面板
|
- [x] 选择模板后更新预览和参数面板
|
||||||
- [ ] UI 不直接修改模板 JSON
|
- [x] UI 不直接修改模板 JSON
|
||||||
- [ ] 自定义模板重启后仍可用
|
- [x] 自定义模板重启后仍可用
|
||||||
|
|
||||||
## 12. 导出面板 UI
|
## 12. 导出面板 UI
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user