diff --git a/src/tagstudio/core/enums.py b/src/tagstudio/core/enums.py index 2a11a12fd..31e383b6a 100644 --- a/src/tagstudio/core/enums.py +++ b/src/tagstudio/core/enums.py @@ -15,11 +15,21 @@ class SettingItems(str, enum.Enum): LIBS_LIST = "libs_list" WINDOW_SHOW_LIBS = "window_show_libs" SHOW_FILENAMES = "show_filenames" + SHOW_FILEPATH = "show_filepath" AUTOPLAY = "autoplay_videos" THUMB_CACHE_SIZE_LIMIT = "thumb_cache_size_limit" LANGUAGE = "language" +class ShowFilepathOption(int, enum.Enum): + """Values representing the options for the "show_filenames" setting.""" + + SHOW_FULL_PATHS = 0 + SHOW_RELATIVE_PATHS = 1 + SHOW_FILENAMES_ONLY = 2 + DEFAULT = SHOW_RELATIVE_PATHS + + class Theme(str, enum.Enum): COLOR_BG_DARK = "#65000000" COLOR_BG_LIGHT = "#22000000" diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index c400c4a08..378397892 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -6,7 +6,7 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import QComboBox, QFormLayout, QLabel, QVBoxLayout, QWidget -from tagstudio.core.enums import SettingItems +from tagstudio.core.enums import SettingItems, ShowFilepathOption from tagstudio.qt.translations import Translations from tagstudio.qt.widgets.panel import PanelWidget @@ -63,6 +63,25 @@ def __init__(self, driver): ) self.form_layout.addRow(language_label, self.language_combobox) + filepath_option_map: dict[int, 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"], + } + self.filepath_combobox = QComboBox() + self.filepath_combobox.addItems(list(filepath_option_map.values())) + filepath_option: int = int( + driver.settings.value( + SettingItems.SHOW_FILEPATH, defaultValue=ShowFilepathOption.DEFAULT.value, type=int + ) + ) + filepath_option = 0 if filepath_option not in filepath_option_map else filepath_option + self.filepath_combobox.setCurrentIndex(filepath_option) + self.filepath_combobox.currentIndexChanged.connect(self.apply_filepath_setting) + self.form_layout.addRow(Translations["settings.filepath.label"], self.filepath_combobox) + self.root_layout.addWidget(self.form_container) self.root_layout.addStretch(1) self.root_layout.addWidget(self.restart_label) @@ -70,3 +89,19 @@ def __init__(self, driver): def get_language(self) -> str: values: list[str] = list(self.languages.values()) return values[self.language_combobox.currentIndex()] + + def apply_filepath_setting(self): + selected_value = self.filepath_combobox.currentIndex() + self.driver.settings.setValue(SettingItems.SHOW_FILEPATH, selected_value) + self.driver.update_recent_lib_menu() + self.driver.preview_panel.update_widgets() + library_directory = self.driver.lib.library_dir + if selected_value == ShowFilepathOption.SHOW_FULL_PATHS: + display_path = library_directory + else: + display_path = library_directory.name + self.driver.main_window.setWindowTitle( + Translations.format( + "app.title", base_title=self.driver.base_title, library_dir=display_path + ) + ) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 33ba48e16..034fb3f03 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -54,7 +54,7 @@ import tagstudio.qt.resources_rc # noqa: F401 from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE, VERSION, VERSION_BRANCH from tagstudio.core.driver import DriverMixin -from tagstudio.core.enums import LibraryPrefs, MacroID, SettingItems +from tagstudio.core.enums import LibraryPrefs, MacroID, SettingItems, ShowFilepathOption from tagstudio.core.library.alchemy.enums import ( FieldTypeEnum, FilterState, @@ -1749,6 +1749,11 @@ def update_recent_lib_menu(self): """Updates the recent library menu from the latest values from the settings file.""" actions: list[QAction] = [] lib_items: dict[str, tuple[str, str]] = {} + filepath_option: int = int( + self.settings.value( + SettingItems.SHOW_FILEPATH, defaultValue=ShowFilepathOption.DEFAULT.value, type=int + ) + ) settings = self.settings settings.beginGroup(SettingItems.LIBS_LIST) @@ -1767,7 +1772,10 @@ def update_recent_lib_menu(self): for library_key in libs_sorted: path = Path(library_key[1][0]) action = QAction(self.open_recent_library_menu) - action.setText(str(path)) + if filepath_option == ShowFilepathOption.SHOW_FULL_PATHS: + action.setText(str(path)) + else: + action.setText(str(Path(path).name)) action.triggered.connect(lambda checked=False, p=path: self.open_library(p)) actions.append(action) @@ -1822,7 +1830,15 @@ def update_language_settings(self, language: str): def open_library(self, path: Path) -> None: """Open a TagStudio library.""" - message = Translations.format("splash.opening_library", library_path=str(path)) + filepath_option: int = int( + self.settings.value( + SettingItems.SHOW_FILEPATH, defaultValue=ShowFilepathOption.DEFAULT.value, type=int + ) + ) + library_dir_display = ( + path if filepath_option == ShowFilepathOption.SHOW_FULL_PATHS else path.name + ) + message = Translations.format("splash.opening_library", library_path=library_dir_display) self.main_window.landing_widget.set_status_label(message) self.main_window.statusbar.showMessage(message, 3) self.main_window.repaint() @@ -1867,12 +1883,23 @@ def init_library(self, path: Path, open_status: LibraryStatus): if self.lib.entries_count < 10000: self.add_new_files_callback() + library_dir_display = self.lib.library_dir + filepath_option: int = int( + self.settings.value( + SettingItems.SHOW_FILEPATH, defaultValue=ShowFilepathOption.DEFAULT.value, type=int + ) + ) + if filepath_option == ShowFilepathOption.SHOW_FULL_PATHS: + library_dir_display = self.lib.library_dir + else: + library_dir_display = self.lib.library_dir.name + self.update_libs_list(path) self.main_window.setWindowTitle( Translations.format( "app.title", base_title=self.base_title, - library_dir=self.lib.library_dir, + library_dir=library_dir_display, ) ) self.main_window.setAcceptDrops(True) diff --git a/src/tagstudio/qt/widgets/preview/file_attributes.py b/src/tagstudio/qt/widgets/preview/file_attributes.py index c9e84c701..c6f3c0b2c 100644 --- a/src/tagstudio/qt/widgets/preview/file_attributes.py +++ b/src/tagstudio/qt/widgets/preview/file_attributes.py @@ -17,7 +17,7 @@ from PySide6.QtGui import QGuiApplication from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget -from tagstudio.core.enums import Theme +from tagstudio.core.enums import SettingItems, ShowFilepathOption, Theme from tagstudio.core.library.alchemy.library import Library from tagstudio.core.media_types import MediaCategories from tagstudio.qt.helpers.file_opener import FileOpenerHelper, FileOpenerLabel @@ -96,6 +96,8 @@ def __init__(self, library: Library, driver: "QtDriver"): root_layout.addWidget(self.file_label) root_layout.addWidget(self.date_container) root_layout.addWidget(self.dimensions_label) + self.library = library + self.driver = driver def update_date_label(self, filepath: Path | None = None) -> None: """Update the "Date Created" and "Date Modified" file property labels.""" @@ -142,6 +144,18 @@ def update_stats(self, filepath: Path | None = None, ext: str = ".", stats: dict self.dimensions_label.setText("") self.dimensions_label.setHidden(True) else: + filepath_option = self.driver.settings.value( + SettingItems.SHOW_FILEPATH, defaultValue=ShowFilepathOption.DEFAULT.value, type=int + ) + self.library_path = self.library.library_dir + display_path = filepath + if filepath_option == ShowFilepathOption.SHOW_FULL_PATHS: + display_path = filepath + elif filepath_option == ShowFilepathOption.SHOW_RELATIVE_PATHS: + display_path = Path(filepath).relative_to(self.library_path) + elif filepath_option == ShowFilepathOption.SHOW_FILENAMES_ONLY: + display_path = Path(filepath.name) + self.layout().setSpacing(6) self.file_label.setAlignment(Qt.AlignmentFlag.AlignLeft) self.file_label.set_file_path(filepath) @@ -149,12 +163,14 @@ def update_stats(self, filepath: Path | None = None, ext: str = ".", stats: dict file_str: str = "" separator: str = f"{os.path.sep}" # Gray - for i, part in enumerate(filepath.parts): + for i, part in enumerate(display_path.parts): part_ = part.strip(os.path.sep) - if i != len(filepath.parts) - 1: - file_str += f"{'\u200b'.join(part_)}{separator}" + if i != len(display_path.parts) - 1: + file_str += f"{"\u200b".join(part_)}{separator}" else: - file_str += f"
{'\u200b'.join(part_)}" + if file_str != "": + file_str += "
" + file_str += f"{"\u200b".join(part_)}" self.file_label.setText(file_str) self.file_label.setCursor(Qt.CursorShape.PointingHandCursor) self.opener = FileOpenerHelper(filepath) diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index d1dc2caa2..4bec8a99a 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -218,6 +218,10 @@ "edit.copy_fields": "Copy Fields", "edit.paste_fields": "Paste Fields", "settings.clear_thumb_cache.title": "Clear Thumbnail Cache", + "settings.filepath.label": "Filepath Visibility", + "settings.filepath.option.full": "Show Full Paths", + "settings.filepath.option.name": "Show Filenames Only", + "settings.filepath.option.relative": "Show Relative Paths", "settings.language": "Language", "settings.open_library_on_start": "Open Library on Start", "settings.restart_required": "Please restart TagStudio for changes to take effect.", diff --git a/tests/qt/test_file_path_options.py b/tests/qt/test_file_path_options.py new file mode 100644 index 000000000..642bcaa84 --- /dev/null +++ b/tests/qt/test_file_path_options.py @@ -0,0 +1,130 @@ +import os +import pathlib +from unittest.mock import patch + +import pytest +from PySide6.QtGui import ( + QAction, +) +from PySide6.QtWidgets import QMenu, QMenuBar + +from tagstudio.core.enums import SettingItems, ShowFilepathOption +from tagstudio.core.library.alchemy.library import LibraryStatus +from tagstudio.qt.modals.settings_panel import SettingsPanel +from tagstudio.qt.widgets.preview_panel import PreviewPanel + + +# Tests to see if the file path setting is applied correctly +@pytest.mark.parametrize( + "filepath_option", + [ + ShowFilepathOption.SHOW_FULL_PATHS.value, + ShowFilepathOption.SHOW_RELATIVE_PATHS.value, + ShowFilepathOption.SHOW_FILENAMES_ONLY.value, + ], +) +def test_filepath_setting(qtbot, qt_driver, filepath_option): + settings_panel = SettingsPanel(qt_driver) + qtbot.addWidget(settings_panel) + + # Mock the update_recent_lib_menu method + with patch.object(qt_driver, "update_recent_lib_menu", return_value=None): + # Set the file path option + settings_panel.filepath_combobox.setCurrentIndex(filepath_option) + settings_panel.apply_filepath_setting() + + # Assert the setting is applied + assert qt_driver.settings.value(SettingItems.SHOW_FILEPATH) == filepath_option + + +# Tests to see if the file paths are being displayed correctly +@pytest.mark.parametrize( + "filepath_option, expected_path", + [ + ( + ShowFilepathOption.SHOW_FULL_PATHS, + lambda library: pathlib.Path(library.library_dir / "one/two/bar.md"), + ), + (ShowFilepathOption.SHOW_RELATIVE_PATHS, lambda library: pathlib.Path("one/two/bar.md")), + (ShowFilepathOption.SHOW_FILENAMES_ONLY, lambda library: pathlib.Path("bar.md")), + ], +) +def test_file_path_display(qt_driver, library, filepath_option, expected_path): + panel = PreviewPanel(library, qt_driver) + + # Select 2 + qt_driver.toggle_item_selection(2, append=False, bridge=False) + panel.update_widgets() + + with patch.object(qt_driver.settings, "value", return_value=filepath_option): + # Apply the mock value + filename = library.get_entry(2).path + panel.file_attrs.update_stats(filepath=pathlib.Path(library.library_dir / filename)) + + # Generate the expected file string. + # This is copied directly from the file_attributes.py file + # can be imported as a function in the future + display_path = expected_path(library) + file_str: str = "" + separator: str = f"{os.path.sep}" # Gray + for i, part in enumerate(display_path.parts): + part_ = part.strip(os.path.sep) + if i != len(display_path.parts) - 1: + file_str += f"{"\u200b".join(part_)}{separator}" + else: + if file_str != "": + file_str += "
" + file_str += f"{"\u200b".join(part_)}" + + # Assert the file path is displayed correctly + assert panel.file_attrs.file_label.text() == file_str + + +@pytest.mark.parametrize( + "filepath_option, expected_title", + [ + ( + ShowFilepathOption.SHOW_FULL_PATHS.value, + lambda path, base_title: f"{base_title} - Library '{path}'", + ), + ( + ShowFilepathOption.SHOW_RELATIVE_PATHS.value, + lambda path, base_title: f"{base_title} - Library '{path.name}'", + ), + ( + ShowFilepathOption.SHOW_FILENAMES_ONLY.value, + lambda path, base_title: f"{base_title} - Library '{path.name}'", + ), + ], +) +def test_title_update(qtbot, qt_driver, filepath_option, expected_title): + base_title = qt_driver.base_title + test_path = pathlib.Path("/dev/null") + open_status = LibraryStatus( + success=True, + library_path=test_path, + message="", + msg_description="", + ) + # Set the file path option + qt_driver.settings.setValue(SettingItems.SHOW_FILEPATH, filepath_option) + menu_bar = QMenuBar() + + qt_driver.open_recent_library_menu = QMenu(menu_bar) + qt_driver.manage_file_ext_action = QAction(menu_bar) + qt_driver.save_library_backup_action = QAction(menu_bar) + qt_driver.close_library_action = QAction(menu_bar) + qt_driver.refresh_dir_action = QAction(menu_bar) + qt_driver.tag_manager_action = QAction(menu_bar) + qt_driver.color_manager_action = QAction(menu_bar) + qt_driver.new_tag_action = QAction(menu_bar) + qt_driver.fix_dupe_files_action = QAction(menu_bar) + qt_driver.fix_unlinked_entries_action = QAction(menu_bar) + qt_driver.clear_thumb_cache_action = QAction(menu_bar) + qt_driver.folders_to_tags_action = QAction(menu_bar) + + # Trigger the update + qt_driver.init_library(pathlib.Path(test_path), open_status) + + # Assert the title is updated correctly + qt_driver.main_window.setWindowTitle.assert_called_with(expected_title(test_path, base_title))