diff --git a/tagstudio/src/qt/modals/drop_import.py b/tagstudio/src/qt/modals/drop_import.py index 9b13b3d11..56babeb9b 100644 --- a/tagstudio/src/qt/modals/drop_import.py +++ b/tagstudio/src/qt/modals/drop_import.py @@ -182,7 +182,7 @@ def displayed_text(x): pw.from_iterable_function( self.copy_files, displayed_text, - self.driver.add_new_files_callback, + self.driver.refresh_directories, self.deleteLater, ) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index b6e36ae4a..77acbdbc3 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -14,7 +14,6 @@ import re import sys import time -import webbrowser from pathlib import Path from queue import Queue @@ -22,10 +21,8 @@ import src.qt.resources_rc # noqa: F401 import structlog from humanfriendly import format_timespan -from PySide6 import QtCore from PySide6.QtCore import QObject, QSettings, Qt, QThread, QThreadPool, QTimer, Signal from PySide6.QtGui import ( - QAction, QColor, QDragEnterEvent, QDragMoveEvent, @@ -42,8 +39,6 @@ QComboBox, QFileDialog, QLineEdit, - QMenu, - QMenuBar, QMessageBox, QPushButton, QScrollArea, @@ -75,16 +70,12 @@ from src.qt.helpers.custom_runnable import CustomRunnable from src.qt.helpers.function_iterator import FunctionIterator from src.qt.main_window import Ui_MainWindow -from src.qt.modals.build_tag import BuildTagPanel from src.qt.modals.drop_import import DropImportModal -from src.qt.modals.file_extension import FileExtensionModal -from src.qt.modals.fix_dupes import FixDupeFilesModal -from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal -from src.qt.modals.folders_to_tags import FoldersToTagsModal from src.qt.modals.tag_database import TagDatabasePanel from src.qt.resource_manager import ResourceManager from src.qt.translations import Translations from src.qt.widgets.item_thumb import BadgeType, ItemThumb +from src.qt.widgets.menu_bar import MenuBar from src.qt.widgets.migration_modal import JsonMigrationModal from src.qt.widgets.panel import PanelModal from src.qt.widgets.preview_panel import PreviewPanel @@ -188,7 +179,7 @@ def init_workers(self): self.thumb_threads.append(thread) thread.start() - def open_library_from_dialog(self): + def open_create_library_modal(self): dir = QFileDialog.getExistingDirectory( parent=None, caption=Translations["window.title.open_create_library"], @@ -267,228 +258,26 @@ def start(self) -> None: icon.addFile(str(icon_path)) app.setWindowIcon(icon) - menu_bar = QMenuBar(self.main_window) - self.main_window.setMenuBar(menu_bar) - menu_bar.setNativeMenuBar(True) - - file_menu = QMenu(menu_bar) - Translations.translate_qobject(file_menu, "menu.file") - edit_menu = QMenu(menu_bar) - Translations.translate_qobject(edit_menu, "generic.edit_alt") - view_menu = QMenu(menu_bar) - Translations.translate_qobject(view_menu, "menu.view") - tools_menu = QMenu(menu_bar) - Translations.translate_qobject(tools_menu, "menu.tools") - macros_menu = QMenu(menu_bar) - Translations.translate_qobject(macros_menu, "menu.macros") - help_menu = QMenu(menu_bar) - Translations.translate_qobject(help_menu, "menu.help") - - # File Menu ============================================================ - open_library_action = QAction(menu_bar) - Translations.translate_qobject(open_library_action, "menu.file.open_create_library") - open_library_action.triggered.connect(lambda: self.open_library_from_dialog()) - open_library_action.setShortcut( - QtCore.QKeyCombination( - QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), - QtCore.Qt.Key.Key_O, - ) - ) - open_library_action.setToolTip("Ctrl+O") - file_menu.addAction(open_library_action) - - self.open_recent_library_menu = QMenu(menu_bar) - Translations.translate_qobject( - self.open_recent_library_menu, "menu.file.open_recent_library" - ) - file_menu.addMenu(self.open_recent_library_menu) - self.update_recent_lib_menu() - - open_on_start_action = QAction(self) - Translations.translate_qobject(open_on_start_action, "settings.open_library_on_start") - open_on_start_action.setCheckable(True) - open_on_start_action.setChecked( - bool(self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool)) - ) - open_on_start_action.triggered.connect( - lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked) - ) - file_menu.addAction(open_on_start_action) - - file_menu.addSeparator() - - save_library_backup_action = QAction(menu_bar) - Translations.translate_qobject(save_library_backup_action, "menu.file.save_backup") - save_library_backup_action.triggered.connect( - lambda: self.callback_library_needed_check(self.backup_library) - ) - save_library_backup_action.setShortcut( - QtCore.QKeyCombination( - QtCore.Qt.KeyboardModifier( - QtCore.Qt.KeyboardModifier.ControlModifier - | QtCore.Qt.KeyboardModifier.ShiftModifier - ), - QtCore.Qt.Key.Key_S, - ) - ) - save_library_backup_action.setStatusTip("Ctrl+Shift+S") - file_menu.addAction(save_library_backup_action) - - file_menu.addSeparator() - - add_new_files_action = QAction(menu_bar) - Translations.translate_qobject(add_new_files_action, "menu.file.refresh_directories") - add_new_files_action.triggered.connect( - lambda: self.callback_library_needed_check(self.add_new_files_callback) - ) - add_new_files_action.setShortcut( - QtCore.QKeyCombination( - QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), - QtCore.Qt.Key.Key_R, - ) - ) - add_new_files_action.setStatusTip("Ctrl+R") - file_menu.addAction(add_new_files_action) - file_menu.addSeparator() - - close_library_action = QAction(menu_bar) - Translations.translate_qobject(close_library_action, "menu.file.close_library") - close_library_action.triggered.connect(self.close_library) - file_menu.addAction(close_library_action) - file_menu.addSeparator() - - # Edit Menu ============================================================ - new_tag_action = QAction(menu_bar) - Translations.translate_qobject(new_tag_action, "menu.edit.new_tag") - new_tag_action.triggered.connect(lambda: self.add_tag_action_callback()) - new_tag_action.setShortcut( - QtCore.QKeyCombination( - QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), - QtCore.Qt.Key.Key_T, - ) - ) - new_tag_action.setToolTip("Ctrl+T") - edit_menu.addAction(new_tag_action) - - edit_menu.addSeparator() - - select_all_action = QAction(menu_bar) - Translations.translate_qobject(select_all_action, "select.all") - select_all_action.triggered.connect(self.select_all_action_callback) - select_all_action.setShortcut( - QtCore.QKeyCombination( - QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), - QtCore.Qt.Key.Key_A, - ) - ) - select_all_action.setToolTip("Ctrl+A") - edit_menu.addAction(select_all_action) - - clear_select_action = QAction(menu_bar) - Translations.translate_qobject(clear_select_action, "select.clear") - clear_select_action.triggered.connect(self.clear_select_action_callback) - clear_select_action.setShortcut(QtCore.Qt.Key.Key_Escape) - clear_select_action.setToolTip("Esc") - edit_menu.addAction(clear_select_action) - - edit_menu.addSeparator() - - manage_file_extensions_action = QAction(menu_bar) - Translations.translate_qobject( - manage_file_extensions_action, "menu.edit.manage_file_extensions" - ) - manage_file_extensions_action.triggered.connect(self.show_file_extension_modal) - edit_menu.addAction(manage_file_extensions_action) - - tag_database_action = QAction(menu_bar) - Translations.translate_qobject(tag_database_action, "menu.edit.manage_tags") - tag_database_action.triggered.connect(lambda: self.show_tag_database()) - edit_menu.addAction(tag_database_action) - - # View Menu ============================================================ - show_libs_list_action = QAction(menu_bar) - Translations.translate_qobject(show_libs_list_action, "settings.show_recent_libraries") - show_libs_list_action.setCheckable(True) - show_libs_list_action.setChecked( - bool(self.settings.value(SettingItems.WINDOW_SHOW_LIBS, defaultValue=True, type=bool)) - ) - - show_filenames_action = QAction(menu_bar) - Translations.translate_qobject(show_filenames_action, "settings.show_filenames_in_grid") - show_filenames_action.setCheckable(True) - show_filenames_action.setChecked( - bool(self.settings.value(SettingItems.SHOW_FILENAMES, defaultValue=True, type=bool)) - ) - show_filenames_action.triggered.connect( - lambda checked: ( - self.settings.setValue(SettingItems.SHOW_FILENAMES, checked), - self.show_grid_filenames(checked), - ) - ) - view_menu.addAction(show_filenames_action) - - # Tools Menu =========================================================== - def create_fix_unlinked_entries_modal(): - if not hasattr(self, "unlinked_modal"): - self.unlinked_modal = FixUnlinkedEntriesModal(self.lib, self) - self.unlinked_modal.show() - - fix_unlinked_entries_action = QAction(menu_bar) - Translations.translate_qobject( - fix_unlinked_entries_action, "menu.tools.fix_unlinked_entries" - ) - fix_unlinked_entries_action.triggered.connect(create_fix_unlinked_entries_modal) - tools_menu.addAction(fix_unlinked_entries_action) - - def create_dupe_files_modal(): - if not hasattr(self, "dupe_modal"): - self.dupe_modal = FixDupeFilesModal(self.lib, self) - self.dupe_modal.show() - - fix_dupe_files_action = QAction(menu_bar) - Translations.translate_qobject(fix_dupe_files_action, "menu.tools.fix_duplicate_files") - fix_dupe_files_action.triggered.connect(create_dupe_files_modal) - tools_menu.addAction(fix_dupe_files_action) - - # create_collage_action = QAction("Create Collage", menu_bar) - # create_collage_action.triggered.connect(lambda: self.create_collage()) - # tools_menu.addAction(create_collage_action) - - # Macros Menu ========================================================== - self.autofill_action = QAction("Autofill", menu_bar) - self.autofill_action.triggered.connect( + self.menu_bar = MenuBar(self.main_window, self.settings, self.lib, self) + self.menu_bar.create_library_modal_signal.connect(self.open_create_library_modal) + self.menu_bar.open_library_signal.connect(self.open_library) + self.menu_bar.backup_library_signal.connect(self.backup_library) + self.menu_bar.refresh_directories_signal.connect(self.refresh_directories) + self.menu_bar.close_library_signal.connect(self.close_library) + self.menu_bar.select_all_items_signal.connect(self.select_all_items) + self.menu_bar.clear_selection_signal.connect(self.clear_selection) + self.menu_bar.filter_items_signal.connect(self.filter_items) + self.menu_bar.tag_database_modal_signal.connect(self.open_tag_database_modal) + self.menu_bar.show_grid_filenames_signal.connect(self.show_grid_filenames) + self.menu_bar.autofill_macro_signal.connect( lambda: ( self.run_macros(MacroID.AUTOFILL, self.selected), self.preview_panel.update_widgets(), ) ) - macros_menu.addAction(self.autofill_action) - - def create_folders_tags_modal(): - if not hasattr(self, "folders_modal"): - self.folders_modal = FoldersToTagsModal(self.lib, self) - self.folders_modal.show() - - folders_to_tags_action = QAction(menu_bar) - Translations.translate_qobject(folders_to_tags_action, "menu.macros.folders_to_tags") - folders_to_tags_action.triggered.connect(create_folders_tags_modal) - macros_menu.addAction(folders_to_tags_action) - - # Help Menu ============================================================ - self.repo_action = QAction(menu_bar) - Translations.translate_qobject(self.repo_action, "help.visit_github") - self.repo_action.triggered.connect( - lambda: webbrowser.open("https://github.com/TagStudioDev/TagStudio") - ) - help_menu.addAction(self.repo_action) - self.set_macro_menu_viability() - - menu_bar.addMenu(file_menu) - menu_bar.addMenu(edit_menu) - menu_bar.addMenu(view_menu) - menu_bar.addMenu(tools_menu) - menu_bar.addMenu(macros_menu) - menu_bar.addMenu(help_menu) + self.menu_bar.set_macro_actions_disabled(not self.selected) + self.main_window.setMenuBar(self.menu_bar) + self.menu_bar.set_library_actions_disabled(True) self.main_window.searchField.textChanged.connect(self.update_completions_list) @@ -632,11 +421,6 @@ def show_grid_filenames(self, value: bool): for thumb in self.item_thumbs: thumb.set_filename_visibility(value) - def callback_library_needed_check(self, func): - """Check if loaded library has valid path before executing the button function.""" - if self.lib.library_dir: - func() - def handle_sigterm(self): self.shutdown() @@ -667,6 +451,7 @@ def close_library(self, is_shutdown: bool = False): self.settings.sync() self.lib.close() + self.menu_bar.set_library_actions_disabled(True) self.thumb_job_queue.queue.clear() if is_shutdown: @@ -692,6 +477,8 @@ def close_library(self, is_shutdown: bool = False): ) def backup_library(self): + if not self.lib.library_dir: + return logger.info("Backing Up Library...") self.main_window.statusbar.showMessage(Translations["status.library_backup_in_progress"]) start_time = time.time() @@ -705,29 +492,7 @@ def backup_library(self): ) ) - def add_tag_action_callback(self): - panel = BuildTagPanel(self.lib) - self.modal = PanelModal( - panel, - has_save=True, - ) - Translations.translate_with_setter(self.modal.setTitle, "tag.new") - Translations.translate_with_setter(self.modal.setWindowTitle, "tag.add") - - self.modal.saved.connect( - lambda: ( - self.lib.add_tag( - panel.build_tag(), - set(panel.parent_ids), - set(panel.alias_names), - set(panel.alias_ids), - ), - self.modal.hide(), - ) - ) - self.modal.show() - - def select_all_action_callback(self): + def select_all_items(self): """Set the selection to all visible items.""" self.selected.clear() for item in self.item_thumbs: @@ -735,18 +500,18 @@ def select_all_action_callback(self): self.selected.append(item.item_id) item.thumb_button.set_selected(True) - self.set_macro_menu_viability() + self.menu_bar.set_macro_actions_disabled(not self.selected) self.preview_panel.update_widgets() - def clear_select_action_callback(self): + def clear_selection(self): self.selected.clear() for item in self.item_thumbs: item.thumb_button.set_selected(False) - self.set_macro_menu_viability() + self.menu_bar.set_macro_actions_disabled(not self.selected) self.preview_panel.update_widgets() - def show_tag_database(self): + def open_tag_database_modal(self): self.modal = PanelModal( widget=TagDatabasePanel(self.lib), done_callback=self.preview_panel.update_widgets, @@ -756,20 +521,11 @@ def show_tag_database(self): Translations.translate_with_setter(self.modal.setWindowTitle, "tag_manager.title") self.modal.show() - def show_file_extension_modal(self): - panel = FileExtensionModal(self.lib) - self.modal = PanelModal( - panel, - has_save=True, - ) - Translations.translate_with_setter(self.modal.setTitle, "ignore_list.title") - Translations.translate_with_setter(self.modal.setWindowTitle, "ignore_list.title") - - self.modal.saved.connect(lambda: (panel.save(), self.filter_items())) - self.modal.show() - - def add_new_files_callback(self): + def refresh_directories(self): """Run when user initiates adding new files to the Library.""" + if not self.lib.library_dir: + return + tracker = RefreshDirTracker(self.lib) pw = ProgressWidget( @@ -1086,12 +842,9 @@ def toggle_item_selection(self, item_id: int, append: bool, bridge: bool): else: it.thumb_button.set_selected(False) - self.set_macro_menu_viability() + self.menu_bar.set_macro_actions_disabled(not self.selected) self.preview_panel.update_widgets() - def set_macro_menu_viability(self): - self.autofill_action.setDisabled(not self.selected) - def update_completions_list(self, text: str) -> None: matches = re.search( r"((?:.* )?)(mediatype|filetype|path|tag|tag_id):(\"?[A-Za-z0-9\ \t]+\"?)?", text @@ -1309,12 +1062,6 @@ def filter_items(self, filter: FilterState | None = None) -> None: self.pages_count, self.filter.page_index, emit=False ) - def remove_recent_library(self, item_key: str): - self.settings.beginGroup(SettingItems.LIBS_LIST) - self.settings.remove(item_key) - self.settings.endGroup() - self.settings.sync() - def update_libs_list(self, path: Path | str): """Add library to list in SettingItems.LIBS_LIST.""" item_limit: int = 5 @@ -1340,63 +1087,7 @@ def update_libs_list(self, path: Path | str): self.settings.endGroup() self.settings.sync() - self.update_recent_lib_menu() - - 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]] = {} - - settings = self.settings - settings.beginGroup(SettingItems.LIBS_LIST) - for item_tstamp in settings.allKeys(): - val = str(settings.value(item_tstamp, type=str)) - cut_val = val - if len(val) > 45: - cut_val = f"{val[0:10]} ... {val[-10:]}" - lib_items[item_tstamp] = (val, cut_val) - - # Sort lib_items by the key - libs_sorted = sorted(lib_items.items(), key=lambda item: item[0], reverse=True) - settings.endGroup() - - # Create actions for each library - for library_key in libs_sorted: - path = Path(library_key[1][0]) - action = QAction(self.open_recent_library_menu) - action.setText(str(path)) - action.triggered.connect(lambda checked=False, p=path: self.open_library(p)) - actions.append(action) - - clear_recent_action = QAction(self.open_recent_library_menu) - Translations.translate_qobject(clear_recent_action, "menu.file.clear_recent_libraries") - clear_recent_action.triggered.connect(self.clear_recent_libs) - actions.append(clear_recent_action) - - # Clear previous actions - for action in self.open_recent_library_menu.actions(): - self.open_recent_library_menu.removeAction(action) - - # Add new actions - for action in actions: - self.open_recent_library_menu.addAction(action) - - # Only enable add "clear recent" if there are still recent libraries. - if len(actions) > 1: - self.open_recent_library_menu.setDisabled(False) - self.open_recent_library_menu.addSeparator() - self.open_recent_library_menu.addAction(clear_recent_action) - else: - self.open_recent_library_menu.setDisabled(True) - - def clear_recent_libs(self): - """Clear the list of recent libraries from the settings file.""" - settings = self.settings - settings.beginGroup(SettingItems.LIBS_LIST) - self.settings.remove("") - self.settings.endGroup() - self.settings.sync() - self.update_recent_lib_menu() + self.menu_bar.update_recent_lib_menu() def open_library(self, path: Path) -> None: """Open a TagStudio library.""" @@ -1440,7 +1131,7 @@ def init_library(self, path: Path, open_status: LibraryStatus): # TODO - make this call optional if self.lib.entries_count < 10000: - self.add_new_files_callback() + self.refresh_directories() self.update_libs_list(path) Translations.translate_with_setter( @@ -1458,6 +1149,7 @@ def init_library(self, path: Path, open_status: LibraryStatus): self.filter_items() self.main_window.toggle_landing_page(enabled=False) + self.menu_bar.set_library_actions_disabled(False) return open_status def drop_event(self, event: QDropEvent): diff --git a/tagstudio/src/qt/widgets/landing.py b/tagstudio/src/qt/widgets/landing.py index 3f6b15942..aa49107b1 100644 --- a/tagstudio/src/qt/widgets/landing.py +++ b/tagstudio/src/qt/widgets/landing.py @@ -66,7 +66,7 @@ def __init__(self, driver: "QtDriver", pixel_ratio: float): Translations.translate_qobject( self.open_button, "landing.open_create_library", shortcut=open_shortcut_text ) - self.open_button.clicked.connect(self.driver.open_library_from_dialog) + self.open_button.clicked.connect(self.driver.open_create_library_modal) # Create status label -------------------------------------------------- self.status_label = QLabel() diff --git a/tagstudio/src/qt/widgets/menu_bar.py b/tagstudio/src/qt/widgets/menu_bar.py new file mode 100644 index 000000000..0c2ecc24b --- /dev/null +++ b/tagstudio/src/qt/widgets/menu_bar.py @@ -0,0 +1,375 @@ +import webbrowser +from pathlib import Path + +from PySide6 import QtCore +from PySide6.QtCore import Signal +from PySide6.QtGui import ( + QAction, +) +from PySide6.QtWidgets import ( + QMenu, + QMenuBar, +) +from src.core.enums import SettingItems +from src.qt.modals.build_tag import BuildTagPanel +from src.qt.modals.file_extension import FileExtensionModal +from src.qt.modals.fix_dupes import FixDupeFilesModal +from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal +from src.qt.modals.folders_to_tags import FoldersToTagsModal +from src.qt.translations import Translations +from src.qt.widgets.panel import PanelModal + + +class MenuBar(QMenuBar): + """Menubar for the main window.""" + + create_library_modal_signal = Signal() + open_library_signal = Signal(Path) + backup_library_signal = Signal() + refresh_directories_signal = Signal() + close_library_signal = Signal() + select_all_items_signal = Signal() + clear_selection_signal = Signal() + filter_items_signal = Signal() + tag_database_modal_signal = Signal() + show_grid_filenames_signal = Signal(bool) + autofill_macro_signal = Signal() + + def __init__(self, parent, settings, lib, driver): + super().__init__(parent) + self.settings = settings + self.lib = lib + self.driver = driver + + self.setNativeMenuBar(True) + + self.file_menu = QMenu(self) + Translations.translate_qobject(self.file_menu, "menu.file") + self.edit_menu = QMenu(self) + Translations.translate_qobject(self.edit_menu, "generic.edit_alt") + self.view_menu = QMenu(self) + Translations.translate_qobject(self.view_menu, "menu.view") + self.tools_menu = QMenu(self) + Translations.translate_qobject(self.tools_menu, "menu.tools") + self.macros_menu = QMenu(self) + Translations.translate_qobject(self.macros_menu, "menu.macros") + self.help_menu = QMenu(self) + Translations.translate_qobject(self.help_menu, "menu.help") + + # File Menu ============================================================ + self.open_library_action = QAction(self) + Translations.translate_qobject(self.open_library_action, "menu.file.open_create_library") + self.open_library_action.triggered.connect(lambda: self.create_library_modal_signal.emit()) + + self.open_library_action.setShortcut( + QtCore.QKeyCombination( + QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), + QtCore.Qt.Key.Key_O, + ) + ) + self.open_library_action.setToolTip("Ctrl+O") + self.file_menu.addAction(self.open_library_action) + + self.open_recent_action = QMenu(self) + Translations.translate_qobject(self.open_recent_action, "menu.file.open_recent_library") + self.file_menu.addMenu(self.open_recent_action) + self.update_recent_lib_menu() + + self.open_on_start_action = QAction(self) + Translations.translate_qobject(self.open_on_start_action, "settings.open_library_on_start") + self.open_on_start_action.setCheckable(True) + self.open_on_start_action.setChecked( + bool(self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool)) + ) + self.open_on_start_action.triggered.connect( + lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked) + ) + self.file_menu.addAction(self.open_on_start_action) + + self.file_menu.addSeparator() + + self.save_library_backup_action = QAction(self) + Translations.translate_qobject(self.save_library_backup_action, "menu.file.save_backup") + self.save_library_backup_action.triggered.connect(lambda: self.backup_library_signal.emit()) + self.save_library_backup_action.setShortcut( + QtCore.QKeyCombination( + QtCore.Qt.KeyboardModifier( + QtCore.Qt.KeyboardModifier.ControlModifier + | QtCore.Qt.KeyboardModifier.ShiftModifier + ), + QtCore.Qt.Key.Key_S, + ) + ) + self.save_library_backup_action.setStatusTip("Ctrl+Shift+S") + self.file_menu.addAction(self.save_library_backup_action) + + self.file_menu.addSeparator() + + self.refresh_directories_action = QAction(self) + Translations.translate_qobject( + self.refresh_directories_action, "menu.file.refresh_directories" + ) + self.refresh_directories_action.triggered.connect( + lambda: self.refresh_directories_signal.emit() + ) + self.refresh_directories_action.setShortcut( + QtCore.QKeyCombination( + QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), + QtCore.Qt.Key.Key_R, + ) + ) + self.refresh_directories_action.setStatusTip("Ctrl+R") + self.file_menu.addAction(self.refresh_directories_action) + self.file_menu.addSeparator() + + self.close_library_action = QAction(self) + Translations.translate_qobject(self.close_library_action, "menu.file.close_library") + self.close_library_action.triggered.connect(lambda: self.close_library_signal.emit()) + self.file_menu.addAction(self.close_library_action) + self.file_menu.addSeparator() + + # Edit Menu ============================================================ + self.new_tag_action = QAction(self) + Translations.translate_qobject(self.new_tag_action, "menu.edit.new_tag") + self.new_tag_action.triggered.connect(self._open_add_tag_modal) + self.new_tag_action.setShortcut( + QtCore.QKeyCombination( + QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), + QtCore.Qt.Key.Key_T, + ) + ) + self.new_tag_action.setToolTip("Ctrl+T") + self.edit_menu.addAction(self.new_tag_action) + + self.edit_menu.addSeparator() + + self.select_all_action = QAction(self) + Translations.translate_qobject(self.select_all_action, "select.all") + self.select_all_action.triggered.connect(lambda: self.select_all_items_signal.emit()) + self.select_all_action.setShortcut( + QtCore.QKeyCombination( + QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), + QtCore.Qt.Key.Key_A, + ) + ) + self.select_all_action.setToolTip("Ctrl+A") + self.edit_menu.addAction(self.select_all_action) + + self.clear_select_action = QAction(self) + Translations.translate_qobject(self.clear_select_action, "select.clear") + self.clear_select_action.triggered.connect(lambda: self.clear_selection_signal.emit()) + self.clear_select_action.setShortcut(QtCore.Qt.Key.Key_Escape) + self.clear_select_action.setToolTip("Esc") + self.edit_menu.addAction(self.clear_select_action) + + self.edit_menu.addSeparator() + + self.manage_file_extensions_action = QAction(self) + Translations.translate_qobject( + self.manage_file_extensions_action, "menu.edit.manage_file_extensions" + ) + self.manage_file_extensions_action.triggered.connect(self._open_file_extension_modal) + self.edit_menu.addAction(self.manage_file_extensions_action) + + self.tag_database_action = QAction(self) + Translations.translate_qobject(self.tag_database_action, "menu.edit.manage_tags") + self.tag_database_action.triggered.connect(lambda: self.tag_database_modal_signal.emit()) + self.edit_menu.addAction(self.tag_database_action) + + # View Menu ============================================================ + self.show_libs_list_action = QAction(self) + Translations.translate_qobject(self.show_libs_list_action, "settings.show_recent_libraries") + self.show_libs_list_action.setCheckable(True) + self.show_libs_list_action.setChecked( + bool(self.settings.value(SettingItems.WINDOW_SHOW_LIBS, defaultValue=True, type=bool)) + ) + self.view_menu.addAction(self.show_libs_list_action) + + self.show_filenames_action = QAction(self) + Translations.translate_qobject( + self.show_filenames_action, "settings.show_filenames_in_grid" + ) + self.show_filenames_action.setCheckable(True) + self.show_filenames_action.setChecked( + bool(self.settings.value(SettingItems.SHOW_FILENAMES, defaultValue=True, type=bool)) + ) + self.show_filenames_action.triggered.connect( + lambda checked: ( + self.settings.setValue(SettingItems.SHOW_FILENAMES, checked), + self.show_grid_filenames_signal.emit(checked), + ) + ) + self.view_menu.addAction(self.show_filenames_action) + + # Tools Menu =========================================================== + self.fix_unlinked_entries_action = QAction(self) + Translations.translate_qobject( + self.fix_unlinked_entries_action, "menu.tools.fix_unlinked_entries" + ) + self.fix_unlinked_entries_action.triggered.connect(self._open_fix_unlinked_entries_modal) + self.tools_menu.addAction(self.fix_unlinked_entries_action) + + self.fix_dupe_files_action = QAction(self) + Translations.translate_qobject(self.fix_dupe_files_action, "menu.tools.fix_duplicate_files") + self.fix_dupe_files_action.triggered.connect(self._open_dupe_files_modal) + self.tools_menu.addAction(self.fix_dupe_files_action) + + # create_collage_action = QAction("Create Collage", self) + # create_collage_action.triggered.connect(lambda: self.create_collage()) + # tools_menu.addAction(create_collage_action) + + # Macros Menu ========================================================== + self.autofill_action = QAction("Autofill", self) + self.autofill_action.triggered.connect(lambda: self.autofill_macro_signal.emit()) + self.macros_menu.addAction(self.autofill_action) + + self.folders_to_tags_action = QAction(self) + Translations.translate_qobject(self.folders_to_tags_action, "menu.macros.folders_to_tags") + self.folders_to_tags_action.triggered.connect(self._open_folders_to_tags_modal) + self.macros_menu.addAction(self.folders_to_tags_action) + + # Help Menu ============================================================ + self.repo_action = QAction(self) + Translations.translate_qobject(self.repo_action, "help.visit_github") + self.repo_action.triggered.connect( + lambda: webbrowser.open("https://github.com/TagStudioDev/TagStudio") + ) + self.help_menu.addAction(self.repo_action) + + self.addMenu(self.file_menu) + self.addMenu(self.edit_menu) + self.addMenu(self.view_menu) + self.addMenu(self.tools_menu) + self.addMenu(self.macros_menu) + self.addMenu(self.help_menu) + + def _open_folders_to_tags_modal(self): + if not hasattr(self, "folders_modal"): + self.folders_modal = FoldersToTagsModal(self.lib, self.driver) + self.folders_modal.show() + + def _open_add_tag_modal(self): + panel = BuildTagPanel(self.lib) + self.modal = PanelModal( + panel, + has_save=True, + ) + Translations.translate_with_setter(self.modal.setTitle, "tag.new") + Translations.translate_with_setter(self.modal.setWindowTitle, "tag.add") + + self.modal.saved.connect( + lambda: ( + self.lib.add_tag( + panel.build_tag(), + set(panel.parent_ids), + set(panel.alias_names), + set(panel.alias_ids), + ), + self.modal.hide(), + ) + ) + self.modal.show() + + def _open_file_extension_modal(self): + panel = FileExtensionModal(self.lib) + self.modal = PanelModal( + panel, + has_save=True, + ) + Translations.translate_with_setter(self.modal.setTitle, "ignore_list.title") + Translations.translate_with_setter(self.modal.setWindowTitle, "ignore_list.title") + + self.modal.saved.connect(lambda: (panel.save(), self.filter_items_signal.emit())) + self.modal.show() + + def _open_fix_unlinked_entries_modal(self): + if not hasattr(self, "unlinked_modal"): + self.unlinked_modal = FixUnlinkedEntriesModal(self.lib, self.driver) + self.unlinked_modal.show() + + def _open_dupe_files_modal(self): + if not hasattr(self, "dupe_modal"): + self.dupe_modal = FixDupeFilesModal(self.lib, self.driver) + self.dupe_modal.show() + + def _clear_recent_libs(self): + """Clear the list of recent libraries from the settings file.""" + settings = self.settings + settings.beginGroup(SettingItems.LIBS_LIST) + self.settings.remove("") + self.settings.endGroup() + self.settings.sync() + self.update_recent_lib_menu() + + def _remove_recent_library(self, item_key: str): + self.settings.beginGroup(SettingItems.LIBS_LIST) + self.settings.remove(item_key) + self.settings.endGroup() + self.settings.sync() + + def set_library_actions_disabled(self, value: bool): + actions: list[QAction] = [ + self.save_library_backup_action, + self.refresh_directories_action, + self.close_library_action, + self.new_tag_action, + self.select_all_action, + self.clear_select_action, + self.manage_file_extensions_action, + self.tag_database_action, + self.fix_unlinked_entries_action, + self.fix_dupe_files_action, + ] + for action in actions: + action.setDisabled(value) + + def set_macro_actions_disabled(self, value: bool): + self.autofill_action.setDisabled(value) + self.folders_to_tags_action.setDisabled(value) + + 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]] = {} + + settings = self.settings + settings.beginGroup(SettingItems.LIBS_LIST) + for item_tstamp in settings.allKeys(): + val = str(settings.value(item_tstamp, type=str)) + cut_val = val + if len(val) > 45: + cut_val = f"{val[0:10]} ... {val[-10:]}" + lib_items[item_tstamp] = (val, cut_val) + + # Sort lib_items by the key + libs_sorted = sorted(lib_items.items(), key=lambda item: item[0], reverse=True) + settings.endGroup() + + # Create actions for each library + for library_key in libs_sorted: + path = Path(library_key[1][0]) + action = QAction(self.open_recent_action) + action.setText(str(path)) + action.triggered.connect(lambda checked=False, p=path: self.open_library_signal.emit(p)) + actions.append(action) + + clear_recent_action = QAction(self.open_recent_action) + Translations.translate_qobject(clear_recent_action, "menu.file.clear_recent_libraries") + clear_recent_action.triggered.connect(self._clear_recent_libs) + actions.append(clear_recent_action) + + # Clear previous actions + for action in self.open_recent_action.actions(): + self.open_recent_action.removeAction(action) + + # Add new actions + for action in actions: + self.open_recent_action.addAction(action) + + # Only enable add "clear recent" if there are still recent libraries. + if len(actions) > 1: + self.open_recent_action.setDisabled(False) + self.open_recent_action.addSeparator() + self.open_recent_action.addAction(clear_recent_action) + else: + self.open_recent_action.setDisabled(True)