fix(gui): keep cover candidate controls below images
This commit is contained in:
+118
-35
@@ -30,6 +30,58 @@ class _CoverThumbnailLabel(QLabel):
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
|
||||
class _CoverCandidateCard(QWidget):
|
||||
EXTRA_WIDTH = 16
|
||||
|
||||
def __init__(self, thumbnail, radio, meta, parent=None):
|
||||
super().__init__(parent)
|
||||
self.thumbnail = thumbnail
|
||||
self.radio = radio
|
||||
self.meta = meta
|
||||
self.card_layout = QVBoxLayout(self)
|
||||
self.card_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.card_layout.setSpacing(6)
|
||||
self.card_layout.addWidget(self.thumbnail)
|
||||
self.card_layout.addWidget(self.radio)
|
||||
self.card_layout.addWidget(self.meta)
|
||||
|
||||
@staticmethod
|
||||
def _widget_height(widget, width):
|
||||
height = widget.heightForWidth(width) if widget.hasHeightForWidth() else -1
|
||||
if height < 0:
|
||||
height = widget.sizeHint().height()
|
||||
return max(height, widget.minimumHeight(), widget.minimumSizeHint().height())
|
||||
|
||||
def set_thumbnail_size(self, size):
|
||||
size = max(1, int(size))
|
||||
self.setFixedWidth(size + self.EXTRA_WIDTH)
|
||||
self.thumbnail.setFixedSize(size, size)
|
||||
self.sync_geometry()
|
||||
|
||||
def sync_geometry(self):
|
||||
margins = self.card_layout.contentsMargins()
|
||||
content_width = max(1, self.width() - margins.left() - margins.right())
|
||||
widget_heights = [
|
||||
self.thumbnail.height(),
|
||||
self._widget_height(self.radio, content_width),
|
||||
self._widget_height(self.meta, content_width),
|
||||
]
|
||||
required_height = (
|
||||
margins.top()
|
||||
+ margins.bottom()
|
||||
+ sum(widget_heights)
|
||||
+ max(0, len(widget_heights) - 1) * max(0, self.card_layout.spacing())
|
||||
)
|
||||
self.card_layout.invalidate()
|
||||
self.setFixedHeight(required_height)
|
||||
self.card_layout.activate()
|
||||
self.updateGeometry()
|
||||
return required_height
|
||||
|
||||
def control_height(self):
|
||||
return max(0, self.height() - self.thumbnail.height())
|
||||
|
||||
|
||||
class OriginalImageDialog(QDialog):
|
||||
def __init__(self, image_path, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -190,7 +242,7 @@ class _SegmentedProgressBar(QProgressBar):
|
||||
class CoverGalleryDialog(QDialog):
|
||||
THUMBNAIL_SIZE = 180
|
||||
MAX_THUMBNAIL_SIZE = 360
|
||||
CANDIDATE_TEXT_HEIGHT = 72
|
||||
CANDIDATE_CONTROL_FALLBACK_HEIGHT = 72
|
||||
OLD_THUMBNAIL_SIZE = 300
|
||||
|
||||
def __init__(
|
||||
@@ -320,7 +372,11 @@ class CoverGalleryDialog(QDialog):
|
||||
self.candidate_layout.setSpacing(12)
|
||||
if self.candidates:
|
||||
for index, candidate_path in enumerate(self.candidates):
|
||||
self.candidate_layout.addWidget(self._candidate_item(candidate_path, index))
|
||||
self.candidate_layout.addWidget(
|
||||
self._candidate_item(candidate_path, index),
|
||||
0,
|
||||
Qt.AlignTop,
|
||||
)
|
||||
self.candidate_layout.addStretch(1)
|
||||
else:
|
||||
empty_label = QLabel("暂无生成封面图片")
|
||||
@@ -334,17 +390,12 @@ class CoverGalleryDialog(QDialog):
|
||||
return panel
|
||||
|
||||
def _candidate_item(self, candidate_path, index):
|
||||
item = QWidget()
|
||||
item.setFixedWidth(self.candidate_thumbnail_size + 16)
|
||||
layout = QVBoxLayout(item)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
thumbnail = _CoverThumbnailLabel(candidate_path, self.open_original_image)
|
||||
thumbnail.setObjectName("coverCandidateThumbnail")
|
||||
thumbnail.setAlignment(Qt.AlignCenter)
|
||||
image = _load_image(candidate_path)
|
||||
self.candidate_images[candidate_path] = image
|
||||
self.candidate_thumbnails[candidate_path] = thumbnail
|
||||
self.candidate_items[candidate_path] = item
|
||||
if image.isNull():
|
||||
thumbnail.setText("图片读取失败")
|
||||
else:
|
||||
@@ -358,8 +409,7 @@ class CoverGalleryDialog(QDialog):
|
||||
)
|
||||
)
|
||||
)
|
||||
thumbnail.setFixedSize(self.candidate_thumbnail_size, self.candidate_thumbnail_size)
|
||||
radio = QRadioButton(_candidate_label(candidate_path, self.current_path))
|
||||
radio = QRadioButton(_candidate_label(candidate_path, self.current_path, index))
|
||||
radio.setObjectName(f"coverCandidateRadio{index}")
|
||||
radio.setFocusPolicy(Qt.ClickFocus)
|
||||
radio.toggled.connect(
|
||||
@@ -370,10 +420,16 @@ class CoverGalleryDialog(QDialog):
|
||||
meta = QLabel(_candidate_meta(candidate_path, self.current_path))
|
||||
meta.setWordWrap(True)
|
||||
meta.setObjectName("coverCandidateMetaLabel")
|
||||
layout.addWidget(thumbnail)
|
||||
layout.addWidget(radio)
|
||||
layout.addWidget(meta)
|
||||
layout.addStretch(1)
|
||||
basename = os.path.basename(str(candidate_path or ""))
|
||||
file_tooltip = f"完整文件名:{basename}"
|
||||
radio.setToolTip(file_tooltip)
|
||||
meta.setToolTip(file_tooltip)
|
||||
thumbnail.setToolTip(f"双击查看原图\n{file_tooltip}")
|
||||
item = _CoverCandidateCard(thumbnail, radio, meta)
|
||||
item.setObjectName("coverCandidateCard")
|
||||
item.setToolTip(file_tooltip)
|
||||
item.set_thumbnail_size(self.candidate_thumbnail_size)
|
||||
self.candidate_items[candidate_path] = item
|
||||
return item
|
||||
|
||||
def _sync_candidate_content_size(self):
|
||||
@@ -381,27 +437,40 @@ class CoverGalleryDialog(QDialog):
|
||||
content = getattr(self, "candidate_content", None)
|
||||
if layout is None or content is None:
|
||||
return
|
||||
candidate_items = [
|
||||
self.candidate_items[candidate_path]
|
||||
for candidate_path in self.candidates
|
||||
if candidate_path in self.candidate_items
|
||||
]
|
||||
for item in candidate_items:
|
||||
if isinstance(item, _CoverCandidateCard):
|
||||
item.sync_geometry()
|
||||
layout.invalidate()
|
||||
layout.activate()
|
||||
margins = layout.contentsMargins()
|
||||
spacing = max(0, layout.spacing())
|
||||
hint = layout.sizeHint()
|
||||
if self.candidates:
|
||||
candidate_count = len(self.candidates)
|
||||
if candidate_items:
|
||||
min_width = (
|
||||
margins.left()
|
||||
+ margins.right()
|
||||
+ candidate_count * (self.candidate_thumbnail_size + 16)
|
||||
+ max(0, candidate_count - 1) * spacing
|
||||
+ sum(item.width() for item in candidate_items)
|
||||
+ max(0, len(candidate_items) - 1) * spacing
|
||||
)
|
||||
min_height = (
|
||||
margins.top()
|
||||
+ margins.bottom()
|
||||
+ max(item.height() for item in candidate_items)
|
||||
)
|
||||
else:
|
||||
min_width = margins.left() + margins.right() + 320
|
||||
min_height = max(
|
||||
self.candidate_thumbnail_size,
|
||||
hint.height(),
|
||||
)
|
||||
size = QSize(max(min_width, hint.width()), min_height)
|
||||
min_height = max(self.THUMBNAIL_SIZE, hint.height())
|
||||
size = QSize(max(min_width, hint.width()), max(1, min_height))
|
||||
content.setMinimumSize(size)
|
||||
content.resize(size)
|
||||
layout.invalidate()
|
||||
layout.setGeometry(content.rect())
|
||||
layout.activate()
|
||||
content.updateGeometry()
|
||||
scroll = getattr(self, "candidate_scroll", None)
|
||||
if scroll is not None:
|
||||
@@ -425,30 +494,38 @@ class CoverGalleryDialog(QDialog):
|
||||
width_budget = (
|
||||
viewport_width
|
||||
- max(0, visible_count - 1) * spacing
|
||||
- visible_count * 16
|
||||
- visible_count * _CoverCandidateCard.EXTRA_WIDTH
|
||||
) // visible_count
|
||||
height_budget = viewport_height - self.CANDIDATE_TEXT_HEIGHT
|
||||
control_heights = [
|
||||
item.control_height()
|
||||
for item in self.candidate_items.values()
|
||||
if isinstance(item, _CoverCandidateCard)
|
||||
]
|
||||
control_height = max(
|
||||
control_heights,
|
||||
default=self.CANDIDATE_CONTROL_FALLBACK_HEIGHT,
|
||||
)
|
||||
height_budget = viewport_height - control_height
|
||||
target = min(width_budget, height_budget, self.MAX_THUMBNAIL_SIZE)
|
||||
return max(self.THUMBNAIL_SIZE, int(target))
|
||||
|
||||
def _update_candidate_thumbnail_layout(self):
|
||||
def _update_candidate_thumbnail_layout(self, force=False):
|
||||
if not self.candidates:
|
||||
self._sync_candidate_content_size()
|
||||
return
|
||||
target_size = self._candidate_thumbnail_target_size()
|
||||
if target_size == self.candidate_thumbnail_size:
|
||||
self._sync_candidate_content_size()
|
||||
return
|
||||
size_changed = target_size != self.candidate_thumbnail_size
|
||||
self.candidate_thumbnail_size = target_size
|
||||
for candidate_path in self.candidates:
|
||||
item = self.candidate_items.get(candidate_path)
|
||||
thumbnail = self.candidate_thumbnails.get(candidate_path)
|
||||
image = self.candidate_images.get(candidate_path)
|
||||
if item is not None:
|
||||
item.setFixedWidth(target_size + 16)
|
||||
if isinstance(item, _CoverCandidateCard):
|
||||
item.set_thumbnail_size(target_size)
|
||||
if thumbnail is None:
|
||||
continue
|
||||
thumbnail.setFixedSize(target_size, target_size)
|
||||
if not size_changed and not force:
|
||||
continue
|
||||
thumbnail.clear()
|
||||
if image is None or image.isNull():
|
||||
thumbnail.setText("图片读取失败")
|
||||
@@ -743,7 +820,11 @@ class CoverGalleryDialog(QDialog):
|
||||
self.button_group.setExclusive(True)
|
||||
if self.candidates:
|
||||
for index, candidate_path in enumerate(self.candidates):
|
||||
self.candidate_layout.addWidget(self._candidate_item(candidate_path, index))
|
||||
self.candidate_layout.addWidget(
|
||||
self._candidate_item(candidate_path, index),
|
||||
0,
|
||||
Qt.AlignTop,
|
||||
)
|
||||
self.candidate_layout.addStretch(1)
|
||||
else:
|
||||
empty_label = QLabel("暂无生成封面图片")
|
||||
@@ -751,7 +832,9 @@ class CoverGalleryDialog(QDialog):
|
||||
empty_label.setMinimumSize(320, self.THUMBNAIL_SIZE)
|
||||
empty_label.setAlignment(Qt.AlignCenter)
|
||||
self.candidate_layout.addWidget(empty_label)
|
||||
self._sync_candidate_content_size()
|
||||
self._candidate_resize_timer.stop()
|
||||
self._update_candidate_thumbnail_layout(force=True)
|
||||
self.candidate_scroll.horizontalScrollBar().setValue(0)
|
||||
self._candidate_resize_timer.start()
|
||||
self._install_navigation_event_filters()
|
||||
|
||||
@@ -805,8 +888,8 @@ def _set_image_label(label, image_path, size, empty_text):
|
||||
)
|
||||
|
||||
|
||||
def _candidate_label(candidate_path, current_path):
|
||||
return "当前生效" if _same_file(candidate_path, current_path) else os.path.basename(candidate_path)
|
||||
def _candidate_label(candidate_path, current_path, index):
|
||||
return "当前生效" if _same_file(candidate_path, current_path) else f"候选 {int(index) + 1}"
|
||||
|
||||
|
||||
def _candidate_meta(candidate_path, current_path):
|
||||
|
||||
Reference in New Issue
Block a user