feat: thumbnail grid, aspect-aware templates, and UI hierarchy polish
Material list: - Switch asset list from one-per-row to a wrapping thumbnail grid (IconMode cards: thumbnail + corner checkbox + elided name, subfolder moved to tooltip). Templates: - Template.to_transform_state now contains-fits the print inside the target box by the print's native aspect ratio (no stretch); falls back to the box when print size is unknown. - Feed print native size into template_panel for aspect-aware fitting. - Shrink built-in template boxes to realistic print proportions. UI hierarchy: - Promote 打开文件夹 buttons to primary (filled accent). - Add inline 归零 (reset angle) and a 重置位置/尺寸/角度 button to the transform panel; wire it to re-apply the selected template. - Hide 保存 for built-in templates instead of leaving it greyed. Docs: record template box/contain behavior (PRD) and the grid layout, button hierarchy, and reset/save-visibility rules (UI design). Tests: add 3 aspect-fit cases for to_transform_state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtCore import QSize, Qt, Signal
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
@@ -24,7 +24,9 @@ from services.file_service import scan_image_folder
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_THUMB_SIZE = 52 # thumbnail size in px
|
||||
_THUMB_SIZE = 84 # thumbnail size in px
|
||||
_CARD_W = 104 # grid cell width
|
||||
_CARD_H = 116 # grid cell height
|
||||
|
||||
# CheckState int values (avoids enum import issues across PySide6 versions)
|
||||
_UNCHECKED = 0
|
||||
@@ -33,7 +35,7 @@ _CHECKED = 2
|
||||
|
||||
|
||||
class _AssetItemWidget(QWidget):
|
||||
"""Single row: checkbox + thumbnail + subfolder label + filename."""
|
||||
"""Grid card: thumbnail with checkbox overlay on top, filename below."""
|
||||
|
||||
check_changed = Signal()
|
||||
|
||||
@@ -51,46 +53,51 @@ class _AssetItemWidget(QWidget):
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
row = QHBoxLayout(self)
|
||||
row.setContentsMargins(4, 3, 4, 3)
|
||||
row.setSpacing(6)
|
||||
self.setObjectName("assetCard")
|
||||
self.setFixedSize(_CARD_W, _CARD_H)
|
||||
|
||||
# Batch-selection checkbox
|
||||
self._check = QCheckBox()
|
||||
self._check.setChecked(self._asset.selected)
|
||||
self._check.stateChanged.connect(self._on_check_changed)
|
||||
col = QVBoxLayout(self)
|
||||
col.setContentsMargins(4, 4, 4, 4)
|
||||
col.setSpacing(3)
|
||||
|
||||
# Thumbnail
|
||||
self._thumb = QLabel()
|
||||
self._thumb.setFixedSize(_THUMB_SIZE, _THUMB_SIZE)
|
||||
self._thumb.setAlignment(Qt.AlignCenter)
|
||||
# Thumbnail with the batch-selection checkbox overlaid on its corner
|
||||
thumb_box = QWidget()
|
||||
thumb_box.setFixedSize(_THUMB_SIZE, _THUMB_SIZE)
|
||||
|
||||
self._thumb = QLabel(thumb_box)
|
||||
self._thumb.setObjectName("thumbLabel")
|
||||
self._thumb.setGeometry(0, 0, _THUMB_SIZE, _THUMB_SIZE)
|
||||
self._thumb.setAlignment(Qt.AlignCenter)
|
||||
self._load_thumbnail()
|
||||
|
||||
# Info column: subfolder + filename
|
||||
info_col = QWidget()
|
||||
info_layout = QVBoxLayout(info_col)
|
||||
info_layout.setContentsMargins(0, 0, 0, 0)
|
||||
info_layout.setSpacing(2)
|
||||
self._check = QCheckBox(thumb_box)
|
||||
self._check.setObjectName("cardCheck")
|
||||
self._check.setChecked(self._asset.selected)
|
||||
self._check.stateChanged.connect(self._on_check_changed)
|
||||
self._check.move(3, 3)
|
||||
|
||||
subfolder = self._compute_subfolder()
|
||||
self._folder_label = QLabel(subfolder)
|
||||
self._folder_label.setObjectName("subfolderLabel")
|
||||
self._folder_label.setVisible(bool(subfolder))
|
||||
thumb_row = QHBoxLayout()
|
||||
thumb_row.setContentsMargins(0, 0, 0, 0)
|
||||
thumb_row.addStretch()
|
||||
thumb_row.addWidget(thumb_box)
|
||||
thumb_row.addStretch()
|
||||
col.addLayout(thumb_row)
|
||||
|
||||
self._name_label = QLabel(Path(self._asset.path).name)
|
||||
# Filename (middle-elided to fit the card width)
|
||||
name = Path(self._asset.path).name
|
||||
self._name_label = QLabel()
|
||||
self._name_label.setObjectName("fileNameLabel")
|
||||
self._name_label.setWordWrap(False)
|
||||
self._name_label.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
|
||||
elided = self._name_label.fontMetrics().elidedText(
|
||||
name, Qt.ElideMiddle, _CARD_W - 10
|
||||
)
|
||||
self._name_label.setText(elided)
|
||||
col.addWidget(self._name_label)
|
||||
col.addStretch()
|
||||
|
||||
info_layout.addWidget(self._folder_label)
|
||||
info_layout.addWidget(self._name_label)
|
||||
info_layout.addStretch()
|
||||
|
||||
row.addWidget(self._check, 0, Qt.AlignVCenter)
|
||||
row.addWidget(self._thumb)
|
||||
row.addWidget(info_col, 1)
|
||||
|
||||
self.setFixedHeight(64)
|
||||
# Subfolder + filename shown on hover (no room for a label in the grid)
|
||||
subfolder = self._compute_subfolder()
|
||||
self.setToolTip("{}/{}".format(subfolder, name) if subfolder else name)
|
||||
|
||||
def _compute_subfolder(self) -> str:
|
||||
asset_path = Path(self._asset.path)
|
||||
@@ -136,7 +143,9 @@ class _AssetItemWidget(QWidget):
|
||||
def set_preview_selected(self, active: bool):
|
||||
"""Highlight or clear the current-preview indicator."""
|
||||
if active:
|
||||
self.setStyleSheet("background-color: #cce4f7;")
|
||||
self.setStyleSheet(
|
||||
"#assetCard { background-color: #cce4f7; border-radius: 4px; }"
|
||||
)
|
||||
else:
|
||||
self.setStyleSheet("")
|
||||
|
||||
@@ -166,11 +175,19 @@ class _AssetPanel(QWidget):
|
||||
|
||||
self._list = QListWidget()
|
||||
self._list.setObjectName("assetList")
|
||||
# Thumbnail grid: wrap items left-to-right, reflow on resize
|
||||
self._list.setViewMode(QListWidget.IconMode)
|
||||
self._list.setFlow(QListWidget.LeftToRight)
|
||||
self._list.setWrapping(True)
|
||||
self._list.setResizeMode(QListWidget.Adjust)
|
||||
self._list.setMovement(QListWidget.Static)
|
||||
self._list.setUniformItemSizes(True)
|
||||
self._list.setGridSize(QSize(_CARD_W + 6, _CARD_H + 6))
|
||||
self._list.setSpacing(2)
|
||||
self._list.setVerticalScrollMode(QListWidget.ScrollPerPixel)
|
||||
self._list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
# Disable QListWidget's own selection highlight — preview uses custom style
|
||||
self._list.setSelectionMode(QListWidget.NoSelection)
|
||||
self._list.setSpacing(1)
|
||||
self._list.itemClicked.connect(self._on_item_clicked)
|
||||
|
||||
col.addWidget(self._list, 1)
|
||||
@@ -384,12 +401,15 @@ class ImageListPanel(QWidget):
|
||||
}
|
||||
#openFolderBtn {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid #d6d6d6;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
padding: 5px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #0078d4;
|
||||
}
|
||||
#openFolderBtn:hover { background: #e8e8e8; }
|
||||
#openFolderBtn:hover { background: #106ebe; }
|
||||
#openFolderBtn:pressed { background: #005a9e; }
|
||||
#ctrlRow {
|
||||
background-color: #fafafa;
|
||||
border-bottom: 1px solid #eeeeee;
|
||||
@@ -410,13 +430,9 @@ class ImageListPanel(QWidget):
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
#subfolderLabel {
|
||||
font-size: 10px;
|
||||
color: #888888;
|
||||
}
|
||||
#fileNameLabel {
|
||||
font-size: 12px;
|
||||
color: #1a1a1a;
|
||||
font-size: 11px;
|
||||
color: #333333;
|
||||
}
|
||||
#thumbLabel {
|
||||
background-color: #f0f0f0;
|
||||
@@ -425,6 +441,10 @@ class ImageListPanel(QWidget):
|
||||
color: #aaaaaa;
|
||||
font-size: 16px;
|
||||
}
|
||||
#cardCheck {
|
||||
background-color: rgba(255, 255, 255, 0.75);
|
||||
border-radius: 2px;
|
||||
}
|
||||
QSplitter#assetSplitter::handle {
|
||||
background-color: #d6d6d6;
|
||||
height: 3px;
|
||||
|
||||
Reference in New Issue
Block a user