diff --git a/tagstudio/src/core/enums.py b/tagstudio/src/core/enums.py index d4a9aa3d9..416e8c645 100644 --- a/tagstudio/src/core/enums.py +++ b/tagstudio/src/core/enums.py @@ -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" diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index a16f89107..80160cfff 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -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, @@ -264,15 +256,12 @@ 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 ============================================================ - # file_menu.addAction(QAction('&New Library', menu_bar)) - # file_menu.addAction(QAction('&Open Library', menu_bar)) - open_library_action = QAction("&Open/Create Library", menu_bar) open_library_action.triggered.connect(lambda: self.open_library_from_dialog()) open_library_action.setShortcut( @@ -302,8 +291,6 @@ def start(self) -> None: file_menu.addSeparator() - # refresh_lib_action = QAction('&Refresh Directories', self.main_window) - # refresh_lib_action.triggered.connect(lambda: self.lib.refresh_dir()) add_new_files_action = QAction("&Refresh Directories", menu_bar) add_new_files_action.triggered.connect( lambda: self.callback_library_needed_check(self.add_new_files_callback) @@ -315,13 +302,23 @@ def start(self) -> None: ) ) add_new_files_action.setStatusTip("Ctrl+R") - # file_menu.addAction(refresh_lib_action) file_menu.addAction(add_new_files_action) file_menu.addSeparator() 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) # Edit Menu ============================================================ new_tag_action = QAction("New &Tag", menu_bar) @@ -364,15 +361,32 @@ 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( - bool(self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool)) + # View Menu ============================================================ + 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)) ) - check_action.triggered.connect( - lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked) + show_libs_list_action.triggered.connect( + lambda checked: ( + self.settings.setValue(SettingItems.WINDOW_SHOW_LIBS, checked), + self.toggle_libs_list(checked), + ) ) - window_menu.addAction(check_action) + 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.show_grid_filenames(checked), + ) + ) + view_menu.addAction(show_filenames_action) # Tools Menu =========================================================== def create_fix_unlinked_entries_modal(): @@ -407,19 +421,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) @@ -429,7 +430,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") @@ -439,9 +440,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) @@ -551,6 +552,10 @@ def toggle_libs_list(self, value: bool): self.preview_panel.libs_flow_container.hide() self.preview_panel.update() + def show_grid_filenames(self, value: bool): + for thumb in self.item_thumbs: + thumb.set_filename_visibility(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: @@ -829,9 +834,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.set_filename_visibility(it.show_filename_label) self.flow_container.layout().setSpacing( min(self.thumb_size // spacing_divisor, min_spacing) ) @@ -879,7 +884,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) diff --git a/tagstudio/src/qt/widgets/item_thumb.py b/tagstudio/src/qt/widgets/item_thumb.py index ab8b44efb..9935fe4e7 100644 --- a/tagstudio/src/qt/widgets/item_thumb.py +++ b/tagstudio/src/qt/widgets/item_thumb.py @@ -110,6 +110,8 @@ class ItemThumb(FlowWidget): "padding-left: 1px;" ) + filename_style = "font-size:10px;" + def __init__( self, mode: ItemType, @@ -117,6 +119,7 @@ def __init__( driver: "QtDriver", thumb_size: tuple[int, int], grid_idx: int, + show_filename_label: bool = False, ): super().__init__() self.grid_idx = grid_idx @@ -125,10 +128,24 @@ 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 @@ -136,6 +153,8 @@ def __init__( # | | # |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 ============================================================ @@ -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) # +----------+ # |[~~~~~~~~]| @@ -160,7 +179,7 @@ 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) # +----------+ # |[~~~~~~~~]| @@ -168,7 +187,7 @@ def __init__( # | | | # | v | # +----------+ - self.base_layout.addStretch(2) + self.thumb_layout.addStretch(2) # +----------+ # |[~~~~~~~~]| @@ -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_text(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("") @@ -285,6 +305,16 @@ 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) + if not show_filename_label: + self.file_label.setHidden(True) + + self.base_layout.addWidget(self.thumb_container) + self.base_layout.addWidget(self.file_label) + self.set_mode(mode) @property @@ -298,11 +328,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) @@ -312,7 +342,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) @@ -321,7 +351,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) @@ -366,14 +396,40 @@ def set_count(self, count: str) -> None: self.ext_badge.setHidden(True) self.count_badge.setHidden(True) + def set_filename_text(self, filename: Path | str | None): + self.file_label.setText(str(filename)) + + def set_filename_visibility(self, set_visible: bool): + """Toggle the visibility of the filename label. + + Args: + set_visible (bool): Show the filename, true or false. + """ + if set_visible: + if self.file_label.isHidden(): + 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 = set_visible + 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) diff --git a/tagstudio/src/qt/widgets/thumb_renderer.py b/tagstudio/src/qt/widgets/thumb_renderer.py index ac0abb962..b17f519ad 100644 --- a/tagstudio/src/qt/widgets/thumb_renderer.py +++ b/tagstudio/src/qt/widgets/thumb_renderer.py @@ -72,7 +72,7 @@ class ThumbRenderer(QObject): """A class for rendering image and file thumbnails.""" rm: ResourceManager = ResourceManager() - updated = Signal(float, QPixmap, QSize, str) + updated = Signal(float, QPixmap, QSize, str, str) updated_ratio = Signal(float) def __init__(self) -> None: @@ -1208,8 +1208,15 @@ def render_unlinked() -> Image.Image: math.ceil(adj_size / pixel_ratio), math.ceil(final.size[1] / pixel_ratio), ), + str(_filepath.name), _filepath.suffix.lower(), ) else: - self.updated.emit(timestamp, QPixmap(), QSize(*base_size), _filepath.suffix.lower()) + self.updated.emit( + timestamp, + QPixmap(), + QSize(*base_size), + str(_filepath.name), + _filepath.suffix.lower(), + )