diff --git a/src/tagstudio/core/macro_parser.py b/src/tagstudio/core/macro_parser.py index d19dfe05..13969ab8 100644 --- a/src/tagstudio/core/macro_parser.py +++ b/src/tagstudio/core/macro_parser.py @@ -106,6 +106,37 @@ class AddTagInstruction(Instruction): return str(self.tag_strings) +def get_macro_name( + macro_path: Path, +) -> str: + """Return the name of a macro, as read from the file. + + Defaults to the filename if no name is declared or able to be read. + + Args: + macro_path (Path): The full path of the macro file. + """ + name = macro_path.name + logger.info("[MacroParser] Parsing Macro for Name", macro_path=macro_path) + + if not macro_path.exists(): + logger.error("[MacroParser] Macro path does not exist", macro_path=macro_path) + return name + + if not macro_path.exists(): + logger.error("[MacroParser] Filepath does not exist", macro_path=macro_path) + return name + + with open(macro_path) as f: + try: + macro = toml.load(f) + name = str(macro.get("name", name)) + except toml.TomlDecodeError as e: + logger.error("[MacroParser] Could not parse macro", macro_path=macro_path, error=e) + logger.info("[MacroParser] Macro Name:", name=name, macro_path=macro_path) + return name + + def parse_macro_file( macro_path: Path, filepath: Path, @@ -123,7 +154,7 @@ def parse_macro_file( logger.error("[MacroParser] Macro path does not exist", macro_path=macro_path) return results - if not macro_path.exists(): + if not filepath.exists(): logger.error("[MacroParser] Filepath does not exist", filepath=filepath) return results @@ -131,11 +162,7 @@ def parse_macro_file( try: macro = toml.load(f) except toml.TomlDecodeError as e: - logger.error( - "[MacroParser] Could not parse macro", - path=macro_path, - error=e, - ) + logger.error("[MacroParser] Could not parse macro", macro_path=macro_path, error=e) return results logger.info(macro) @@ -156,15 +183,15 @@ def parse_macro_file( logger.info(f"[MacroParser] Schema Version: {schema_ver}") # Load Triggers - triggers = macro[TRIGGERS] - if not isinstance(triggers, list): + triggers = macro.get(TRIGGERS) + if triggers and not isinstance(triggers, list): logger.error( f"[MacroParser] Incorrect type for {TRIGGERS}, expected list", triggers=triggers ) # Parse each action table for table_key in macro: - if table_key in {SCHEMA_VERSION, TRIGGERS}: + if table_key in {SCHEMA_VERSION, TRIGGERS, NAME}: continue logger.info("[MacroParser] Parsing Table", table_key=table_key) @@ -175,7 +202,7 @@ def parse_macro_file( source_filters: list[str] = table.get(SOURCE_FILER, []) conditions_met: bool = False if not source_filters: - logger.info('[MacroParser] No "{SOURCE_FILER}" provided') + conditions_met = True else: for filter_ in source_filters: if glob.globmatch(filepath, filter_, flags=glob.GLOBSTAR): diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 20e46af7..d7e218c3 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -21,12 +21,14 @@ from pathlib import Path from queue import Queue from shutil import which from typing import Generic, TypeVar +from unittest.mock import Mock from warnings import catch_warnings import structlog from humanfriendly import format_size, format_timespan from PySide6.QtCore import QObject, QSettings, Qt, QThread, QThreadPool, QTimer, Signal from PySide6.QtGui import ( + QAction, QColor, QDragEnterEvent, QDragMoveEvent, @@ -68,6 +70,7 @@ from tagstudio.core.library.refresh import RefreshTracker from tagstudio.core.macro_parser import ( Instruction, exec_instructions, + get_macro_name, parse_macro_file, ) from tagstudio.core.media_types import MediaCategories @@ -75,8 +78,6 @@ from tagstudio.core.query_lang.util import ParsingError from tagstudio.core.utils.types import unwrap from tagstudio.qt.cache_manager import CacheManager from tagstudio.qt.controllers.ffmpeg_missing_message_box import FfmpegMissingMessageBox - -# this import has side-effect of import PySide resources from tagstudio.qt.controllers.fix_ignored_modal_controller import FixIgnoredEntriesModal from tagstudio.qt.controllers.ignore_modal_controller import IgnoreModal from tagstudio.qt.controllers.library_info_window_controller import LibraryInfoWindow @@ -105,6 +106,7 @@ from tagstudio.qt.resource_manager import ResourceManager from tagstudio.qt.translations import Translations from tagstudio.qt.utils.custom_runnable import CustomRunnable from tagstudio.qt.utils.file_deleter import delete_file +from tagstudio.qt.utils.file_opener import open_file from tagstudio.qt.utils.function_iterator import FunctionIterator from tagstudio.qt.views.main_window import MainWindow from tagstudio.qt.views.panel_modal import PanelModal @@ -553,20 +555,8 @@ class QtDriver(DriverMixin, QObject): # endregion # region Macros Menu ========================================================== - - self.main_window.menu_bar.test_macro_1_action.triggered.connect( - lambda: ( - self.run_macros(self.main_window.menu_bar.test_macro_1, self.selected), - # self.main_window.preview_panel.update_widgets(update_preview=False), - ) - ) - - self.main_window.menu_bar.test_macro_2_action.triggered.connect( - lambda: ( - self.run_macros(self.main_window.menu_bar.test_macro_2, self.selected), - # self.preview_panel.update_widgets(update_preview=False), - ) - ) + self.main_window.menu_bar.macros_menu.aboutToShow.connect(self.update_macros_menu) + self.update_macros_menu() def create_folders_tags_modal(): if not hasattr(self, "folders_modal"): @@ -589,8 +579,6 @@ class QtDriver(DriverMixin, QObject): # endregion - # endregion - self.main_window.search_field.textChanged.connect(self.update_completions_list) self.main_window.preview_panel.field_containers_widget.archived_updated.connect( @@ -782,6 +770,7 @@ class QtDriver(DriverMixin, QObject): self.set_clipboard_menu_viability() self.set_select_actions_visibility() + self.update_macros_menu(clear=True) if hasattr(self, "library_info_window"): self.library_info_window.close() @@ -1114,6 +1103,7 @@ class QtDriver(DriverMixin, QObject): """Run a specific Macro on a group of given entry_ids.""" for entry_id in entry_ids: self.run_macro(macro_name, entry_id) + self.main_window.preview_panel.update_preview() def run_macro(self, macro_name: str, entry_id: int): """Run a specific Macro on an Entry given a Macro name.""" @@ -1268,8 +1258,11 @@ class QtDriver(DriverMixin, QObject): self.main_window.preview_panel.set_selection(self.selected) + # TODO: Remove? def set_macro_menu_viability(self): - self.main_window.menu_bar.test_macro_1_action.setDisabled(not self.selected) + # for action in self.macros_menu.actions(): + # action.setDisabled(not self.selected) + pass def set_clipboard_menu_viability(self): if len(self.selected) == 1: @@ -1543,6 +1536,53 @@ class QtDriver(DriverMixin, QObject): self.cached_values.sync() self.update_recent_lib_menu() + def update_macros_menu(self, clear: bool = False): + if not self.main_window.menu_bar.macros_menu or isinstance( + self.main_window.menu_bar.macros_menu, Mock + ): # NOTE: Needed for tests? + return + + # Create actions for each macro + actions: list[QAction] = [] + if self.lib.library_dir and not clear: + macros_path = self.lib.library_dir / TS_FOLDER_NAME / MACROS_FOLDER_NAME + for f in macros_path.glob("*"): + logger.info(f) + if f.suffix != ".toml" or f.is_dir() or f.name.startswith("._"): + continue + action = QAction( + get_macro_name(f), self.main_window.menu_bar.macros_menu.parentWidget() + ) + action.triggered.connect( + lambda checked=False, name=f.name: (self.run_macros(name, self.selected)), + ) + actions.append(action) + + open_folder = QAction("Open Macros Folder...", self.main_window.menu_bar.macros_menu) + open_folder.triggered.connect(self.open_macros_folder) + actions.append(open_folder) + + if clear: + open_folder.setEnabled(False) + + # Clear previous actions + for action in self.main_window.menu_bar.macros_menu.actions(): + self.main_window.menu_bar.macros_menu.removeAction(action) + + # Add new actions + for action in actions: + self.main_window.menu_bar.macros_menu.addAction(action) + + self.main_window.menu_bar.macros_menu.addSeparator() + self.main_window.menu_bar.macros_menu.addAction(open_folder) + + def open_macros_folder(self): + if not self.lib.library_dir: + return + path = self.lib.library_dir / TS_FOLDER_NAME / MACROS_FOLDER_NAME + path.mkdir(exist_ok=True) + open_file(path, file_manager=True, is_dir=True) + def open_settings_modal(self): SettingsPanel.build_modal(self).show() @@ -1625,6 +1665,7 @@ class QtDriver(DriverMixin, QObject): library_dir_display = self.lib.library_dir.name self.update_libs_list(path) + self.update_macros_menu() self.main_window.setWindowTitle( Translations.format( "app.title", @@ -1651,7 +1692,7 @@ class QtDriver(DriverMixin, QObject): self.main_window.menu_bar.folders_to_tags_action.setEnabled(True) self.main_window.menu_bar.library_info_action.setEnabled(True) - self.main_window.preview_panel.set_selection(self.selected) + self.main_window.preview_panel.set_selection() # page (re)rendering, extract eventually initial_state = BrowsingState( diff --git a/src/tagstudio/qt/utils/file_opener.py b/src/tagstudio/qt/utils/file_opener.py index e1b27c27..bbd31bd4 100644 --- a/src/tagstudio/qt/utils/file_opener.py +++ b/src/tagstudio/qt/utils/file_opener.py @@ -20,13 +20,19 @@ from tagstudio.core.utils.types import unwrap logger = structlog.get_logger(__name__) -def open_file(path: str | Path, file_manager: bool = False, windows_start_command: bool = False): +def open_file( + path: str | Path, + file_manager: bool = False, + is_dir: bool = False, + windows_start_command: bool = False, +): """Open a file in the default application or file explorer. Args: path (str): The path to the file to open. file_manager (bool, optional): Whether to open the file in the file manager (e.g. Finder on macOS). Defaults to False. + is_dir (bool): True if the path points towards a directory, false if a file. windows_start_command (bool): Flag to determine if the older 'start' command should be used on Windows for opening files. This fixes issues on some systems in niche cases. """ @@ -77,7 +83,7 @@ def open_file(path: str | Path, file_manager: bool = False, windows_start_comman if sys.platform == "darwin": command_name = "open" command_args = [str(path)] - if file_manager: + if file_manager and not is_dir: # will reveal in Finder command_args.append("-R") else: diff --git a/src/tagstudio/qt/views/preview_panel_view.py b/src/tagstudio/qt/views/preview_panel_view.py index 649fa8e3..1a5af1c6 100644 --- a/src/tagstudio/qt/views/preview_panel_view.py +++ b/src/tagstudio/qt/views/preview_panel_view.py @@ -133,6 +133,10 @@ class PreviewPanelView(QWidget): def _set_selection_callback(self): raise NotImplementedError() + def update_preview(self): + """Refresh the panel's widgets to use current library data.""" + self.set_selection(self._selected) + def set_selection(self, selected: list[int] | None = None, update_preview: bool = True): """Render the panel widgets with the newest data from the Library. diff --git a/tests/qt/test_file_path_options.py b/tests/qt/test_file_path_options.py index 412cd56f..535bbe6b 100644 --- a/tests/qt/test_file_path_options.py +++ b/tests/qt/test_file_path_options.py @@ -145,6 +145,7 @@ def test_title_update( qt_driver.main_window.menu_bar.fix_dupe_files_action = QAction(menu_bar) qt_driver.main_window.menu_bar.clear_thumb_cache_action = QAction(menu_bar) qt_driver.main_window.menu_bar.folders_to_tags_action = QAction(menu_bar) + qt_driver.main_window.menu_bar.macros_menu = None # Trigger the update qt_driver._init_library(library_dir, open_status) # pyright: ignore[reportPrivateUsage]