From a2b9237be40ec827d08a556eb23dcd660f5cfc2f Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Sun, 9 Feb 2025 18:33:29 -0800 Subject: [PATCH] fix(ui): expand usage of esc for closing modals (#793) * fix(ui): expand usage of esc for closing modals * chore: remove log statements * refactor: use Qt enum in place of magic number * ui: use enter key to save panel widgets --- tagstudio/src/qt/modals/add_field.py | 28 ++++++++++--------- tagstudio/src/qt/modals/delete_unlinked.py | 15 ++++++++-- tagstudio/src/qt/modals/drop_import.py | 12 +++++++- tagstudio/src/qt/modals/fix_dupes.py | 15 ++++++++-- tagstudio/src/qt/modals/fix_unlinked.py | 15 ++++++++-- tagstudio/src/qt/modals/folders_to_tags.py | 16 +++++++++-- tagstudio/src/qt/modals/tag_search.py | 10 +++---- .../src/qt/widgets/paged_panel/paged_panel.py | 13 ++++++++- tagstudio/src/qt/widgets/panel.py | 20 ++++++++++--- .../qt/widgets/preview/field_containers.py | 3 +- 10 files changed, 109 insertions(+), 38 deletions(-) diff --git a/tagstudio/src/qt/modals/add_field.py b/tagstudio/src/qt/modals/add_field.py index bea2255b..34e4021b 100644 --- a/tagstudio/src/qt/modals/add_field.py +++ b/tagstudio/src/qt/modals/add_field.py @@ -1,8 +1,10 @@ -# 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 structlog +from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( QHBoxLayout, @@ -16,7 +18,10 @@ from PySide6.QtWidgets import ( from src.core.library import Library from src.qt.translations import Translations +logger = structlog.get_logger(__name__) + +# NOTE: This class doesn't inherit from PanelWidget? Seems like it predates that system? class AddFieldModal(QWidget): done = Signal(list) @@ -35,11 +40,7 @@ class AddFieldModal(QWidget): self.title_widget = QLabel() self.title_widget.setObjectName("fieldTitle") self.title_widget.setWordWrap(True) - self.title_widget.setStyleSheet( - # 'background:blue;' - # 'text-align:center;' - "font-weight:bold;" "font-size:14px;" "padding-top: 6px" "" - ) + self.title_widget.setStyleSheet("font-weight:bold;" "font-size:14px;" "padding-top: 6px;") Translations.translate_qobject(self.title_widget, "library.field.add") self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -50,18 +51,13 @@ class AddFieldModal(QWidget): self.button_layout.setContentsMargins(6, 6, 6, 6) self.button_layout.addStretch(1) - # self.cancel_button = QPushButton() - # self.cancel_button.setText('Cancel') - self.cancel_button = QPushButton() Translations.translate_qobject(self.cancel_button, "generic.cancel") self.cancel_button.clicked.connect(self.hide) - # self.cancel_button.clicked.connect(widget.reset) self.button_layout.addWidget(self.cancel_button) self.save_button = QPushButton() Translations.translate_qobject(self.save_button, "generic.add") - # self.save_button.setAutoDefault(True) self.save_button.setDefault(True) self.save_button.clicked.connect(self.hide) self.save_button.clicked.connect( @@ -74,8 +70,6 @@ class AddFieldModal(QWidget): self.root_layout.addWidget(self.title_widget) self.root_layout.addWidget(self.list_widget) - # self.root_layout.setStretch(1,2) - self.root_layout.addStretch(1) self.root_layout.addWidget(self.button_container) @@ -85,5 +79,13 @@ class AddFieldModal(QWidget): item = QListWidgetItem(f"{df.name} ({df.type.value})") item.setData(Qt.ItemDataRole.UserRole, df.key) self.list_widget.addItem(item) + self.list_widget.setFocus() super().show() + + def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 + if event.key() == QtCore.Qt.Key.Key_Escape: + self.cancel_button.click() + else: # Other key presses + pass + return super().keyPressEvent(event) diff --git a/tagstudio/src/qt/modals/delete_unlinked.py b/tagstudio/src/qt/modals/delete_unlinked.py index e3770c57..e427f30b 100644 --- a/tagstudio/src/qt/modals/delete_unlinked.py +++ b/tagstudio/src/qt/modals/delete_unlinked.py @@ -1,9 +1,10 @@ -# 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 typing +from typing import TYPE_CHECKING, override +from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt, QThreadPool, Signal from PySide6.QtGui import QStandardItem, QStandardItemModel from PySide6.QtWidgets import ( @@ -20,7 +21,7 @@ from src.qt.translations import Translations from src.qt.widgets.progress import ProgressWidget # Only import for type checking/autocompletion, will not be imported at runtime. -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -111,3 +112,11 @@ class DeleteUnlinkedEntriesModal(QWidget): self.done.emit(), ) ) + + @override + def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 + if event.key() == QtCore.Qt.Key.Key_Escape: + self.cancel_button.click() + else: # Other key presses + pass + return super().keyPressEvent(event) diff --git a/tagstudio/src/qt/modals/drop_import.py b/tagstudio/src/qt/modals/drop_import.py index 9b13b3d1..4354403f 100644 --- a/tagstudio/src/qt/modals/drop_import.py +++ b/tagstudio/src/qt/modals/drop_import.py @@ -1,12 +1,14 @@ +# Copyright (C) 2025 # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio import enum import shutil from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override import structlog +from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt, QUrl from PySide6.QtGui import QStandardItem, QStandardItemModel from PySide6.QtWidgets import ( @@ -232,3 +234,11 @@ class DropImportModal(QWidget): ) index += 1 return filepath.name + + @override + def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 + if event.key() == QtCore.Qt.Key.Key_Escape: + self.cancel_button.click() + else: # Other key presses + pass + return super().keyPressEvent(event) diff --git a/tagstudio/src/qt/modals/fix_dupes.py b/tagstudio/src/qt/modals/fix_dupes.py index 76261039..da504b6b 100644 --- a/tagstudio/src/qt/modals/fix_dupes.py +++ b/tagstudio/src/qt/modals/fix_dupes.py @@ -1,10 +1,11 @@ -# 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 typing +from typing import TYPE_CHECKING, override +from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QFileDialog, @@ -20,7 +21,7 @@ from src.qt.modals.mirror_entities import MirrorEntriesModal from src.qt.translations import Translations # Only import for type checking/autocompletion, will not be imported at runtime. -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -135,3 +136,11 @@ class FixDupeFilesModal(QWidget): self.dupe_count.setText( Translations.translate_formatted("file.duplicates.matches", count=count) ) + + @override + def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 + if event.key() == QtCore.Qt.Key.Key_Escape: + self.done_button.click() + else: # Other key presses + pass + return super().keyPressEvent(event) diff --git a/tagstudio/src/qt/modals/fix_unlinked.py b/tagstudio/src/qt/modals/fix_unlinked.py index 6a4b6ce1..e4eb5ed4 100644 --- a/tagstudio/src/qt/modals/fix_unlinked.py +++ b/tagstudio/src/qt/modals/fix_unlinked.py @@ -1,10 +1,11 @@ -# 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 typing +from typing import TYPE_CHECKING, override +from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from src.core.library import Library @@ -16,7 +17,7 @@ from src.qt.translations import Translations from src.qt.widgets.progress import ProgressWidget # Only import for type checking/autocompletion, will not be imported at runtime. -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -144,3 +145,11 @@ class FixUnlinkedEntriesModal(QWidget): "entries.unlinked.missing_count.some", count=self.missing_count ) ) + + @override + def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 + if event.key() == QtCore.Qt.Key.Key_Escape: + self.done_button.click() + else: # Other key presses + pass + return super().keyPressEvent(event) diff --git a/tagstudio/src/qt/modals/folders_to_tags.py b/tagstudio/src/qt/modals/folders_to_tags.py index 16ba54d2..d37941ae 100644 --- a/tagstudio/src/qt/modals/folders_to_tags.py +++ b/tagstudio/src/qt/modals/folders_to_tags.py @@ -4,10 +4,12 @@ import math -import typing +from collections.abc import Sequence from dataclasses import dataclass, field +from typing import TYPE_CHECKING, override import structlog +from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QFrame, @@ -25,7 +27,7 @@ from src.core.palette import ColorType, get_tag_color from src.qt.flowlayout import FlowLayout from src.qt.translations import Translations -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from src.qt.ts_qt import QtDriver logger = structlog.get_logger(__name__) @@ -104,7 +106,7 @@ def generate_preview_data(library: Library) -> BranchData: branch.dirs[tag.name] = BranchData(tag=tag) branch = branch.dirs[tag.name] - def _add_folders_to_tree(items: typing.Sequence[str]) -> BranchData: + def _add_folders_to_tree(items: Sequence[str]) -> BranchData: branch = tree for folder in items: if folder not in branch.dirs: @@ -245,6 +247,14 @@ class FoldersToTagsModal(QWidget): if isinstance(child, TreeItem): child.set_all_branches(hidden) + @override + def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 + if event.key() == QtCore.Qt.Key.Key_Escape: + self.close() + else: # Other key presses + pass + return super().keyPressEvent(event) + class TreeItem(QWidget): def __init__(self, data: BranchData, parent_tag: Tag | None = None): diff --git a/tagstudio/src/qt/modals/tag_search.py b/tagstudio/src/qt/modals/tag_search.py index d3021f98..af559500 100644 --- a/tagstudio/src/qt/modals/tag_search.py +++ b/tagstudio/src/qt/modals/tag_search.py @@ -1,10 +1,10 @@ -# 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 contextlib -import typing +from typing import TYPE_CHECKING, override from warnings import catch_warnings import src.qt.modals.build_tag as build_tag @@ -36,7 +36,7 @@ from src.qt.widgets.tag import ( logger = structlog.get_logger(__name__) # Only import for type checking/autocompletion, will not be imported at runtime. -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from src.qt.modals.build_tag import BuildTagPanel @@ -337,16 +337,16 @@ class TagSearchPanel(PanelWidget): self.search_field.setFocus() return super().showEvent(event) + @override def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 # When Escape is pressed, focus back on the search box. # If focus is already on the search box, close the modal. if event.key() == QtCore.Qt.Key.Key_Escape: if self.search_field.hasFocus(): - self.parentWidget().hide() + return super().keyPressEvent(event) else: self.search_field.setFocus() self.search_field.selectAll() - return super().keyPressEvent(event) def remove_tag(self, tag: Tag): pass diff --git a/tagstudio/src/qt/widgets/paged_panel/paged_panel.py b/tagstudio/src/qt/widgets/paged_panel/paged_panel.py index de84d078..3cdc09f5 100644 --- a/tagstudio/src/qt/widgets/paged_panel/paged_panel.py +++ b/tagstudio/src/qt/widgets/paged_panel/paged_panel.py @@ -1,8 +1,11 @@ -# 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 +from typing import override + import structlog +from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QHBoxLayout, @@ -110,3 +113,11 @@ class PagedPanel(QWidget): item.setHidden(False) elif isinstance(item, int): self.button_nav_layout.addStretch(item) + + @override + def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 + if event.key() == QtCore.Qt.Key.Key_Escape: + self.close() + else: # Other key presses + pass + return super().keyPressEvent(event) diff --git a/tagstudio/src/qt/widgets/panel.py b/tagstudio/src/qt/widgets/panel.py index 141849bd..226ab3f8 100755 --- a/tagstudio/src/qt/widgets/panel.py +++ b/tagstudio/src/qt/widgets/panel.py @@ -1,14 +1,16 @@ -# 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 logging from typing import Callable +import structlog from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from src.qt.translations import Translations +logger = structlog.get_logger(__name__) + class PanelModal(QWidget): saved = Signal() @@ -96,7 +98,10 @@ class PanelModal(QWidget): widget.parent_post_init() def closeEvent(self, event): # noqa: N802 - self.done_button.click() + if self.cancel_button: + self.cancel_button.click() + elif self.done_button: + self.done_button.click() event.accept() def setTitle(self, title: str): # noqa: N802 @@ -125,12 +130,19 @@ class PanelWidget(QWidget): pass def add_callback(self, callback: Callable, event: str = "returnPressed"): - logging.warning(f"add_callback not implemented for {self.__class__.__name__}") + logger.warning(f"[PanelModal] add_callback not implemented for {self.__class__.__name__}") def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 if event.key() == QtCore.Qt.Key.Key_Escape: if self.panel_cancel_button: self.panel_cancel_button.click() + elif self.panel_done_button: + self.panel_done_button.click() + elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter: + if self.panel_save_button: + self.panel_save_button.click() + elif self.panel_done_button: + self.panel_done_button.click() else: # Other key presses pass return super().keyPressEvent(event) diff --git a/tagstudio/src/qt/widgets/preview/field_containers.py b/tagstudio/src/qt/widgets/preview/field_containers.py index 5c1cde83..5cc76bd5 100644 --- a/tagstudio/src/qt/widgets/preview/field_containers.py +++ b/tagstudio/src/qt/widgets/preview/field_containers.py @@ -500,10 +500,9 @@ class FieldContainers(QWidget): Translations["generic.cancel_alt"], QMessageBox.ButtonRole.DestructiveRole ) remove_mb.addButton("&Remove", QMessageBox.ButtonRole.RejectRole) - remove_mb.setDefaultButton(cancel_button) remove_mb.setEscapeButton(cancel_button) result = remove_mb.exec_() - if result == 3: # TODO - what is this magic number? + if result == QMessageBox.ButtonRole.ActionRole.value: callback() def emit_badge_signals(self, tag_ids: list[int] | set[int], emit_on_absent: bool = True):