Move InfoPanel from app.py to info_panel.py (no behavior change)

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.
This commit is contained in:
pax 2026-04-08 14:39:08 -05:00
parent 9d99ecfcb5
commit eded7790af
2 changed files with 187 additions and 171 deletions

View File

@ -51,177 +51,6 @@ from .settings import SettingsDialog
log = logging.getLogger("booru") 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-tag<Category>Color` 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'<a href="{source_full}" style="color: #4fc3f7;">{source_display}</a>'
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}<br>"
f"Rating: {escape(post.rating or 'unknown')}<br>"
f"Filetype: {filetype}<br>"
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 -- # -- Main App --
class BooruApp(QMainWindow): class BooruApp(QMainWindow):
@ -3554,3 +3383,4 @@ def run() -> None:
from .search_state import SearchState # re-export for refactor compat from .search_state import SearchState # re-export for refactor compat
from .log_handler import LogHandler # 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 .async_signals import AsyncSignals # re-export for refactor compat
from .info_panel import InfoPanel # re-export for refactor compat

View File

@ -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-tag<Category>Color` 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'<a href="{source_full}" style="color: #4fc3f7;">{source_display}</a>'
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}<br>"
f"Rating: {escape(post.rating or 'unknown')}<br>"
f"Filetype: {filetype}<br>"
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()