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:
@@ -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)
|
||||
|
||||
@@ -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("加载衣服和印花图片后即可导出。")
|
||||
|
||||
@@ -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; }
|
||||
""")
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user