ui: tweak UI, full height inspector, floating pagination bar

This commit is contained in:
Travis Abendshien
2026-09-11 21:17:24 -07:00
parent 9478e96bb9
commit dd7a1e797e
8 changed files with 107 additions and 122 deletions
+35 -37
View File
@@ -31,7 +31,6 @@ from PySide6.QtWidgets import (
QSizePolicy,
QSpacerItem,
QSplitter,
QStatusBar,
QVBoxLayout,
QWidget,
)
@@ -475,6 +474,7 @@ class MainWindow(QMainWindow):
# initialized in setup_extra_input_bar
self.extra_input_layout: QHBoxLayout
self.results_label: QLabel
self.sorting_mode_combobox: QComboBox
self.sorting_direction_combobox: QComboBox
self.thumb_size_combobox: QComboBox
@@ -482,6 +482,8 @@ class MainWindow(QMainWindow):
# initialized in setup_content
self.content_layout: QHBoxLayout
self.content_splitter: QSplitter
self.central_content: QWidget
self.central_content_layout: QVBoxLayout
# initialized in setup_entry_list
self.entry_list_container: QWidget
@@ -499,14 +501,12 @@ class MainWindow(QMainWindow):
if not self.objectName():
self.setObjectName("MainWindow")
self.resize(1316, 740)
self.resize(1280, 720)
self.setup_menu_bar()
self.setup_central_widget(driver)
self.setup_status_bar()
QMetaObject.connectSlotsByName(self)
# NOTE: These are old attempts to allow for a translucent/acrylic
@@ -536,6 +536,7 @@ class MainWindow(QMainWindow):
self.central_widget.setObjectName("central_widget")
self.central_layout = QGridLayout(self.central_widget)
self.central_layout.setObjectName("central_layout")
self.central_layout.setContentsMargins(0, 9, 0, 0)
self.setup_search_bar()
self.setup_extra_input_bar()
@@ -547,6 +548,7 @@ class MainWindow(QMainWindow):
self.search_bar_layout = QHBoxLayout()
self.search_bar_layout.setObjectName("search_bar_layout")
self.search_bar_layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize)
self.search_bar_layout.setContentsMargins(9, 0, 0, 0)
self.back_button = QPushButton(self.central_widget)
back_icon: Image.Image = self.rm.bxs_left_arrow
@@ -584,12 +586,19 @@ class MainWindow(QMainWindow):
self.search_button.setMinimumSize(QSize(0, 32))
self.search_bar_layout.addWidget(self.search_button)
self.central_layout.addLayout(self.search_bar_layout, 3, 0, 1, 1)
def setup_extra_input_bar(self):
"""Sets up inputs for sorting settings and thumbnail size."""
self.extra_input_layout = QHBoxLayout()
self.extra_input_layout.setObjectName("extra_input_layout")
self.extra_input_layout.setContentsMargins(9, 0, 0, 0)
self.results_label = QLabel("")
self.results_label.setObjectName("results_label")
self.extra_input_layout.addWidget(self.results_label)
self.extra_input_layout.addItem(
QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
)
## Show hidden entries checkbox
self.show_hidden_entries_widget = QWidget()
@@ -610,11 +619,6 @@ class MainWindow(QMainWindow):
self.extra_input_layout.addWidget(self.show_hidden_entries_widget)
## Spacer
self.extra_input_layout.addItem(
QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
)
## Sorting Mode Dropdown
self.sorting_mode_combobox = QComboBox(self.central_widget)
self.sorting_mode_combobox.setObjectName("sorting_mode_combobox")
@@ -651,28 +655,39 @@ class MainWindow(QMainWindow):
self.thumb_size_combobox.addItem(size[0], size[1])
self.thumb_size_combobox.setCurrentIndex(2) # Default: Medium
self.central_layout.addLayout(self.extra_input_layout, 5, 0, 1, 1)
def setup_content(self, driver: QtDriver):
self.content_layout = QHBoxLayout()
self.content_layout.setObjectName("content_layout")
self.content_layout.setContentsMargins(0, 0, 0, 0)
self.content_splitter = QSplitter()
self.content_splitter.setObjectName("content_splitter")
self.content_splitter.setHandleWidth(12)
self.central_content = QWidget()
self.central_content.setObjectName("central_content")
self.central_content_layout = QVBoxLayout(self.central_content)
self.central_content_layout.setObjectName("central_content_layout")
self.central_content_layout.setContentsMargins(0, 0, 0, 0)
self.central_content_layout.setSpacing(6)
self.central_content_layout.addLayout(self.search_bar_layout)
self.central_content_layout.addLayout(self.extra_input_layout)
self.setup_entry_list(driver)
self.content_splitter.addWidget(self.central_content)
self.setup_preview_panel(driver)
self.content_splitter.setStretchFactor(0, 1)
self.content_layout.addWidget(self.content_splitter)
self.central_layout.addLayout(self.content_layout, 10, 0, 1, 1)
self.central_layout.addLayout(self.content_layout, 0, 0, 1, 1)
def setup_entry_list(self, driver: QtDriver):
self.entry_list_container = QWidget()
self.entry_list_layout = QVBoxLayout(self.entry_list_container)
self.entry_list_layout.setSpacing(0)
self.entry_list_layout.setContentsMargins(9, 0, 0, 0)
self.entry_scroll_area = QScrollArea()
self.entry_scroll_area.setObjectName("entry_scroll_area")
@@ -687,8 +702,10 @@ class MainWindow(QMainWindow):
self.thumb_grid = QWidget()
self.thumb_grid.setObjectName("thumb_grid")
self.thumb_layout = ThumbGridLayout(driver, self.entry_scroll_area)
self.thumb_layout.setSpacing(min(self.thumb_size // 10, 12))
# Padding so floating pagination bar doesn't block bottom of scroll contents
self.thumb_layout = ThumbGridLayout(
driver, self.entry_scroll_area, bottom_padding=Pagination.HEIGHT
)
self.thumb_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.thumb_grid.setLayout(self.thumb_layout)
self.entry_scroll_area.setWidget(self.thumb_grid)
@@ -697,34 +714,15 @@ class MainWindow(QMainWindow):
self.entry_list_layout.addWidget(self.banner)
self.entry_list_layout.addWidget(self.entry_scroll_area)
self.landing_widget = LandingWidget(driver, self.devicePixelRatio())
self.entry_list_layout.addWidget(self.landing_widget)
self.pagination = Pagination()
self.entry_list_layout.addWidget(self.pagination)
self.content_splitter.addWidget(self.entry_list_container)
self.pagination = Pagination(self.entry_list_container)
self.central_content_layout.addWidget(self.entry_list_container)
def setup_preview_panel(self, driver: QtDriver):
self.preview_panel = Inspector(driver)
self.content_splitter.addWidget(self.preview_panel)
def setup_status_bar(self):
# BUG: Clicking the status bar does not count as losing focus on other widgets
# (for example, the "Add Tag" line edit). Can this be fixed?
self.status_bar = QStatusBar(self)
self.status_bar.setObjectName("status_bar")
status_bar_size_policy = QSizePolicy(
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum
)
status_bar_size_policy.setHorizontalStretch(0)
status_bar_size_policy.setVerticalStretch(0)
status_bar_size_policy.setHeightForWidth(self.status_bar.sizePolicy().hasHeightForWidth())
self.status_bar.setSizePolicy(status_bar_size_policy)
self.status_bar.setSizeGripEnabled(False)
self.setStatusBar(self.status_bar)
# endregion
def toggle_landing_page(self, enabled: bool):
+35 -4
View File
@@ -7,18 +7,23 @@ from typing import cast, override
from warnings import catch_warnings
from PIL import Image, ImageQt
from PySide6.QtCore import QSize, Signal
from PySide6.QtCore import QEvent, QObject, QSize, Qt, Signal
from PySide6.QtGui import QIntValidator, QPixmap
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QPushButton, QSizePolicy, QWidget
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QPushButton, QWidget
from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay
from tagstudio.qt.views.styles.stylesheets import pagination_style
# TODO: Split to use MVC guidelines.
class Pagination(QWidget):
"""Widget containing controls for navigating between pages of items."""
HEIGHT = 36 # Button row height (24) plus 6px above and below.
RIGHT_MARGIN = 6
SCROLLBAR_WIDTH = 14
index = Signal(int)
def __init__(self, parent: QWidget | None = None) -> None:
@@ -37,11 +42,18 @@ class Pagination(QWidget):
# [----------- ROOT LAYOUT ------------]
self.setHidden(True)
self.setObjectName("pagination")
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground)
self.setFixedHeight(self.HEIGHT)
self.setStyleSheet(pagination_style())
self.root_layout = QHBoxLayout(self)
self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
self.root_layout.setContentsMargins(0, 6, 0, 0)
self.root_layout.setContentsMargins(0, 6, 0, 6)
self.root_layout.setSpacing(3)
if parent is not None:
parent.installEventFilter(self)
self._sync_geometry()
# [<] ----------------------------------
self.prev_button = QPushButton()
prev_icon: Image.Image = self.rm.bxs_left_arrow
@@ -60,6 +72,7 @@ class Pagination(QWidget):
self.start_ellipses = QLabel()
self.start_ellipses.setMinimumSize(self.button_size)
self.start_ellipses.setMaximumSize(self.button_size)
# self.start_ellipses.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.start_ellipses.setText(". . .")
# --------- [3][4] ---------------------
@@ -88,6 +101,7 @@ class Pagination(QWidget):
self.end_ellipses = QLabel()
self.end_ellipses.setMinimumSize(self.button_size)
self.end_ellipses.setMaximumSize(self.button_size)
# self.end_ellipses.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.end_ellipses.setText(". . .")
# ----------------------------- [42] ---
@@ -258,6 +272,8 @@ class Pagination(QWidget):
self.start_buffer_layout.itemAt(i - 1).widget().setHidden(True)
self.setHidden(False)
self._sync_geometry()
self.raise_()
self.validator.setTop(page_count)
if emit:
@@ -287,6 +303,21 @@ class Pagination(QWidget):
end_button.setHidden(True)
self.end_buffer_layout.addWidget(end_button)
@override
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
if watched is self.parentWidget() and event.type() == QEvent.Type.Resize:
self._sync_geometry()
return super().eventFilter(watched, event)
def _sync_geometry(self):
parent = self.parentWidget()
if parent is None:
return
# Stays out of the way of the entry view scrollbar
right_inset = max(self.RIGHT_MARGIN, self.SCROLLBAR_WIDTH)
width = max(0, parent.width() - right_inset)
self.setGeometry(0, parent.height() - self.HEIGHT, width, self.HEIGHT)
class Validator(QIntValidator):
def __init__(self, bottom: int, top: int) -> None:
+6 -58
View File
@@ -654,7 +654,6 @@ class QtDriver(DriverMixin, QObject):
self.shutdown()
def show_error_message(self, error_name: str, error_desc: str | None = None):
self.main_window.status_bar.showMessage(error_name, Qt.AlignmentFlag.AlignLeft)
self.main_window.landing_widget.set_status_label(error_name)
self.main_window.setWindowTitle(f"{self.base_title} - {error_name}")
@@ -689,7 +688,7 @@ class QtDriver(DriverMixin, QObject):
.with_show_hidden_entries(self.main_window.show_hidden_entries)
)
except ParsingError as e:
self.main_window.status_bar.showMessage(
self.main_window.results_label.setText(
f"{Translations['status.results.invalid_syntax']} "
f'"{self.main_window.search_field.text()}"'
)
@@ -797,15 +796,13 @@ class QtDriver(DriverMixin, QObject):
self._banner_context = None
self.main_window.banner.hide_banner(force=True)
self.main_window.status_bar.showMessage(Translations["status.library_closing"])
start_time = time.time()
self.cached_values.setValue(AppCacheItems.LAST_LIBRARY, str(self.lib.library_dir))
self.cached_values.sync()
# Reset library state
self.main_window.preview_panel.set_selection(self.selected)
self.main_window.search_field.setText("")
self.main_window.results_label.setText("")
scrollbar: QScrollArea = self.main_window.entry_scroll_area
scrollbar.verticalScrollBar().setValue(0)
self.__reset_navigation()
@@ -860,26 +857,9 @@ class QtDriver(DriverMixin, QObject):
if self.main_window.menu_bar.add_tag_to_selected_action:
self.main_window.menu_bar.add_tag_to_selected_action.setEnabled(False)
end_time = time.time()
self.main_window.status_bar.showMessage(
Translations.format(
"status.library_closed", time_span=format_timespan(end_time - start_time)
)
)
def backup_library(self):
logger.info("Backing Up Library...")
self.main_window.status_bar.showMessage(Translations["status.library_backup_in_progress"])
start_time = time.time()
target_path = Library.save_library_backup_to_disk(unwrap(self.lib.library_dir))
end_time = time.time()
self.main_window.status_bar.showMessage(
Translations.format(
"status.library_backup_success",
path=target_path,
time_span=format_timespan(end_time - start_time),
)
)
Library.save_library_backup_to_disk(unwrap(self.lib.library_dir))
def emit_badge_signals(self, tag_ids: list[int] | set[int], emit_on_absent: bool = True):
"""Emit any connected signals for updating badge icons."""
@@ -967,7 +947,6 @@ class QtDriver(DriverMixin, QObject):
"""
entry: Entry | None = None
pending: list[tuple[int | None, Path]] = []
deleted_count: int = 0
selected = self.selected
library_dir = unwrap(self.lib.library_dir)
@@ -991,37 +970,17 @@ class QtDriver(DriverMixin, QObject):
return_code == QMessageBox.ButtonRole.DestructiveRole.value
and return_code != QMessageBox.ButtonRole.ActionRole.value
):
for i, tup in enumerate(pending):
e_id, f = tup
for e_id, f in pending:
if (origin_path == f) or (not origin_path):
self.main_window.preview_panel.stop_media_playback()
msg = Translations.format(
"status.deleting_file", i=i, count=len(pending), path=f
)
self.main_window.status_bar.showMessage(msg)
self.main_window.status_bar.repaint()
if e_id is not None:
self.lib.remove_entries([e_id])
if delete_file(library_dir / f):
deleted_count += 1
delete_file(library_dir / f)
self.clear_select_action_callback()
self.update_browsing_state()
if deleted_count > 0 and deleted_count != len(pending):
msg = Translations.format("status.deleted_partial_warning", count=deleted_count)
else:
index = min(deleted_count, 2)
msg = (
Translations["status.deleted_none"],
Translations["status.deleted_file_singular"],
Translations.format("status.deleted_file_plural", count=deleted_count),
)[index]
self.main_window.status_bar.showMessage(msg)
self.main_window.status_bar.repaint()
def delete_file_confirmation(self, count: int, filename: Path | None = None) -> int:
"""A confirmation dialogue box for deleting files.
@@ -1398,9 +1357,6 @@ class QtDriver(DriverMixin, QObject):
def thumb_size_callback(self, size: int):
"""Perform actions needed when the thumbnail size selection is changed."""
spacing_divisor: int = 10
min_spacing: int = 12
self.update_thumbs()
blank_icon: QIcon = QIcon()
for it in self.main_window.thumb_layout._item_thumbs:
@@ -1410,9 +1366,6 @@ class QtDriver(DriverMixin, QObject):
it.setFixedSize(self.main_window.thumb_size, self.main_window.thumb_size)
it.thumb_button.thumb_size = (self.main_window.thumb_size, self.main_window.thumb_size)
it.set_filename_visibility(it.show_filename_label)
self.main_window.thumb_layout.setSpacing(
min(self.main_window.thumb_size // spacing_divisor, min_spacing)
)
def show_hidden_entries_callback(self):
logger.info("Show Hidden Entries Changed", exclude=self.main_window.show_hidden_entries)
@@ -1680,10 +1633,6 @@ class QtDriver(DriverMixin, QObject):
self.main_window.search_field.setText(self.browsing_history.current.query or "")
# inform user about running search
self.main_window.status_bar.showMessage(Translations["status.library_search_query"])
self.main_window.status_bar.repaint()
# search the library
start_time = time.time()
Ignore.get_patterns(self.lib.library_dir, include_global=True)
@@ -1692,7 +1641,7 @@ class QtDriver(DriverMixin, QObject):
end_time = time.time()
# inform user about completed search
self.main_window.status_bar.showMessage(
self.main_window.results_label.setText(
Translations.format(
"status.results_found",
count=results.total_count,
@@ -1814,7 +1763,6 @@ class QtDriver(DriverMixin, QObject):
)
message = Translations.format("splash.opening_library", library_path=library_dir_display)
self.main_window.landing_widget.set_status_label(message)
self.main_window.status_bar.showMessage(message, 3)
self.main_window.repaint()
if self.lib.library_dir:
+1 -1
View File
@@ -30,7 +30,7 @@ logger = structlog.get_logger(__name__)
class InspectorView(QVBoxLayout):
def __init__(self, driver: QtDriver, pixel_ratio: float) -> None:
super().__init__()
self.setContentsMargins(0, 0, 0, 0)
self.setContentsMargins(0, 0, 9, 9)
self.setSpacing(6)
rm = ResourceManager()
@@ -24,13 +24,18 @@ if TYPE_CHECKING:
class ThumbGridLayout(QLayout):
SPACING = 9
# Id of first visible entry
visible_changed = Signal(int)
def __init__(self, driver: QtDriver, scroll_area: QScrollArea) -> None:
def __init__(self, driver: QtDriver, scroll_area: QScrollArea, bottom_padding: int = 0) -> None:
super().__init__(None)
self.setContentsMargins(0, 0, 0, 0)
self.setSpacing(self.SPACING)
self.driver: QtDriver = driver
self.scroll_area: QScrollArea = scroll_area
self._bottom_padding: int = bottom_padding
self._item_thumbs: list[ItemThumb] = []
self._items: list[QLayoutItem] = []
@@ -183,8 +188,8 @@ class ThumbGridLayout(QLayout):
width = arg__1
per_row, _, height_offset = self._size(width)
if per_row == 0:
return height_offset
return math.ceil(len(self._entry_ids) / per_row) * height_offset
return height_offset + self._bottom_padding
return math.ceil(len(self._entry_ids) / per_row) * height_offset + self._bottom_padding
@override
def setGeometry(self, arg__1: QRect) -> None:
+8 -9
View File
@@ -47,7 +47,6 @@ class PreviewThumbView(QWidget):
check_ffmpeg = Signal(bool)
stats_updated = Signal(Path, FileAttributeData)
__img_button_size: tuple[int, int]
__image_ratio: float
_current_file: Path | None
@@ -58,7 +57,7 @@ class PreviewThumbView(QWidget):
super().__init__()
self._driver = driver
self.__img_button_size = (266, 266)
self._preview_size: tuple[int, int] = (272, 272)
self.__image_ratio = 1.0
self.__should_render_on_resize = False
@@ -79,7 +78,7 @@ class PreviewThumbView(QWidget):
delete_action.triggered.connect(self._delete_action_callback)
self.__button_wrapper = QPushButton()
self.__button_wrapper.setMinimumSize(*self.__img_button_size)
self.__button_wrapper.setMinimumSize(*self._preview_size)
self.__button_wrapper.setFlat(True)
self.__button_wrapper.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
self.__button_wrapper.addAction(open_file_action)
@@ -93,7 +92,7 @@ class PreviewThumbView(QWidget):
self.__stacked_page_setup(self.__preview_img_page, self.__button_wrapper)
self.__preview_gif = QLabel()
self.__preview_gif.setMinimumSize(*self.__img_button_size)
self.__preview_gif.setMinimumSize(*self._preview_size)
self.__preview_gif.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
self.__preview_gif.setCursor(Qt.CursorShape.ArrowCursor)
self.__preview_gif.addAction(open_file_action)
@@ -129,7 +128,7 @@ class PreviewThumbView(QWidget):
self.__image_layout.addWidget(self.__preview_gif_page)
self.__image_layout.addWidget(self.__media_player_page)
self.setMinimumSize(*self.__img_button_size)
self.setMinimumSize(*self._preview_size)
self.hide_preview()
@@ -194,7 +193,7 @@ class PreviewThumbView(QWidget):
adj_size = QSize(int(adj_width), int(adj_height))
self.__img_button_size = (int(adj_width), int(adj_height))
self._preview_size = (int(adj_width), int(adj_height))
self.__button_wrapper.setMaximumSize(adj_size)
self.__button_wrapper.setIconSize(adj_size)
self.__preview_gif.setMaximumSize(adj_size)
@@ -243,8 +242,8 @@ class PreviewThumbView(QWidget):
self.__should_render_on_resize = True
self.__rendered_res = (
math.ceil(self.__img_button_size[0] * THUMB_SIZE_FACTOR),
math.ceil(self.__img_button_size[1] * THUMB_SIZE_FACTOR),
math.ceil(self._preview_size[0] * THUMB_SIZE_FACTOR),
math.ceil(self._preview_size[1] * THUMB_SIZE_FACTOR),
)
# TODO: Make driver update the cache manager reference here instead of passing the driver.
@@ -343,7 +342,7 @@ class PreviewThumbView(QWidget):
if (
self._current_file is not None
and self.__should_render_on_resize
and self.__rendered_res < self.__img_button_size
and self.__rendered_res < self._preview_size
):
self.__render_thumb(self._current_file)
@@ -688,6 +688,20 @@ def banner_progress_style() -> str:
"""
def pagination_style() -> str:
"""Style for the pagination bar."""
bg = QColor(banner_progress_bg_color())
bg.setAlpha(200)
border = "rgba(200, 200, 200, 30)"
return f"""
QWidget#pagination {{
background-color: rgba{bg.toTuple()};
border-top: 1px solid {border};
}}
"""
def banner_progress_chunk_color() -> QColor:
"""Fill color for the banner's custom-painted progress bar chunk."""
is_dark = _is_dark_theme()
@@ -373,17 +373,7 @@
"sorting.direction.descending": "Descending",
"sorting.mode.random": "Random",
"splash.opening_library": "Opening Library \"{library_path}\"…",
"status.deleted_file_plural": "Deleted {count} files!",
"status.deleted_file_singular": "Deleted 1 file!",
"status.deleted_none": "No files deleted.",
"status.deleted_partial_warning": "Only deleted {count} file(s)! Check if any of the files are currently missing or in use.",
"status.deleting_file": "Deleting file [{i}/{count}]: \"{path}\"…",
"status.library_backup_in_progress": "Saving Library Backup…",
"status.library_backup_success": "Library Backup Saved at: \"{path}\" ({time_span})",
"status.library_closed": "Library Closed ({time_span})",
"status.library_closing": "Closing Library…",
"status.library_save_success": "Library Saved and Closed!",
"status.library_search_query": "Searching Library…",
"status.library_version_expected": "Expected:",
"status.library_version_found": "Found:",
"status.library_version_mismatch": "Library Version Mismatch!",