Files
cmbot/src/app/widgets/export_panel.py
T

266 lines
9.3 KiB
Python
Raw Normal View History

import logging
from pathlib import Path
from typing import Optional
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QComboBox,
QFileDialog,
QFormLayout,
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
logger = logging.getLogger(__name__)
2026-06-15 15:53:01 +08:00
class ExportPanel(QWidget):
"""Right-panel section: output settings and single-image export."""
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_hint_label())
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()))
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) -> QFormLayout:
form = QFormLayout()
form.setSpacing(6)
form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
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")
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
form.addRow("质量", self._quality_combo)
return form
def _make_hint_label(self) -> QLabel:
self._hint_lbl = QLabel("加载衣服和印花图片后即可导出。")
self._hint_lbl.setObjectName("exportHint")
self._hint_lbl.setWordWrap(True)
return self._hint_lbl
def _make_export_btn(self) -> QPushButton:
self._export_btn = QPushButton("导出当前单张")
self._export_btn.setObjectName("exportPrimaryBtn")
self._export_btn.setEnabled(False)
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 set_hint(self, text: str):
"""Update the operation-scope hint label."""
self._hint_lbl.setText(text)
# ── slot handlers ────────────────────────────────────────────────────────
def _on_format_changed(self, _index: int):
fmt = self._format_combo.currentData()
self._quality_combo.setEnabled(fmt == "JPG")
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
out_dir = self._dir_input.text().strip() or str(get_output_dir())
fmt = self._format_combo.currentData()
quality = self._quality_combo.currentData() if fmt == "JPG" else 95
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
options = ExportOptions(
output_dir=out_dir,
output_format=fmt,
quality=quality,
)
output_path = make_safe_output_path(
out_dir, self._garment_path, self._print_path, fmt
)
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;
}
#exportHint {
font-size: 11px;
color: #777777;
background: #fffbe6;
border: 1px solid #ffe58f;
border-radius: 3px;
padding: 4px 6px;
}
#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;
}
""")