feat: dynamically load macros into menu

This commit is contained in:
Travis Abendshien
2025-03-12 23:44:09 -07:00
parent 684b7ae485
commit 2d92dd3060
4 changed files with 114 additions and 31 deletions
+37 -10
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):
+68 -19
View File
@@ -20,13 +20,16 @@ from collections import OrderedDict
from functools import partial
from pathlib import Path
from queue import Queue
from typing import TypeVar
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 # pyright: ignore[reportUnknownVariableType]
from PySide6.QtCore import QObject, QSettings, Qt, QThread, QThreadPool, QTimer, Signal
from PySide6.QtGui import (
QAction,
QColor,
QDragEnterEvent,
QDragMoveEvent,
@@ -38,6 +41,11 @@ from PySide6.QtGui import (
QPalette,
)
from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox, QPushButton, QScrollArea
from tagstudio.qt.controllers.ffmpeg_missing_message_box import FfmpegMissingMessageBox
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
from tagstudio.qt.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings, Theme
import tagstudio.qt.resources_rc # noqa: F401 # pyright: ignore[reportUnusedImport]
from tagstudio.core.constants import (
@@ -61,6 +69,7 @@ from tagstudio.core.macro_parser import (
Instruction,
TagResult,
exec_instructions,
get_macro_name,
parse_macro_file,
)
from tagstudio.core.media_types import MediaCategories
@@ -98,6 +107,7 @@ from tagstudio.qt.mixed.tag_color_manager import TagColorManager
from tagstudio.qt.resource_manager import ResourceManager
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.search_panel_view import SearchPanelView
from tagstudio.qt.views.styles.palette import ColorType, Palette, UiColor, get_ui_color
@@ -611,20 +621,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"):
@@ -647,8 +645,6 @@ class QtDriver(DriverMixin, QObject):
# endregion
# endregion
self.main_window.search_field.textChanged.connect(self.update_completions_list)
def on_visible_changed(entry_id: int | None):
@@ -857,6 +853,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()
@@ -1198,6 +1195,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."""
@@ -1360,8 +1358,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:
@@ -1648,6 +1649,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()
@@ -1746,6 +1794,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",
@@ -1773,7 +1822,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(
+8 -2
View File
@@ -15,13 +15,19 @@ from tagstudio.core.utils.silent_subprocess import silent_popen # pyright: igno
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.
"""
@@ -72,7 +78,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:
+1
View File
@@ -142,6 +142,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)