Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): show filenames in thumbnail grid (Closes #85) #633

Merged
merged 10 commits into from
Dec 20, 2024
1 change: 1 addition & 0 deletions tagstudio/src/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class SettingItems(str, enum.Enum):
LAST_LIBRARY = "last_library"
LIBS_LIST = "libs_list"
WINDOW_SHOW_LIBS = "window_show_libs"
SHOW_FILENAMES = "show_filenames"
AUTOPLAY = "autoplay_videos"


Expand Down
94 changes: 61 additions & 33 deletions tagstudio/src/qt/ts_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,7 @@
import structlog
from humanfriendly import format_timespan
from PySide6 import QtCore
from PySide6.QtCore import (
QObject,
QSettings,
Qt,
QThread,
QThreadPool,
QTimer,
Signal,
)
from PySide6.QtCore import QObject, QSettings, Qt, QThread, QThreadPool, QTimer, Signal
from PySide6.QtGui import (
QAction,
QColor,
Expand Down Expand Up @@ -264,9 +256,9 @@ def start(self) -> None:

file_menu = QMenu("&File", menu_bar)
edit_menu = QMenu("&Edit", menu_bar)
view_menu = QMenu("&View", menu_bar)
tools_menu = QMenu("&Tools", menu_bar)
macros_menu = QMenu("&Macros", menu_bar)
window_menu = QMenu("&Window", menu_bar)
help_menu = QMenu("&Help", menu_bar)

# File Menu ============================================================
Expand Down Expand Up @@ -322,6 +314,17 @@ def start(self) -> None:
close_library_action = QAction("&Close Library", menu_bar)
close_library_action.triggered.connect(self.close_library)
file_menu.addAction(close_library_action)
file_menu.addSeparator()

open_on_start_action = QAction("Open Library on Start", self)
open_on_start_action.setCheckable(True)
open_on_start_action.setChecked(
bool(self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool))
)
open_on_start_action.triggered.connect(
lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked)
)
file_menu.addAction(open_on_start_action)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Open Library on Start" option exists twice, once in View and once in File, and (un)checking one of them doesn't update the visual state of the other


# Edit Menu ============================================================
new_tag_action = QAction("New &Tag", menu_bar)
Expand Down Expand Up @@ -364,15 +367,42 @@ def start(self) -> None:
tag_database_action.triggered.connect(lambda: self.show_tag_database())
edit_menu.addAction(tag_database_action)

check_action = QAction("Open library on start", self)
check_action.setCheckable(True)
check_action.setChecked(
# View Menu ============================================================
open_on_start_action = QAction("Open Library on Start", self)
open_on_start_action.setCheckable(True)
open_on_start_action.setChecked(
bool(self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool))
)
check_action.triggered.connect(
open_on_start_action.triggered.connect(
lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked)
)
window_menu.addAction(check_action)
view_menu.addAction(open_on_start_action)

show_libs_list_action = QAction("Show Recent Libraries", menu_bar)
show_libs_list_action.setCheckable(True)
show_libs_list_action.setChecked(
bool(self.settings.value(SettingItems.WINDOW_SHOW_LIBS, defaultValue=True, type=bool))
)
show_libs_list_action.triggered.connect(
lambda checked: (
self.settings.setValue(SettingItems.WINDOW_SHOW_LIBS, checked),
self.toggle_libs_list(checked),
)
)
view_menu.addAction(show_libs_list_action)

show_filenames_action = QAction("Show Filenames in Grid", menu_bar)
show_filenames_action.setCheckable(True)
show_filenames_action.setChecked(
bool(self.settings.value(SettingItems.SHOW_FILENAMES, defaultValue=True, type=bool))
)
show_filenames_action.triggered.connect(
lambda checked: (
self.settings.setValue(SettingItems.SHOW_FILENAMES, checked),
self.toggle_grid_filenames(checked),
)
)
view_menu.addAction(show_filenames_action)

# Tools Menu ===========================================================
def create_fix_unlinked_entries_modal():
Expand Down Expand Up @@ -407,19 +437,6 @@ def create_dupe_files_modal():
)
macros_menu.addAction(self.autofill_action)

show_libs_list_action = QAction("Show Recent Libraries", menu_bar)
show_libs_list_action.setCheckable(True)
show_libs_list_action.setChecked(
bool(self.settings.value(SettingItems.WINDOW_SHOW_LIBS, defaultValue=True, type=bool))
)
show_libs_list_action.triggered.connect(
lambda checked: (
self.settings.setValue(SettingItems.WINDOW_SHOW_LIBS, checked),
self.toggle_libs_list(checked),
)
)
window_menu.addAction(show_libs_list_action)

def create_folders_tags_modal():
if not hasattr(self, "folders_modal"):
self.folders_modal = FoldersToTagsModal(self.lib, self)
Expand All @@ -429,7 +446,7 @@ def create_folders_tags_modal():
folders_to_tags_action.triggered.connect(create_folders_tags_modal)
macros_menu.addAction(folders_to_tags_action)

