diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 33ba48e16..407985c1e 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -17,6 +17,7 @@ import sys import time from pathlib import Path +from platform import system from queue import Queue from warnings import catch_warnings @@ -97,6 +98,7 @@ from tagstudio.qt.widgets.panel import PanelModal from tagstudio.qt.widgets.preview_panel import PreviewPanel from tagstudio.qt.widgets.progress import ProgressWidget +from tagstudio.qt.widgets.thumb_button import ThumbButton from tagstudio.qt.widgets.thumb_renderer import ThumbRenderer BADGE_TAGS = { @@ -406,6 +408,21 @@ def start(self) -> None: file_menu.addAction(self.refresh_dir_action) file_menu.addSeparator() + self.open_selected_action = QAction(Translations["file.open_files.title.plural"], self) + self.open_selected_action.triggered.connect(self.open_selected_files) + if system() == "Darwin": + shortcut: QtCore.QKeyCombination | Qt.Key = QtCore.QKeyCombination( + QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), + QtCore.Qt.Key.Key_Down, + ) + else: + shortcut = Qt.Key.Key_Return + + self.open_selected_action.setShortcut(shortcut) + self.open_selected_action.setEnabled(False) + file_menu.addAction(self.open_selected_action) + file_menu.addSeparator() + self.close_library_action = QAction(Translations["menu.file.close_library"], menu_bar) self.close_library_action.triggered.connect(self.close_library) self.close_library_action.setEnabled(False) @@ -1426,6 +1443,40 @@ def toggle_item_selection(self, item_id: int, append: bool, bridge: bool): self.preview_panel.update_widgets() + def open_selected_files(self): + if not ( + QApplication.focusWidget() == self.main_window.scrollArea + or isinstance(QApplication.focusWidget(), ThumbButton) + ): + return + count = len(self.selected) + result = QMessageBox.ButtonRole.ActionRole + + if count >= 5: # Only confirm if we have lots of files + confirm_open = QMessageBox() + confirm_open.setText(Translations.format("file.open_files.warning", count=count)) + confirm_open.setWindowTitle(Translations["file.open_files.title"]) + confirm_open.setIcon(QMessageBox.Icon.Question) + + cancel_button = confirm_open.addButton( + Translations["generic.cancel_alt"], QMessageBox.ButtonRole.RejectRole + ) + confirm_open.setEscapeButton(cancel_button) + + open_button = confirm_open.addButton( + Translations["generic.open"], QMessageBox.ButtonRole.ActionRole + ) + confirm_open.setDefaultButton(open_button) + + result = QMessageBox.ButtonRole(confirm_open.exec()) + + if result == QMessageBox.ButtonRole.ActionRole: + opened = [] + for it in self.item_thumbs: + if it.item_id in self.selected and it.item_id not in opened: + it.opener.open_file() + opened.append(it.item_id) + def set_macro_menu_viability(self): # self.autofill_action.setDisabled(not self.selected) pass @@ -1453,11 +1504,20 @@ def set_select_actions_visibility(self): self.add_tag_to_selected_action.setEnabled(True) self.clear_select_action.setEnabled(True) self.delete_file_action.setEnabled(True) + + self.open_selected_action.setEnabled(True) + if len(self.selected) == 1: + self.open_selected_action.setText(Translations["file.open_files.title.singular"]) + else: + self.open_selected_action.setText(Translations["file.open_files.title.plural"]) else: self.add_tag_to_selected_action.setEnabled(False) self.clear_select_action.setEnabled(False) self.delete_file_action.setEnabled(False) + self.open_selected_action.setEnabled(False) + self.open_selected_action.setText(Translations["file.open_files.title.plural"]) + def update_completions_list(self, text: str) -> None: matches = re.search( r"((?:.* )?)(mediatype|filetype|path|tag|tag_id):(\"?[A-Za-z0-9\ \t]+\"?)?", text diff --git a/src/tagstudio/qt/widgets/item_thumb.py b/src/tagstudio/qt/widgets/item_thumb.py index 8b4f15d27..0c055fb44 100644 --- a/src/tagstudio/qt/widgets/item_thumb.py +++ b/src/tagstudio/qt/widgets/item_thumb.py @@ -230,6 +230,7 @@ def __init__( self.thumb_button.addAction(open_file_action) self.thumb_button.addAction(open_explorer_action) self.thumb_button.addAction(self.delete_action) + self.thumb_button.double_clicked.connect(self.opener.open_file) # Static Badges ======================================================== diff --git a/src/tagstudio/qt/widgets/thumb_button.py b/src/tagstudio/qt/widgets/thumb_button.py index 51c17ad69..d725b79fa 100644 --- a/src/tagstudio/qt/widgets/thumb_button.py +++ b/src/tagstudio/qt/widgets/thumb_button.py @@ -1,32 +1,35 @@ -# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Copyright (C) 2025 Travis Abendshien (CyanVoxel). # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio import sys +from typing import override from PySide6 import QtCore -from PySide6.QtCore import QEvent +from PySide6.QtCore import QEvent, Signal from PySide6.QtGui import ( QColor, QEnterEvent, + QMouseEvent, QPainter, QPainterPath, QPaintEvent, QPalette, QPen, ) -from PySide6.QtWidgets import QWidget +from PySide6.QtWidgets import QPushButton, QWidget -from tagstudio.qt.helpers.qbutton_wrapper import QPushButtonWrapper +class ThumbButton(QPushButton): + double_clicked = Signal() -class ThumbButton(QPushButtonWrapper): def __init__(self, parent: QWidget, thumb_size: tuple[int, int]) -> None: # noqa: N802 super().__init__(parent) self.thumb_size: tuple[int, int] = thumb_size self.hovered = False self.selected = False + self.double_click = False # NOTE: As of PySide 6.8.0.1, the QPalette.ColorRole.Accent role no longer works on Windows. # The QPalette.ColorRole.AlternateBase does for some reason, but not on macOS. @@ -85,8 +88,9 @@ def __init__(self, parent: QWidget, thumb_size: tuple[int, int]) -> None: # noq self.hover_color.alpha(), ) - def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 - super().paintEvent(event) + @override + def paintEvent(self, arg__1: QPaintEvent) -> None: # noqa: N802 + super().paintEvent(arg__1) if self.hovered or self.selected: painter = QPainter() painter.begin(self) @@ -125,11 +129,13 @@ def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 painter.end() + @override def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802 self.hovered = True self.repaint() return super().enterEvent(event) + @override def leaveEvent(self, event: QEvent) -> None: # noqa: N802 self.hovered = False self.repaint() @@ -138,3 +144,18 @@ def leaveEvent(self, event: QEvent) -> None: # noqa: N802 def set_selected(self, value: bool) -> None: # noqa: N802 self.selected = value self.repaint() + + @override + def mousePressEvent(self, e: QMouseEvent) -> None: # noqa: N802 + self.double_click = False + + @override + def mouseDoubleClickEvent(self, event: QMouseEvent) -> None: # noqa: N802 + self.double_click = True + + @override + def mouseReleaseEvent(self, e: QMouseEvent) -> None: # noqa: N802 + if self.double_click: + self.double_clicked.emit() + else: + self.clicked.emit() diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index c85f18779..b14aa88f8 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -84,6 +84,9 @@ "file.not_found": "File Not Found", "file.open_file_with": "Open file with", "file.open_file": "Open file", + "file.open_files.title.plural": "Open Selected Files", + "file.open_files.title.singular": "Open Selected File", + "file.open_files.warning": "Are you sure you want to open {count} files?", "file.open_location.generic": "Show file in file explorer", "file.open_location.mac": "Reveal in Finder", "file.open_location.windows": "Show in File Explorer", @@ -112,6 +115,7 @@ "generic.navigation.back": "Back", "generic.navigation.next": "Next", "generic.none": "None", + "generic.open": "Open", "generic.overwrite_alt": "&Overwrite", "generic.overwrite": "Overwrite", "generic.paste": "Paste",