feat: template delete, whole-reset relocation, and compact param/export rows

- Template delete: add a 删除 button (reuses template_service.delete_template)
  with a confirm dialog; hidden for built-ins like 保存; reloads and re-selects
  the first built-in after deletion, persisting the new selection.
- Whole reset: move the template-wide reset out of the template area to the
  bottom of the transform panel and rename it to 位置尺寸旋转重置为当前模板, so it
  sits with the params it resets, beside the three per-section resets.
- Compact layout: pair X/Y and 宽/高 onto single rows (short labels), and put
  the export 格式/质量 selectors on one row. Sync logic unchanged.

Docs: PRD 6.6, UI design 7.1/7.2/7.3/7.4/7.5/10, tasks.md 17.4/17.5/17.6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 17:45:09 +08:00
co-authored by Claude Opus 4.8
parent f767ec2660
commit 7fe770e80d
7 changed files with 223 additions and 56 deletions
+14
View File
@@ -277,6 +277,8 @@ class MainWindow(QMainWindow):
self.transform_panel.reset_position.connect(lambda: self._reset_aspect("pos"))
self.transform_panel.reset_size.connect(lambda: self._reset_aspect("size"))
self.transform_panel.reset_rotation.connect(lambda: self._reset_aspect("rot"))
# Whole reset: restore position + size + rotation to the selected template
self.transform_panel.reset_all.connect(self._reset_all_to_template)
# ── Template panel ─────────────────────────────────────────────────
# Template selection updates canvas, param panel, and queue template
@@ -403,6 +405,18 @@ class MainWindow(QMainWindow):
new.rotation = tpl.rotation
self._apply_preview_transform(new)
def _reset_all_to_template(self):
"""Restore position + size + rotation to the selected template at once."""
tpl = self.template_panel.current_template_state()
cur = self.image_canvas.get_transform()
if tpl is None or cur is None:
return
new = TransformState(
x=tpl.x, y=tpl.y, width=tpl.width, height=tpl.height,
rotation=tpl.rotation, keep_aspect_ratio=cur.keep_aspect_ratio,
)
self._apply_preview_transform(new)
def _apply_preview_transform(self, state):
"""Push a transform to the canvas + panels as if it were a user edit."""
self.image_canvas.set_transform(state)
+9 -8
View File
@@ -6,7 +6,6 @@ from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QComboBox,
QFileDialog,
QFormLayout,
QHBoxLayout,
QLabel,
QLineEdit,
@@ -77,17 +76,15 @@ class ExportPanel(QWidget):
row.addWidget(browse_btn)
return row
def _make_format_form(self) -> QFormLayout:
form = QFormLayout()
form.setSpacing(6)
form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
def _make_format_form(self) -> QHBoxLayout:
row = QHBoxLayout()
row.setSpacing(6)
self._format_combo = QComboBox()
self._format_combo.setObjectName("exportCombo")
self._format_combo.addItem("PNG", "PNG")
self._format_combo.addItem("JPG", "JPG")
self._format_combo.currentIndexChanged.connect(self._on_format_changed)
form.addRow("格式", self._format_combo)
self._quality_combo = QComboBox()
self._quality_combo.setObjectName("exportCombo")
@@ -95,9 +92,13 @@ class ExportPanel(QWidget):
self._quality_combo.addItem(label, val)
self._quality_combo.setCurrentIndex(1) # default: 高 (90)
self._quality_combo.setEnabled(False) # enabled only for JPG
form.addRow("质量", self._quality_combo)
return form
# 格式 and 质量 share one row (quality lights up only for JPG)
row.addWidget(QLabel("格式"))
row.addWidget(self._format_combo, 1)
row.addWidget(QLabel("质量"))
row.addWidget(self._quality_combo, 1)
return row
def _make_hint_label(self) -> QLabel:
self._hint_lbl = QLabel("加载衣服和印花图片后即可导出。")
+43 -20
View File
@@ -14,7 +14,7 @@ from PySide6.QtWidgets import (
)
from core.models import Template, TransformState
from services.template_service import add_template, get_all_templates
from services.template_service import add_template, delete_template, get_all_templates
logger = logging.getLogger(__name__)
@@ -75,16 +75,16 @@ class TemplatePanel(QWidget):
self._saveas_btn.setToolTip("将当前参数保存为新模板")
self._saveas_btn.clicked.connect(self._save_as_template)
self._delete_btn = QPushButton("删除")
self._delete_btn.setObjectName("templateDeleteBtn")
self._delete_btn.setToolTip("删除当前自定义模板")
self._delete_btn.clicked.connect(self._delete_template)
btn_row.addWidget(self._save_btn)
btn_row.addWidget(self._saveas_btn)
btn_row.addWidget(self._delete_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 ─────────────────────────────────────────────
@@ -113,9 +113,10 @@ class TemplatePanel(QWidget):
def _update_button_states(self):
t = self._current_template()
is_builtin = (t is None) or (t.type == "builtin")
# "保存" overwrites a custom template — meaningless for built-ins, so
# hide it entirely rather than leaving a permanently-greyed button.
# "保存"/"删除" only apply to custom templates — hide them for built-ins
# rather than leaving permanently-greyed buttons.
self._save_btn.setVisible(not is_builtin)
self._delete_btn.setVisible(not is_builtin)
def _current_template(self) -> Optional[Template]:
return self._combo.currentData()
@@ -182,17 +183,6 @@ class TemplatePanel(QWidget):
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 = self._state_for_template(t)
self.template_applied.emit(state)
def _save_template(self):
"""Overwrite the selected custom template with the current transform."""
t = self._current_template()
@@ -233,6 +223,29 @@ class TemplatePanel(QWidget):
self._load_templates(select_name=name)
self.template_changed.emit(name)
def _delete_template(self):
"""Delete the selected custom template after confirmation."""
t = self._current_template()
if not t or t.type == "builtin":
return
reply = QMessageBox.question(
self, "删除模板",
"确定删除模板「{}」?此操作不可撤销。".format(t.name),
QMessageBox.Yes | QMessageBox.No, QMessageBox.No,
)
if reply != QMessageBox.Yes:
return
try:
delete_template(t.name)
except ValueError as exc:
QMessageBox.warning(self, "删除失败", str(exc))
return
logger.info("Template deleted: %s", t.name)
# Reload; selection falls back to the first (built-in) template
self._load_templates()
cur = self._current_template()
self.template_changed.emit(cur.name if cur else "")
# ── helpers ──────────────────────────────────────────────────────────────
def _state_to_template(self, name: str) -> Template:
@@ -279,4 +292,14 @@ class TemplatePanel(QWidget):
#templateBtn:hover { background: #e0e0e0; }
#templateBtn:pressed { background: #d0d0d0; }
#templateBtn:disabled { color: #aaaaaa; background: #f7f7f7; }
#templateDeleteBtn {
font-size: 12px;
padding: 4px 8px;
border: 1px solid #d6d6d6;
border-radius: 3px;
background: #f0f0f0;
color: #c42b1c;
}
#templateDeleteBtn:hover { background: #fde7e9; border-color: #c42b1c; }
#templateDeleteBtn:pressed { background: #f9d4d7; }
""")
+66 -14
View File
@@ -27,6 +27,7 @@ class TransformPanel(QWidget):
reset_position = Signal()
reset_size = Signal()
reset_rotation = Signal()
reset_all = Signal() # "位置尺寸旋转重置为当前模板" — reset all three at once
def __init__(self, parent=None):
super().__init__(parent)
@@ -46,6 +47,7 @@ class TransformPanel(QWidget):
col.addWidget(self._make_section("位置", self._make_pos_form(), "pos"))
col.addWidget(self._make_section("尺寸", self._make_size_form(), "size"))
col.addWidget(self._make_section("旋转", self._make_rot_form(), "rot"))
col.addWidget(self._make_footer())
def _make_section(self, title: str, body: QWidget, reset_tag: str) -> QWidget:
section = QWidget()
@@ -82,18 +84,20 @@ class TransformPanel(QWidget):
def _make_pos_form(self) -> QWidget:
w = QWidget()
w.setObjectName("transformForm")
form = QFormLayout(w)
form.setContentsMargins(10, 6, 10, 6)
form.setSpacing(6)
form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
row = QHBoxLayout(w)
row.setContentsMargins(10, 6, 10, 6)
row.setSpacing(6)
self._x_spin = self._make_spin(-99999, 99999, "px")
self._y_spin = self._make_spin(-99999, 99999, "px")
self._x_spin.valueChanged.connect(self._on_xy_changed)
self._y_spin.valueChanged.connect(self._on_xy_changed)
form.addRow("X 坐标", self._x_spin)
form.addRow("Y 坐标", self._y_spin)
# X and Y are a coordinate pair → keep them on one row
row.addWidget(self._make_field_label("X"))
row.addWidget(self._x_spin, 1)
row.addWidget(self._make_field_label("Y"))
row.addWidget(self._y_spin, 1)
return w
def _make_size_form(self) -> QWidget:
@@ -103,18 +107,20 @@ class TransformPanel(QWidget):
col.setContentsMargins(10, 6, 10, 6)
col.setSpacing(6)
form = QFormLayout()
form.setSpacing(6)
form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
self._w_spin = self._make_spin(_MIN_SIZE, 99999, "px")
self._h_spin = self._make_spin(_MIN_SIZE, 99999, "px")
self._w_spin.valueChanged.connect(self._on_width_changed)
self._h_spin.valueChanged.connect(self._on_height_changed)
form.addRow("宽度", self._w_spin)
form.addRow("高度", self._h_spin)
col.addLayout(form)
# Width and height are a pair → keep them on one row
size_row = QHBoxLayout()
size_row.setContentsMargins(0, 0, 0, 0)
size_row.setSpacing(6)
size_row.addWidget(self._make_field_label("宽"))
size_row.addWidget(self._w_spin, 1)
size_row.addWidget(self._make_field_label("高"))
size_row.addWidget(self._h_spin, 1)
col.addLayout(size_row)
self._lock_cb = QCheckBox("锁定宽高比例")
self._lock_cb.setChecked(True)
@@ -157,6 +163,21 @@ class TransformPanel(QWidget):
col.addLayout(btn_row)
return w
def _make_footer(self) -> QWidget:
footer = QWidget()
footer.setObjectName("transformFooter")
col = QVBoxLayout(footer)
col.setContentsMargins(10, 8, 10, 10)
col.setSpacing(0)
self._reset_all_btn = QPushButton("位置尺寸旋转重置为当前模板")
self._reset_all_btn.setObjectName("transformResetAllBtn")
self._reset_all_btn.setToolTip("将位置、尺寸、旋转一并还原为当前选中模板")
self._reset_all_btn.setEnabled(False)
self._reset_all_btn.clicked.connect(lambda: self.reset_all.emit())
col.addWidget(self._reset_all_btn)
return footer
def _emit_reset(self, tag: str):
if tag == "pos":
self.reset_position.emit()
@@ -166,9 +187,16 @@ class TransformPanel(QWidget):
self.reset_rotation.emit()
def set_resets_enabled(self, enabled: bool):
"""Enable/disable the three section reset buttons together."""
"""Enable/disable the section resets and the全部重置 button together."""
for btn in self._reset_btns.values():
btn.setEnabled(enabled)
self._reset_all_btn.setEnabled(enabled)
@staticmethod
def _make_field_label(text: str) -> QLabel:
lbl = QLabel(text)
lbl.setObjectName("fieldLabel")
return lbl
@staticmethod
def _make_spin(minimum: float, maximum: float, suffix: str) -> QDoubleSpinBox:
@@ -209,6 +237,11 @@ class TransformPanel(QWidget):
#transformForm {
background-color: #ffffff;
}
#fieldLabel {
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
font-size: 12px;
color: #555555;
}
#transformSpin {
font-size: 12px;
border: 1px solid #d6d6d6;
@@ -227,6 +260,25 @@ class TransformPanel(QWidget):
}
#rotBtn:hover { background: #e0e0e0; }
#rotBtn:pressed { background: #d0d0d0; }
#transformFooter {
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
#transformResetAllBtn {
font-size: 12px;
color: #555555;
background: #f0f0f0;
border: 1px solid #d6d6d6;
border-radius: 4px;
padding: 6px 0;
}
#transformResetAllBtn:hover {
background: #eaf3fc;
color: #0078d4;
border-color: #0078d4;
}
#transformResetAllBtn:pressed { background: #d6e8f9; }
#transformResetAllBtn:disabled { color: #c0c0c0; background: #f7f7f7; }
""")
# ── public API ────────────────────────────────────────────────────────────