From dda1a3d85354d4c1d2070bbb887a70f350859f21 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:58:18 -0700 Subject: [PATCH 1/4] fix: use translation keys where available (port #1290) Co-authored-by: Math-Bee <172338034+Math-Bee@users.noreply.github.com> --- .../controllers/edit_field_template_modal.py | 6 +-- src/tagstudio/qt/controllers/main_window.py | 12 +++--- src/tagstudio/qt/controllers/splash.py | 21 +++++----- .../qt/controllers/tag_search_panel.py | 8 +++- .../qt/controllers/tiles/color_data.py | 2 +- .../qt/controllers/tiles/tile_list.py | 6 ++- src/tagstudio/qt/mixed/about_modal.py | 15 ++++--- src/tagstudio/qt/mixed/folders_to_tags.py | 6 +-- src/tagstudio/qt/mixed/settings_panel.py | 40 ++++++++++--------- src/tagstudio/qt/qt_driver.py | 10 +++-- 10 files changed, 67 insertions(+), 59 deletions(-) diff --git a/src/tagstudio/qt/controllers/edit_field_template_modal.py b/src/tagstudio/qt/controllers/edit_field_template_modal.py index f8ebaf990c..a8915bec39 100644 --- a/src/tagstudio/qt/controllers/edit_field_template_modal.py +++ b/src/tagstudio/qt/controllers/edit_field_template_modal.py @@ -19,8 +19,8 @@ # TODO: Use newer MVC style guidelines class EditFieldTemplateModal(EditFieldTemplateModalView): field_type_map: dict[str, str] = { - "TextFieldTemplate": Translations["field_type.text"], - "DatetimeFieldTemplate": Translations["field_type.datetime"], + "TextFieldTemplate": "field_type.text", + "DatetimeFieldTemplate": "field_type.datetime", } DEFAULT_TYPE_INDEX = 0 @@ -32,7 +32,7 @@ def __init__(self, field_template: BaseFieldTemplate | None = None) -> None: self.old_field_type: str = "" for k, v in EditFieldTemplateModal.field_type_map.items(): - self._type_combobox.addItem(v, k) + self._type_combobox.addItem(Translations[v], k) self.__connect_callbacks() self.set_field_template(field_template) diff --git a/src/tagstudio/qt/controllers/main_window.py b/src/tagstudio/qt/controllers/main_window.py index 46c50bd58d..ef13a769ff 100644 --- a/src/tagstudio/qt/controllers/main_window.py +++ b/src/tagstudio/qt/controllers/main_window.py @@ -455,11 +455,11 @@ def rebuild_open_recent_library_menu( # View Component class MainWindow(QMainWindow): THUMB_SIZES: list[tuple[str, int]] = [ - (Translations["home.thumbnail_size.extra_large"], 256), - (Translations["home.thumbnail_size.large"], 192), - (Translations["home.thumbnail_size.medium"], 128), - (Translations["home.thumbnail_size.small"], 96), - (Translations["home.thumbnail_size.mini"], 76), + ("home.thumbnail_size.extra_large", 256), + ("home.thumbnail_size.large", 192), + ("home.thumbnail_size.medium", 128), + ("home.thumbnail_size.small", 96), + ("home.thumbnail_size.mini", 76), ] def __init__(self, driver: QtDriver, parent: QWidget | None = None) -> None: @@ -656,7 +656,7 @@ def setup_extra_input_bar(self): self.thumb_size_combobox.setMaximumWidth(352) self.extra_input_layout.addWidget(self.thumb_size_combobox) for size in MainWindow.THUMB_SIZES: - self.thumb_size_combobox.addItem(size[0], size[1]) + self.thumb_size_combobox.addItem(Translations[size[0]], size[1]) self.thumb_size_combobox.setCurrentIndex(2) # Default: Medium def setup_content(self, driver: QtDriver): diff --git a/src/tagstudio/qt/controllers/splash.py b/src/tagstudio/qt/controllers/splash.py index 2a39028f12..90866af52d 100644 --- a/src/tagstudio/qt/controllers/splash.py +++ b/src/tagstudio/qt/controllers/splash.py @@ -23,12 +23,6 @@ class SplashScreen: """The custom splash screen widget for TagStudio.""" - VERSION_STR: str = " ".join( - [ - f"{Translations['about.version']}", - f"{VERSION} {(' (' + Translations[BUILD_TYPE] + ')') if BUILD_TYPE else ''}", - ] - ) DEFAULT_SPLASH = Splash.AURORA def __init__( @@ -68,6 +62,13 @@ def get_pixmap(self) -> QPixmap: case _: pass + version_str = " ".join( + [ + f"{Translations['about.version']}", + f"{VERSION} {(' (' + Translations[BUILD_TYPE] + ')') if BUILD_TYPE else ''}", + ] + ) + # TODO: Store any differing data elsewhere and load dynamically instead of hardcoding. match self.splash_name: case Splash.CLASSIC: @@ -88,7 +89,7 @@ def get_pixmap(self) -> QPixmap: painter.drawText( QRect(0, -50, 960, 540), int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), - SplashScreen.VERSION_STR, + version_str, ) case Splash.GOO_GEARS: @@ -110,7 +111,7 @@ def get_pixmap(self) -> QPixmap: painter.setPen(pen) painter.drawText( QRect(40, 420, 960, 540), - SplashScreen.VERSION_STR, + version_str, ) case Splash.NINETY_FIVE: @@ -136,7 +137,7 @@ def get_pixmap(self) -> QPixmap: painter.drawText( QRect(-30, 25, 960, 540), int(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight), - SplashScreen.VERSION_STR, + version_str, ) case Splash.AURORA: @@ -157,7 +158,7 @@ def get_pixmap(self) -> QPixmap: painter.drawText( QRect(0, -50, 960, 540), int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), - SplashScreen.VERSION_STR, + version_str, ) case _: diff --git a/src/tagstudio/qt/controllers/tag_search_panel.py b/src/tagstudio/qt/controllers/tag_search_panel.py index c7f083721f..b297166c35 100644 --- a/src/tagstudio/qt/controllers/tag_search_panel.py +++ b/src/tagstudio/qt/controllers/tag_search_panel.py @@ -88,11 +88,17 @@ def _on_item_remove(self, item: Tag) -> None: return message_box = QMessageBox( - QMessageBox.Icon.Question, + QMessageBox.Icon.Warning, Translations["tag.remove"], Translations.format("tag.confirm_delete", tag_name=self._lib.tag_display_name(item)), QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel, ) + unwrap(message_box.button(QMessageBox.StandardButton.Ok)).setText( + Translations["generic.delete_alt"] + ) + unwrap(message_box.button(QMessageBox.StandardButton.Cancel)).setText( + Translations["generic.cancel_alt"] + ) result = message_box.exec() diff --git a/src/tagstudio/qt/controllers/tiles/color_data.py b/src/tagstudio/qt/controllers/tiles/color_data.py index b232adf226..423c1fd4c9 100644 --- a/src/tagstudio/qt/controllers/tiles/color_data.py +++ b/src/tagstudio/qt/controllers/tiles/color_data.py @@ -95,7 +95,7 @@ def set_colors(self, colors: Iterable[TagColorGroup]): TagColorGroup( slug="slug", namespace=self.namespace, - name="Color", + name=Translations["color.placeholder"], primary="#FFFFFF", secondary=None, ) diff --git a/src/tagstudio/qt/controllers/tiles/tile_list.py b/src/tagstudio/qt/controllers/tiles/tile_list.py index ff3b792026..d2f7960422 100644 --- a/src/tagstudio/qt/controllers/tiles/tile_list.py +++ b/src/tagstudio/qt/controllers/tiles/tile_list.py @@ -420,12 +420,14 @@ def update_datetime_field(self, field: DatetimeField, name: str, value: str) -> def remove_message_box(self, prompt: str, callback: Callable[..., None]) -> None: remove_mb = QMessageBox() remove_mb.setText(prompt) - remove_mb.setWindowTitle(Translations["Remove Field"]) + remove_mb.setWindowTitle(Translations["field.remove"]) remove_mb.setIcon(QMessageBox.Icon.Warning) cancel_button = remove_mb.addButton( Translations["generic.cancel_alt"], QMessageBox.ButtonRole.RejectRole ) - remove_mb.addButton("&Remove", QMessageBox.ButtonRole.DestructiveRole) + remove_mb.addButton( + Translations["generic.remove_alt"], QMessageBox.ButtonRole.DestructiveRole + ) remove_mb.setEscapeButton(cancel_button) result = remove_mb.exec_() if result == QMessageBox.ButtonRole.ActionRole.value: diff --git a/src/tagstudio/qt/mixed/about_modal.py b/src/tagstudio/qt/mixed/about_modal.py index ad166f9fad..48298f07e7 100644 --- a/src/tagstudio/qt/mixed/about_modal.py +++ b/src/tagstudio/qt/mixed/about_modal.py @@ -44,13 +44,6 @@ class AboutModal(QWidget): """Modal window showing information about the TagStudio application.""" - VERSION_STR: str = " ".join( - [ - f"{Translations['about.version']}", - f"{VERSION} {(' (' + Translations[BUILD_TYPE] + ')') if BUILD_TYPE else ''}", - ] - ) - def __init__(self, config_path: Path | str): super().__init__() self.setWindowTitle(Translations["about.title"]) @@ -89,7 +82,13 @@ def __init__(self, config_path: Path | str): self.logo_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) # Version -------------------------------------------------------------- - self.version_label = QLabel(header(AboutModal.VERSION_STR, 2)) + version_str = " ".join( + [ + f"{Translations['about.version']}", + f"{VERSION} {(' (' + Translations[BUILD_TYPE] + ')') if BUILD_TYPE else ''}", + ] + ) + self.version_label = QLabel(header(version_str, 2)) self.version_label.setAlignment(Qt.AlignmentFlag.AlignCenter) # Copyright ------------------------------------------------------------ diff --git a/src/tagstudio/qt/mixed/folders_to_tags.py b/src/tagstudio/qt/mixed/folders_to_tags.py index f2e6914c1a..47bfed84a2 100644 --- a/src/tagstudio/qt/mixed/folders_to_tags.py +++ b/src/tagstudio/qt/mixed/folders_to_tags.py @@ -182,13 +182,9 @@ def __init__(self, library: Library, driver: QtDriver): self.title_widget.setWordWrap(True) self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.desc_widget = QLabel() + self.desc_widget = QLabel(Translations["folders_to_tags.description"]) self.desc_widget.setObjectName("descriptionLabel") self.desc_widget.setWordWrap(True) - self.desc_widget.setText( - """Creates tags based on your folder structure and applies them to your entries. - This tree shows all tags to be created and which entries they will be applied to.""" - ) self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) self.open_close_button_w = QWidget() diff --git a/src/tagstudio/qt/mixed/settings_panel.py b/src/tagstudio/qt/mixed/settings_panel.py index 26ffa079af..45557d0718 100644 --- a/src/tagstudio/qt/mixed/settings_panel.py +++ b/src/tagstudio/qt/mixed/settings_panel.py @@ -43,30 +43,30 @@ class SettingsPanel(ModalContent): driver: 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"], + ShowFilepathOption.SHOW_FULL_PATHS: "settings.filepath.option.full", + ShowFilepathOption.SHOW_RELATIVE_PATHS: "settings.filepath.option.relative", + ShowFilepathOption.SHOW_FILENAMES_ONLY: "settings.filepath.option.name", } theme_map: dict[Theme, str] = { - Theme.SYSTEM: Translations["settings.theme.system"], - Theme.DARK: Translations["settings.theme.dark"], - Theme.LIGHT: Translations["settings.theme.light"], + Theme.SYSTEM: "settings.theme.system", + Theme.DARK: "settings.theme.dark", + Theme.LIGHT: "settings.theme.light", } splash_map: dict[Splash, str] = { - Splash.DEFAULT: Translations["settings.splash.option.default"], - Splash.RANDOM: Translations["settings.splash.option.random"], - Splash.CLASSIC: Translations["settings.splash.option.classic"], - Splash.GOO_GEARS: Translations["settings.splash.option.goo_gears"], - Splash.NINETY_FIVE: Translations["settings.splash.option.ninety_five"], - Splash.AURORA: Translations["settings.splash.option.aurora"], + Splash.DEFAULT: "settings.splash.option.default", + Splash.RANDOM: "settings.splash.option.random", + Splash.CLASSIC: "settings.splash.option.classic", + Splash.GOO_GEARS: "settings.splash.option.goo_gears", + Splash.NINETY_FIVE: "settings.splash.option.ninety_five", + Splash.AURORA: "settings.splash.option.aurora", } tag_click_action_map: dict[TagClickActionOption, str] = { - 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"], + TagClickActionOption.OPEN_EDIT: "settings.tag_click_action.open_edit", + TagClickActionOption.SET_SEARCH: "settings.tag_click_action.set_search", + TagClickActionOption.ADD_TO_SEARCH: "settings.tag_click_action.add_to_search", } date_format_map: dict[str, str] = { @@ -194,7 +194,7 @@ def on_page_size_changed(): # Show Filepath self.filepath_combobox = QComboBox() for k in SettingsPanel.filepath_option_map: - self.filepath_combobox.addItem(SettingsPanel.filepath_option_map[k], k) + self.filepath_combobox.addItem(Translations[SettingsPanel.filepath_option_map[k]], k) filepath_option: ShowFilepathOption = self.driver.settings.show_filepath if filepath_option not in SettingsPanel.filepath_option_map: filepath_option = ShowFilepathOption.DEFAULT @@ -206,7 +206,9 @@ def on_page_size_changed(): # Tag Click Action self.tag_click_action_combobox = QComboBox() for k in SettingsPanel.tag_click_action_map: - self.tag_click_action_combobox.addItem(SettingsPanel.tag_click_action_map[k], k) + self.tag_click_action_combobox.addItem( + Translations[SettingsPanel.tag_click_action_map[k]], k + ) tag_click_action = self.driver.settings.tag_click_action if tag_click_action not in SettingsPanel.tag_click_action_map: tag_click_action = TagClickActionOption.DEFAULT @@ -375,7 +377,7 @@ def __build_appearance_settings(self): # Dark Mode self.theme_combobox = QComboBox() for k in SettingsPanel.theme_map: - self.theme_combobox.addItem(SettingsPanel.theme_map[k], k) + self.theme_combobox.addItem(Translations[SettingsPanel.theme_map[k]], k) theme = self.driver.settings.theme if theme not in SettingsPanel.theme_map: theme = Theme.DEFAULT @@ -386,7 +388,7 @@ def __build_appearance_settings(self): # Splash Screen self.splash_combobox = QComboBox() for k in SettingsPanel.splash_map: - self.splash_combobox.addItem(SettingsPanel.splash_map[k], k) + self.splash_combobox.addItem(Translations[SettingsPanel.splash_map[k]], k) splash = self.driver.settings.splash if splash not in SettingsPanel.splash_map: splash = Splash.DEFAULT diff --git a/src/tagstudio/qt/qt_driver.py b/src/tagstudio/qt/qt_driver.py index f3f812e8d9..f090758901 100644 --- a/src/tagstudio/qt/qt_driver.py +++ b/src/tagstudio/qt/qt_driver.py @@ -1021,9 +1021,9 @@ def delete_file_confirmation(self, count: int, filename: Path | None = None) -> msg.setStyleSheet("font-weight:normal;") msg.setTextFormat(Qt.TextFormat.RichText) msg.setWindowTitle( - Translations["trash.title.singular"] + Translations["trash.dialog.title.singular"] if count == 1 - else Translations["trash.title.plural"] + else Translations["trash.dialog.title.plural"] ) msg.setIcon(QMessageBox.Icon.Warning) if count <= 1: @@ -1048,8 +1048,10 @@ def delete_file_confirmation(self, count: int, filename: Path | None = None) -> f"{perm_warning}
" ) - yes_button: QPushButton = msg.addButton("&Yes", QMessageBox.ButtonRole.YesRole) - msg.addButton("&No", QMessageBox.ButtonRole.NoRole) + yes_button: QPushButton = msg.addButton( + Translations["generic.yes"], QMessageBox.ButtonRole.YesRole + ) + msg.addButton(Translations["generic.no"], QMessageBox.ButtonRole.NoRole) msg.setDefaultButton(yes_button) return msg.exec() From 6d1f267acfb58299f5cc3f723fc2e94969df64af Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:18:45 -0700 Subject: [PATCH 2/4] fix(translations): add missing/fix incorrect translations --- src/tagstudio/qt/controllers/tiles/color_data.py | 2 +- src/tagstudio/qt/views/library_info_window_view.py | 2 +- src/tagstudio/resources/translations/en.json | 12 +++++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/tagstudio/qt/controllers/tiles/color_data.py b/src/tagstudio/qt/controllers/tiles/color_data.py index 423c1fd4c9..e547f45432 100644 --- a/src/tagstudio/qt/controllers/tiles/color_data.py +++ b/src/tagstudio/qt/controllers/tiles/color_data.py @@ -108,7 +108,7 @@ def edit_color(self, color_group: TagColorGroup): self.edit_modal = Modal( build_color_panel, - "Edit Color", + Translations["color.edit"], is_savable=True, ) diff --git a/src/tagstudio/qt/views/library_info_window_view.py b/src/tagstudio/qt/views/library_info_window_view.py index c6de871b21..7c5a2b05d5 100644 --- a/src/tagstudio/qt/views/library_info_window_view.py +++ b/src/tagstudio/qt/views/library_info_window_view.py @@ -39,7 +39,7 @@ def __init__(self, library: Library, driver: QtDriver): self.lib = library self.driver = driver - self.setWindowTitle("Library Information") + self.setWindowTitle(Translations["library_info.window_title"]) self.setMinimumSize(800, 480) self.root_layout = QVBoxLayout(self) self.root_layout.setContentsMargins(PAD, PAD, PAD, PAD) diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 42d84dc44b..45f61b3c15 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -21,7 +21,8 @@ "color_manager.title": "Manage Tag Colors", "color.color_border": "Use Secondary Color for Border", "color.confirm_delete": "Are you sure you want to delete the color \"{color_name}\"?", - "color.delete": "Delete Tag", + "color.delete": "Delete Color", + "color.edit": "Edit Color", "color.import_pack": "Import Color Pack", "color.name": "Name", "color.namespace.delete.prompt": "Are you sure you want to delete this color namespace? This will delete ALL colors in the namespace along with it!", @@ -38,7 +39,7 @@ "drop_import.duplicates_choice.singular": "The following file matches a file path that already exists in the library.", "drop_import.progress.label.initial": "Importing New Files…", "drop_import.progress.label.plural": "Importing New Files…\n{count} Files Imported.{suffix}", - "drop_import.progress.label.singular": "Importing New Files…\n1 File imported.{suffix}", + "drop_import.progress.label.singular": "Importing New Files…\n1 File Imported.{suffix}", "drop_import.progress.window_title": "Import Files", "drop_import.title": "Conflicting File(s)", "edit.color_manager": "Manage Tag Colors", @@ -186,7 +187,7 @@ "json_migration.creating_database_tables": "Creating SQL Database Tables…", "json_migration.description": "
Start and preview the results of the library migration process. The converted library will not be used unless you click \"Finish Migration\".

Library data should either have matching values or feature a \"Matched\" label. Values that do not match will be displayed in red and feature a \"(!)\" symbol next to them.
This process may take up to several minutes for larger libraries.
", "json_migration.discrepancies_found": "Library Discrepancies Found", - "json_migration.discrepancies_found.description": "Discrepancies were found between the original and converted library formats. Please review and choose to whether continue with the migration or to cancel.", + "json_migration.discrepancies_found.description": "Discrepancies were found between the original and converted library formats. Please review and choose whether to continue with the migration or to cancel.", "json_migration.finish_migration": "Finish Migration", "json_migration.heading.aliases": "Aliases:", "json_migration.heading.colors": "Colors:", @@ -251,6 +252,7 @@ "library_info.stats.tags": "Tags:", "library_info.title": "Library '{library_dir}'", "library_info.version": "Library Format Version: {version}", + "library_info.window_title": "Library Information", "library_object.name": "Name", "library_object.name_required": "Name (Required)", "library_object.slug": "ID Slug", @@ -372,7 +374,7 @@ "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.label": "Theme", "settings.theme.light": "Light", "settings.theme.system": "System", "settings.thumb_cache_size.label": "Thumbnail Cache Size", @@ -431,7 +433,7 @@ "tag.parent_tags.info.searches.description": "Files tagged with \"{child}\" will also show up in searches for \"{parent}\" and \"{grandparent}\", so you don't need to apply those parent tags directly to files.
This only works one way: searching for \"{child}\" won't find everything tagged with \"{parent}\".", "tag.parent_tags.info.searches.title": "Searches", "tag.properties": "Properties", - "tag.remove": "Remove Tag", + "tag.remove": "Delete Tag", "tag.search_for_tag": "Search for Tag", "tag.shorthand": "Shorthand", "tag.tag_name_required": "Tag Name (Required)", From b612c9701d9a3f199439dfd11cbfe410deb689da Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:28:19 -0700 Subject: [PATCH 3/4] fix(ui): use consistent/correct button roles --- .../field_template_search_panel.py | 9 +++- .../qt/controllers/tag_search_panel.py | 1 + .../qt/controllers/tiles/color_data.py | 10 ++-- .../qt/controllers/tiles/tile_list.py | 21 ++++---- .../update_available_message_box.py | 1 + src/tagstudio/qt/mixed/drop_import_modal.py | 8 +-- src/tagstudio/qt/mixed/migration_modal.py | 1 + src/tagstudio/qt/mixed/tag_color_manager.py | 9 ++-- src/tagstudio/qt/qt_driver.py | 52 ++++++++++--------- src/tagstudio/resources/translations/en.json | 8 +-- 10 files changed, 68 insertions(+), 52 deletions(-) diff --git a/src/tagstudio/qt/controllers/field_template_search_panel.py b/src/tagstudio/qt/controllers/field_template_search_panel.py index 9847922919..39d1f53f35 100644 --- a/src/tagstudio/qt/controllers/field_template_search_panel.py +++ b/src/tagstudio/qt/controllers/field_template_search_panel.py @@ -89,11 +89,18 @@ def _on_item_remove(self, item: BaseFieldTemplate) -> None: return message_box = QMessageBox( - QMessageBox.Icon.Question, + QMessageBox.Icon.Warning, Translations["field_template.delete"], Translations.format("field_template.confirm_delete", field_template_name=item.name), QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel, ) + unwrap(message_box.button(QMessageBox.StandardButton.Ok)).setText( + Translations["generic.delete_alt"] + ) + unwrap(message_box.button(QMessageBox.StandardButton.Cancel)).setText( + Translations["generic.cancel_alt"] + ) + message_box.setDefaultButton(QMessageBox.StandardButton.Cancel) result = message_box.exec() diff --git a/src/tagstudio/qt/controllers/tag_search_panel.py b/src/tagstudio/qt/controllers/tag_search_panel.py index b297166c35..e6b89b0d72 100644 --- a/src/tagstudio/qt/controllers/tag_search_panel.py +++ b/src/tagstudio/qt/controllers/tag_search_panel.py @@ -99,6 +99,7 @@ def _on_item_remove(self, item: Tag) -> None: unwrap(message_box.button(QMessageBox.StandardButton.Cancel)).setText( Translations["generic.cancel_alt"] ) + message_box.setDefaultButton(QMessageBox.StandardButton.Cancel) result = message_box.exec() diff --git a/src/tagstudio/qt/controllers/tiles/color_data.py b/src/tagstudio/qt/controllers/tiles/color_data.py index e547f45432..d81d5876ab 100644 --- a/src/tagstudio/qt/controllers/tiles/color_data.py +++ b/src/tagstudio/qt/controllers/tiles/color_data.py @@ -126,13 +126,13 @@ def delete_color(self, color_group: TagColorGroup): cancel_button = message_box.addButton( Translations["generic.cancel_alt"], QMessageBox.ButtonRole.RejectRole ) - message_box.addButton( - Translations["generic.delete_alt"], QMessageBox.ButtonRole.DestructiveRole + delete_button = message_box.addButton( + Translations["generic.delete_alt"], QMessageBox.ButtonRole.AcceptRole ) message_box.setEscapeButton(cancel_button) - result = message_box.exec_() - logger.info(QMessageBox.ButtonRole.DestructiveRole.value) - if result != QMessageBox.ButtonRole.ActionRole.value: + message_box.setDefaultButton(cancel_button) + message_box.exec() + if message_box.clickedButton() != delete_button: return logger.info("[ColorData] Removing color", color=color_group) diff --git a/src/tagstudio/qt/controllers/tiles/tile_list.py b/src/tagstudio/qt/controllers/tiles/tile_list.py index d2f7960422..3c263030a9 100644 --- a/src/tagstudio/qt/controllers/tiles/tile_list.py +++ b/src/tagstudio/qt/controllers/tiles/tile_list.py @@ -418,19 +418,20 @@ def update_datetime_field(self, field: DatetimeField, name: str, value: str) -> self.lib.update_datetime_field(entry_ids, field, name, dt.fromisoformat(value)) def remove_message_box(self, prompt: str, callback: Callable[..., None]) -> None: - remove_mb = QMessageBox() - remove_mb.setText(prompt) - remove_mb.setWindowTitle(Translations["field.remove"]) - remove_mb.setIcon(QMessageBox.Icon.Warning) - cancel_button = remove_mb.addButton( + message_box = QMessageBox() + message_box.setText(prompt) + message_box.setWindowTitle(Translations["field.remove"]) + message_box.setIcon(QMessageBox.Icon.Warning) + cancel_button = message_box.addButton( Translations["generic.cancel_alt"], QMessageBox.ButtonRole.RejectRole ) - remove_mb.addButton( - Translations["generic.remove_alt"], QMessageBox.ButtonRole.DestructiveRole + remove_button = message_box.addButton( + Translations["generic.remove_alt"], QMessageBox.ButtonRole.AcceptRole ) - remove_mb.setEscapeButton(cancel_button) - result = remove_mb.exec_() - if result == QMessageBox.ButtonRole.ActionRole.value: + message_box.setEscapeButton(cancel_button) + message_box.setDefaultButton(cancel_button) + message_box.exec() + if message_box.clickedButton() == remove_button: callback() @property diff --git a/src/tagstudio/qt/controllers/update_available_message_box.py b/src/tagstudio/qt/controllers/update_available_message_box.py index 97362438d7..033dc6e0b9 100644 --- a/src/tagstudio/qt/controllers/update_available_message_box.py +++ b/src/tagstudio/qt/controllers/update_available_message_box.py @@ -54,6 +54,7 @@ def __init__(self): partial(QDesktopServices.openUrl, GITHUB_RELEASE_URL) ) self.button(QMessageBox.StandardButton.Ignore).setText(Translations["generic.dont_remind"]) + self.button(QMessageBox.StandardButton.Close).setText(Translations["generic.close"]) red = get_ui_color(ColorType.PRIMARY, UiColor.RED) green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN) diff --git a/src/tagstudio/qt/mixed/drop_import_modal.py b/src/tagstudio/qt/mixed/drop_import_modal.py index 6f54c59257..aeb256d3a7 100644 --- a/src/tagstudio/qt/mixed/drop_import_modal.py +++ b/src/tagstudio/qt/mixed/drop_import_modal.py @@ -63,6 +63,10 @@ def __init__(self, driver: QtDriver): self.button_layout.setContentsMargins(6, 6, 6, 6) self.button_layout.addStretch(1) + self.cancel_button = QPushButton(Translations["generic.cancel_alt"]) + self.cancel_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.CANCEL)) + self.button_layout.addWidget(self.cancel_button) + self.skip_button = QPushButton(Translations["generic.skip_alt"]) self.skip_button.setDefault(True) self.skip_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.SKIP)) @@ -78,10 +82,6 @@ def __init__(self, driver: QtDriver): self.rename_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.RENAME)) self.button_layout.addWidget(self.rename_button) - self.cancel_button = QPushButton(Translations["generic.cancel_alt"]) - self.cancel_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.CANCEL)) - self.button_layout.addWidget(self.cancel_button) - # Layout ===================== self.root_layout.addWidget(self.desc_widget) self.root_layout.addWidget(self.list_view) diff --git a/src/tagstudio/qt/mixed/migration_modal.py b/src/tagstudio/qt/mixed/migration_modal.py index e300cc692a..c2fb54bf2b 100644 --- a/src/tagstudio/qt/mixed/migration_modal.py +++ b/src/tagstudio/qt/mixed/migration_modal.py @@ -462,6 +462,7 @@ def update_sql_value_ui(self, show_msg_box: bool = True): Translations["json_migration.discrepancies_found.description"], ) msg_box.setDetailedText("\n".join(self.discrepancies)) + msg_box.addButton(Translations["generic.close"], QMessageBox.ButtonRole.AcceptRole) msg_box.exec() def finish_migration(self): diff --git a/src/tagstudio/qt/mixed/tag_color_manager.py b/src/tagstudio/qt/mixed/tag_color_manager.py index 19ef50aea7..eb66738d16 100644 --- a/src/tagstudio/qt/mixed/tag_color_manager.py +++ b/src/tagstudio/qt/mixed/tag_color_manager.py @@ -200,12 +200,13 @@ def delete_namespace_dialog(self, prompt: str, callback: Callable[..., Any]) -> cancel_button = message_box.addButton( Translations["generic.cancel_alt"], QMessageBox.ButtonRole.RejectRole ) - message_box.addButton( - Translations["generic.delete_alt"], QMessageBox.ButtonRole.DestructiveRole + delete_button = message_box.addButton( + Translations["generic.delete_alt"], QMessageBox.ButtonRole.AcceptRole ) message_box.setEscapeButton(cancel_button) - result = message_box.exec_() - if result != QMessageBox.ButtonRole.ActionRole.value: + message_box.setDefaultButton(cancel_button) + message_box.exec() + if message_box.clickedButton() != delete_button: return callback() diff --git a/src/tagstudio/qt/qt_driver.py b/src/tagstudio/qt/qt_driver.py index f090758901..28d80bb585 100644 --- a/src/tagstudio/qt/qt_driver.py +++ b/src/tagstudio/qt/qt_driver.py @@ -979,25 +979,19 @@ def delete_files_callback(self, origin_path: str | Path, origin_id: int | None = filepath: Path = entry.path pending.append((item, filepath)) - if pending: - return_code = self.delete_file_confirmation(len(pending), pending[0][1]) - # If there was a confirmation and not a cancellation - if ( - return_code == QMessageBox.ButtonRole.DestructiveRole.value - and return_code != QMessageBox.ButtonRole.ActionRole.value - ): - for e_id, f in pending: - if (origin_path == f) or (not origin_path): - self.main_window.inspector.stop_media_playback() - - if e_id is not None: - self.lib.remove_entries([e_id]) - delete_file(library_dir / f) - - self.clear_select_action_callback() - self.update_browsing_state() - - def delete_file_confirmation(self, count: int, filename: Path | None = None) -> int: + if pending and self.delete_file_confirmation(len(pending), pending[0][1]): + for e_id, f in pending: + if (origin_path == f) or (not origin_path): + self.main_window.inspector.stop_media_playback() + + if e_id is not None: + self.lib.remove_entries([e_id]) + delete_file(library_dir / f) + + self.clear_select_action_callback() + self.update_browsing_state() + + def delete_file_confirmation(self, count: int, filename: Path | None = None) -> bool: """A confirmation dialogue box for deleting files. Args: @@ -1048,13 +1042,23 @@ def delete_file_confirmation(self, count: int, filename: Path | None = None) -> f"{perm_warning}
" ) - yes_button: QPushButton = msg.addButton( - Translations["generic.yes"], QMessageBox.ButtonRole.YesRole + move_key = ( + "menu.delete_selected_files_singular" + if count <= 1 + else "menu.delete_selected_files_plural" + ) + cancel_button = msg.addButton( + Translations["generic.cancel_alt"], QMessageBox.ButtonRole.RejectRole + ) + move_button: QPushButton = msg.addButton( + Translations.format(move_key, trash_term=trash_term()), + QMessageBox.ButtonRole.AcceptRole, ) - msg.addButton(Translations["generic.no"], QMessageBox.ButtonRole.NoRole) - msg.setDefaultButton(yes_button) + msg.setEscapeButton(cancel_button) + msg.setDefaultButton(cancel_button) - return msg.exec() + msg.exec() + return msg.clickedButton() == move_button def _run_sync_step( self, diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 45f61b3c15..32d44c650a 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -70,10 +70,10 @@ "entries.tags": "Tags", "entries.unlinked.description": "Unlinked entries are file entries that can no longer find their original file on disk. Most entries are automatically relinked during a library sync, however some cases require manual review.", "entries.unlinked.description.ambiguous": "For unlinked entries that have ambiguous matches to multiple files in your library, you may manually choose how they get relinked.", - "entries.unlinked.description.deleted": "When you delete files outside of TagStudio, their associated entries become unlinked. You may manually delete any unlinked entries at your own discretion.", + "entries.unlinked.description.deleted": "When you delete files outside of TagStudio, their associated entries become unlinked. You may manually remove any unlinked entries at your own discretion.", "entries.unlinked.relink.manual": "&Manual Relink", - "entries.unlinked.remove": "Delete Unlinked Entries", - "entries.unlinked.remove_alt": "&Delete Unlinked Entries", + "entries.unlinked.remove": "Remove Unlinked Entries", + "entries.unlinked.remove_alt": "Remo&ve Unlinked Entries", "entries.unlinked.review": "Manual &Review", "entries.unlinked.title": "Fix Unlinked Entries", "entries.unlinked.unlinked_count": "Unlinked Entries: {count}", @@ -106,7 +106,7 @@ "file.date_modified": "Date Modified", "file.dimensions": "Dimensions", "file.duplicates.description": "TagStudio supports importing DupeGuru results to manage duplicate files.", - "file.duplicates.dupeguru.advice": "After mirroring, you're free to use DupeGuru to delete the unwanted files. Afterwards, use TagStudio's \"Fix Unlinked Entries\" feature in the Tools menu in order to delete the unlinked Entries.", + "file.duplicates.dupeguru.advice": "After mirroring, you're free to use DupeGuru to delete the unwanted files. Afterwards, use TagStudio's \"Fix Unlinked Entries\" feature in the Tools menu in order to remove the unlinked Entries.", "file.duplicates.dupeguru.file_extension": "DupeGuru Files (*.dupeguru)", "file.duplicates.dupeguru.load_file": "&Load DupeGuru File", "file.duplicates.dupeguru.no_file": "No DupeGuru File Selected", From 62391c609371cd7ce784379bec664943e7f22732 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:49:48 -0700 Subject: [PATCH 4/4] fix(ui): use platform button ordering for Modal class --- .../qt/controllers/changelog_modal.py | 5 ++-- src/tagstudio/qt/views/modal_view.py | 28 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/tagstudio/qt/controllers/changelog_modal.py b/src/tagstudio/qt/controllers/changelog_modal.py index 4604926540..9c916367e0 100644 --- a/src/tagstudio/qt/controllers/changelog_modal.py +++ b/src/tagstudio/qt/controllers/changelog_modal.py @@ -24,6 +24,7 @@ QTextFormat, ) from PySide6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest +from PySide6.QtWidgets import QDialogButtonBox from tagstudio.core.constants import DOCS_URL, GITHUB_REPO_URL from tagstudio.core.utils.types import unwrap @@ -313,8 +314,8 @@ def parent_post_init(self) -> None: assert isinstance(modal_view, ModalView) close_button = unwrap(self.done_button) close_button.setText(Translations["generic.close"]) - modal_view.button_layout.insertWidget( - modal_view.button_layout.indexOf(close_button), self.layout().view_more_button + modal_view.button_box.addButton( + self.layout().view_more_button, QDialogButtonBox.ButtonRole.ActionRole ) # NOTE: A lot of the styling is similar to the "About" window, which eventually needs to be diff --git a/src/tagstudio/qt/views/modal_view.py b/src/tagstudio/qt/views/modal_view.py index 026a06ce99..c8c15ad129 100644 --- a/src/tagstudio/qt/views/modal_view.py +++ b/src/tagstudio/qt/views/modal_view.py @@ -4,7 +4,7 @@ import structlog from PySide6.QtCore import Qt -from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget +from PySide6.QtWidgets import QDialogButtonBox, QLabel, QVBoxLayout from tagstudio.i18n.translations import Translations from tagstudio.qt.controllers.modal_content import ModalContent @@ -28,28 +28,26 @@ def __init__( self.content_widget = content_widget self.setContentsMargins(PAD, PAD if inline_title else PAD * 2, PAD, PAD) - self.button_container = QWidget() - self.button_layout = QHBoxLayout(self.button_container) - self.button_layout.setContentsMargins(PAD, PAD, PAD, PAD) - self.button_layout.addStretch(1) + self.button_box = QDialogButtonBox() + self.button_box.setContentsMargins(PAD, PAD, PAD, PAD) - # [Done] - # - OR - - # [Cancel] [Save] if not is_savable: - done_button = QPushButton(Translations["generic.done"]) + done_button = self.button_box.addButton( + Translations["generic.done"], QDialogButtonBox.ButtonRole.AcceptRole + ) done_button.setAutoDefault(True) self.content_widget.done_button = done_button - self.button_layout.addWidget(done_button) else: - cancel_button = QPushButton(Translations["generic.cancel"]) + cancel_button = self.button_box.addButton( + Translations["generic.cancel"], QDialogButtonBox.ButtonRole.RejectRole + ) self.content_widget.cancel_button = cancel_button - self.button_layout.addWidget(cancel_button) - save_button = QPushButton(Translations["generic.save"]) + save_button = self.button_box.addButton( + Translations["generic.save"], QDialogButtonBox.ButtonRole.AcceptRole + ) save_button.setAutoDefault(True) self.content_widget.save_button = save_button - self.button_layout.addWidget(save_button) if inline_title: self.title_label = QLabel() @@ -61,4 +59,4 @@ def __init__( self.addWidget(content_widget) self.setStretch(1, 2) - self.addWidget(self.button_container) + self.addWidget(self.button_box)