From eded7790afaec31763c24d983cb2ed032bfc5026 Mon Sep 17 00:00:00 2001 From: pax Date: Wed, 8 Apr 2026 14:39:08 -0500 Subject: [PATCH] Move InfoPanel from app.py to info_panel.py (no behavior change) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 11 of the gui/app.py + gui/preview.py structural refactor. Pure copy: the toggleable info panel widget with category-coloured tag list moves to its own module. The new module gets its own `log = logging.getLogger("booru")` at module level — same logger instance the rest of the app uses (logging.getLogger is idempotent by name), matching the existing per-module convention used by grid.py / bookmarks.py / library.py. All six tag-color Qt Properties preserved verbatim. app.py grows another shim line. Shim removed in commit 14. --- booru_viewer/gui/app.py | 172 +----------------------------- booru_viewer/gui/info_panel.py | 186 +++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 171 deletions(-) create mode 100644 booru_viewer/gui/info_panel.py diff --git a/booru_viewer/gui/app.py b/booru_viewer/gui/app.py index b8480b9..3326819 100644 --- a/booru_viewer/gui/app.py +++ b/booru_viewer/gui/app.py @@ -51,177 +51,6 @@ from .settings import SettingsDialog log = logging.getLogger("booru") -# -- Info Panel -- - -class InfoPanel(QWidget): - """Toggleable panel showing post details.""" - - tag_clicked = Signal(str) - - # Tag category colors. Defaults follow the booru convention (Danbooru, - # Gelbooru, etc.) so the panel reads naturally to anyone coming from a - # booru site. Each is exposed as a Qt Property so a custom.qss can - # override it via `qproperty-tagColor` selectors on - # `InfoPanel`. An empty string means "use the default text color" - # (the General category) and is preserved as a sentinel. - _tag_artist_color = QColor("#f2ac08") - _tag_character_color = QColor("#0a0") - _tag_copyright_color = QColor("#c0f") - _tag_species_color = QColor("#e44") - _tag_meta_color = QColor("#888") - _tag_lore_color = QColor("#888") - - def _get_artist(self): return self._tag_artist_color - def _set_artist(self, c): self._tag_artist_color = QColor(c) if isinstance(c, str) else c - tagArtistColor = Property(QColor, _get_artist, _set_artist) - - def _get_character(self): return self._tag_character_color - def _set_character(self, c): self._tag_character_color = QColor(c) if isinstance(c, str) else c - tagCharacterColor = Property(QColor, _get_character, _set_character) - - def _get_copyright(self): return self._tag_copyright_color - def _set_copyright(self, c): self._tag_copyright_color = QColor(c) if isinstance(c, str) else c - tagCopyrightColor = Property(QColor, _get_copyright, _set_copyright) - - def _get_species(self): return self._tag_species_color - def _set_species(self, c): self._tag_species_color = QColor(c) if isinstance(c, str) else c - tagSpeciesColor = Property(QColor, _get_species, _set_species) - - def _get_meta(self): return self._tag_meta_color - def _set_meta(self, c): self._tag_meta_color = QColor(c) if isinstance(c, str) else c - tagMetaColor = Property(QColor, _get_meta, _set_meta) - - def _get_lore(self): return self._tag_lore_color - def _set_lore(self, c): self._tag_lore_color = QColor(c) if isinstance(c, str) else c - tagLoreColor = Property(QColor, _get_lore, _set_lore) - - def _category_color(self, category: str) -> str: - """Resolve a category name to a hex color string for inline QSS use. - Returns "" for the General category (no override → use default text - color) and unrecognized categories (so callers can render them with - no color attribute set).""" - cat = (category or "").lower() - m = { - "artist": self._tag_artist_color, - "character": self._tag_character_color, - "copyright": self._tag_copyright_color, - "species": self._tag_species_color, - "meta": self._tag_meta_color, - "lore": self._tag_lore_color, - } - c = m.get(cat) - return c.name() if c is not None else "" - - def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent) - layout = QVBoxLayout(self) - layout.setContentsMargins(6, 6, 6, 6) - - self._title = QLabel("No post selected") - self._title.setStyleSheet("font-weight: bold;") - layout.addWidget(self._title) - - self._details = QLabel() - self._details.setWordWrap(True) - self._details.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextBrowserInteraction) - self._details.setMaximumHeight(120) - layout.addWidget(self._details) - - self._tags_label = QLabel("Tags:") - self._tags_label.setStyleSheet("font-weight: bold; margin-top: 8px;") - layout.addWidget(self._tags_label) - - self._tags_scroll = QScrollArea() - self._tags_scroll.setWidgetResizable(True) - self._tags_scroll.setStyleSheet("QScrollArea { border: none; }") - self._tags_widget = QWidget() - self._tags_flow = QVBoxLayout(self._tags_widget) - self._tags_flow.setContentsMargins(0, 0, 0, 0) - self._tags_flow.setSpacing(2) - self._tags_scroll.setWidget(self._tags_widget) - layout.addWidget(self._tags_scroll, stretch=1) - - def set_post(self, post: Post) -> None: - log.debug(f"InfoPanel: tag_categories={list(post.tag_categories.keys()) if post.tag_categories else 'empty'}") - self._title.setText(f"Post #{post.id}") - filetype = Path(post.file_url.split("?")[0]).suffix.lstrip(".").upper() if post.file_url else "unknown" - source = post.source or "none" - # Truncate display text but keep full URL for the link - source_full = source - if len(source) > 60: - source_display = source[:57] + "..." - else: - source_display = source - if source_full.startswith(("http://", "https://")): - source_html = f'{source_display}' - else: - source_html = source_display - from html import escape - self._details.setText( - f"Score: {post.score}\n" - f"Rating: {post.rating or 'unknown'}\n" - f"Filetype: {filetype}" - ) - self._details.setTextFormat(Qt.TextFormat.RichText) - self._details.setText( - f"Score: {post.score}
" - f"Rating: {escape(post.rating or 'unknown')}
" - f"Filetype: {filetype}
" - f"Source: {source_html}" - ) - self._details.setOpenExternalLinks(True) - # Clear old tags - while self._tags_flow.count(): - item = self._tags_flow.takeAt(0) - if item.widget(): - item.widget().deleteLater() - - if post.tag_categories: - # Display tags grouped by category. Colors come from the - # tag*Color Qt Properties so a custom.qss can override any of - # them via `InfoPanel { qproperty-tagCharacterColor: ...; }`. - for category, tags in post.tag_categories.items(): - color = self._category_color(category) - header = QLabel(f"{category}:") - header.setStyleSheet( - f"font-weight: bold; margin-top: 6px; margin-bottom: 2px;" - + (f" color: {color};" if color else "") - ) - self._tags_flow.addWidget(header) - for tag in tags[:50]: - btn = QPushButton(tag) - btn.setFlat(True) - btn.setCursor(Qt.CursorShape.PointingHandCursor) - style = "QPushButton { text-align: left; padding: 1px 4px; border: none;" - if color: - style += f" color: {color};" - style += " }" - btn.setStyleSheet(style) - btn.clicked.connect(lambda checked, t=tag: self.tag_clicked.emit(t)) - self._tags_flow.addWidget(btn) - else: - # Fallback: flat tag list (Gelbooru, Moebooru) - for tag in post.tag_list[:100]: - btn = QPushButton(tag) - btn.setFlat(True) - btn.setCursor(Qt.CursorShape.PointingHandCursor) - btn.setStyleSheet( - "QPushButton { text-align: left; padding: 1px 4px; border: none; }" - ) - btn.clicked.connect(lambda checked, t=tag: self.tag_clicked.emit(t)) - self._tags_flow.addWidget(btn) - self._tags_flow.addStretch() - - def clear(self) -> None: - self._title.setText("No post selected") - self._details.setText("") - while self._tags_flow.count(): - item = self._tags_flow.takeAt(0) - if item.widget(): - item.widget().deleteLater() - - - # -- Main App -- class BooruApp(QMainWindow): @@ -3554,3 +3383,4 @@ def run() -> None: from .search_state import SearchState # re-export for refactor compat from .log_handler import LogHandler # re-export for refactor compat from .async_signals import AsyncSignals # re-export for refactor compat +from .info_panel import InfoPanel # re-export for refactor compat diff --git a/booru_viewer/gui/info_panel.py b/booru_viewer/gui/info_panel.py new file mode 100644 index 0000000..c47e43d --- /dev/null +++ b/booru_viewer/gui/info_panel.py @@ -0,0 +1,186 @@ +"""Toggleable info panel showing post details with category-coloured tags.""" + +from __future__ import annotations + +import logging +from pathlib import Path + +from PySide6.QtCore import Qt, Property, Signal +from PySide6.QtGui import QColor +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QLabel, QScrollArea, QPushButton, +) + +from ..core.api.base import Post + +log = logging.getLogger("booru") + + +# -- Info Panel -- + +class InfoPanel(QWidget): + """Toggleable panel showing post details.""" + + tag_clicked = Signal(str) + + # Tag category colors. Defaults follow the booru convention (Danbooru, + # Gelbooru, etc.) so the panel reads naturally to anyone coming from a + # booru site. Each is exposed as a Qt Property so a custom.qss can + # override it via `qproperty-tagColor` selectors on + # `InfoPanel`. An empty string means "use the default text color" + # (the General category) and is preserved as a sentinel. + _tag_artist_color = QColor("#f2ac08") + _tag_character_color = QColor("#0a0") + _tag_copyright_color = QColor("#c0f") + _tag_species_color = QColor("#e44") + _tag_meta_color = QColor("#888") + _tag_lore_color = QColor("#888") + + def _get_artist(self): return self._tag_artist_color + def _set_artist(self, c): self._tag_artist_color = QColor(c) if isinstance(c, str) else c + tagArtistColor = Property(QColor, _get_artist, _set_artist) + + def _get_character(self): return self._tag_character_color + def _set_character(self, c): self._tag_character_color = QColor(c) if isinstance(c, str) else c + tagCharacterColor = Property(QColor, _get_character, _set_character) + + def _get_copyright(self): return self._tag_copyright_color + def _set_copyright(self, c): self._tag_copyright_color = QColor(c) if isinstance(c, str) else c + tagCopyrightColor = Property(QColor, _get_copyright, _set_copyright) + + def _get_species(self): return self._tag_species_color + def _set_species(self, c): self._tag_species_color = QColor(c) if isinstance(c, str) else c + tagSpeciesColor = Property(QColor, _get_species, _set_species) + + def _get_meta(self): return self._tag_meta_color + def _set_meta(self, c): self._tag_meta_color = QColor(c) if isinstance(c, str) else c + tagMetaColor = Property(QColor, _get_meta, _set_meta) + + def _get_lore(self): return self._tag_lore_color + def _set_lore(self, c): self._tag_lore_color = QColor(c) if isinstance(c, str) else c + tagLoreColor = Property(QColor, _get_lore, _set_lore) + + def _category_color(self, category: str) -> str: + """Resolve a category name to a hex color string for inline QSS use. + Returns "" for the General category (no override → use default text + color) and unrecognized categories (so callers can render them with + no color attribute set).""" + cat = (category or "").lower() + m = { + "artist": self._tag_artist_color, + "character": self._tag_character_color, + "copyright": self._tag_copyright_color, + "species": self._tag_species_color, + "meta": self._tag_meta_color, + "lore": self._tag_lore_color, + } + c = m.get(cat) + return c.name() if c is not None else "" + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + layout = QVBoxLayout(self) + layout.setContentsMargins(6, 6, 6, 6) + + self._title = QLabel("No post selected") + self._title.setStyleSheet("font-weight: bold;") + layout.addWidget(self._title) + + self._details = QLabel() + self._details.setWordWrap(True) + self._details.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextBrowserInteraction) + self._details.setMaximumHeight(120) + layout.addWidget(self._details) + + self._tags_label = QLabel("Tags:") + self._tags_label.setStyleSheet("font-weight: bold; margin-top: 8px;") + layout.addWidget(self._tags_label) + + self._tags_scroll = QScrollArea() + self._tags_scroll.setWidgetResizable(True) + self._tags_scroll.setStyleSheet("QScrollArea { border: none; }") + self._tags_widget = QWidget() + self._tags_flow = QVBoxLayout(self._tags_widget) + self._tags_flow.setContentsMargins(0, 0, 0, 0) + self._tags_flow.setSpacing(2) + self._tags_scroll.setWidget(self._tags_widget) + layout.addWidget(self._tags_scroll, stretch=1) + + def set_post(self, post: Post) -> None: + log.debug(f"InfoPanel: tag_categories={list(post.tag_categories.keys()) if post.tag_categories else 'empty'}") + self._title.setText(f"Post #{post.id}") + filetype = Path(post.file_url.split("?")[0]).suffix.lstrip(".").upper() if post.file_url else "unknown" + source = post.source or "none" + # Truncate display text but keep full URL for the link + source_full = source + if len(source) > 60: + source_display = source[:57] + "..." + else: + source_display = source + if source_full.startswith(("http://", "https://")): + source_html = f'{source_display}' + else: + source_html = source_display + from html import escape + self._details.setText( + f"Score: {post.score}\n" + f"Rating: {post.rating or 'unknown'}\n" + f"Filetype: {filetype}" + ) + self._details.setTextFormat(Qt.TextFormat.RichText) + self._details.setText( + f"Score: {post.score}
" + f"Rating: {escape(post.rating or 'unknown')}
" + f"Filetype: {filetype}
" + f"Source: {source_html}" + ) + self._details.setOpenExternalLinks(True) + # Clear old tags + while self._tags_flow.count(): + item = self._tags_flow.takeAt(0) + if item.widget(): + item.widget().deleteLater() + + if post.tag_categories: + # Display tags grouped by category. Colors come from the + # tag*Color Qt Properties so a custom.qss can override any of + # them via `InfoPanel { qproperty-tagCharacterColor: ...; }`. + for category, tags in post.tag_categories.items(): + color = self._category_color(category) + header = QLabel(f"{category}:") + header.setStyleSheet( + f"font-weight: bold; margin-top: 6px; margin-bottom: 2px;" + + (f" color: {color};" if color else "") + ) + self._tags_flow.addWidget(header) + for tag in tags[:50]: + btn = QPushButton(tag) + btn.setFlat(True) + btn.setCursor(Qt.CursorShape.PointingHandCursor) + style = "QPushButton { text-align: left; padding: 1px 4px; border: none;" + if color: + style += f" color: {color};" + style += " }" + btn.setStyleSheet(style) + btn.clicked.connect(lambda checked, t=tag: self.tag_clicked.emit(t)) + self._tags_flow.addWidget(btn) + else: + # Fallback: flat tag list (Gelbooru, Moebooru) + for tag in post.tag_list[:100]: + btn = QPushButton(tag) + btn.setFlat(True) + btn.setCursor(Qt.CursorShape.PointingHandCursor) + btn.setStyleSheet( + "QPushButton { text-align: left; padding: 1px 4px; border: none; }" + ) + btn.clicked.connect(lambda checked, t=tag: self.tag_clicked.emit(t)) + self._tags_flow.addWidget(btn) + self._tags_flow.addStretch() + + def clear(self) -> None: + self._title.setText("No post selected") + self._details.setText("") + while self._tags_flow.count(): + item = self._tags_flow.takeAt(0) + if item.widget(): + item.widget().deleteLater()