# Help Menu ==========================================================
# Help Menu ============================================================
self.repo_action = QAction("Visit GitHub Repository", menu_bar)
self.repo_action.triggered.connect(
lambda: webbrowser.open("https://github.com/TagStudioDev/TagStudio")
Expand All @@ -439,9 +456,9 @@ def create_folders_tags_modal():

menu_bar.addMenu(file_menu)
menu_bar.addMenu(edit_menu)
menu_bar.addMenu(view_menu)
menu_bar.addMenu(tools_menu)
menu_bar.addMenu(macros_menu)
menu_bar.addMenu(window_menu)
menu_bar.addMenu(help_menu)

self.main_window.searchField.textChanged.connect(self.update_completions_list)
Expand Down Expand Up @@ -551,6 +568,10 @@ def toggle_libs_list(self, value: bool):
self.preview_panel.libs_flow_container.hide()
self.preview_panel.update()

def toggle_grid_filenames(self, value: bool):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't "toggle" it "sets"; it should probably not be called toggle_grid_filenames

for thumb in self.item_thumbs:
thumb.toggle_filename(value)

def callback_library_needed_check(self, func):
"""Check if loaded library has valid path before executing the button function."""
if self.lib.library_dir:
Expand Down Expand Up @@ -829,9 +850,9 @@ def thumb_size_callback(self, index: int):
it.thumb_button.setIcon(blank_icon)
it.resize(self.thumb_size, self.thumb_size)
it.thumb_size = (self.thumb_size, self.thumb_size)
it.setMinimumSize(self.thumb_size, self.thumb_size)
it.setMaximumSize(self.thumb_size, self.thumb_size)
it.setFixedSize(self.thumb_size, self.thumb_size)
it.thumb_button.thumb_size = (self.thumb_size, self.thumb_size)
it.toggle_filename(it.show_filename_label)
self.flow_container.layout().setSpacing(
min(self.thumb_size // spacing_divisor, min_spacing)
)
Expand Down Expand Up @@ -879,7 +900,14 @@ def _init_thumb_grid(self):
# TODO - init after library is loaded, it can have different page_size
for grid_idx in range(self.filter.page_size):
item_thumb = ItemThumb(
None, self.lib, self, (self.thumb_size, self.thumb_size), grid_idx
None,
self.lib,
self,
(self.thumb_size, self.thumb_size),
grid_idx,
bool(
self.settings.value(SettingItems.SHOW_FILENAMES, defaultValue=True, type=bool)
),
)

layout.addWidget(item_thumb)
Expand Down
84 changes: 69 additions & 15 deletions tagstudio/src/qt/widgets/item_thumb.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,16 @@ class ItemThumb(FlowWidget):
"padding-left: 1px;"
)

filename_style = "font-size:10px;"

def __init__(
self,
mode: ItemType,
library: Library,
driver: "QtDriver",
thumb_size: tuple[int, int],
grid_idx: int,
show_filename_label: bool = False,
):
super().__init__()
self.grid_idx = grid_idx
Expand All @@ -125,17 +128,33 @@ def __init__(
self.driver = driver
self.item_id: int | None = None
self.thumb_size: tuple[int, int] = thumb_size
self.show_filename_label: bool = show_filename_label
self.label_height = 12
self.label_spacing = 4
self.setMinimumSize(*thumb_size)
self.setMaximumSize(*thumb_size)
self.setMouseTracking(True)
check_size = 24
self.setFixedSize(
thumb_size[0],
thumb_size[1]
+ ((self.label_height + self.label_spacing) if show_filename_label else 0),
)

self.thumb_container = QWidget()
self.base_layout = QVBoxLayout(self)
self.base_layout.setContentsMargins(0, 0, 0, 0)
self.base_layout.setSpacing(0)
self.setLayout(self.base_layout)

# +----------+
# | ARC FAV| Top Right: Favorite & Archived Badges
# | |
# | |
# |EXT #| Lower Left: File Type, Tag Group Icon, or Collation Icon
# +----------+ Lower Right: Collation Count, Video Length, or Word Count
#
# Filename Underneath: (Optional) Filename

# Thumbnail ============================================================

Expand All @@ -145,9 +164,9 @@ def __init__(
# || ||
# |*--------*|
# +----------+
self.base_layout = QVBoxLayout(self)
self.base_layout.setObjectName("baseLayout")
self.base_layout.setContentsMargins(0, 0, 0, 0)
self.thumb_layout = QVBoxLayout(self.thumb_container)
self.thumb_layout.setObjectName("baseLayout")
self.thumb_layout.setContentsMargins(0, 0, 0, 0)

# +----------+
# |[~~~~~~~~]|
Expand All @@ -160,15 +179,15 @@ def __init__(
self.top_layout.setContentsMargins(6, 6, 6, 6)
self.top_container = QWidget()
self.top_container.setLayout(self.top_layout)
self.base_layout.addWidget(self.top_container)
self.thumb_layout.addWidget(self.top_container)

# +----------+
# |[~~~~~~~~]|
# | ^ |
# | | |
# | v |
# +----------+
self.base_layout.addStretch(2)
self.thumb_layout.addStretch(2)

# +----------+
# |[~~~~~~~~]|
Expand All @@ -181,19 +200,20 @@ def __init__(
self.bottom_layout.setContentsMargins(6, 6, 6, 6)
self.bottom_container = QWidget()
self.bottom_container.setLayout(self.bottom_layout)
self.base_layout.addWidget(self.bottom_container)
self.thumb_layout.addWidget(self.bottom_container)

self.thumb_button = ThumbButton(self, thumb_size)
self.thumb_button = ThumbButton(self.thumb_container, thumb_size)
self.renderer = ThumbRenderer()
self.renderer.updated.connect(
lambda ts, i, s, ext: (
lambda ts, i, s, fn, ext: (
self.update_thumb(ts, image=i),
self.update_size(ts, size=s),
self.set_filename(fn),
self.set_extension(ext),
)
)
self.thumb_button.setFlat(True)
self.thumb_button.setLayout(self.base_layout)
self.thumb_button.setLayout(self.thumb_layout)
self.thumb_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)

self.opener = FileOpenerHelper("")
Expand Down Expand Up @@ -285,6 +305,15 @@ def __init__(
self.badges[badge_type] = badge
self.cb_layout.addWidget(badge)

# Filename Label =======================================================
self.file_label = QLabel(text="Filename")
self.file_label.setStyleSheet(ItemThumb.filename_style)
self.file_label.setMaximumHeight(self.label_height)
self.file_label.setHidden(not show_filename_label)

self.base_layout.addWidget(self.thumb_container)
self.base_layout.addWidget(self.file_label)

self.set_mode(mode)

@property
Expand All @@ -298,11 +327,11 @@ def is_archived(self):
def set_mode(self, mode: ItemType | None) -> None:
if mode is None:
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=True)
self.unsetCursor()
self.thumb_button.unsetCursor()
self.thumb_button.setHidden(True)
elif mode == ItemType.ENTRY and self.mode != ItemType.ENTRY:
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=False)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_button.setHidden(False)
self.cb_container.setHidden(False)
# Count Badge depends on file extension (video length, word count)
Expand All @@ -312,7 +341,7 @@ def set_mode(self, mode: ItemType | None) -> None:
self.ext_badge.setHidden(True)
elif mode == ItemType.COLLATION and self.mode != ItemType.COLLATION:
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=False)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_button.setHidden(False)
self.cb_container.setHidden(True)
self.ext_badge.setHidden(True)
Expand All @@ -321,7 +350,7 @@ def set_mode(self, mode: ItemType | None) -> None:
self.item_type_badge.setHidden(False)
elif mode == ItemType.TAG_GROUP and self.mode != ItemType.TAG_GROUP:
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=False)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.thumb_button.setHidden(False)
self.ext_badge.setHidden(True)
self.count_badge.setHidden(False)
Expand Down Expand Up @@ -366,14 +395,39 @@ def set_count(self, count: str) -> None:
self.ext_badge.setHidden(True)
self.count_badge.setHidden(True)

def set_filename(self, filename: Path | str | None):
self.file_label.setText(str(filename))

def toggle_filename(self, value: bool):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here as with the other method; Doesn't toggle and should therefore probably not be called such

"""Toggle the visibility of the filename label.

Args:
value (bool): Show the filename, true or false.
"""
if value:
self.file_label.setHidden(False)
self.setFixedHeight(self.thumb_size[1] + self.label_height + self.label_spacing)
else:
self.file_label.setHidden(True)
self.setFixedHeight(self.thumb_size[1])
self.show_filename_label = value

def update_thumb(self, timestamp: float, image: QPixmap | None = None):
"""Update attributes of a thumbnail element."""
if timestamp > ItemThumb.update_cutoff:
self.thumb_button.setIcon(image if image else QPixmap())

def update_size(self, timestamp: float, size: QSize):
"""Updates attributes of a thumbnail element."""
if timestamp > ItemThumb.update_cutoff and self.thumb_button.iconSize != size:
"""Updates attributes of a thumbnail element.

Args:
timestamp (float | None): The UTC timestamp for when this call was
originally dispatched. Used to skip outdated jobs.

size (QSize): The new thumbnail size to set.
"""
if timestamp > ItemThumb.update_cutoff:
self.thumb_size = size.toTuple() # type: ignore
self.thumb_button.setIconSize(size)
self.thumb_button.setMinimumSize(size)
self.thumb_button.setMaximumSize(size)
Expand Down
Loading
Loading