- Output filename now includes the print: <garment>_<print>.<ext>, e.g. 1_TY030.png (folder grouping unchanged) (§17.12). - Hide the single-export button (export via the queue) and remove the orphaned static export hint label (§17.13). - Fix: selecting a non-fine-tuned queue item now lays the print out per the selected template (apply_current) instead of load_print's centred default; still guarded by _loading_queue_item so it isn't marked 已微调 (§17.14). Docs: PRD 6.6/6.8/9, UI design 7.6/7.7/10/12.4, tasks.md 17.12–17.14. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
266 lines
9.4 KiB
Python
266 lines
9.4 KiB
Python
import logging
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from PySide6.QtCore import Qt, Signal
|
|
from PySide6.QtWidgets import (
|
|
QComboBox,
|
|
QFileDialog,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QLineEdit,
|
|
QMessageBox,
|
|
QPushButton,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from core.composer import compose
|
|
from core.models import ExportOptions, TransformState
|
|
from services.file_service import (
|
|
get_output_dir,
|
|
make_safe_output_path,
|
|
timestamped_run_dir,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ExportPanel(QWidget):
|
|
"""Right-panel section: output settings and single-image export."""
|
|
|
|
export_options_changed = Signal(object) # ExportOptions when dir/format/quality changes
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._garment_path: Optional[str] = None
|
|
self._print_path: Optional[str] = None
|
|
self._transform: Optional[TransformState] = None
|
|
self._setup_ui()
|
|
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("exportSectionHeader")
|
|
col.addWidget(header)
|
|
|
|
body = QWidget()
|
|
body.setObjectName("exportBody")
|
|
body_col = QVBoxLayout(body)
|
|
body_col.setContentsMargins(10, 8, 10, 10)
|
|
body_col.setSpacing(8)
|
|
|
|
body_col.addLayout(self._make_dir_row())
|
|
body_col.addLayout(self._make_format_form())
|
|
body_col.addWidget(self._make_export_btn())
|
|
|
|
col.addWidget(body)
|
|
|
|
def _make_dir_row(self) -> QHBoxLayout:
|
|
row = QHBoxLayout()
|
|
row.setSpacing(4)
|
|
|
|
self._dir_input = QLineEdit()
|
|
self._dir_input.setObjectName("exportDirInput")
|
|
self._dir_input.setReadOnly(True)
|
|
self._dir_input.setPlaceholderText("输出目录")
|
|
self._dir_input.setText(str(get_output_dir()))
|
|
self._dir_input.textChanged.connect(self._emit_options)
|
|
|
|
browse_btn = QPushButton("浏览…")
|
|
browse_btn.setObjectName("exportBrowseBtn")
|
|
browse_btn.setFixedWidth(52)
|
|
browse_btn.clicked.connect(self._browse_output_dir)
|
|
|
|
row.addWidget(self._dir_input, 1)
|
|
row.addWidget(browse_btn)
|
|
return row
|
|
|
|
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)
|
|
|
|
self._quality_combo = QComboBox()
|
|
self._quality_combo.setObjectName("exportCombo")
|
|
for label, val in [("最高 (95)", 95), ("高 (90)", 90), ("中 (80)", 80), ("低 (60)", 60)]:
|
|
self._quality_combo.addItem(label, val)
|
|
self._quality_combo.setCurrentIndex(1) # default: 高 (90)
|
|
self._quality_combo.setEnabled(False) # enabled only for JPG
|
|
self._quality_combo.currentIndexChanged.connect(self._emit_options)
|
|
|
|
# 格式 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_export_btn(self) -> QPushButton:
|
|
self._export_btn = QPushButton("导出当前单张")
|
|
self._export_btn.setObjectName("exportPrimaryBtn")
|
|
self._export_btn.setEnabled(False)
|
|
self._export_btn.setVisible(False) # hidden: export via the queue
|
|
self._export_btn.clicked.connect(self._do_export)
|
|
return self._export_btn
|
|
|
|
# ── public API ───────────────────────────────────────────────────────────
|
|
|
|
def set_garment(self, path: Optional[str]):
|
|
"""Update current garment path; enables export button when both paths set."""
|
|
self._garment_path = path
|
|
self._update_export_btn()
|
|
|
|
def set_print(self, path: Optional[str]):
|
|
"""Update current print path; enables export button when both paths set."""
|
|
self._print_path = path
|
|
self._update_export_btn()
|
|
|
|
def set_transform(self, state: TransformState):
|
|
"""Store current print transform (used on export)."""
|
|
self._transform = state
|
|
|
|
def current_options(self) -> ExportOptions:
|
|
"""Build ExportOptions from the current output settings."""
|
|
fmt = self._format_combo.currentData()
|
|
return ExportOptions(
|
|
output_dir=self._dir_input.text().strip(),
|
|
output_format=fmt,
|
|
quality=self._quality_combo.currentData() if fmt == "JPG" else 95,
|
|
)
|
|
|
|
# ── slot handlers ────────────────────────────────────────────────────────
|
|
|
|
def _emit_options(self, *_):
|
|
self.export_options_changed.emit(self.current_options())
|
|
|
|
def _on_format_changed(self, _index: int):
|
|
fmt = self._format_combo.currentData()
|
|
self._quality_combo.setEnabled(fmt == "JPG")
|
|
self._emit_options()
|
|
|
|
def _browse_output_dir(self):
|
|
folder = QFileDialog.getExistingDirectory(
|
|
self, "选择输出目录", self._dir_input.text()
|
|
)
|
|
if folder:
|
|
self._dir_input.setText(folder)
|
|
|
|
def _update_export_btn(self):
|
|
self._export_btn.setEnabled(
|
|
bool(self._garment_path and self._print_path)
|
|
)
|
|
|
|
def _do_export(self):
|
|
if not self._garment_path or not self._print_path:
|
|
return
|
|
if not self._transform:
|
|
QMessageBox.information(self, "提示", "当前没有可导出的印花参数。")
|
|
return
|
|
|
|
options = self.current_options()
|
|
out_dir = options.output_dir or str(get_output_dir())
|
|
|
|
out_dir_path = Path(out_dir)
|
|
try:
|
|
out_dir_path.mkdir(parents=True, exist_ok=True)
|
|
except OSError as exc:
|
|
logger.error("Output dir not writable: %s", exc)
|
|
QMessageBox.warning(
|
|
self, "目录错误",
|
|
"输出目录无法创建:\n{}".format(exc),
|
|
)
|
|
return
|
|
|
|
# Group this export under a timestamped run folder (compose creates it)
|
|
run_dir = timestamped_run_dir(out_dir)
|
|
output_path = make_safe_output_path(
|
|
run_dir, self._garment_path, self._print_path, options.output_format
|
|
)
|
|
|
|
result = compose(
|
|
self._garment_path, self._print_path,
|
|
self._transform, options, output_path,
|
|
)
|
|
|
|
if result.success:
|
|
logger.info("Exported: %s", result.output_path)
|
|
QMessageBox.information(
|
|
self, "导出成功",
|
|
"已导出:\n{}".format(result.output_path),
|
|
)
|
|
else:
|
|
logger.error("Export failed: %s", result.error)
|
|
QMessageBox.warning(
|
|
self, "导出失败",
|
|
"导出失败:\n{}".format(result.error),
|
|
)
|
|
|
|
# ── stylesheet ───────────────────────────────────────────────────────────
|
|
|
|
def _apply_stylesheet(self):
|
|
self.setStyleSheet("""
|
|
#exportSectionHeader {
|
|
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;
|
|
}
|
|
#exportBody {
|
|
background-color: #ffffff;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
}
|
|
#exportDirInput {
|
|
font-size: 11px;
|
|
border: 1px solid #d6d6d6;
|
|
border-radius: 3px;
|
|
padding: 2px 4px;
|
|
background: #fafafa;
|
|
color: #444444;
|
|
}
|
|
#exportBrowseBtn {
|
|
font-size: 12px;
|
|
padding: 3px 6px;
|
|
border: 1px solid #d6d6d6;
|
|
border-radius: 3px;
|
|
background: #f0f0f0;
|
|
}
|
|
#exportBrowseBtn:hover { background: #e0e0e0; }
|
|
#exportCombo {
|
|
font-size: 12px;
|
|
border: 1px solid #d6d6d6;
|
|
border-radius: 3px;
|
|
padding: 2px 4px;
|
|
}
|
|
#exportPrimaryBtn {
|
|
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
|
|
font-size: 13px;
|
|
font-weight: bold;
|
|
color: #ffffff;
|
|
background-color: #0078d4;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 7px 0;
|
|
}
|
|
#exportPrimaryBtn:hover { background-color: #106ebe; }
|
|
#exportPrimaryBtn:pressed { background-color: #005a9e; }
|
|
#exportPrimaryBtn:disabled {
|
|
background-color: #c8c8c8;
|
|
color: #888888;
|
|
}
|
|
""")
|