From c100babd9fc80829703d4d2da2985e873e136b02 Mon Sep 17 00:00:00 2001 From: Jann Stute <46534683+Computerdores@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:45:30 +0200 Subject: [PATCH] feat(ui): option to change tag primary click behavior (#945) * feat: add settings field * feat: change click behaviour based on settings value * fix: make ignore comment pyright specific to shut up mypy * fix: add german and english translations for new strings * fix: settings dropdowns were always english not matter the selected language --- src/tagstudio/core/enums.py | 9 ++++ src/tagstudio/core/global_settings.py | 3 +- src/tagstudio/core/library/alchemy/enums.py | 3 ++ src/tagstudio/qt/modals/settings_panel.py | 57 +++++++++++++++----- src/tagstudio/qt/widgets/tag.py | 18 +++++-- src/tagstudio/qt/widgets/tag_box.py | 27 +++++++++- src/tagstudio/resources/translations/de.json | 4 ++ src/tagstudio/resources/translations/en.json | 4 ++ 8 files changed, 106 insertions(+), 19 deletions(-) diff --git a/src/tagstudio/core/enums.py b/src/tagstudio/core/enums.py index 027a7872..467dab7f 100644 --- a/src/tagstudio/core/enums.py +++ b/src/tagstudio/core/enums.py @@ -24,6 +24,15 @@ class ShowFilepathOption(int, enum.Enum): DEFAULT = SHOW_RELATIVE_PATHS +class TagClickActionOption(int, enum.Enum): + """Values representing the options for the "tag_click_action" setting.""" + + OPEN_EDIT = 0 + SET_SEARCH = 1 + ADD_TO_SEARCH = 2 + DEFAULT = OPEN_EDIT + + class Theme(str, enum.Enum): COLOR_BG_DARK = "#65000000" COLOR_BG_LIGHT = "#22000000" diff --git a/src/tagstudio/core/global_settings.py b/src/tagstudio/core/global_settings.py index 56cb4a6a..5320e61e 100644 --- a/src/tagstudio/core/global_settings.py +++ b/src/tagstudio/core/global_settings.py @@ -11,7 +11,7 @@ import structlog import toml from pydantic import BaseModel, Field -from tagstudio.core.enums import ShowFilepathOption +from tagstudio.core.enums import ShowFilepathOption, TagClickActionOption if platform.system() == "Windows": DEFAULT_GLOBAL_SETTINGS_PATH = ( @@ -50,6 +50,7 @@ class GlobalSettings(BaseModel): page_size: int = Field(default=100) show_filepath: ShowFilepathOption = Field(default=ShowFilepathOption.DEFAULT) theme: Theme = Field(default=Theme.SYSTEM) + tag_click_action: TagClickActionOption = Field(default=TagClickActionOption.DEFAULT) date_format: str = Field(default="%x") hour_format: bool = Field(default=True) diff --git a/src/tagstudio/core/library/alchemy/enums.py b/src/tagstudio/core/library/alchemy/enums.py index 980ddd2d..f1d3106c 100644 --- a/src/tagstudio/core/library/alchemy/enums.py +++ b/src/tagstudio/core/library/alchemy/enums.py @@ -125,6 +125,9 @@ class BrowsingState: def with_sorting_direction(self, ascending: bool) -> "BrowsingState": return replace(self, ascending=ascending) + def with_search_query(self, search_query: str) -> "BrowsingState": + return replace(self, query=search_query) + class FieldTypeEnum(enum.Enum): TEXT_LINE = "Text Line" diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index 9202813d..9c040c47 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -17,7 +17,7 @@ from PySide6.QtWidgets import ( QWidget, ) -from tagstudio.core.enums import ShowFilepathOption +from tagstudio.core.enums import ShowFilepathOption, TagClickActionOption from tagstudio.core.global_settings import Theme from tagstudio.qt.translations import DEFAULT_TRANSLATION, LANGUAGES, Translations from tagstudio.qt.widgets.panel import PanelModal, PanelWidget @@ -25,17 +25,11 @@ from tagstudio.qt.widgets.panel import PanelModal, PanelWidget if TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver -FILEPATH_OPTION_MAP: dict[ShowFilepathOption, str] = { - ShowFilepathOption.SHOW_FULL_PATHS: Translations["settings.filepath.option.full"], - ShowFilepathOption.SHOW_RELATIVE_PATHS: Translations["settings.filepath.option.relative"], - ShowFilepathOption.SHOW_FILENAMES_ONLY: Translations["settings.filepath.option.name"], -} +FILEPATH_OPTION_MAP: dict[ShowFilepathOption, str] = {} -THEME_MAP: dict[Theme, str] = { - Theme.DARK: Translations["settings.theme.dark"], - Theme.LIGHT: Translations["settings.theme.light"], - Theme.SYSTEM: Translations["settings.theme.system"], -} +THEME_MAP: dict[Theme, str] = {} + +TAG_CLICK_ACTION_MAP: dict[TagClickActionOption, str] = {} DATE_FORMAT_MAP: dict[str, str] = { "%d/%m/%y": "21/08/24", @@ -61,6 +55,29 @@ class SettingsPanel(PanelWidget): def __init__(self, driver: "QtDriver"): super().__init__() + # set these "constants" because language will be loaded from config shortly after startup + # and we want to use the current language for the dropdowns + global FILEPATH_OPTION_MAP, THEME_MAP, TAG_CLICK_ACTION_MAP + FILEPATH_OPTION_MAP = { + ShowFilepathOption.SHOW_FULL_PATHS: Translations["settings.filepath.option.full"], + ShowFilepathOption.SHOW_RELATIVE_PATHS: Translations[ + "settings.filepath.option.relative" + ], + ShowFilepathOption.SHOW_FILENAMES_ONLY: Translations["settings.filepath.option.name"], + } + THEME_MAP = { + Theme.DARK: Translations["settings.theme.dark"], + Theme.LIGHT: Translations["settings.theme.light"], + Theme.SYSTEM: Translations["settings.theme.system"], + } + TAG_CLICK_ACTION_MAP = { + TagClickActionOption.OPEN_EDIT: Translations["settings.tag_click_action.open_edit"], + TagClickActionOption.SET_SEARCH: Translations["settings.tag_click_action.set_search"], + TagClickActionOption.ADD_TO_SEARCH: Translations[ + "settings.tag_click_action.add_to_search" + ], + } + self.driver = driver self.setMinimumSize(400, 300) @@ -158,13 +175,27 @@ class SettingsPanel(PanelWidget): self.theme_combobox = QComboBox() for k in THEME_MAP: self.theme_combobox.addItem(THEME_MAP[k], k) - theme: Theme = self.driver.settings.theme + theme = self.driver.settings.theme if theme not in THEME_MAP: theme = Theme.DEFAULT self.theme_combobox.setCurrentIndex(list(THEME_MAP.keys()).index(theme)) self.theme_combobox.currentIndexChanged.connect(self.__update_restart_label) form_layout.addRow(Translations["settings.theme.label"], self.theme_combobox) + # Tag Click Action + self.tag_click_action_combobox = QComboBox() + for k in TAG_CLICK_ACTION_MAP: + self.tag_click_action_combobox.addItem(TAG_CLICK_ACTION_MAP[k], k) + tag_click_action = self.driver.settings.tag_click_action + if tag_click_action not in TAG_CLICK_ACTION_MAP: + tag_click_action = TagClickActionOption.DEFAULT + self.tag_click_action_combobox.setCurrentIndex( + list(TAG_CLICK_ACTION_MAP.keys()).index(tag_click_action) + ) + form_layout.addRow( + Translations["settings.tag_click_action.label"], self.tag_click_action_combobox + ) + # Date Format self.dateformat_combobox = QComboBox() for k in DATE_FORMAT_MAP: @@ -206,6 +237,7 @@ class SettingsPanel(PanelWidget): "page_size": int(self.page_size_line_edit.text()), "show_filepath": self.filepath_combobox.currentData(), "theme": self.theme_combobox.currentData(), + "tag_click_action": self.tag_click_action_combobox.currentData(), "date_format": self.dateformat_combobox.currentData(), "hour_format": self.hourformat_checkbox.isChecked(), "zero_padding": self.zeropadding_checkbox.isChecked(), @@ -221,6 +253,7 @@ class SettingsPanel(PanelWidget): driver.settings.page_size = settings["page_size"] driver.settings.show_filepath = settings["show_filepath"] driver.settings.theme = settings["theme"] + driver.settings.tag_click_action = settings["tag_click_action"] driver.settings.date_format = settings["date_format"] driver.settings.hour_format = settings["hour_format"] driver.settings.zero_padding = settings["zero_padding"] diff --git a/src/tagstudio/qt/widgets/tag.py b/src/tagstudio/qt/widgets/tag.py index ca124070..31e68689 100644 --- a/src/tagstudio/qt/widgets/tag.py +++ b/src/tagstudio/qt/widgets/tag.py @@ -98,15 +98,17 @@ class TagWidget(QWidget): on_click = Signal() on_edit = Signal() + tag: Tag | None + def __init__( self, tag: Tag | None, has_edit: bool, has_remove: bool, library: "Library | None" = None, - on_remove_callback: FunctionType = None, - on_click_callback: FunctionType = None, - on_edit_callback: FunctionType = None, + on_remove_callback: FunctionType | None = None, + on_click_callback: FunctionType | None = None, + on_edit_callback: FunctionType | None = None, ) -> None: super().__init__() self.tag = tag @@ -123,10 +125,18 @@ class TagWidget(QWidget): self.bg_button = QPushButton(self) self.bg_button.setFlat(True) + # add callbacks + if on_remove_callback is not None: + self.on_remove.connect(on_remove_callback) + if on_click_callback is not None: + self.on_click.connect(on_click_callback) + if on_edit_callback is not None: + self.on_edit.connect(on_edit_callback) + + # add edit action if has_edit: edit_action = QAction(self) edit_action.setText(Translations["generic.edit"]) - edit_action.triggered.connect(on_edit_callback) edit_action.triggered.connect(self.on_edit.emit) self.bg_button.addAction(edit_action) # if on_click_callback: diff --git a/src/tagstudio/qt/widgets/tag_box.py b/src/tagstudio/qt/widgets/tag_box.py index e437c880..8b005182 100644 --- a/src/tagstudio/qt/widgets/tag_box.py +++ b/src/tagstudio/qt/widgets/tag_box.py @@ -8,6 +8,7 @@ import typing 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.qt.flowlayout import FlowLayout @@ -26,6 +27,8 @@ class TagBoxWidget(FieldWidget): updated = Signal() error_occurred = Signal(Exception) + driver: "QtDriver" + def __init__( self, tags: set[Tag], @@ -50,11 +53,11 @@ class TagBoxWidget(FieldWidget): tags_ = sorted(list(tags), key=lambda tag: self.driver.lib.tag_display_name(tag.id)) logger.info("[TagBoxWidget] Tags:", tags=tags) while self.base_layout.itemAt(0): - self.base_layout.takeAt(0).widget().deleteLater() + self.base_layout.takeAt(0).widget().deleteLater() # pyright: ignore[reportOptionalMemberAccess] for tag in tags_: tag_widget = TagWidget(tag, library=self.driver.lib, has_edit=True, has_remove=True) - tag_widget.on_click.connect(lambda t=tag: self.edit_tag(t)) + tag_widget.on_click.connect(lambda t=tag: self.__on_tag_clicked(t)) tag_widget.on_remove.connect( lambda tag_id=tag.id: ( @@ -73,6 +76,26 @@ class TagBoxWidget(FieldWidget): self.base_layout.addWidget(tag_widget) + def __on_tag_clicked(self, tag: Tag): + match self.driver.settings.tag_click_action: + case TagClickActionOption.OPEN_EDIT: + self.edit_tag(tag) + case TagClickActionOption.SET_SEARCH: + self.driver.update_browsing_state(BrowsingState.from_tag_id(tag.id)) + 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 = BrowsingState.from_tag_id(tag.id).query + assert suffix is not None + self.driver.update_browsing_state( + current.with_search_query( + f"{current.query} {suffix}" if current.query else suffix + ) + ) + def edit_tag(self, tag: Tag): assert isinstance(tag, Tag), f"tag is {type(tag)}" build_tag_panel = BuildTagPanel(self.driver.lib, tag=tag) diff --git a/src/tagstudio/resources/translations/de.json b/src/tagstudio/resources/translations/de.json index 4727ebad..e48bf9d7 100644 --- a/src/tagstudio/resources/translations/de.json +++ b/src/tagstudio/resources/translations/de.json @@ -243,6 +243,10 @@ "settings.restart_required": "Bitte TagStudio neustarten, um Änderungen anzuwenden.", "settings.show_filenames_in_grid": "Dateinamen in Raster darstellen", "settings.show_recent_libraries": "Zuletzt verwendete Bibliotheken anzeigen", + "settings.tag_click_action.label": "Tag Klick Aktion", + "settings.tag_click_action.add_to_search": "Tag zu Suche hinzufügen", + "settings.tag_click_action.open_edit": "Tag bearbeiten", + "settings.tag_click_action.set_search": "Nach Tag suchen", "settings.theme.dark": "Dunkel", "settings.theme.label": "Design:", "settings.theme.light": "Hell", diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 281f69c7..a5bc23c9 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -243,6 +243,10 @@ "settings.restart_required": "Please restart TagStudio for changes to take effect.", "settings.show_filenames_in_grid": "Show Filenames in Grid", "settings.show_recent_libraries": "Show Recent Libraries", + "settings.tag_click_action.label": "Tag Click Action", + "settings.tag_click_action.add_to_search": "Add Tag to Search", + "settings.tag_click_action.open_edit": "Edit Tag", + "settings.tag_click_action.set_search": "Search for Tag", "settings.theme.dark": "Dark", "settings.theme.label": "Theme:", "settings.theme.light": "Light",