feat: dynamically load macros into menu

This commit is contained in:
Travis Abendshien
2025-03-12 23:44:09 -07:00
parent 4675bed373
commit 4a60637202
5 changed files with 111 additions and 32 deletions

View File

@@ -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):

View File

@@ -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(

View File

@@ -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:

View File

@@ -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.

View File

@@ -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]