mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-09-21 18:08:23 +02:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b85a26065e | |||
| 87f43d81f2 | |||
| 83b976b840 | |||
| 5fbe8f7ce6 | |||
| 514455c8a3 | |||
| df6999afc4 | |||
| b2346b3ae1 | |||
| cce3929790 | |||
| e411fd8ed7 | |||
| 763f3a1d4a |
@@ -12,7 +12,7 @@ TagStudio offers built-in preview and thumbnail support for a wide variety of fi
|
||||
|
||||
### :material-image-outline: Images
|
||||
|
||||
Images will generate thumbnails the first time they are viewed or since the last time they were modified. Thumbnails are used in the grid view, but not in the Preview Panel. Animated images will play in the Preview Panel.
|
||||
Images will generate thumbnails the first time they are viewed or since the last time they were modified. Thumbnails are used in the grid view, but not in the Inspector panel. Animated images will play in the Inspector panel.
|
||||
|
||||
| Filetype | Extensions | Animation |
|
||||
| -------------------- | -------------------------------------------------- | :---------------------------------: |
|
||||
@@ -47,7 +47,7 @@ Images will generate thumbnails the first time they are viewed or since the last
|
||||
|
||||
### :material-movie-open: Videos
|
||||
|
||||
Video thumbnails will default to the closest viable frame from the middle of the video. Both thumbnail generation and video playback in the Preview Panel requires [FFmpeg](install.md#optional-dependencies) installed on your system.
|
||||
Video thumbnails will default to the closest viable frame from the middle of the video. Both thumbnail generation and video playback in the Inspector panel requires [FFmpeg](install.md#optional-dependencies) installed on your system.
|
||||
|
||||
| Filetype | Extensions | Dependencies |
|
||||
| --------------------- | ----------------------- | :----------: |
|
||||
@@ -65,7 +65,7 @@ Video thumbnails will default to the closest viable frame from the middle of the
|
||||
|
||||
### :material-sine-wave: Audio
|
||||
|
||||
Audio thumbnails will default to embedded cover art (if any) and fallback to generated waveform thumbnails. Audio file playback is supported in the Preview Panel if you have [FFmpeg](install.md#optional-dependencies) installed on your system. Audio waveforms are currently not cached.
|
||||
Audio thumbnails will default to embedded cover art (if any) and fallback to generated waveform thumbnails. Audio file playback is supported in the Inspector panel if you have [FFmpeg](install.md#optional-dependencies) installed on your system. Audio waveforms are currently not cached.
|
||||
|
||||
| Filetype | Extensions | Dependencies |
|
||||
| ------------------- | ------------------------ | :----------: |
|
||||
@@ -103,7 +103,7 @@ Preview support for office documents or well-known project file formats varies b
|
||||
|
||||
### :material-archive: Archives
|
||||
|
||||
Archive thumbnails will display the first image from the archive within the Preview Panel.
|
||||
Archive thumbnails will display the first image from the archive within the Inspector panel.
|
||||
|
||||
| Filetype | Extensions |
|
||||
| -------- | -------------- |
|
||||
@@ -129,7 +129,7 @@ Archive thumbnails will display the first image from the archive within the Prev
|
||||
|
||||
### :material-format-font: Fonts
|
||||
|
||||
Font thumbnails will use a "Aa" example preview of the font, with a full alphanumeric of the font available in the Preview Panel.
|
||||
Font thumbnails will use a "Aa" example preview of the font, with a full alphanumeric of the font available in the Inspector panel.
|
||||
|
||||
| Filetype | Extensions |
|
||||
| -------------------- | ----------------- |
|
||||
@@ -143,7 +143,7 @@ Font thumbnails will use a "Aa" example preview of the font, with a full alphanu
|
||||
!!! info "Plain Text Support"
|
||||
TagStudio supports the *vast* majority of files considered to be "[plain text](https://en.wikipedia.org/wiki/Plain_text)". If an extension or format is not listed here, odds are it's still supported anyway.
|
||||
|
||||
Text files render the first 256 bytes of text information to an image preview for thumbnails and the Preview Panel. Improved thumbnails, full scrollable text, and syntax highlighting are on our [roadmap](roadmap.md#uiux) for future features.
|
||||
Text files render the first 256 bytes of text information to an image preview for thumbnails and the Inspector panel. Improved thumbnails, full scrollable text, and syntax highlighting are on our [roadmap](roadmap.md#uiux) for future features.
|
||||
|
||||
| Filetype | Extensions | Syntax Highlighting |
|
||||
| ---------- | --------------------------------------------- | :--------------------------------: |
|
||||
|
||||
@@ -139,7 +139,7 @@ class FileRenderer:
|
||||
Args:
|
||||
size (tuple[int, int]): The size of the graphic.
|
||||
pixel_ratio (float): The screen pixel ratio.
|
||||
scale_radius (bool): Option to scale the radius up (Used for Preview Panel).
|
||||
scale_radius (bool): Option to scale the radius up (Used by the Inspector).
|
||||
"""
|
||||
thumb_scale: int = 512
|
||||
radius_scale: float = 1
|
||||
@@ -223,7 +223,7 @@ class FileRenderer:
|
||||
Args:
|
||||
size (tuple[int,int]): The size of the graphic.
|
||||
pixel_ratio (float): The screen pixel ratio.
|
||||
radius_scale (float): The scale factor of the border radius (Used by Preview Panel).
|
||||
radius_scale (float): The scale factor of the border radius (Used by the Inspector).
|
||||
"""
|
||||
smooth_factor: int = 2
|
||||
radius_factor: int = 8
|
||||
|
||||
@@ -20,7 +20,8 @@ from PySide6.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.qt.views.banner_view import BannerView
|
||||
from tagstudio.qt.views.styles.stylesheets import (
|
||||
BANNER_CORNER_RADIUS,
|
||||
PAD,
|
||||
RADIUS,
|
||||
banner_notice_bg_color,
|
||||
banner_notice_style,
|
||||
banner_progress_bg_color,
|
||||
@@ -52,7 +53,7 @@ class _BannerBackground(QWidget):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(QRectF(self.rect()), BANNER_CORNER_RADIUS, BANNER_CORNER_RADIUS)
|
||||
path.addRoundedRect(QRectF(self.rect()), RADIUS, RADIUS)
|
||||
painter.fillPath(path, self._bg_color)
|
||||
painter.end()
|
||||
|
||||
@@ -61,8 +62,7 @@ class Banner(QWidget):
|
||||
"""A notification banner with an optional progress bar, action button, and close button."""
|
||||
|
||||
CONTENT_HEIGHT = 36
|
||||
GAP = 6
|
||||
HEIGHT = CONTENT_HEIGHT + GAP
|
||||
HEIGHT = CONTENT_HEIGHT + PAD
|
||||
ANIMATION_MS = 250
|
||||
COLOR_ANIMATION_MS = 250
|
||||
MIN_VISIBLE_MS = 3000
|
||||
@@ -78,7 +78,7 @@ class Banner(QWidget):
|
||||
self.setMaximumHeight(0)
|
||||
|
||||
outer_layout = QVBoxLayout(self)
|
||||
outer_layout.setContentsMargins(0, 0, 0, self.GAP)
|
||||
outer_layout.setContentsMargins(0, 0, 0, PAD)
|
||||
outer_layout.setSpacing(0)
|
||||
|
||||
self._background = _BannerBackground(self)
|
||||
@@ -95,7 +95,7 @@ class Banner(QWidget):
|
||||
self._progress_bg_color = banner_progress_bg_color()
|
||||
self._background.setStyleSheet(self._notice_style)
|
||||
self._background.set_bg_color(self._notice_bg_color)
|
||||
self.view.progress_bar.set_corner_radius(BANNER_CORNER_RADIUS)
|
||||
self.view.progress_bar.set_corner_radius(RADIUS)
|
||||
self.view.progress_bar.set_chunk_color(banner_progress_chunk_color())
|
||||
|
||||
self._card_color_anim = QVariantAnimation(self)
|
||||
|
||||
@@ -28,8 +28,8 @@ from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.i18n.translations import FIELD_TYPE_KEYS, Translations
|
||||
from tagstudio.qt.controllers.edit_text import EditText
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.tiles.tile_list import TileList
|
||||
from tagstudio.qt.mixed.datetime_picker import DatetimePicker
|
||||
from tagstudio.qt.mixed.field_containers import FieldContainers
|
||||
from tagstudio.qt.mixed.file_attributes import FileAttributeData
|
||||
from tagstudio.qt.views.inspector_view import InspectorView
|
||||
|
||||
@@ -186,8 +186,8 @@ class Inspector(QWidget):
|
||||
self.layout().containers.update_from_entry(self._selected[0])
|
||||
|
||||
def _edit_field(self, entry_id: int, field: BaseField) -> None:
|
||||
# TODO: A lot of this code is similar to or straight up shared with FieldContainers.
|
||||
# It's possible to reuse it later, after a FieldContainers refactor.
|
||||
# TODO: A lot of this code is similar to or straight up shared with TileList.
|
||||
# It's possible to reuse it later, after a TileList refactor.
|
||||
field_name_key: str = FIELD_TYPE_KEYS.get(field.class_name, "field_type.unknown")
|
||||
|
||||
if type(field) is TextField:
|
||||
@@ -274,14 +274,14 @@ class Inspector(QWidget):
|
||||
self._set_selection_callback()
|
||||
|
||||
except Exception as e:
|
||||
logger.error("[Preview Panel] Error updating selection", error=e)
|
||||
logger.error("[Inspector] Error updating selection", error=e)
|
||||
|
||||
def stop_media_playback(self) -> None:
|
||||
"""Stop any media playback in the inspector."""
|
||||
self.layout().preview_thumb.media_player.stop()
|
||||
|
||||
@property
|
||||
def containers(self) -> FieldContainers:
|
||||
def containers(self) -> TileList:
|
||||
return self.layout().containers
|
||||
|
||||
@override
|
||||
|
||||
@@ -31,7 +31,6 @@ from PySide6.QtWidgets import (
|
||||
QSizePolicy,
|
||||
QSpacerItem,
|
||||
QSplitter,
|
||||
QStatusBar,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
@@ -42,13 +41,13 @@ from tagstudio.i18n.platform_strings import trash_term
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.banner import Banner
|
||||
from tagstudio.qt.controllers.inspector import Inspector
|
||||
from tagstudio.qt.controllers.pagination import Pagination
|
||||
from tagstudio.qt.helpers.mnemonics import assign_mnemonics
|
||||
from tagstudio.qt.mixed.landing import LandingWidget
|
||||
from tagstudio.qt.mixed.pagination import Pagination
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.views.layouts.thumb_grid_layout import ThumbGridLayout
|
||||
from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay
|
||||
from tagstudio.qt.views.styles.stylesheets import checkbox_style
|
||||
from tagstudio.qt.views.styles.stylesheets import HALF_PAD, PAD, WIN_PAD, checkbox_style
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -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
|
||||
@@ -493,20 +495,18 @@ class MainWindow(QMainWindow):
|
||||
self.landing_widget: LandingWidget
|
||||
self.pagination: Pagination
|
||||
|
||||
# initialized in setup_preview_panel
|
||||
self.preview_panel: Inspector
|
||||
# initialized in setup_inspector
|
||||
self.inspector: Inspector
|
||||
# endregion
|
||||
|
||||
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, WIN_PAD, 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(WIN_PAD, 0, 0, 0)
|
||||
|
||||
self.back_button = QPushButton(self.central_widget)
|
||||
back_icon: Image.Image = self.rm.bxs_left_arrow
|
||||
@@ -584,19 +586,26 @@ 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(WIN_PAD, 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()
|
||||
self.show_hidden_entries_layout = QHBoxLayout(self.show_hidden_entries_widget)
|
||||
self.show_hidden_entries_layout.setStretch(1, 1)
|
||||
self.show_hidden_entries_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.show_hidden_entries_layout.setSpacing(6)
|
||||
self.show_hidden_entries_layout.setSpacing(PAD)
|
||||
self.show_hidden_entries_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.show_hidden_entries_title = QLabel(Translations["home.show_hidden_entries"])
|
||||
self.show_hidden_entries_checkbox = QCheckBox()
|
||||
@@ -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.content_splitter.setHandleWidth(PAD + HALF_PAD)
|
||||
|
||||
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(PAD)
|
||||
self.central_content_layout.addLayout(self.search_bar_layout)
|
||||
self.central_content_layout.addLayout(self.extra_input_layout)
|
||||
|
||||
self.setup_entry_list(driver)
|
||||
self.setup_preview_panel(driver)
|
||||
self.content_splitter.addWidget(self.central_content)
|
||||
|
||||
self.setup_inspector(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(WIN_PAD, 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,33 +714,14 @@ 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_container)
|
||||
self.central_content_layout.addWidget(self.entry_list_container)
|
||||
|
||||
self.pagination = Pagination()
|
||||
self.entry_list_layout.addWidget(self.pagination)
|
||||
|
||||
self.content_splitter.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)
|
||||
def setup_inspector(self, driver: QtDriver):
|
||||
self.inspector = Inspector(driver)
|
||||
self.content_splitter.addWidget(self.inspector)
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from typing import override
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import QEvent, QObject, Qt, Signal
|
||||
from PySide6.QtWidgets import QHBoxLayout, QPushButton, QWidget
|
||||
|
||||
from tagstudio.qt.views.pagination_view import PaginationView
|
||||
from tagstudio.qt.views.styles.stylesheets import pagination_style
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class Pagination(QWidget):
|
||||
HEIGHT = 36
|
||||
_RIGHT_MARGIN = 6
|
||||
_SCROLLBAR_WIDTH = 14
|
||||
|
||||
index = Signal(int)
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setHidden(True)
|
||||
self.setObjectName("pagination")
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground)
|
||||
self.setFixedHeight(self.HEIGHT)
|
||||
self.setStyleSheet(pagination_style())
|
||||
|
||||
self.setLayout(PaginationView())
|
||||
self._connect_callbacks()
|
||||
|
||||
if parent is not None:
|
||||
parent.installEventFilter(self)
|
||||
self._sync_geometry()
|
||||
|
||||
@override
|
||||
def layout(self) -> PaginationView:
|
||||
return super().layout() # pyright: ignore[reportReturnType]
|
||||
|
||||
def _connect_callbacks(self) -> None:
|
||||
self.layout().current_page_field.returnPressed.connect(
|
||||
lambda: self._goto_page(int(self.layout().current_page_field.text()) - 1)
|
||||
)
|
||||
|
||||
def update_buttons(self, page_count: int, index: int, emit: bool = True):
|
||||
if index < 0:
|
||||
raise ValueError("Negative index detected")
|
||||
|
||||
view = self.layout()
|
||||
for i in range(0, 10):
|
||||
if button := self._get_button_at(view.start_buffer_layout, i):
|
||||
button.setHidden(True)
|
||||
if button := self._get_button_at(view.end_buffer_layout, i):
|
||||
button.setHidden(True)
|
||||
|
||||
end_page = page_count - 1
|
||||
# Hide everything if there are only one or less pages.
|
||||
if page_count <= 1:
|
||||
self.setHidden(True)
|
||||
|
||||
# Enable/Disable Next + Prev Buttons
|
||||
elif page_count > 1:
|
||||
if index == 0:
|
||||
view.prev_button.setDisabled(True)
|
||||
else:
|
||||
self._assign_click(view.prev_button, index - 1)
|
||||
view.prev_button.setDisabled(False)
|
||||
|
||||
if index == end_page:
|
||||
view.next_button.setDisabled(True)
|
||||
else:
|
||||
self._assign_click(view.next_button, index + 1)
|
||||
view.next_button.setDisabled(False)
|
||||
|
||||
# Set Ellipses Sizes
|
||||
if 8 <= page_count <= 11:
|
||||
end_scale = max(1, page_count - index - 6)
|
||||
start_scale = max(1, index - 5)
|
||||
elif page_count > 11:
|
||||
end_scale = max(1, 7 - index)
|
||||
start_scale = max(1, (7 - (end_page - index)))
|
||||
else:
|
||||
end_scale, start_scale = 1, 1
|
||||
|
||||
if page_count >= 8:
|
||||
end_size = view.BUTTON_SIZE.width() * end_scale + (3 * (end_scale - 1))
|
||||
start_size = view.BUTTON_SIZE.width() * start_scale + (3 * (start_scale - 1))
|
||||
view.end_ellipses.setMinimumWidth(end_size)
|
||||
view.end_ellipses.setMaximumWidth(end_size)
|
||||
view.start_ellipses.setMinimumWidth(start_size)
|
||||
view.start_ellipses.setMaximumWidth(start_size)
|
||||
|
||||
# Enable/Disable Ellipses
|
||||
if index <= view.BUFFER_PAGE_COUNT + 1:
|
||||
view.start_ellipses.setHidden(True)
|
||||
else:
|
||||
view.start_ellipses.setHidden(False)
|
||||
self._assign_click(view.start_button, 0)
|
||||
if index >= (page_count - view.BUFFER_PAGE_COUNT - 2):
|
||||
view.end_ellipses.setHidden(True)
|
||||
else:
|
||||
view.end_ellipses.setHidden(False)
|
||||
|
||||
# Hide/Unhide Start + End Buttons
|
||||
if index != 0:
|
||||
view.start_button.setText("1")
|
||||
self._assign_click(view.start_button, 0)
|
||||
view.start_button.setHidden(False)
|
||||
else:
|
||||
view.start_button.setHidden(True)
|
||||
if index != page_count - 1:
|
||||
view.end_button.setText(str(page_count))
|
||||
self._assign_click(view.end_button, page_count - 1)
|
||||
view.end_button.setHidden(False)
|
||||
else:
|
||||
view.end_button.setHidden(True)
|
||||
|
||||
if index == 0 or index == 1:
|
||||
view.start_buffer_container.setHidden(True)
|
||||
else:
|
||||
view.start_buffer_container.setHidden(False)
|
||||
|
||||
if index == page_count - 1 or index == page_count - 2:
|
||||
view.end_buffer_container.setHidden(True)
|
||||
else:
|
||||
view.end_buffer_container.setHidden(False)
|
||||
|
||||
# Current Field and Buffer Pages
|
||||
for i in range(0, page_count):
|
||||
# Set Field
|
||||
if i == index:
|
||||
if button := self._get_button_at(view.start_buffer_layout, i):
|
||||
button.setHidden(True)
|
||||
if button := self._get_button_at(view.end_buffer_layout, i):
|
||||
button.setHidden(True)
|
||||
view.current_page_field.setText(str(i + 1))
|
||||
|
||||
start_offset = max(0, index - view.BUFFER_PAGE_COUNT * 2)
|
||||
end_offset = min(page_count - 1, index)
|
||||
if i < index:
|
||||
if (i != 0) and i >= index - view.BUFFER_PAGE_COUNT:
|
||||
if button := self._get_button_at(
|
||||
view.start_buffer_layout, i - start_offset
|
||||
):
|
||||
button.setHidden(False)
|
||||
button.setText(str(i + 1))
|
||||
self._assign_click(button, i)
|
||||
else:
|
||||
if button := self._get_button_at(view.start_buffer_layout, i):
|
||||
button.setHidden(True)
|
||||
if button := self._get_button_at(view.end_buffer_layout, i):
|
||||
button.setHidden(True)
|
||||
elif i > index:
|
||||
if i != page_count - 1 and i <= index + view.BUFFER_PAGE_COUNT:
|
||||
if button := self._get_button_at(view.end_buffer_layout, i - end_offset):
|
||||
button.setHidden(False)
|
||||
button.setText(str(i + 1))
|
||||
self._assign_click(button, i)
|
||||
else:
|
||||
if button := self._get_button_at(view.end_buffer_layout, i):
|
||||
button.setHidden(True)
|
||||
for j in range(0, view.BUFFER_PAGE_COUNT):
|
||||
if button := self._get_button_at(
|
||||
view.end_buffer_layout, i - end_offset + j
|
||||
):
|
||||
button.setHidden(True)
|
||||
|
||||
if button := self._get_button_at(view.start_buffer_layout, i - 1):
|
||||
button.setHidden(True)
|
||||
|
||||
self.setHidden(False)
|
||||
self._sync_geometry()
|
||||
self.raise_()
|
||||
|
||||
view.validator.setTop(page_count)
|
||||
if emit:
|
||||
self.index.emit(index)
|
||||
|
||||
def _goto_page(self, index: int):
|
||||
self.update_buttons(self.layout().validator.top(), index)
|
||||
|
||||
def _assign_click(self, button: QPushButton, index: int):
|
||||
with catch_warnings(record=True):
|
||||
button.clicked.disconnect()
|
||||
button.clicked.connect(lambda checked=False, i=index: self._goto_page(i))
|
||||
|
||||
@staticmethod
|
||||
def _get_button_at(layout: QHBoxLayout, index: int) -> QPushButton | None:
|
||||
"""Safely return a button in a layout at a given index, if it exists."""
|
||||
item = layout.itemAt(index)
|
||||
widget = item.widget() if item else None
|
||||
return widget if isinstance(widget, QPushButton) else None
|
||||
|
||||
@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)
|
||||
@@ -1,101 +0,0 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
|
||||
from tagstudio.core.enums import TagClickActionOption
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.views.tag_box_view import TagBoxWidgetView
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Use newer MVC style guidelines
|
||||
class TagBoxWidget(TagBoxWidgetView):
|
||||
on_update = Signal()
|
||||
|
||||
__entries: list[int] = []
|
||||
|
||||
def __init__(self, title: str, driver: QtDriver):
|
||||
super().__init__(title, driver)
|
||||
self.__driver = driver
|
||||
|
||||
def set_entries(self, entries: list[int]) -> None:
|
||||
self.__entries = entries
|
||||
|
||||
@override
|
||||
def _on_click(self, tag: Tag) -> None:
|
||||
match self.__driver.settings.tag_click_action:
|
||||
case TagClickActionOption.OPEN_EDIT:
|
||||
self._on_edit(tag)
|
||||
case TagClickActionOption.SET_SEARCH:
|
||||
self.__driver.update_browsing_state(
|
||||
BrowsingState.from_tag_id(tag.id, self.__driver.browsing_history.current)
|
||||
)
|
||||
case TagClickActionOption.ADD_TO_SEARCH:
|
||||
# NOTE: modifying the ast and then setting that would be nicer
|
||||
# than this string manipulation, but also much more complex,
|
||||
# due to needing to implement a visitor that turns an AST to a string
|
||||
# So if that exists when you read this, change the following accordingly.
|
||||
current = self.__driver.browsing_history.current
|
||||
suffix = unwrap(
|
||||
BrowsingState.from_tag_id(tag.id, self.__driver.browsing_history.current).query
|
||||
)
|
||||
self.__driver.update_browsing_state(
|
||||
current.with_search_query(
|
||||
f"{current.query} {suffix}" if current.query else suffix
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def _on_remove(self, tag: Tag) -> None:
|
||||
logger.info(
|
||||
"[TagBoxWidget] remove_tag",
|
||||
selected=self.__entries,
|
||||
)
|
||||
|
||||
for entry_id in self.__entries:
|
||||
self.__driver.lib.remove_tags_from_entries(entry_id, tag.id)
|
||||
|
||||
self.on_update.emit()
|
||||
|
||||
@override
|
||||
def _on_edit(self, tag: Tag) -> None:
|
||||
build_tag_panel = BuildTagPanel(self.__driver.lib, tag=tag)
|
||||
|
||||
edit_modal = Modal(
|
||||
build_tag_panel,
|
||||
self.__driver.lib.tag_display_name(tag),
|
||||
"Edit Tag",
|
||||
is_savable=True,
|
||||
)
|
||||
edit_modal.saved.connect(partial(self._update_tag_callback, build_tag_panel))
|
||||
edit_modal.show()
|
||||
|
||||
def _update_tag_callback(self, build_tag_panel: BuildTagPanel):
|
||||
self.__driver.lib.update_tag(
|
||||
build_tag_panel.build_tag(),
|
||||
parent_ids=set(build_tag_panel.parent_ids),
|
||||
aliases=set(build_tag_panel.aliases),
|
||||
exclusion_ids=set(build_tag_panel.exclusion_ids),
|
||||
)
|
||||
self.on_update.emit()
|
||||
|
||||
@override
|
||||
def _on_search(self, tag: Tag) -> None:
|
||||
self.__driver.main_window.search_field.setText(f"tag_id:{tag.id}")
|
||||
self.__driver.update_browsing_state(
|
||||
BrowsingState.from_tag_id(tag.id, self.__driver.browsing_history.current)
|
||||
)
|
||||
+19
-17
@@ -4,6 +4,7 @@
|
||||
|
||||
import typing
|
||||
from collections.abc import Iterable
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
@@ -11,14 +12,13 @@ from PySide6.QtWidgets import QMessageBox, QPushButton
|
||||
|
||||
from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.tiles.tile_data import TileData
|
||||
from tagstudio.qt.mixed.build_color import BuildColorPanel
|
||||
from tagstudio.qt.mixed.field_widget import FieldWidget
|
||||
from tagstudio.qt.mixed.tag_color_label import TagColorLabel
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
|
||||
from tagstudio.qt.views.styles.stylesheets import add_button_style
|
||||
from tagstudio.qt.views.tiles.color_data_view import ColorDataView
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
@@ -26,8 +26,9 @@ if typing.TYPE_CHECKING:
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class ColorBoxWidget(FieldWidget):
|
||||
class ColorData(TileData):
|
||||
"""An inner widget for color names that goes in a Tile widget."""
|
||||
|
||||
updated = Signal()
|
||||
|
||||
def __init__(
|
||||
@@ -41,16 +42,15 @@ class ColorBoxWidget(FieldWidget):
|
||||
self.lib: Library = library
|
||||
|
||||
title = "" if not self.lib.engine else self.lib.get_namespace_name(group)
|
||||
super().__init__(title)
|
||||
|
||||
self.setObjectName("colorBox")
|
||||
self.base_layout = FlowLayout()
|
||||
self.base_layout.enable_grid_optimizations(value=True)
|
||||
self.base_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self.base_layout)
|
||||
super().__init__(title, ColorDataView())
|
||||
self.setObjectName("color_data")
|
||||
|
||||
self.set_colors(self.colors)
|
||||
|
||||
@override
|
||||
def layout(self) -> ColorDataView:
|
||||
return super().layout() # pyright: ignore[reportReturnType]
|
||||
|
||||
def set_colors(self, colors: Iterable[TagColorGroup]):
|
||||
colors_ = sorted(
|
||||
list(colors), key=lambda color: self.lib.get_namespace_name(color.namespace)
|
||||
@@ -59,8 +59,10 @@ class ColorBoxWidget(FieldWidget):
|
||||
max_width = 60
|
||||
color_widgets: list[TagColorLabel] = []
|
||||
|
||||
while self.base_layout.itemAt(0):
|
||||
unwrap(self.base_layout.takeAt(0)).widget().deleteLater() # pyright: ignore[reportOptionalMemberAccess]
|
||||
layout = self.layout()
|
||||
while item := layout.takeAt(0):
|
||||
if widget := item.widget():
|
||||
widget.deleteLater()
|
||||
|
||||
for color in colors_:
|
||||
color_widget = TagColorLabel(
|
||||
@@ -76,7 +78,7 @@ class ColorBoxWidget(FieldWidget):
|
||||
color_widget.on_remove.connect(lambda c=color: self.delete_color(c))
|
||||
|
||||
color_widgets.append(color_widget)
|
||||
self.base_layout.addWidget(color_widget)
|
||||
self.layout().addWidget(color_widget)
|
||||
|
||||
for color_widget in color_widgets:
|
||||
color_widget.setFixedWidth(max_width)
|
||||
@@ -98,7 +100,7 @@ class ColorBoxWidget(FieldWidget):
|
||||
)
|
||||
)
|
||||
)
|
||||
self.base_layout.addWidget(add_button)
|
||||
self.layout().addWidget(add_button)
|
||||
|
||||
def edit_color(self, color_group: TagColorGroup):
|
||||
build_color_panel = BuildColorPanel(self.lib, color_group)
|
||||
@@ -132,6 +134,6 @@ class ColorBoxWidget(FieldWidget):
|
||||
if result != QMessageBox.ButtonRole.ActionRole.value:
|
||||
return
|
||||
|
||||
logger.info("[ColorBoxWidget] Removing color", color=color_group)
|
||||
logger.info("[ColorData] Removing color", color=color_group)
|
||||
self.lib.delete_color(color_group)
|
||||
self.updated.emit()
|
||||
@@ -0,0 +1,122 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from collections.abc import Iterable
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
|
||||
from tagstudio.core.enums import TagClickActionOption
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.tiles.tile_data import TileData
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.views.tiles.tag_data_view import TagDataView
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TagData(TileData):
|
||||
"""An inner widget for tags that go in a Tile widget."""
|
||||
|
||||
on_update = Signal()
|
||||
|
||||
def __init__(self, title: str, driver: QtDriver):
|
||||
self._driver = driver
|
||||
self._lib = driver.lib
|
||||
self._entries: list[int] = []
|
||||
super().__init__(title, TagDataView())
|
||||
self.setObjectName("tag_data")
|
||||
|
||||
@override
|
||||
def layout(self) -> TagDataView:
|
||||
return super().layout() # pyright: ignore[reportReturnType]
|
||||
|
||||
def set_entries(self, entries: list[int]) -> None:
|
||||
self._entries = entries
|
||||
|
||||
def set_tags(self, tags: Iterable[Tag]) -> None:
|
||||
tags_ = sorted(list(tags), key=lambda tag: self._lib.tag_display_name(tag))
|
||||
logger.info("[TagData] Tags:", tags=tags)
|
||||
|
||||
layout = self.layout()
|
||||
while item := layout.takeAt(0):
|
||||
if widget := item.widget():
|
||||
widget.deleteLater()
|
||||
|
||||
for tag in tags_:
|
||||
tag_widget = TagWidget(tag, library=self._lib, has_edit=True, has_remove=True)
|
||||
tag_widget.on_click.connect(lambda t=tag: self._on_click(t))
|
||||
tag_widget.on_remove.connect(lambda t=tag: self._on_remove(t))
|
||||
tag_widget.on_edit.connect(lambda t=tag: self._on_edit(t))
|
||||
tag_widget.search_for_tag_action.triggered.connect(
|
||||
lambda checked=False, t=tag: self._on_search(t)
|
||||
)
|
||||
layout.addWidget(tag_widget)
|
||||
|
||||
def _on_click(self, tag: Tag) -> None:
|
||||
match self._driver.settings.tag_click_action:
|
||||
case TagClickActionOption.OPEN_EDIT:
|
||||
self._on_edit(tag)
|
||||
case TagClickActionOption.SET_SEARCH:
|
||||
self._driver.update_browsing_state(
|
||||
BrowsingState.from_tag_id(tag.id, self._driver.browsing_history.current)
|
||||
)
|
||||
case TagClickActionOption.ADD_TO_SEARCH:
|
||||
# NOTE: modifying the ast and then setting that would be nicer
|
||||
# than this string manipulation, but also much more complex,
|
||||
# due to needing to implement a visitor that turns an AST to a string
|
||||
# So if that exists when you read this, change the following accordingly.
|
||||
current = self._driver.browsing_history.current
|
||||
suffix = unwrap(
|
||||
BrowsingState.from_tag_id(tag.id, self._driver.browsing_history.current).query
|
||||
)
|
||||
self._driver.update_browsing_state(
|
||||
current.with_search_query(
|
||||
f"{current.query} {suffix}" if current.query else suffix
|
||||
)
|
||||
)
|
||||
|
||||
def _on_remove(self, tag: Tag) -> None:
|
||||
logger.info("[TagData] remove_tag", selected=self._entries)
|
||||
|
||||
for entry_id in self._entries:
|
||||
self._driver.lib.remove_tags_from_entries(entry_id, tag.id)
|
||||
|
||||
self.on_update.emit()
|
||||
|
||||
def _on_edit(self, tag: Tag) -> None:
|
||||
build_tag_panel = BuildTagPanel(self._driver.lib, tag=tag)
|
||||
|
||||
edit_modal = Modal(
|
||||
build_tag_panel,
|
||||
self._driver.lib.tag_display_name(tag),
|
||||
"Edit Tag",
|
||||
is_savable=True,
|
||||
)
|
||||
edit_modal.saved.connect(partial(self._update_tag_callback, build_tag_panel))
|
||||
edit_modal.show()
|
||||
|
||||
def _update_tag_callback(self, build_tag_panel: BuildTagPanel):
|
||||
self._driver.lib.update_tag(
|
||||
build_tag_panel.build_tag(),
|
||||
parent_ids=set(build_tag_panel.parent_ids),
|
||||
aliases=set(build_tag_panel.aliases),
|
||||
exclusion_ids=set(build_tag_panel.exclusion_ids),
|
||||
)
|
||||
self.on_update.emit()
|
||||
|
||||
def _on_search(self, tag: Tag) -> None:
|
||||
self._driver.main_window.search_field.setText(f"tag_id:{tag.id}")
|
||||
self._driver.update_browsing_state(
|
||||
BrowsingState.from_tag_id(tag.id, self._driver.browsing_history.current)
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
import re
|
||||
from typing import override
|
||||
|
||||
from tagstudio.qt.controllers.tiles.tile_data import TileData
|
||||
from tagstudio.qt.views.tiles.text_data_view import TextDataView
|
||||
|
||||
|
||||
class TextData(TileData):
|
||||
"""An inner widget for any text that goes in a Tile widget.
|
||||
|
||||
Can include text fields, date fields, etc.
|
||||
"""
|
||||
|
||||
def __init__(self, title: str, text: str) -> None:
|
||||
super().__init__(title, TextDataView())
|
||||
self.setObjectName("text_data")
|
||||
self.set_text(text)
|
||||
|
||||
@override
|
||||
def layout(self) -> TextDataView:
|
||||
return super().layout() # pyright: ignore[reportReturnType]
|
||||
|
||||
def set_text(self, text: str):
|
||||
text = linkify(text)
|
||||
self.layout().text_label.setText(text)
|
||||
|
||||
|
||||
# Regex from https://stackoverflow.com/a/6041965
|
||||
def linkify(text: str):
|
||||
url_pattern = r"(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#\-*]*[\w@?^=%&\/~+#\-*])" # noqa: E501
|
||||
return re.sub(
|
||||
url_pattern,
|
||||
lambda url: f'<a href="{url.group(0)}">{url.group(0)}</a>',
|
||||
text,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import override
|
||||
from warnings import catch_warnings
|
||||
|
||||
from PySide6.QtCore import QEvent, Qt
|
||||
from PySide6.QtGui import QEnterEvent, QResizeEvent
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.views.styles.stylesheets import container_style, header
|
||||
from tagstudio.qt.views.tiles.tile_view import TileView
|
||||
|
||||
|
||||
class Tile(QWidget):
|
||||
"""Wraps a title, action buttons, and data such as text or tags inside a single widget."""
|
||||
|
||||
def __init__(self, title: str = "TILE") -> None:
|
||||
super().__init__()
|
||||
self.setObjectName("tile")
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground)
|
||||
self.copy_callback: Callable[[], None] | None = None
|
||||
self.edit_callback: Callable[[], None] | None = None
|
||||
self.remove_callback: Callable[[], None] | None = None
|
||||
|
||||
self.setLayout(TileView())
|
||||
self.set_title(title)
|
||||
self.setStyleSheet(container_style())
|
||||
|
||||
@override
|
||||
def layout(self) -> TileView:
|
||||
return super().layout() # pyright: ignore[reportReturnType]
|
||||
|
||||
def set_copy_callback(self, callback: Callable[[], None] | None = None) -> None:
|
||||
with catch_warnings(record=True):
|
||||
self.layout().copy_button.clicked.disconnect()
|
||||
|
||||
self.copy_callback = callback
|
||||
if callback:
|
||||
self.layout().copy_button.clicked.connect(callback)
|
||||
|
||||
def set_edit_callback(self, callback: Callable[[], None] | None = None) -> None:
|
||||
with catch_warnings(record=True):
|
||||
self.layout().edit_button.clicked.disconnect()
|
||||
|
||||
self.edit_callback = callback
|
||||
if callback:
|
||||
self.layout().edit_button.clicked.connect(callback)
|
||||
|
||||
def set_remove_callback(self, callback: Callable[[], None] | None = None) -> None:
|
||||
with catch_warnings(record=True):
|
||||
self.layout().remove_button.clicked.disconnect()
|
||||
|
||||
self.remove_callback = callback
|
||||
if callback:
|
||||
self.layout().remove_button.clicked.connect(callback)
|
||||
|
||||
def set_inner_widget(self, widget: QWidget) -> None:
|
||||
if item := self.layout().data_layout.itemAt(0):
|
||||
old: QWidget = unwrap(item.widget())
|
||||
self.layout().data_layout.removeWidget(old)
|
||||
old.deleteLater()
|
||||
|
||||
self.layout().data_layout.addWidget(widget)
|
||||
|
||||
def get_inner_widget(self) -> QWidget | None:
|
||||
if item := self.layout().data_layout.itemAt(0):
|
||||
return item.widget()
|
||||
return None
|
||||
|
||||
def set_title(self, title: str) -> None:
|
||||
self.title = header(title, 4)
|
||||
self.layout().title_widget.setText(self.title)
|
||||
|
||||
@override
|
||||
def enterEvent(self, event: QEnterEvent) -> None:
|
||||
# NOTE: You could pass the hover event to the inner widget if needed.
|
||||
layout = self.layout()
|
||||
if self.copy_callback:
|
||||
layout.copy_button.setHidden(False)
|
||||
if self.edit_callback:
|
||||
layout.edit_button.setHidden(False)
|
||||
if self.remove_callback:
|
||||
layout.remove_button.setHidden(False)
|
||||
return super().enterEvent(event)
|
||||
|
||||
@override
|
||||
def leaveEvent(self, event: QEvent) -> None:
|
||||
layout = self.layout()
|
||||
if self.copy_callback:
|
||||
layout.copy_button.setHidden(True)
|
||||
if self.edit_callback:
|
||||
layout.edit_button.setHidden(True)
|
||||
if self.remove_callback:
|
||||
layout.remove_button.setHidden(True)
|
||||
return super().leaveEvent(event)
|
||||
|
||||
@override
|
||||
def resizeEvent(self, event: QResizeEvent) -> None:
|
||||
self.layout().title_widget.setFixedWidth(int(event.size().width() // 1.5))
|
||||
return super().resizeEvent(event)
|
||||
@@ -0,0 +1,14 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from PySide6.QtWidgets import QLayout, QWidget
|
||||
|
||||
|
||||
class TileData(QWidget):
|
||||
"""A base class for widgets that go in a Tile widget."""
|
||||
|
||||
def __init__(self, title: str, view: QLayout) -> None:
|
||||
super().__init__()
|
||||
self.title: str = title
|
||||
self.setLayout(view)
|
||||
+58
-105
@@ -6,19 +6,11 @@ import typing
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime as dt
|
||||
from functools import partial
|
||||
from warnings import catch_warnings
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QMessageBox,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QMessageBox, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.fields import (
|
||||
BaseField,
|
||||
@@ -32,11 +24,11 @@ from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.i18n.translations import FIELD_TYPE_KEYS, Translations
|
||||
from tagstudio.qt.controllers.edit_text import EditText
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.tag_box import TagBoxWidget
|
||||
from tagstudio.qt.controllers.tiles.tag_data import TagData
|
||||
from tagstudio.qt.controllers.tiles.text_data import TextData
|
||||
from tagstudio.qt.controllers.tiles.tile import Tile
|
||||
from tagstudio.qt.mixed.datetime_picker import DatetimePicker
|
||||
from tagstudio.qt.mixed.field_widget import FieldContainer
|
||||
from tagstudio.qt.mixed.text_field import TextContainerWidget
|
||||
from tagstudio.qt.views.styles.stylesheets import inset_container_style
|
||||
from tagstudio.qt.views.tiles.tile_list_view import TileListView
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
@@ -44,9 +36,8 @@ if typing.TYPE_CHECKING:
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class FieldContainers(QWidget):
|
||||
"""Widget for the tag and field containers displayed inside the Preview Panel."""
|
||||
class TileList(QWidget):
|
||||
"""A styled list of Tile widgets."""
|
||||
|
||||
on_tags_update = Signal()
|
||||
|
||||
@@ -55,45 +46,16 @@ class FieldContainers(QWidget):
|
||||
|
||||
self.lib = library
|
||||
self.driver: QtDriver = driver
|
||||
self.initialized = False
|
||||
self.is_open: bool = False
|
||||
self.common_fields: list = []
|
||||
self.mixed_fields: list = []
|
||||
self.cached_entries: list[Entry] = []
|
||||
self._containers: list[FieldContainer] = []
|
||||
self._tiles: list[Tile] = []
|
||||
|
||||
self.scroll_layout = QVBoxLayout()
|
||||
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.scroll_layout.setContentsMargins(3, 3, 3, 3)
|
||||
self.scroll_layout.setSpacing(6)
|
||||
# TODO: Reimplement mixed entry editing
|
||||
|
||||
scroll_container: QWidget = QWidget()
|
||||
scroll_container.setObjectName("entryScrollContainer")
|
||||
scroll_container.setLayout(self.scroll_layout)
|
||||
self.setLayout(TileListView())
|
||||
|
||||
info_section = QWidget()
|
||||
info_layout = QVBoxLayout(info_section)
|
||||
info_layout.setContentsMargins(0, 0, 0, 0)
|
||||
info_layout.setSpacing(0)
|
||||
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area.setObjectName("entryScrollArea")
|
||||
self.scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
|
||||
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
|
||||
# NOTE: I would rather have this style applied to the scroll_area
|
||||
# background and NOT the scroll container background, so that the
|
||||
# rounded corners are maintained when scrolling. I was unable to
|
||||
# find the right trick to only select that particular element.
|
||||
self.scroll_area.setStyleSheet(inset_container_style("entryScrollContainer"))
|
||||
self.scroll_area.setWidget(scroll_container)
|
||||
|
||||
root_layout = QHBoxLayout(self)
|
||||
root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
root_layout.addWidget(self.scroll_area)
|
||||
@override
|
||||
def layout(self) -> TileListView:
|
||||
return super().layout() # pyright: ignore[reportReturnType]
|
||||
|
||||
@property
|
||||
def top_entry_id(self) -> int:
|
||||
@@ -102,7 +64,7 @@ class FieldContainers(QWidget):
|
||||
|
||||
def update_from_entry(self, entry_id: int, update_badges: bool = True) -> None:
|
||||
"""Update tags and fields from a single Entry source."""
|
||||
logger.warning("[FieldContainers] Updating Selection", entry_id=entry_id)
|
||||
logger.warning("[TileList] Updating Selection", entry_id=entry_id)
|
||||
|
||||
entry = unwrap(self.lib.get_entry_full(entry_id))
|
||||
self.cached_entries = [entry]
|
||||
@@ -118,9 +80,7 @@ class FieldContainers(QWidget):
|
||||
if entry_tags:
|
||||
categories = self.get_tag_categories(entry_tags)
|
||||
for cat, tags in sorted(categories.items(), key=lambda kv: (kv[0] is None, kv)):
|
||||
self.write_tag_container(
|
||||
container_index, tags=tags, category_tag=cat, is_mixed=False
|
||||
)
|
||||
self.write_tag_tile(container_index, tags=tags, category_tag=cat, is_mixed=False)
|
||||
container_index += 1
|
||||
container_len += 1
|
||||
if update_badges:
|
||||
@@ -128,11 +88,11 @@ class FieldContainers(QWidget):
|
||||
|
||||
# Write field container(s)
|
||||
for index, field in enumerate(entry_fields, start=container_index):
|
||||
self.write_field_container(index, field, is_mixed=False)
|
||||
self.write_field_tile(index, field, is_mixed=False)
|
||||
|
||||
# Hide leftover container(s)
|
||||
if len(self._containers) > container_len:
|
||||
for i, c in enumerate(self._containers):
|
||||
if len(self._tiles) > container_len:
|
||||
for i, c in enumerate(self._tiles):
|
||||
if i > (container_len - 1):
|
||||
c.setHidden(True)
|
||||
|
||||
@@ -151,7 +111,7 @@ class FieldContainers(QWidget):
|
||||
|
||||
def hide_containers(self) -> None:
|
||||
"""Hide all field and tag containers."""
|
||||
for c in self._containers:
|
||||
for c in self._tiles:
|
||||
c.setHidden(True)
|
||||
|
||||
def get_tag_categories(self, tags: set[Tag]) -> dict[Tag | None, set[Tag]]:
|
||||
@@ -215,7 +175,7 @@ class FieldContainers(QWidget):
|
||||
assert isinstance(field_templates, list)
|
||||
|
||||
logger.info(
|
||||
"[FieldContainers][add_field_to_selected]",
|
||||
"[TileList][add_field_to_selected]",
|
||||
selected=self.driver.selected,
|
||||
fields=[
|
||||
(field_template.class_name, field_template.id) for field_template in field_templates
|
||||
@@ -225,7 +185,7 @@ class FieldContainers(QWidget):
|
||||
for entry_id in self.driver.selected:
|
||||
for field_template in field_templates:
|
||||
logger.info(
|
||||
"[FieldContainers][add_field_to_selected] Adding field",
|
||||
"[TileList][add_field_to_selected] Adding field",
|
||||
name=field_template.name,
|
||||
type=field_template.class_name,
|
||||
)
|
||||
@@ -239,7 +199,7 @@ class FieldContainers(QWidget):
|
||||
if isinstance(tag_ids, int):
|
||||
tag_ids = [tag_ids]
|
||||
logger.info(
|
||||
"[FieldContainers][add_tags_to_selected]",
|
||||
"[TileList][add_tags_to_selected]",
|
||||
selected=self.driver.selected,
|
||||
tag_ids=tag_ids,
|
||||
)
|
||||
@@ -266,8 +226,8 @@ class FieldContainers(QWidget):
|
||||
self._remove_field(field)
|
||||
self.update_from_entry(entry_id)
|
||||
|
||||
def write_field_container(self, index: int, field: BaseField, is_mixed: bool = False) -> None:
|
||||
"""Update/Create data for a field FieldContainer.
|
||||
def write_field_tile(self, index: int, field: BaseField, is_mixed: bool = False) -> None:
|
||||
"""Update/Create data for a field Tile.
|
||||
|
||||
Args:
|
||||
index(int): The container index.
|
||||
@@ -276,9 +236,7 @@ class FieldContainers(QWidget):
|
||||
If True, field is not present in all selected items.
|
||||
"""
|
||||
|
||||
def write_text_container(
|
||||
container: FieldContainer, field: TextField, title: str, is_mixed: bool
|
||||
):
|
||||
def write_text_tile(container: Tile, field: TextField, title: str, is_mixed: bool):
|
||||
container.set_title(field.name)
|
||||
|
||||
# Normalize line endings in any text content.
|
||||
@@ -288,7 +246,7 @@ class FieldContainers(QWidget):
|
||||
else:
|
||||
text = f"<i>{Translations['field.mixed_data']}</i>"
|
||||
|
||||
inner_widget = TextContainerWidget(title, text)
|
||||
inner_widget = TextData(title, text)
|
||||
container.set_inner_widget(inner_widget)
|
||||
|
||||
if not is_mixed:
|
||||
@@ -311,7 +269,7 @@ class FieldContainers(QWidget):
|
||||
)
|
||||
|
||||
def write_datetime_container(
|
||||
container: FieldContainer, field: DatetimeField, title: str, is_mixed: bool
|
||||
container: Tile, field: DatetimeField, title: str, is_mixed: bool
|
||||
):
|
||||
container.set_title(field.name)
|
||||
|
||||
@@ -326,7 +284,7 @@ class FieldContainers(QWidget):
|
||||
else:
|
||||
text = f"<i>{Translations['field.mixed_data']}</i>"
|
||||
|
||||
inner_widget = TextContainerWidget(title, text)
|
||||
inner_widget = TextData(title, text)
|
||||
container.set_inner_widget(inner_widget)
|
||||
|
||||
if not is_mixed:
|
||||
@@ -350,7 +308,7 @@ class FieldContainers(QWidget):
|
||||
|
||||
def write_unknown_container():
|
||||
container.set_title(field.name)
|
||||
inner_widget = TextContainerWidget(title, field.name)
|
||||
inner_widget = TextData(title, field.name)
|
||||
container.set_inner_widget(inner_widget)
|
||||
container.set_remove_callback(
|
||||
lambda: self.remove_message_box(
|
||||
@@ -360,19 +318,19 @@ class FieldContainers(QWidget):
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"[FieldContainers][write_container]",
|
||||
"[TileList][write_container]",
|
||||
index=index,
|
||||
name=field.name,
|
||||
type=field.class_name,
|
||||
)
|
||||
|
||||
# Create new containers if necessary
|
||||
if len(self._containers) < (index + 1):
|
||||
container = FieldContainer()
|
||||
self._containers.append(container)
|
||||
self.scroll_layout.addWidget(container)
|
||||
if len(self._tiles) < (index + 1):
|
||||
container = Tile()
|
||||
self._tiles.append(container)
|
||||
self.layout().scroll_layout.addWidget(container)
|
||||
else:
|
||||
container = self._containers[index]
|
||||
container = self._tiles[index]
|
||||
|
||||
# Set field title
|
||||
field_name_key: str = FIELD_TYPE_KEYS.get(field.class_name, "field_type.unknown")
|
||||
@@ -380,7 +338,7 @@ class FieldContainers(QWidget):
|
||||
|
||||
# Write containers
|
||||
if type(field) is TextField:
|
||||
write_text_container(container, field, title, is_mixed)
|
||||
write_text_tile(container, field, title, is_mixed)
|
||||
elif type(field) is DatetimeField:
|
||||
write_datetime_container(container, field, title, is_mixed)
|
||||
else:
|
||||
@@ -388,10 +346,10 @@ class FieldContainers(QWidget):
|
||||
|
||||
container.setHidden(False)
|
||||
|
||||
def write_tag_container(
|
||||
def write_tag_tile(
|
||||
self, index: int, tags: set[Tag], category_tag: Tag | None = None, is_mixed: bool = False
|
||||
) -> None:
|
||||
"""Update/Create tag data for a tag FieldContainer.
|
||||
"""Update/Create tag data for a tag Tile.
|
||||
|
||||
Args:
|
||||
index(int): The container index.
|
||||
@@ -400,38 +358,33 @@ class FieldContainers(QWidget):
|
||||
is_mixed(bool): Relevant when multiple items are selected.
|
||||
If True, field is not present in all selected items.
|
||||
"""
|
||||
logger.info("[FieldContainers][write_tag_container]", index=index)
|
||||
if len(self._containers) < (index + 1):
|
||||
container = FieldContainer()
|
||||
self._containers.append(container)
|
||||
self.scroll_layout.addWidget(container)
|
||||
logger.info("[TileList][write_tag_tile]", index=index)
|
||||
if len(self._tiles) < (index + 1):
|
||||
container = Tile()
|
||||
self._tiles.append(container)
|
||||
self.layout().scroll_layout.addWidget(container)
|
||||
else:
|
||||
container = self._containers[index]
|
||||
container = self._tiles[index]
|
||||
|
||||
container.set_title(Translations["entries.tags"] if not category_tag else category_tag.name)
|
||||
|
||||
if not is_mixed:
|
||||
inner_widget = container.get_inner_widget()
|
||||
|
||||
if isinstance(inner_widget, TagBoxWidget):
|
||||
with catch_warnings(record=True):
|
||||
inner_widget.on_update.disconnect()
|
||||
|
||||
else:
|
||||
inner_widget = TagBoxWidget(Translations["entries.tags"], self.driver)
|
||||
if not isinstance(inner_widget, TagData):
|
||||
inner_widget = TagData(Translations["entries.tags"], self.driver)
|
||||
container.set_inner_widget(inner_widget)
|
||||
inner_widget.on_update.connect(
|
||||
lambda: (
|
||||
self.update_from_entry(self.cached_entries[0].id, update_badges=True),
|
||||
self.on_tags_update.emit(),
|
||||
)
|
||||
)
|
||||
inner_widget.set_entries([e.id for e in self.cached_entries])
|
||||
inner_widget.set_tags(tags)
|
||||
|
||||
inner_widget.on_update.connect(
|
||||
lambda: (
|
||||
self.update_from_entry(self.cached_entries[0].id, update_badges=True),
|
||||
self.on_tags_update.emit(),
|
||||
)
|
||||
)
|
||||
else:
|
||||
text = f"<i>{Translations['field.mixed_data']}</i>"
|
||||
inner_widget = TextContainerWidget("Mixed Tags", text) # NOTE: Unlocalized but unused
|
||||
inner_widget = TextData("Mixed Tags", text) # NOTE: Unlocalized but unused
|
||||
container.set_inner_widget(inner_widget)
|
||||
|
||||
container.set_edit_callback()
|
||||
@@ -441,7 +394,7 @@ class FieldContainers(QWidget):
|
||||
def _remove_field(self, field: BaseField) -> None:
|
||||
"""Remove a field from all selected Entries."""
|
||||
logger.info(
|
||||
"[FieldContainers] Removing Field",
|
||||
"[TileList] Removing Field",
|
||||
field=field,
|
||||
selected=[x.path for x in self.cached_entries],
|
||||
)
|
||||
@@ -470,9 +423,9 @@ class FieldContainers(QWidget):
|
||||
remove_mb.setWindowTitle(Translations["Remove Field"])
|
||||
remove_mb.setIcon(QMessageBox.Icon.Warning)
|
||||
cancel_button = remove_mb.addButton(
|
||||
Translations["generic.cancel_alt"], QMessageBox.ButtonRole.DestructiveRole
|
||||
Translations["generic.cancel_alt"], QMessageBox.ButtonRole.RejectRole
|
||||
)
|
||||
remove_mb.addButton("&Remove", QMessageBox.ButtonRole.RejectRole)
|
||||
remove_mb.addButton("&Remove", QMessageBox.ButtonRole.DestructiveRole)
|
||||
remove_mb.setEscapeButton(cancel_button)
|
||||
result = remove_mb.exec_()
|
||||
if result == QMessageBox.ButtonRole.ActionRole.value:
|
||||
@@ -1,189 +0,0 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import override
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
from PIL import ImageQt
|
||||
from PySide6.QtCore import QEvent, QSize, Qt
|
||||
from PySide6.QtGui import QEnterEvent, QPixmap, QResizeEvent
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
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 container_style, header
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class FieldContainer(QWidget):
|
||||
rm: ResourceManager = ResourceManager()
|
||||
copy_icon = auto_theme_overlay(rm.copy, inverse=True)
|
||||
edit_icon = auto_theme_overlay(rm.edit, inverse=True)
|
||||
trash_icon = auto_theme_overlay(rm.trash, inverse=True)
|
||||
|
||||
# TODO: There should be a global button theme somewhere.
|
||||
|
||||
def __init__(self, title: str = "Field", inline: bool = True) -> None:
|
||||
super().__init__()
|
||||
self.setObjectName("fieldContainer")
|
||||
self.title: str = title
|
||||
self.copy_callback: Callable[[], None] | None = None
|
||||
self.edit_callback: Callable[[], None] | None = None
|
||||
self.remove_callback: Callable[[], None] | None = None
|
||||
button_size = 22
|
||||
icon_margins = 4
|
||||
icon_size = button_size - icon_margins
|
||||
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setObjectName("baseLayout")
|
||||
self.root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.inner_layout = QVBoxLayout()
|
||||
self.inner_layout.setObjectName("innerLayout")
|
||||
self.inner_layout.setContentsMargins(3, 0, 0, 3)
|
||||
self.inner_layout.setSpacing(0)
|
||||
self.field_container = QWidget()
|
||||
self.field_container.setObjectName("fieldContainer")
|
||||
self.field_container.setLayout(self.inner_layout)
|
||||
self.root_layout.addWidget(self.field_container)
|
||||
|
||||
self.title_container = QWidget()
|
||||
self.title_layout = QHBoxLayout(self.title_container)
|
||||
self.title_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.title_layout.setObjectName("fieldLayout")
|
||||
self.title_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.title_layout.setSpacing(0)
|
||||
self.inner_layout.addWidget(self.title_container)
|
||||
|
||||
self.title_widget = QLabel()
|
||||
self.title_widget.setMinimumHeight(button_size)
|
||||
self.title_widget.setObjectName("fieldTitle")
|
||||
self.title_widget.setWordWrap(True)
|
||||
self.title_widget.setText(title)
|
||||
self.title_layout.addWidget(self.title_widget)
|
||||
self.title_layout.addStretch(2)
|
||||
|
||||
self.copy_button = QPushButton()
|
||||
self.copy_button.setObjectName("copyButton")
|
||||
self.copy_button.setMinimumSize(button_size, button_size)
|
||||
self.copy_button.setMaximumSize(button_size, button_size)
|
||||
self.copy_button.setFlat(True)
|
||||
self.copy_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(FieldContainer.copy_icon)))
|
||||
self.copy_button.setIconSize(QSize(20, 20))
|
||||
self.copy_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.title_layout.addWidget(self.copy_button)
|
||||
self.copy_button.setHidden(True)
|
||||
|
||||
self.edit_button = QPushButton()
|
||||
self.edit_button.setObjectName("editButton")
|
||||
self.edit_button.setMinimumSize(button_size, button_size)
|
||||
self.edit_button.setMaximumSize(button_size, button_size)
|
||||
self.edit_button.setFlat(True)
|
||||
self.edit_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(FieldContainer.edit_icon)))
|
||||
self.edit_button.setIconSize(QSize(icon_size, icon_size))
|
||||
self.edit_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.title_layout.addWidget(self.edit_button)
|
||||
self.edit_button.setHidden(True)
|
||||
|
||||
self.remove_button = QPushButton()
|
||||
self.remove_button.setObjectName("removeButton")
|
||||
self.remove_button.setMinimumSize(button_size, button_size)
|
||||
self.remove_button.setMaximumSize(button_size, button_size)
|
||||
self.remove_button.setFlat(True)
|
||||
self.remove_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(FieldContainer.trash_icon)))
|
||||
self.remove_button.setIconSize(QSize(icon_size, icon_size))
|
||||
self.remove_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.title_layout.addWidget(self.remove_button)
|
||||
self.remove_button.setHidden(True)
|
||||
|
||||
self.field = QWidget()
|
||||
self.field.setObjectName("field")
|
||||
self.field_layout = QHBoxLayout()
|
||||
self.field_layout.setObjectName("fieldLayout")
|
||||
self.field_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.field.setLayout(self.field_layout)
|
||||
self.inner_layout.addWidget(self.field)
|
||||
|
||||
self.set_title(title)
|
||||
self.setStyleSheet(container_style())
|
||||
|
||||
def set_copy_callback(self, callback: Callable[[], None] | None = None) -> None:
|
||||
with catch_warnings(record=True):
|
||||
self.copy_button.clicked.disconnect()
|
||||
|
||||
self.copy_callback = callback
|
||||
if callback:
|
||||
self.copy_button.clicked.connect(callback)
|
||||
|
||||
def set_edit_callback(self, callback: Callable[[], None] | None = None) -> None:
|
||||
with catch_warnings(record=True):
|
||||
self.edit_button.clicked.disconnect()
|
||||
|
||||
self.edit_callback = callback
|
||||
if callback:
|
||||
self.edit_button.clicked.connect(callback)
|
||||
|
||||
def set_remove_callback(self, callback: Callable[[], None] | None = None) -> None:
|
||||
with catch_warnings(record=True):
|
||||
self.remove_button.clicked.disconnect()
|
||||
|
||||
self.remove_callback = callback
|
||||
if callback:
|
||||
self.remove_button.clicked.connect(callback)
|
||||
|
||||
def set_inner_widget(self, widget: FieldWidget) -> None:
|
||||
item = self.field_layout.itemAt(0)
|
||||
if item:
|
||||
old: QWidget = unwrap(item.widget())
|
||||
self.field_layout.removeWidget(old)
|
||||
old.deleteLater()
|
||||
|
||||
self.field_layout.addWidget(widget)
|
||||
|
||||
def get_inner_widget(self) -> QWidget | None:
|
||||
item = self.field_layout.itemAt(0)
|
||||
if item:
|
||||
return item.widget()
|
||||
return None
|
||||
|
||||
def set_title(self, title: str) -> None:
|
||||
self.title = header(title, 4)
|
||||
self.title_widget.setText(self.title)
|
||||
|
||||
@override
|
||||
def enterEvent(self, event: QEnterEvent) -> None:
|
||||
# NOTE: You could pass the hover event to the FieldWidget if needed.
|
||||
if self.copy_callback:
|
||||
self.copy_button.setHidden(False)
|
||||
if self.edit_callback:
|
||||
self.edit_button.setHidden(False)
|
||||
if self.remove_callback:
|
||||
self.remove_button.setHidden(False)
|
||||
return super().enterEvent(event)
|
||||
|
||||
@override
|
||||
def leaveEvent(self, event: QEvent) -> None:
|
||||
if self.copy_callback:
|
||||
self.copy_button.setHidden(True)
|
||||
if self.edit_callback:
|
||||
self.edit_button.setHidden(True)
|
||||
if self.remove_callback:
|
||||
self.remove_button.setHidden(True)
|
||||
return super().leaveEvent(event)
|
||||
|
||||
@override
|
||||
def resizeEvent(self, event: QResizeEvent) -> None:
|
||||
self.title_widget.setFixedWidth(int(event.size().width() // 1.5))
|
||||
return super().resizeEvent(event)
|
||||
|
||||
|
||||
class FieldWidget(QWidget):
|
||||
def __init__(self, title: str) -> None:
|
||||
super().__init__()
|
||||
self.title: str = title
|
||||
@@ -229,9 +229,7 @@ class FoldersToTagsModal(QWidget):
|
||||
def on_apply(self):
|
||||
folders_to_tags(self.library)
|
||||
self.close()
|
||||
self.driver.main_window.preview_panel.set_selection(
|
||||
self.driver.selected, update_preview=False
|
||||
)
|
||||
self.driver.main_window.inspector.set_selection(self.driver.selected, update_preview=False)
|
||||
|
||||
@override
|
||||
def showEvent(self, event: QtGui.QShowEvent):
|
||||
|
||||
@@ -492,9 +492,7 @@ class ItemThumb(FlowWidget):
|
||||
):
|
||||
selected = self.driver._selected
|
||||
if len(selected) == 1 and entry_id in selected:
|
||||
self.driver.main_window.preview_panel.containers.update_toggled_tag(
|
||||
tag_id, toggle_value
|
||||
)
|
||||
self.driver.main_window.inspector.containers.update_toggled_tag(tag_id, toggle_value)
|
||||
|
||||
@override
|
||||
def mouseMoveEvent(self, event: QMouseEvent) -> None:
|
||||
|
||||
@@ -87,7 +87,7 @@ class MirrorEntriesModal(QWidget):
|
||||
pw.from_iterable_function(
|
||||
self.mirror_entries_runnable,
|
||||
displayed_text,
|
||||
lambda s=self.driver.selected: self.driver.main_window.preview_panel.set_selection(s),
|
||||
lambda s=self.driver.selected: self.driver.main_window.inspector.set_selection(s),
|
||||
self.done.emit,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
# pyright: reportOptionalMemberAccess=false
|
||||
|
||||
from typing import cast, override
|
||||
from warnings import catch_warnings
|
||||
|
||||
from PIL import Image, ImageQt
|
||||
from PySide6.QtCore import QSize, Signal
|
||||
from PySide6.QtGui import QIntValidator, QPixmap
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QPushButton, QSizePolicy, QWidget
|
||||
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class Pagination(QWidget):
|
||||
"""Widget containing controls for navigating between pages of items."""
|
||||
|
||||
index = Signal(int)
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent=parent)
|
||||
self.rm = ResourceManager()
|
||||
self.page_count: int = 0
|
||||
self.current_page_index: int = 0
|
||||
self.buffer_page_count: int = 4
|
||||
self.button_size = QSize(32, 24)
|
||||
|
||||
# ------------ UI EXAMPLE --------------
|
||||
# [<] [1]...[3][4] [5] [6][7]...[42] [>]
|
||||
# ^^^^ <-- 2 Buffer Pages
|
||||
# Center Page Number is Editable Text
|
||||
# --------------------------------------
|
||||
|
||||
# [----------- ROOT LAYOUT ------------]
|
||||
self.setHidden(True)
|
||||
self.root_layout = QHBoxLayout(self)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
|
||||
self.root_layout.setContentsMargins(0, 6, 0, 0)
|
||||
self.root_layout.setSpacing(3)
|
||||
|
||||
# [<] ----------------------------------
|
||||
self.prev_button = QPushButton()
|
||||
prev_icon: Image.Image = self.rm.bxs_left_arrow
|
||||
prev_icon = auto_theme_overlay(prev_icon, use_alpha=False)
|
||||
self.prev_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(prev_icon)))
|
||||
self.prev_button.setIconSize(QSize(12, 12))
|
||||
self.prev_button.setMinimumSize(self.button_size)
|
||||
self.prev_button.setMaximumSize(self.button_size)
|
||||
|
||||
# --- [1] ------------------------------
|
||||
self.start_button = QPushButton()
|
||||
self.start_button.setMinimumSize(self.button_size)
|
||||
self.start_button.setMaximumSize(self.button_size)
|
||||
|
||||
# ------ ... ---------------------------
|
||||
self.start_ellipses = QLabel()
|
||||
self.start_ellipses.setMinimumSize(self.button_size)
|
||||
self.start_ellipses.setMaximumSize(self.button_size)
|
||||
self.start_ellipses.setText(". . .")
|
||||
|
||||
# --------- [3][4] ---------------------
|
||||
self.start_buffer_container = QWidget()
|
||||
self.start_buffer_layout = QHBoxLayout(self.start_buffer_container)
|
||||
self.start_buffer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.start_buffer_layout.setSpacing(3)
|
||||
|
||||
# ---------------- [5] -----------------
|
||||
self.current_page_field = QLineEdit()
|
||||
self.current_page_field.setMinimumSize(self.button_size)
|
||||
self.current_page_field.setMaximumSize(self.button_size)
|
||||
self.validator = Validator(1, self.page_count)
|
||||
self.current_page_field.setValidator(self.validator)
|
||||
self.current_page_field.returnPressed.connect(
|
||||
lambda: self._goto_page(int(self.current_page_field.text()) - 1)
|
||||
)
|
||||
|
||||
# -------------------- [6][7] ----------
|
||||
self.end_buffer_container = QWidget()
|
||||
self.end_buffer_layout = QHBoxLayout(self.end_buffer_container)
|
||||
self.end_buffer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.end_buffer_layout.setSpacing(3)
|
||||
|
||||
# -------------------------- ... -------
|
||||
self.end_ellipses = QLabel()
|
||||
self.end_ellipses.setMinimumSize(self.button_size)
|
||||
self.end_ellipses.setMaximumSize(self.button_size)
|
||||
self.end_ellipses.setText(". . .")
|
||||
|
||||
# ----------------------------- [42] ---
|
||||
self.end_button = QPushButton()
|
||||
self.end_button.setMinimumSize(self.button_size)
|
||||
self.end_button.setMaximumSize(self.button_size)
|
||||
|
||||
# ---------------------------------- [>]
|
||||
self.next_button = QPushButton()
|
||||
next_icon: Image.Image = self.rm.bxs_right_arrow
|
||||
next_icon = auto_theme_overlay(next_icon, use_alpha=False)
|
||||
self.next_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(next_icon)))
|
||||
self.next_button.setIconSize(QSize(12, 12))
|
||||
self.next_button.setMinimumSize(self.button_size)
|
||||
self.next_button.setMaximumSize(self.button_size)
|
||||
|
||||
# Add Widgets to Root Layout
|
||||
self.root_layout.addStretch(1)
|
||||
self.root_layout.addWidget(self.prev_button)
|
||||
self.root_layout.addWidget(self.start_button)
|
||||
self.root_layout.addWidget(self.start_ellipses)
|
||||
self.root_layout.addWidget(self.start_buffer_container)
|
||||
self.root_layout.addWidget(self.current_page_field)
|
||||
self.root_layout.addWidget(self.end_buffer_container)
|
||||
self.root_layout.addWidget(self.end_ellipses)
|
||||
self.root_layout.addWidget(self.end_button)
|
||||
self.root_layout.addWidget(self.next_button)
|
||||
self.root_layout.addStretch(1)
|
||||
|
||||
self._populate_buffer_buttons()
|
||||
|
||||
def update_buttons(self, page_count: int, index: int, emit: bool = True):
|
||||
# Guard
|
||||
if index < 0:
|
||||
raise ValueError("Negative index detected")
|
||||
|
||||
for i in range(0, 10):
|
||||
if self.start_buffer_layout.itemAt(i):
|
||||
self.start_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
if self.end_buffer_layout.itemAt(i):
|
||||
self.end_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
|
||||
end_page = page_count - 1
|
||||
if page_count <= 1:
|
||||
# Hide everything if there are only one or less pages.
|
||||
# [-------------- HIDDEN --------------]
|
||||
self.setHidden(True)
|
||||
|
||||
elif page_count > 1:
|
||||
# Enable/Disable Next+Prev Buttons
|
||||
if index == 0:
|
||||
self.prev_button.setDisabled(True)
|
||||
else:
|
||||
self._assign_click(self.prev_button, index - 1)
|
||||
self.prev_button.setDisabled(False)
|
||||
|
||||
if index == end_page:
|
||||
self.next_button.setDisabled(True)
|
||||
else:
|
||||
self._assign_click(self.next_button, index + 1)
|
||||
self.next_button.setDisabled(False)
|
||||
|
||||
# Set Ellipses Sizes
|
||||
if 8 <= page_count <= 11:
|
||||
end_scale = max(1, page_count - index - 6)
|
||||
srt_scale = max(1, index - 5)
|
||||
elif page_count > 11:
|
||||
end_scale = max(1, 7 - index)
|
||||
srt_scale = max(1, (7 - (end_page - index)))
|
||||
|
||||
if page_count >= 8:
|
||||
end_size = self.button_size.width() * end_scale + (3 * (end_scale - 1)) # pyright: ignore[reportPossiblyUnboundVariable]
|
||||
srt_size = self.button_size.width() * srt_scale + (3 * (srt_scale - 1)) # pyright: ignore[reportPossiblyUnboundVariable]
|
||||
self.end_ellipses.setMinimumWidth(end_size)
|
||||
self.end_ellipses.setMaximumWidth(end_size)
|
||||
self.start_ellipses.setMinimumWidth(srt_size)
|
||||
self.start_ellipses.setMaximumWidth(srt_size)
|
||||
|
||||
# Enable/Disable Ellipses
|
||||
if index <= self.buffer_page_count + 1:
|
||||
self.start_ellipses.setHidden(True)
|
||||
else:
|
||||
self.start_ellipses.setHidden(False)
|
||||
self._assign_click(self.start_button, 0)
|
||||
if index >= (page_count - self.buffer_page_count - 2):
|
||||
self.end_ellipses.setHidden(True)
|
||||
else:
|
||||
self.end_ellipses.setHidden(False)
|
||||
|
||||
# Hide/Unhide Start+End Buttons
|
||||
if index != 0:
|
||||
self.start_button.setText("1")
|
||||
self._assign_click(self.start_button, 0)
|
||||
self.start_button.setHidden(False)
|
||||
else:
|
||||
self.start_button.setHidden(True)
|
||||
if index != page_count - 1:
|
||||
self.end_button.setText(str(page_count))
|
||||
self._assign_click(self.end_button, page_count - 1)
|
||||
self.end_button.setHidden(False)
|
||||
else:
|
||||
self.end_button.setHidden(True)
|
||||
|
||||
if index == 0 or index == 1:
|
||||
self.start_buffer_container.setHidden(True)
|
||||
else:
|
||||
self.start_buffer_container.setHidden(False)
|
||||
|
||||
if index == page_count - 1 or index == page_count - 2:
|
||||
self.end_buffer_container.setHidden(True)
|
||||
else:
|
||||
self.end_buffer_container.setHidden(False)
|
||||
|
||||
# Current Field and Buffer Pages
|
||||
sbc = 0
|
||||
for i in range(0, page_count):
|
||||
# Set Field
|
||||
if i == index:
|
||||
if self.start_buffer_layout.itemAt(i):
|
||||
self.start_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
if self.end_buffer_layout.itemAt(i):
|
||||
self.end_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
sbc += 1
|
||||
self.current_page_field.setText(str(i + 1))
|
||||
|
||||
start_offset = max(0, (index - 4) - 4)
|
||||
end_offset = min(page_count - 1, (index + 4) - 4)
|
||||
if i < index:
|
||||
if (i != 0) and i >= index - 4:
|
||||
self.start_buffer_layout.itemAt(i - start_offset).widget().setHidden(False)
|
||||
self.start_buffer_layout.itemAt(i - start_offset).widget().setText( # pyright: ignore[reportAttributeAccessIssue]
|
||||
str(i + 1)
|
||||
)
|
||||
self._assign_click(
|
||||
cast(
|
||||
QPushButton,
|
||||
self.start_buffer_layout.itemAt(i - start_offset).widget(),
|
||||
),
|
||||
i,
|
||||
)
|
||||
sbc += 1
|
||||
else:
|
||||
if self.start_buffer_layout.itemAt(i):
|
||||
self.start_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
if self.end_buffer_layout.itemAt(i):
|
||||
self.end_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
elif i > index:
|
||||
if i != page_count - 1 and i <= index + 4:
|
||||
self.end_buffer_layout.itemAt(i - end_offset).widget().setHidden(False)
|
||||
self.end_buffer_layout.itemAt(i - end_offset).widget().setText(str(i + 1)) # pyright: ignore[reportAttributeAccessIssue]
|
||||
self._assign_click(
|
||||
cast(
|
||||
QPushButton,
|
||||
self.end_buffer_layout.itemAt(i - end_offset).widget(),
|
||||
),
|
||||
i,
|
||||
)
|
||||
else:
|
||||
if self.end_buffer_layout.itemAt(i):
|
||||
self.end_buffer_layout.itemAt(i).widget().setHidden(True)
|
||||
for j in range(0, self.buffer_page_count):
|
||||
if self.end_buffer_layout.itemAt(i - end_offset + j):
|
||||
self.end_buffer_layout.itemAt(
|
||||
i - end_offset + j
|
||||
).widget().setHidden(True)
|
||||
|
||||
if self.start_buffer_layout.itemAt(i - 1):
|
||||
self.start_buffer_layout.itemAt(i - 1).widget().setHidden(True)
|
||||
|
||||
self.setHidden(False)
|
||||
|
||||
self.validator.setTop(page_count)
|
||||
if emit:
|
||||
self.index.emit(index)
|
||||
self.current_page_index = index
|
||||
self.page_count = page_count
|
||||
|
||||
def _goto_page(self, index: int):
|
||||
self.update_buttons(self.page_count, index)
|
||||
|
||||
def _assign_click(self, button: QPushButton, index: int):
|
||||
with catch_warnings(record=True):
|
||||
button.clicked.disconnect()
|
||||
button.clicked.connect(lambda checked=False, i=index: self._goto_page(i))
|
||||
|
||||
def _populate_buffer_buttons(self):
|
||||
for _ in range(max(self.buffer_page_count * 2, 5)):
|
||||
button = QPushButton()
|
||||
button.setMinimumSize(self.button_size)
|
||||
button.setMaximumSize(self.button_size)
|
||||
button.setHidden(True)
|
||||
self.start_buffer_layout.addWidget(button)
|
||||
|
||||
end_button = QPushButton()
|
||||
end_button.setMinimumSize(self.button_size)
|
||||
end_button.setMaximumSize(self.button_size)
|
||||
end_button.setHidden(True)
|
||||
self.end_buffer_layout.addWidget(end_button)
|
||||
|
||||
|
||||
class Validator(QIntValidator):
|
||||
def __init__(self, bottom: int, top: int) -> None:
|
||||
super().__init__(bottom, top)
|
||||
|
||||
@override
|
||||
def fixup(self, input: str) -> str:
|
||||
input = input.strip("0")
|
||||
return super().fixup(str(self.top()) if input else "1")
|
||||
@@ -429,7 +429,7 @@ class SettingsPanel(ModalContent):
|
||||
# Apply changes
|
||||
# Show File Path
|
||||
driver.update_recent_lib_menu()
|
||||
driver.main_window.preview_panel.set_selection(self.driver.selected)
|
||||
driver.main_window.inspector.set_selection(self.driver.selected)
|
||||
library_directory = driver.lib.library_dir
|
||||
if settings["show_filepath"] == ShowFilepathOption.SHOW_FULL_PATHS:
|
||||
display_path = library_directory or ""
|
||||
|
||||
@@ -26,9 +26,9 @@ from tagstudio.core.enums import ThemePalette
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.tiles.color_data import ColorData
|
||||
from tagstudio.qt.controllers.tiles.tile import Tile
|
||||
from tagstudio.qt.mixed.build_namespace import BuildNamespacePanel
|
||||
from tagstudio.qt.mixed.color_box import ColorBoxWidget
|
||||
from tagstudio.qt.mixed.field_widget import FieldContainer
|
||||
from tagstudio.qt.views.styles.stylesheets import header
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -119,22 +119,22 @@ class TagColorManager(QWidget):
|
||||
for group, colors in self.driver.lib.tag_color_groups.items():
|
||||
if not group.startswith(RESERVED_NAMESPACE_PREFIX):
|
||||
all_default = False
|
||||
color_box = ColorBoxWidget(group, colors, self.driver.lib)
|
||||
color_box.updated.connect(
|
||||
color_data = ColorData(group, colors, self.driver.lib)
|
||||
color_data.updated.connect(
|
||||
lambda: (
|
||||
self.reset(),
|
||||
self.setup_color_groups(),
|
||||
()
|
||||
if len(self.driver.selected) < 1
|
||||
else self.driver.main_window.preview_panel.containers.update_from_entry( # noqa: E501
|
||||
else self.driver.main_window.inspector.containers.update_from_entry( # noqa: E501
|
||||
self.driver.selected[0], update_badges=False
|
||||
),
|
||||
)
|
||||
)
|
||||
field_container = FieldContainer(self.driver.lib.get_namespace_name(group))
|
||||
field_container.set_inner_widget(color_box)
|
||||
tile = Tile(self.driver.lib.get_namespace_name(group))
|
||||
tile.set_inner_widget(color_data)
|
||||
if not group.startswith(RESERVED_NAMESPACE_PREFIX):
|
||||
field_container.set_remove_callback(
|
||||
tile.set_remove_callback(
|
||||
lambda checked=False, g=group: self.delete_namespace_dialog(
|
||||
prompt=Translations["color.namespace.delete.prompt"],
|
||||
callback=lambda namespace=g: (
|
||||
@@ -143,14 +143,14 @@ class TagColorManager(QWidget):
|
||||
self.setup_color_groups(),
|
||||
()
|
||||
if len(self.driver.selected) < 1
|
||||
else self.driver.main_window.preview_panel.containers.update_from_entry( # noqa: E501
|
||||
else self.driver.main_window.inspector.containers.update_from_entry( # noqa: E501
|
||||
self.driver.selected[0], update_badges=False
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
self.scroll_layout.addWidget(field_container)
|
||||
self.scroll_layout.addWidget(tile)
|
||||
|
||||
if all_default:
|
||||
ns_container = QWidget()
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
import re
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel
|
||||
|
||||
from tagstudio.qt.mixed.field_widget import FieldWidget
|
||||
|
||||
|
||||
# TODO: Split to use MVC guidelines.
|
||||
class TextContainerWidget(FieldWidget):
|
||||
def __init__(self, title: str, text: str) -> None:
|
||||
super().__init__(title)
|
||||
self.setObjectName("textBox")
|
||||
self.base_layout = QHBoxLayout()
|
||||
self.base_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self.base_layout)
|
||||
self.text_label = QLabel()
|
||||
self.text_label.setStyleSheet("font-size: 12px")
|
||||
self.text_label.setWordWrap(True)
|
||||
self.text_label.setTextFormat(Qt.TextFormat.MarkdownText)
|
||||
self.text_label.setOpenExternalLinks(True)
|
||||
self.text_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
self.base_layout.addWidget(self.text_label)
|
||||
self.set_text(text)
|
||||
|
||||
def set_text(self, text: str):
|
||||
text = linkify(text)
|
||||
self.text_label.setText(text)
|
||||
|
||||
|
||||
# Regex from https://stackoverflow.com/a/6041965
|
||||
def linkify(text: str):
|
||||
url_pattern = r"(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#\-*]*[\w@?^=%&\/~+#\-*])" # noqa: E501
|
||||
return re.sub(
|
||||
url_pattern,
|
||||
lambda url: f'<a href="{url.group(0)}">{url.group(0)}</a>',
|
||||
text,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
@@ -387,7 +387,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
tsp.search_for_tag.connect(self.search_for_tag_callback)
|
||||
self.tag_manager = Modal(tsp, Translations["tag_manager.title"])
|
||||
self.tag_manager.done.connect(
|
||||
lambda checked=False: self.main_window.preview_panel.set_selection(
|
||||
lambda checked=False: self.main_window.inspector.set_selection(
|
||||
self.selected, update_preview=False
|
||||
)
|
||||
)
|
||||
@@ -406,7 +406,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
is_savable=False,
|
||||
)
|
||||
self.field_template_manager.done.connect(
|
||||
lambda checked=False: self.main_window.preview_panel.set_selection(
|
||||
lambda checked=False: self.main_window.inspector.set_selection(
|
||||
self.selected, update_preview=False
|
||||
)
|
||||
)
|
||||
@@ -422,7 +422,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
add_tag_tsp.item_chosen.connect(
|
||||
lambda chosen_tag: (
|
||||
self.add_tags_to_selected_callback([chosen_tag]),
|
||||
self.main_window.preview_panel.set_selection(self.selected, update_preview=False),
|
||||
self.main_window.inspector.set_selection(self.selected, update_preview=False),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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.inspector.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()
|
||||
@@ -833,7 +830,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.library_info_window.close()
|
||||
|
||||
self.main_window.thumb_layout.set_entries([])
|
||||
self.main_window.preview_panel.set_selection(self.selected)
|
||||
self.main_window.inspector.set_selection(self.selected)
|
||||
self.main_window.toggle_landing_page(enabled=True)
|
||||
self.main_window.pagination.setHidden(True)
|
||||
try:
|
||||
@@ -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."""
|
||||
@@ -928,7 +908,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.set_clipboard_menu_viability()
|
||||
self.set_select_actions_visibility()
|
||||
|
||||
self.main_window.preview_panel.set_selection(self.selected, update_preview=False)
|
||||
self.main_window.inspector.set_selection(self.selected, update_preview=False)
|
||||
|
||||
def select_inverse_action_callback(self):
|
||||
"""Invert the selection of all visible items."""
|
||||
@@ -937,14 +917,14 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.set_clipboard_menu_viability()
|
||||
self.set_select_actions_visibility()
|
||||
|
||||
self.main_window.preview_panel.set_selection(self.selected, update_preview=False)
|
||||
self.main_window.inspector.set_selection(self.selected, update_preview=False)
|
||||
|
||||
def clear_select_action_callback(self):
|
||||
self.clear_selected()
|
||||
|
||||
self.set_select_actions_visibility()
|
||||
self.set_clipboard_menu_viability()
|
||||
self.main_window.preview_panel.set_selection(self.selected)
|
||||
self.main_window.inspector.set_selection(self.selected)
|
||||
|
||||
def add_tags_to_selected_callback(self, tag_ids: list[int]):
|
||||
selected: list[int] = self.selected
|
||||
@@ -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()
|
||||
self.main_window.inspector.stop_media_playback()
|
||||
|
||||
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)
|
||||
@@ -1483,7 +1436,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
if TAG_FAVORITE in self.copy_buffer["tags"]:
|
||||
self.update_badges({BadgeType.FAVORITE: True}, origin_id=0, add_tags=False)
|
||||
else:
|
||||
self.main_window.preview_panel.set_selection(self.selected)
|
||||
self.main_window.inspector.set_selection(self.selected)
|
||||
|
||||
def toggle_item_selection(self, item_id: int, append: bool, bridge: bool):
|
||||
"""Toggle the selection of an item in the Thumbnail Grid.
|
||||
@@ -1512,7 +1465,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.set_clipboard_menu_viability()
|
||||
self.set_select_actions_visibility()
|
||||
|
||||
self.main_window.preview_panel.set_selection(self.selected)
|
||||
self.main_window.inspector.set_selection(self.selected)
|
||||
|
||||
def set_clipboard_menu_viability(self):
|
||||
if len(self.selected) == 1:
|
||||
@@ -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,11 +1641,14 @@ class QtDriver(DriverMixin, QObject):
|
||||
end_time = time.time()
|
||||
|
||||
# inform user about completed search
|
||||
self.main_window.status_bar.showMessage(
|
||||
time_span = f"<a style=color:'gray'>({format_timespan(end_time - start_time).title()})</a>"
|
||||
count = f"<b>{results.total_count:,}</b>"
|
||||
self.main_window.results_label.setText(
|
||||
Translations.format(
|
||||
"status.results_found",
|
||||
count=results.total_count,
|
||||
time_span=format_timespan(end_time - start_time),
|
||||
# Show "X Results" on query, "X Items" on none
|
||||
"status.results_found" if state and state.query else "status.items",
|
||||
count=count,
|
||||
time_span=time_span,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1814,7 +1766,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:
|
||||
@@ -1921,7 +1872,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.main_window.menu_bar.folders_to_tags_action.setEnabled(True)
|
||||
self.main_window.menu_bar.library_info_action.setEnabled(True)
|
||||
|
||||
self.main_window.preview_panel.set_selection(self.selected)
|
||||
self.main_window.inspector.set_selection(self.selected)
|
||||
|
||||
# page (re)rendering, extract eventually
|
||||
initial_state = BrowsingState(
|
||||
|
||||
@@ -17,7 +17,7 @@ from PySide6.QtWidgets import (
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.clickable_label import ClickableLabel
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.views.styles.stylesheets import checkbox_style
|
||||
from tagstudio.qt.views.styles.stylesheets import PAD, checkbox_style
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -29,7 +29,7 @@ class EditFieldTemplateModalView(ModalContent):
|
||||
# Layout Init
|
||||
self.setMinimumSize(460, 200)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.root_layout.setContentsMargins(PAD, 0, PAD, 0)
|
||||
self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
# Field Name
|
||||
@@ -64,14 +64,14 @@ class EditFieldTemplateModalView(ModalContent):
|
||||
self._text_field_attributes_layout = QHBoxLayout(self._text_field_attributes_widget)
|
||||
self._text_field_attributes_layout.setStretch(1, 1)
|
||||
self._text_field_attributes_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._text_field_attributes_layout.setSpacing(6)
|
||||
self._text_field_attributes_layout.setSpacing(PAD)
|
||||
|
||||
# Is Multiline
|
||||
self._multiline_widget = QWidget()
|
||||
self._multiline_layout = QHBoxLayout(self._multiline_widget)
|
||||
self._multiline_layout.setStretch(1, 1)
|
||||
self._multiline_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._multiline_layout.setSpacing(6)
|
||||
self._multiline_layout.setSpacing(PAD)
|
||||
self._multiline_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self._multiline_title = ClickableLabel(Translations["field.text.is_multiline"])
|
||||
self._multiline_checkbox = QCheckBox()
|
||||
|
||||
@@ -16,7 +16,7 @@ from PySide6.QtWidgets import (
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.clickable_label import ClickableLabel
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.views.styles.stylesheets import checkbox_style, title_line_edit_style
|
||||
from tagstudio.qt.views.styles.stylesheets import PAD, checkbox_style, title_line_edit_style
|
||||
|
||||
|
||||
class EditTextView(ModalContent):
|
||||
@@ -24,7 +24,7 @@ class EditTextView(ModalContent):
|
||||
super().__init__()
|
||||
self.setMinimumSize(480, 240)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.root_layout.setContentsMargins(PAD, 0, PAD, 0)
|
||||
|
||||
self.name_field = QLineEdit()
|
||||
self.name_field.setStyleSheet(title_line_edit_style())
|
||||
|
||||
@@ -10,8 +10,8 @@ from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWi
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.views.styles.stylesheets import PAD
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
@@ -27,7 +27,7 @@ class FixIgnoredEntriesModalView(QWidget):
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(400, 300)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.root_layout.setContentsMargins(PAD, PAD, PAD, PAD)
|
||||
|
||||
self.ignored_desc_widget = QLabel(Translations["entries.ignored.description"])
|
||||
self.ignored_desc_widget.setObjectName("ignoredDescriptionLabel")
|
||||
@@ -43,7 +43,7 @@ class FixIgnoredEntriesModalView(QWidget):
|
||||
|
||||
self.button_container = QWidget()
|
||||
self.button_layout = QHBoxLayout(self.button_container)
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.button_layout.setContentsMargins(PAD, PAD, PAD, PAD)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
self.done_button = QPushButton(Translations["generic.done_alt"])
|
||||
@@ -59,9 +59,9 @@ class FixIgnoredEntriesModalView(QWidget):
|
||||
self.root_layout.addWidget(self.button_container)
|
||||
|
||||
@override
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
|
||||
if event.key() == QtCore.Qt.Key.Key_Escape:
|
||||
self.done_button.click()
|
||||
else: # Other key presses
|
||||
else:
|
||||
pass
|
||||
return super().keyPressEvent(event)
|
||||
|
||||
@@ -16,6 +16,7 @@ from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.views.styles.stylesheets import PAD
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -30,7 +31,7 @@ class IgnoreModalView(ModalContent):
|
||||
|
||||
self.setMinimumSize(640, 460)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.root_layout.setContentsMargins(PAD, 0, PAD, 0)
|
||||
self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
self.text_edit = QPlainTextEdit()
|
||||
@@ -44,6 +45,6 @@ class IgnoreModalView(ModalContent):
|
||||
Translations.format("ignore.open_file", ts_ignore=IGNORE_NAME)
|
||||
)
|
||||
|
||||
# Add Widgets to Layout ================================================
|
||||
# Finalize Layout
|
||||
self.root_layout.addWidget(self.text_edit)
|
||||
self.root_layout.addWidget(self.open_button)
|
||||
|
||||
@@ -16,10 +16,16 @@ from tagstudio.qt.controllers.field_suggest_box import FieldSuggestBox
|
||||
from tagstudio.qt.controllers.preview_thumb import PreviewThumb
|
||||
from tagstudio.qt.controllers.return_button import ReturnButton
|
||||
from tagstudio.qt.controllers.tag_suggest_box import TagSuggestBox
|
||||
from tagstudio.qt.mixed.field_containers import FieldContainers
|
||||
from tagstudio.qt.controllers.tiles.tile_list import TileList
|
||||
from tagstudio.qt.mixed.file_attributes import FileAttributes
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.views.styles.stylesheets import button_style, preview_warning_style
|
||||
from tagstudio.qt.views.styles.stylesheets import (
|
||||
HALF_PAD,
|
||||
PAD,
|
||||
WIN_PAD,
|
||||
button_style,
|
||||
preview_warning_style,
|
||||
)
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
@@ -30,8 +36,8 @@ 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.setSpacing(6)
|
||||
self.setContentsMargins(0, 0, WIN_PAD, WIN_PAD)
|
||||
self.setSpacing(PAD)
|
||||
rm = ResourceManager()
|
||||
|
||||
# Search/Create Boxes
|
||||
@@ -47,19 +53,19 @@ class InspectorView(QVBoxLayout):
|
||||
|
||||
self.preview_thumb = PreviewThumb(driver.lib, driver)
|
||||
self.file_attrs = FileAttributes(driver.lib, driver)
|
||||
self.containers = FieldContainers(driver.lib, driver)
|
||||
self.containers = TileList(driver.lib, driver)
|
||||
|
||||
# Visual Preview
|
||||
preview_section = QWidget()
|
||||
preview_layout = QVBoxLayout(preview_section)
|
||||
preview_layout.setContentsMargins(0, 0, 0, 0)
|
||||
preview_layout.setSpacing(6)
|
||||
preview_layout.setSpacing(PAD)
|
||||
|
||||
# Warning Banner (Missing FFmpeg, etc.)
|
||||
self.warning_banner = QWidget()
|
||||
self.warning_banner.setObjectName("ffmpeg_widget")
|
||||
ffmpeg_warning_layout = QHBoxLayout(self.warning_banner)
|
||||
ffmpeg_warning_layout.setContentsMargins(3, 3, 3, 3)
|
||||
ffmpeg_warning_layout.setContentsMargins(HALF_PAD, HALF_PAD, HALF_PAD, HALF_PAD)
|
||||
self.warning_banner.setStyleSheet(preview_warning_style())
|
||||
ffmpeg_warning_label = QLabel(
|
||||
Translations.format(
|
||||
@@ -86,17 +92,17 @@ class InspectorView(QVBoxLayout):
|
||||
info_section = QWidget()
|
||||
info_layout = QVBoxLayout(info_section)
|
||||
info_layout.setContentsMargins(0, 0, 0, 0)
|
||||
info_layout.setSpacing(6)
|
||||
info_layout.setSpacing(PAD)
|
||||
|
||||
splitter = QSplitter()
|
||||
splitter.setOrientation(Qt.Orientation.Vertical)
|
||||
splitter.setHandleWidth(12)
|
||||
splitter.setHandleWidth(PAD * 2)
|
||||
|
||||
# Add Tag/Field Buttons
|
||||
add_buttons_container = QWidget()
|
||||
add_buttons_layout = QHBoxLayout(add_buttons_container)
|
||||
add_buttons_layout.setContentsMargins(0, 0, 0, 0)
|
||||
add_buttons_layout.setSpacing(6)
|
||||
add_buttons_layout.setSpacing(PAD)
|
||||
|
||||
self.add_tag_button = ReturnButton(Translations["tag.add"])
|
||||
self.add_tag_button.setEnabled(False)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -24,7 +24,7 @@ from PySide6.QtWidgets import (
|
||||
from tagstudio.i18n.platform_strings import open_file_str
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay
|
||||
from tagstudio.qt.views.styles.stylesheets import header
|
||||
from tagstudio.qt.views.styles.stylesheets import PAD, header
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
@@ -42,7 +42,7 @@ class LibraryInfoWindowView(QWidget):
|
||||
self.setWindowTitle("Library Information")
|
||||
self.setMinimumSize(800, 480)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.root_layout.setContentsMargins(PAD, PAD, PAD, PAD)
|
||||
|
||||
row_height: int = 22
|
||||
icon_margin: int = 4
|
||||
@@ -58,7 +58,7 @@ class LibraryInfoWindowView(QWidget):
|
||||
self.body_widget = QWidget()
|
||||
self.body_layout = QHBoxLayout(self.body_widget)
|
||||
self.body_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.body_layout.setSpacing(6)
|
||||
self.body_layout.setSpacing(PAD)
|
||||
|
||||
# Statistics -----------------------------------------------------------
|
||||
self.stats_widget = QWidget()
|
||||
@@ -440,7 +440,7 @@ class LibraryInfoWindowView(QWidget):
|
||||
# Details --------------------------------------------------------------
|
||||
self.details_container = QWidget()
|
||||
self.details_layout = QHBoxLayout(self.details_container)
|
||||
self.details_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.details_layout.setContentsMargins(PAD, 0, PAD, 0)
|
||||
opacity_effect_details = QGraphicsOpacityEffect(self)
|
||||
opacity_effect_details.setOpacity(0.5)
|
||||
|
||||
@@ -451,7 +451,7 @@ class LibraryInfoWindowView(QWidget):
|
||||
# Buttons --------------------------------------------------------------
|
||||
self.button_container = QWidget()
|
||||
self.button_layout = QHBoxLayout(self.button_container)
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.button_layout.setContentsMargins(PAD, PAD, PAD, PAD)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
self.close_button = QPushButton(Translations["generic.close"])
|
||||
|
||||
@@ -8,7 +8,7 @@ from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWi
|
||||
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.views.styles.stylesheets import header
|
||||
from tagstudio.qt.views.styles.stylesheets import PAD, header
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -25,11 +25,11 @@ class ModalView(QVBoxLayout):
|
||||
):
|
||||
super().__init__()
|
||||
self.content_widget = content_widget
|
||||
self.setContentsMargins(6, 6 if inline_title else 12, 6, 6)
|
||||
self.setContentsMargins(PAD, PAD if inline_title else PAD * 2, PAD, PAD)
|
||||
|
||||
self.button_container = QWidget()
|
||||
self.button_layout = QHBoxLayout(self.button_container)
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.button_layout.setContentsMargins(PAD, PAD, PAD, PAD)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
# [Done]
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from typing import override
|
||||
|
||||
from PIL import Image, ImageQt
|
||||
from PySide6.QtCore import QSize
|
||||
from PySide6.QtGui import QIntValidator, QPixmap, Qt
|
||||
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 HALF_PAD, PAD
|
||||
|
||||
|
||||
class PageValidator(QIntValidator):
|
||||
@override
|
||||
def fixup(self, input: str) -> str:
|
||||
input = input.strip("0")
|
||||
return super().fixup(str(self.top()) if input else "1")
|
||||
|
||||
|
||||
class PaginationView(QHBoxLayout):
|
||||
# NOTE: UI Example:
|
||||
# [<] [1]...[3][4] [5] [6][7]...[42] [>]
|
||||
# ^^^^ <-- 2 Buffer Pages
|
||||
# Center Page Number is Editable Text
|
||||
|
||||
BUTTON_SIZE = QSize(32, 24)
|
||||
BUFFER_PAGE_COUNT = 4
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setContentsMargins(0, PAD, 0, PAD)
|
||||
self.setSpacing(HALF_PAD)
|
||||
_rm = ResourceManager()
|
||||
|
||||
# [<] ----------------------------------
|
||||
self.prev_button = QPushButton()
|
||||
prev_icon: Image.Image = auto_theme_overlay(_rm.bxs_left_arrow, use_alpha=False)
|
||||
self.prev_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(prev_icon)))
|
||||
self.prev_button.setIconSize(QSize(12, 12))
|
||||
self.prev_button.setMinimumSize(self.BUTTON_SIZE)
|
||||
self.prev_button.setMaximumSize(self.BUTTON_SIZE)
|
||||
|
||||
# --- [1] ------------------------------
|
||||
self.start_button = QPushButton()
|
||||
self.start_button.setMinimumSize(self.BUTTON_SIZE)
|
||||
self.start_button.setMaximumSize(self.BUTTON_SIZE)
|
||||
|
||||
# ------ ... ---------------------------
|
||||
self.start_ellipses = QLabel(". . .")
|
||||
self.start_ellipses.setMinimumSize(self.BUTTON_SIZE)
|
||||
self.start_ellipses.setMaximumSize(self.BUTTON_SIZE)
|
||||
|
||||
# --------- [3][4] ---------------------
|
||||
self.start_buffer_container = QWidget()
|
||||
self.start_buffer_layout = QHBoxLayout(self.start_buffer_container)
|
||||
self.start_buffer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.start_buffer_layout.setSpacing(3)
|
||||
|
||||
# ---------------- [5] -----------------
|
||||
self.current_page_field = QLineEdit()
|
||||
self.current_page_field.setMinimumSize(self.BUTTON_SIZE)
|
||||
self.current_page_field.setMaximumSize(self.BUTTON_SIZE)
|
||||
self.current_page_field.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.validator = PageValidator(1, 0)
|
||||
self.current_page_field.setValidator(self.validator)
|
||||
|
||||
# -------------------- [6][7] ----------
|
||||
self.end_buffer_container = QWidget()
|
||||
self.end_buffer_layout = QHBoxLayout(self.end_buffer_container)
|
||||
self.end_buffer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.end_buffer_layout.setSpacing(3)
|
||||
|
||||
# -------------------------- ... -------
|
||||
self.end_ellipses = QLabel(". . .")
|
||||
self.end_ellipses.setMinimumSize(self.BUTTON_SIZE)
|
||||
self.end_ellipses.setMaximumSize(self.BUTTON_SIZE)
|
||||
|
||||
# ----------------------------- [42] ---
|
||||
self.end_button = QPushButton()
|
||||
self.end_button.setMinimumSize(self.BUTTON_SIZE)
|
||||
self.end_button.setMaximumSize(self.BUTTON_SIZE)
|
||||
|
||||
# ---------------------------------- [>]
|
||||
self.next_button = QPushButton()
|
||||
next_icon: Image.Image = auto_theme_overlay(_rm.bxs_right_arrow, use_alpha=False)
|
||||
self.next_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(next_icon)))
|
||||
self.next_button.setIconSize(QSize(12, 12))
|
||||
self.next_button.setMinimumSize(self.BUTTON_SIZE)
|
||||
self.next_button.setMaximumSize(self.BUTTON_SIZE)
|
||||
|
||||
# Finalize Layout
|
||||
self.addStretch(1)
|
||||
self.addWidget(self.prev_button)
|
||||
self.addWidget(self.start_button)
|
||||
self.addWidget(self.start_ellipses)
|
||||
self.addWidget(self.start_buffer_container)
|
||||
self.addWidget(self.current_page_field)
|
||||
self.addWidget(self.end_buffer_container)
|
||||
self.addWidget(self.end_ellipses)
|
||||
self.addWidget(self.end_button)
|
||||
self.addWidget(self.next_button)
|
||||
self.addStretch(1)
|
||||
self._populate_buffer_buttons()
|
||||
|
||||
def _populate_buffer_buttons(self) -> None:
|
||||
for _ in range(max(self.BUFFER_PAGE_COUNT * 2, 5)):
|
||||
button = QPushButton()
|
||||
button.setMinimumSize(self.BUTTON_SIZE)
|
||||
button.setMaximumSize(self.BUTTON_SIZE)
|
||||
button.setHidden(True)
|
||||
self.start_buffer_layout.addWidget(button)
|
||||
|
||||
end_button = QPushButton()
|
||||
end_button.setMinimumSize(self.BUTTON_SIZE)
|
||||
end_button.setMaximumSize(self.BUTTON_SIZE)
|
||||
end_button.setHidden(True)
|
||||
self.end_buffer_layout.addWidget(end_button)
|
||||
@@ -42,12 +42,11 @@ class PreviewType(Enum):
|
||||
|
||||
# TODO: Use newer MVC style guidelines
|
||||
class PreviewThumbView(QWidget):
|
||||
"""The Preview Panel Widget."""
|
||||
"""The file preview thumbnail widget."""
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.i18n.translations import Translations
|
||||
from tagstudio.qt.views.styles.stylesheets import list_button_style
|
||||
from tagstudio.qt.views.styles.stylesheets import PAD, list_button_style
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
@@ -29,13 +29,13 @@ class SearchPanelView(QVBoxLayout):
|
||||
) -> None:
|
||||
self.is_chooser: bool = is_chooser
|
||||
super().__init__()
|
||||
self.setContentsMargins(6, 0, 6, 0)
|
||||
self.setContentsMargins(PAD, 0, PAD, 0)
|
||||
|
||||
# Limit container
|
||||
self.limit_container = QWidget()
|
||||
self.limit_layout = QHBoxLayout(self.limit_container)
|
||||
self.limit_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.limit_layout.setSpacing(12)
|
||||
self.limit_layout.setSpacing(PAD * 2)
|
||||
self.limit_layout.addStretch(1)
|
||||
self.limit_title = QLabel(Translations["home.search.view_limit"])
|
||||
self.limit_layout.addWidget(self.limit_title)
|
||||
@@ -57,7 +57,7 @@ class SearchPanelView(QVBoxLayout):
|
||||
# Scroll area
|
||||
self.scroll_contents = QWidget()
|
||||
self.scroll_layout = QVBoxLayout(self.scroll_contents)
|
||||
self.scroll_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.scroll_layout.setContentsMargins(PAD, 0, PAD, 0)
|
||||
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area.setWidget(self.scroll_contents)
|
||||
|
||||
@@ -23,7 +23,7 @@ def auto_theme_overlay(
|
||||
|
||||
Args:
|
||||
image (Image): The PIL Image object to apply an overlay to.
|
||||
inverse (bool): Option inverse the overlay color relative to the current theme.
|
||||
inverse (bool): Option to invert the overlay color relative to the current theme.
|
||||
use_alpha (bool): Option to retain the base image's alpha value when applying the overlay.
|
||||
"""
|
||||
dark_fg: str = _THEME_DARK_FG[:-2] if not use_alpha else _THEME_DARK_FG
|
||||
|
||||
@@ -19,8 +19,12 @@ from tagstudio.qt.views.styles.palette import (
|
||||
# TODO: There's plenty of good opportunities here to consolidate similar styles.
|
||||
# Work should be done to more closely use Qt's theming systems rather than override them.
|
||||
|
||||
# Shared with RoundedProgressBar.set_corner_radius() so both use the exact same corner arc.
|
||||
BANNER_CORNER_RADIUS = 6
|
||||
|
||||
RADIUS = 6 # Standard corner radius
|
||||
INNER_RADIUS = RADIUS // 2 # Inner/small corder radius
|
||||
PAD = 6 # Standard padding value
|
||||
HALF_PAD = PAD // 2
|
||||
WIN_PAD = 12 # Padding used for window edge margins
|
||||
|
||||
|
||||
def add_button_style() -> str:
|
||||
@@ -31,7 +35,7 @@ def add_button_style() -> str:
|
||||
color: {get_tag_color(ColorType.TEXT, TagColorEnum.DEFAULT)};
|
||||
font-weight: 600;
|
||||
border-color: {get_tag_color(ColorType.BORDER, TagColorEnum.DEFAULT)};
|
||||
border-radius: 6px;
|
||||
border-radius: {RADIUS}px;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
padding-right: 4px;
|
||||
@@ -59,7 +63,7 @@ def button_style() -> str:
|
||||
return f"""
|
||||
QPushButton{{
|
||||
background-color: {ThemePalette.COLOR_BG.value};
|
||||
border-radius: 6px;
|
||||
border-radius: {RADIUS}px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding: 0px 12px;
|
||||
@@ -103,7 +107,7 @@ def line_edit_style_main() -> str:
|
||||
return f"""
|
||||
QLineEdit{{
|
||||
background: {bg_color};
|
||||
border-radius: 6px;
|
||||
border-radius: {RADIUS}px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding: 0px 4px;
|
||||
@@ -150,7 +154,7 @@ def colored_checkbox_style(
|
||||
background: rgba{primary_color.toTuple()};
|
||||
color: rgba{text_color.toTuple()};
|
||||
border-color: rgba{border_color.toTuple()};
|
||||
border-radius: 6px;
|
||||
border-radius: {RADIUS}px;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
}}
|
||||
@@ -184,7 +188,7 @@ def colored_radio_button_style(
|
||||
background: rgba{primary_color.toTuple()};
|
||||
color: rgba{text_color.toTuple()};
|
||||
border-color: rgba{border_color.toTuple()};
|
||||
border-radius: 6px;
|
||||
border-radius: {RADIUS}px;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
}}
|
||||
@@ -230,7 +234,7 @@ def color_swatch_style(
|
||||
color: rgba{text_color.toTuple()};
|
||||
border-color: rgba{border_color.toTuple()};
|
||||
{bottom_color_str}
|
||||
border-radius: 3px;
|
||||
border-radius: {INNER_RADIUS}px;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
}}
|
||||
@@ -249,22 +253,22 @@ def color_swatch_style(
|
||||
QRadioButton::focus{{
|
||||
outline-style: solid;
|
||||
outline-width: 2px;
|
||||
outline-radius: 3px;
|
||||
outline-radius: {INNER_RADIUS}px;
|
||||
outline-color: rgba{highlight_color.toTuple()};
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
def container_style() -> str:
|
||||
"""Style used for field containers."""
|
||||
"""Style used for Tile widgets."""
|
||||
return f"""
|
||||
QWidget#fieldContainer{{
|
||||
QWidget#tile{{
|
||||
border-radius: 4px;
|
||||
}}
|
||||
QWidget#fieldContainer::hover{{
|
||||
QWidget#tile::hover{{
|
||||
background-color: {ThemePalette.COLOR_HOVER.value};
|
||||
}}
|
||||
QWidget#fieldContainer::pressed{{
|
||||
QWidget#tile::pressed{{
|
||||
background-color: {ThemePalette.COLOR_PRESSED.value};
|
||||
}}
|
||||
"""
|
||||
@@ -278,7 +282,7 @@ def form_content_style() -> str:
|
||||
if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
|
||||
else ThemePalette.COLOR_BG_LIGHT.value
|
||||
};
|
||||
border-radius: 3px;
|
||||
border-radius: {INNER_RADIUS}px;
|
||||
font-weight: 500;
|
||||
padding: 1px;
|
||||
}}
|
||||
@@ -313,7 +317,7 @@ def list_button_style(
|
||||
font-weight: 600;
|
||||
{"font: italic;" if italic else ""}
|
||||
border-color: rgba{border_color.toTuple()};
|
||||
border-radius: 6px;
|
||||
border-radius: {RADIUS}px;
|
||||
border-style: {border_style};
|
||||
border-width: 2px;
|
||||
padding-right: 4px;
|
||||
@@ -354,7 +358,7 @@ def properties_style() -> str:
|
||||
font-family: Oxanium;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
border-radius: {INNER_RADIUS}px;
|
||||
padding-top: 4px;
|
||||
padding-right: 1px;
|
||||
padding-bottom: 1px;
|
||||
@@ -376,7 +380,7 @@ def tag_style(
|
||||
color: rgba{text_color.toTuple()};
|
||||
font-weight: 600;
|
||||
border-color: rgba{border_color.toTuple()};
|
||||
border-radius: 6px;
|
||||
border-radius: {RADIUS}px;
|
||||
border-style: {border_style};
|
||||
border-width: 2px;
|
||||
font-size: 13px;
|
||||
@@ -419,7 +423,7 @@ def tag_remove_button_style(
|
||||
color: rgba{text_color.toTuple()};
|
||||
border-color: rgba{highlight_color.toTuple()};
|
||||
border-width: 2;
|
||||
border-radius: 6px;
|
||||
border-radius: {RADIUS}px;
|
||||
}}
|
||||
QPushButton::pressed{{
|
||||
background: rgba{border_color.toTuple()};
|
||||
@@ -458,7 +462,7 @@ def inset_container_style(object_name: str = "") -> str:
|
||||
return f"""
|
||||
QWidget{"#" + object_name if object_name else ""}{{
|
||||
background: {bg_color};
|
||||
border-radius: 6px;
|
||||
border-radius: {RADIUS}px;
|
||||
}}
|
||||
"""
|
||||
|
||||
@@ -475,8 +479,8 @@ def autofill_scroll_top_style(object_name: str = "") -> str:
|
||||
return f"""
|
||||
QWidget{"#" + object_name if object_name else ""}{{
|
||||
background: {bg_color};
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
border-top-left-radius: {RADIUS}px;
|
||||
border-top-right-radius: {RADIUS}px;
|
||||
border: none;
|
||||
}}
|
||||
"""
|
||||
@@ -493,8 +497,8 @@ def autofill_scroll_top_focus_style(object_name: str = "") -> str:
|
||||
return f"""
|
||||
QWidget{"#" + object_name if object_name else ""}{{
|
||||
background: {bg_color};
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
border-top-left-radius: {RADIUS}px;
|
||||
border-top-right-radius: {RADIUS}px;
|
||||
border: solid;
|
||||
border-width: 2px 2px 0px 2px;
|
||||
border-color: rgba{Palette.accent().toTuple()};
|
||||
@@ -513,8 +517,8 @@ def autofill_line_edit_style() -> str:
|
||||
return f"""
|
||||
QLineEdit{{
|
||||
background: {bg_color};
|
||||
border-radius: 6px;
|
||||
padding: 3px 6px;
|
||||
border-radius: {RADIUS}px;
|
||||
padding: {HALF_PAD}px {PAD}px;
|
||||
}}
|
||||
QLineEdit::focus{{
|
||||
padding: 4px 4px;
|
||||
@@ -538,9 +542,9 @@ def autofill_line_edit_top_style() -> str:
|
||||
background: {bg_color};
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
padding: 0px 0px 2px 6px;
|
||||
border-bottom-left-radius: {RADIUS}px;
|
||||
border-bottom-right-radius: {RADIUS}px;
|
||||
padding: 0px 0px 2px {RADIUS}px;
|
||||
}}
|
||||
QLineEdit::focus{{
|
||||
padding: 4px 4px;
|
||||
@@ -555,7 +559,7 @@ def preview_warning_style() -> str:
|
||||
return f"""
|
||||
QWidget#ffmpeg_widget {{
|
||||
background: {get_ui_color(ColorType.DARK_ACCENT, UiColor.RED)};
|
||||
border-radius: 6px;
|
||||
border-radius: {RADIUS}px;
|
||||
}}
|
||||
"""
|
||||
|
||||
@@ -598,7 +602,7 @@ def banner_close_button_style() -> str:
|
||||
background: transparent;
|
||||
color: rgba{text_color.toTuple()};
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
border-radius: {INNER_RADIUS}px;
|
||||
}}
|
||||
{_banner_button_hover_style("bannerCloseButton")}
|
||||
"""
|
||||
@@ -614,7 +618,7 @@ def banner_action_button_style() -> str:
|
||||
background-color: rgba{accent.toTuple()};
|
||||
color: rgba{text_color.toTuple()};
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
border-radius: {INNER_RADIUS}px;
|
||||
padding: 4px 8px;
|
||||
outline: none;
|
||||
}}
|
||||
@@ -655,7 +659,7 @@ def banner_close_button_progress_style() -> str:
|
||||
background: transparent;
|
||||
color: rgba{text_color.toTuple()};
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
border-radius: {INNER_RADIUS}px;
|
||||
}}
|
||||
QPushButton#bannerCloseButton::hover {{
|
||||
background-color: {hover};
|
||||
@@ -688,6 +692,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()
|
||||
|
||||
@@ -10,6 +10,7 @@ from PySide6.QtWidgets import QFrame, QHBoxLayout, QSizePolicy, QVBoxLayout, QWi
|
||||
from tagstudio.qt.controllers.autofill_line_edit import AutofillLineEdit
|
||||
from tagstudio.qt.controllers.horizontal_scroll_area import HorizontalScrollArea
|
||||
from tagstudio.qt.views.styles.stylesheets import (
|
||||
PAD,
|
||||
autofill_line_edit_style,
|
||||
autofill_scroll_top_style,
|
||||
)
|
||||
@@ -36,7 +37,7 @@ class SuggestBoxView(QVBoxLayout):
|
||||
# Autocomplete ScrollArea
|
||||
contents = QWidget()
|
||||
self.content_layout = QHBoxLayout(contents)
|
||||
self.content_layout.setSpacing(6)
|
||||
self.content_layout.setSpacing(PAD)
|
||||
self.content_layout.setAlignment(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft)
|
||||
self.content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
scroll_area_container = QWidget()
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.qt.mixed.field_widget import FieldWidget
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.qt_driver import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# TODO: Use newer MVC style guidelines
|
||||
class TagBoxWidgetView(FieldWidget):
|
||||
__lib: Library
|
||||
|
||||
def __init__(self, title: str, driver: QtDriver) -> None:
|
||||
super().__init__(title)
|
||||
self.__lib = driver.lib
|
||||
|
||||
self.__root_layout = FlowLayout()
|
||||
self.__root_layout.enable_grid_optimizations(value=False)
|
||||
self.__root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self.__root_layout)
|
||||
|
||||
def set_tags(self, tags: Iterable[Tag]) -> None:
|
||||
tags_ = sorted(list(tags), key=lambda tag: self.__lib.tag_display_name(tag))
|
||||
logger.info("[TagBoxWidget] Tags:", tags=tags)
|
||||
while self.__root_layout.itemAt(0):
|
||||
self.__root_layout.takeAt(0).widget().deleteLater() # pyright: ignore[reportOptionalMemberAccess]
|
||||
|
||||
for tag in tags_:
|
||||
tag_widget = TagWidget(tag, library=self.__lib, has_edit=True, has_remove=True)
|
||||
tag_widget.on_click.connect(lambda t=tag: self._on_click(t))
|
||||
tag_widget.on_remove.connect(lambda t=tag: self._on_remove(t))
|
||||
tag_widget.on_edit.connect(lambda t=tag: self._on_edit(t))
|
||||
tag_widget.search_for_tag_action.triggered.connect(
|
||||
lambda checked=False, t=tag: self._on_search(t)
|
||||
)
|
||||
self.__root_layout.addWidget(tag_widget)
|
||||
|
||||
def _on_click(self, tag: Tag) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def _on_remove(self, tag: Tag) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def _on_edit(self, tag: Tag) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def _on_search(self, tag: Tag) -> None:
|
||||
raise NotImplementedError
|
||||
@@ -10,6 +10,7 @@ from PySide6.QtGui import QColor, QEnterEvent, QPainter, QPainterPath, QPaintEve
|
||||
from PySide6.QtWidgets import QPushButton, QWidget
|
||||
|
||||
from tagstudio.qt.views.styles.palette import Palette
|
||||
from tagstudio.qt.views.styles.stylesheets import RADIUS
|
||||
|
||||
|
||||
# TODO: Use newer MVC style guidelines
|
||||
@@ -46,7 +47,6 @@ class ThumbButton(QPushButton):
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
path = QPainterPath()
|
||||
width = 3
|
||||
radius = 6
|
||||
path.addRoundedRect(
|
||||
QtCore.QRectF(
|
||||
width / 2,
|
||||
@@ -54,8 +54,8 @@ class ThumbButton(QPushButton):
|
||||
self.thumb_size[0] - width,
|
||||
self.thumb_size[1] - width,
|
||||
),
|
||||
radius,
|
||||
radius,
|
||||
RADIUS,
|
||||
RADIUS,
|
||||
)
|
||||
|
||||
if self.selected:
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
|
||||
|
||||
|
||||
class ColorDataView(FlowLayout):
|
||||
"""The layout used for a ColorData widget."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.enable_grid_optimizations(value=True)
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -0,0 +1,14 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
|
||||
|
||||
|
||||
class TagDataView(FlowLayout):
|
||||
"""The layout used for a TagData widget."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.enable_grid_optimizations(value=False)
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -0,0 +1,22 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel
|
||||
|
||||
|
||||
class TextDataView(QHBoxLayout):
|
||||
"""The layout used for a TextData widget."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.text_label = QLabel()
|
||||
self.text_label.setStyleSheet("font-size: 12px")
|
||||
self.text_label.setWordWrap(True)
|
||||
self.text_label.setTextFormat(Qt.TextFormat.MarkdownText)
|
||||
self.text_label.setOpenExternalLinks(True)
|
||||
self.text_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
self.addWidget(self.text_label)
|
||||
@@ -0,0 +1,40 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QFrame, QHBoxLayout, QScrollArea, QSizePolicy, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.qt.views.styles.stylesheets import HALF_PAD, PAD, inset_container_style
|
||||
|
||||
|
||||
class TileListView(QHBoxLayout):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.scroll_layout = QVBoxLayout()
|
||||
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.scroll_layout.setContentsMargins(HALF_PAD, HALF_PAD, HALF_PAD, HALF_PAD)
|
||||
self.scroll_layout.setSpacing(PAD)
|
||||
|
||||
scroll_container = QWidget()
|
||||
scroll_container.setObjectName("tile_scroll_container")
|
||||
scroll_container.setLayout(self.scroll_layout)
|
||||
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area.setObjectName("tile_scroll_area")
|
||||
self.scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
|
||||
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
|
||||
# NOTE: I would rather have this style applied to the scroll_area
|
||||
# background and NOT the scroll container background, so that the
|
||||
# rounded corners are maintained when scrolling. I was unable to
|
||||
# find the right trick to only select that particular element.
|
||||
self.scroll_area.setStyleSheet(inset_container_style("tile_scroll_container"))
|
||||
self.scroll_area.setWidget(scroll_container)
|
||||
|
||||
self.addWidget(self.scroll_area)
|
||||
@@ -0,0 +1,86 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from PIL import ImageQt
|
||||
from PySide6.QtCore import QSize, Qt
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, 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 HALF_PAD
|
||||
|
||||
# TODO: There should be a global button theme somewhere.
|
||||
_BUTTON_SIZE = 22
|
||||
_ICON_MARGIN = 4
|
||||
_ICON_SIZE = _BUTTON_SIZE - _ICON_MARGIN
|
||||
|
||||
|
||||
class TileView(QVBoxLayout):
|
||||
"""The layout used for a Tile widget."""
|
||||
|
||||
_rm = ResourceManager()
|
||||
copy_icon = auto_theme_overlay(_rm.copy, inverse=True)
|
||||
edit_icon = auto_theme_overlay(_rm.edit, inverse=True)
|
||||
trash_icon = auto_theme_overlay(_rm.trash, inverse=True)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.inner_layout = QVBoxLayout()
|
||||
self.inner_layout.setContentsMargins(HALF_PAD, 0, 0, HALF_PAD)
|
||||
self.inner_layout.setSpacing(0)
|
||||
self.field_container = QWidget()
|
||||
self.field_container.setLayout(self.inner_layout)
|
||||
self.addWidget(self.field_container)
|
||||
|
||||
self.title_container = QWidget()
|
||||
self.title_layout = QHBoxLayout(self.title_container)
|
||||
self.title_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.title_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.title_layout.setSpacing(0)
|
||||
self.inner_layout.addWidget(self.title_container)
|
||||
|
||||
self.title_widget = QLabel()
|
||||
self.title_widget.setMinimumHeight(_BUTTON_SIZE)
|
||||
self.title_widget.setWordWrap(True)
|
||||
self.title_layout.addWidget(self.title_widget)
|
||||
self.title_layout.addStretch(2)
|
||||
|
||||
self.copy_button = QPushButton()
|
||||
self.copy_button.setMinimumSize(_BUTTON_SIZE, _BUTTON_SIZE)
|
||||
self.copy_button.setMaximumSize(_BUTTON_SIZE, _BUTTON_SIZE)
|
||||
self.copy_button.setFlat(True)
|
||||
self.copy_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.copy_icon)))
|
||||
self.copy_button.setIconSize(QSize(_ICON_SIZE, _ICON_SIZE))
|
||||
self.copy_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.title_layout.addWidget(self.copy_button)
|
||||
self.copy_button.setHidden(True)
|
||||
|
||||
self.edit_button = QPushButton()
|
||||
self.edit_button.setMinimumSize(_BUTTON_SIZE, _BUTTON_SIZE)
|
||||
self.edit_button.setMaximumSize(_BUTTON_SIZE, _BUTTON_SIZE)
|
||||
self.edit_button.setFlat(True)
|
||||
self.edit_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.edit_icon)))
|
||||
self.edit_button.setIconSize(QSize(_ICON_SIZE, _ICON_SIZE))
|
||||
self.edit_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.title_layout.addWidget(self.edit_button)
|
||||
self.edit_button.setHidden(True)
|
||||
|
||||
self.remove_button = QPushButton()
|
||||
self.remove_button.setMinimumSize(_BUTTON_SIZE, _BUTTON_SIZE)
|
||||
self.remove_button.setMaximumSize(_BUTTON_SIZE, _BUTTON_SIZE)
|
||||
self.remove_button.setFlat(True)
|
||||
self.remove_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.trash_icon)))
|
||||
self.remove_button.setIconSize(QSize(_ICON_SIZE, _ICON_SIZE))
|
||||
self.remove_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.title_layout.addWidget(self.remove_button)
|
||||
self.remove_button.setHidden(True)
|
||||
|
||||
self.data = QWidget()
|
||||
self.data_layout = QHBoxLayout()
|
||||
self.data_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.data.setLayout(self.data_layout)
|
||||
self.inner_layout.addWidget(self.data)
|
||||
@@ -373,22 +373,12 @@
|
||||
"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.items": "{count} Items {time_span}",
|
||||
"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!",
|
||||
"status.results": "Results",
|
||||
"status.results_found": "{count} Results Found ({time_span})",
|
||||
"status.results_found": "{count} Results {time_span}",
|
||||
"status.results.invalid_syntax": "Invalid Search Syntax:",
|
||||
"tag_manager.title": "Library Tags",
|
||||
"tag.add": "Add Tag",
|
||||
|
||||
@@ -21,7 +21,7 @@ def test_update_selection_empty(qt_driver: QtDriver):
|
||||
panel.set_selection(qt_driver.selected)
|
||||
|
||||
# FieldContainer should hide all containers
|
||||
for container in panel.containers._containers:
|
||||
for container in panel.containers._tiles:
|
||||
assert container.isHidden()
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ def test_update_selection_single(qt_driver: QtDriver, entry_full: Entry):
|
||||
panel.set_selection(qt_driver.selected)
|
||||
|
||||
# FieldContainer should show all applicable tags and field containers
|
||||
for container in panel.containers._containers:
|
||||
for container in panel.containers._tiles:
|
||||
assert not container.isHidden()
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def test_update_selection_multiple(qt_driver: QtDriver):
|
||||
panel.set_selection(qt_driver.selected)
|
||||
|
||||
# FieldContainer should show mixed field editing
|
||||
for container in panel.containers._containers:
|
||||
for container in panel.containers._tiles:
|
||||
assert container.isHidden()
|
||||
|
||||
|
||||
@@ -137,8 +137,8 @@ def test_meta_tag_category(qt_driver: QtDriver, entry_full: Entry):
|
||||
panel.set_selection(qt_driver.selected)
|
||||
|
||||
# FieldContainer should hide all containers
|
||||
assert len(panel.containers._containers) == 3
|
||||
for i, container in enumerate(panel.containers._containers):
|
||||
assert len(panel.containers._tiles) == 3
|
||||
for i, container in enumerate(panel.containers._tiles):
|
||||
match i:
|
||||
case 0:
|
||||
# Check if the container is the Meta Tags category
|
||||
@@ -170,8 +170,8 @@ def test_custom_tag_category(qt_driver: QtDriver, entry_full: Entry):
|
||||
panel.set_selection(qt_driver.selected)
|
||||
|
||||
# FieldContainer should hide all containers
|
||||
assert len(panel.containers._containers) == 3
|
||||
for i, container in enumerate(panel.containers._containers):
|
||||
assert len(panel.containers._tiles) == 3
|
||||
for i, container in enumerate(panel.containers._tiles):
|
||||
match i:
|
||||
case 0:
|
||||
# Check if the container is the Meta Tags category
|
||||
@@ -206,5 +206,5 @@ def test_exclude_tag_category(
|
||||
qt_driver.toggle_item_selection(entry.id, append=False, bridge=False)
|
||||
panel.set_selection(qt_driver.selected)
|
||||
|
||||
assert len(panel.containers._containers) == 1
|
||||
assert panel.containers._containers[0].title == "<h4>Tags</h4>"
|
||||
assert len(panel.containers._tiles) == 1
|
||||
assert panel.containers._tiles[0].title == "<h4>Tags</h4>"
|
||||
Reference in New Issue
Block a user