diff --git a/.github/workflows/scripts/update_version.py b/.github/workflows/scripts/update_version.py deleted file mode 100644 index b5600ab..0000000 --- a/.github/workflows/scripts/update_version.py +++ /dev/null @@ -1,79 +0,0 @@ -import argparse -import os -import json -import re - - -def collapse_json(text, list_length=4): - for length in range(list_length): - re_pattern = r'\[' + (r'\s*(.+)\s*,' * length)[:-1] + r'\]' - re_repl = r'[' + ''.join(r'\{}, '.format(i+1) for i in range(length))[:-2] + r']' - - text = re.sub(re_pattern, re_repl, text) - - return text - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - - def parse_list(text): - return text.split(",") - parser.add_argument("-files", type=parse_list, default=[], nargs='?', const=[]) - parser.add_argument("-removed", type=parse_list, default=[], nargs='?', const=[]) - args = parser.parse_args() - - addon_directory = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) - version = (0, 0, 0) - with open(os.path.join(addon_directory, "ActRec/__init__.py"), 'r', encoding='utf-8') as file: - for line in file.readlines(): - if "version" in line: - version = eval("%s)" % line.split(":")[1].split(")")[0].strip()) - break - with open(os.path.join(addon_directory, "ActRec/actrec/config.py"), 'r', encoding='utf-8') as file: - for line in file.readlines(): - if "version" in line: - check_version = eval(line.split("=")[1].strip()) - if check_version > version: - version = check_version - break - - print("Update to Version %s\nFiles: %s\nRemoved: %s" % (version, args.files, args.removed)) - - version = list(version) - with open(os.path.join(addon_directory, "download_file.json"), 'r', encoding='utf-8') as download_file: - data = json.loads(download_file.read()) - data_files = data["files"] - for file in args.files: - if data_files.get(file, None): - data_files[file] = version - data_remove = data["remove"] - for file in args.removed: - if file not in data_remove and file.startswith("ActRec/"): - data_remove.append(file) - data["version"] = version - with open(os.path.join(addon_directory, "download_file.json"), 'w', encoding='utf-8') as download_file: - download_file.write(collapse_json(json.dumps(data, ensure_ascii=False, indent=4))) - - lines = [] - with open(os.path.join(addon_directory, "ActRec/__init__.py"), 'r', encoding='utf-8') as file: - for line in file.read().splitlines(): - if "version" in line: - split = line.split(": ") - sub_split = split[1].split(")") - line = "%s: %s%s" % (split[0], str(tuple(version)), sub_split[-1]) - lines.append(line) - with open(os.path.join(addon_directory, "ActRec/__init__.py"), 'w', encoding='utf-8') as file: - file.write("\n".join(lines)) - file.write("\n") - - lines = [] - with open(os.path.join(addon_directory, "ActRec/actrec/config.py"), 'r', encoding='utf-8') as file: - for line in file.read().splitlines(): - if "version" in line: - split = line.split("=") - line = "version = %s" % str(tuple(version)) - lines.append(line) - with open(os.path.join(addon_directory, "ActRec/actrec/config.py"), 'w', encoding='utf-8') as file: - file.write("\n".join(lines)) - file.write("\n") diff --git a/.github/workflows/unit_test_addon.yml b/.github/workflows/unit_test_addon.yml index 857e02e..b7ea94e 100644 --- a/.github/workflows/unit_test_addon.yml +++ b/.github/workflows/unit_test_addon.yml @@ -14,9 +14,9 @@ jobs: strategy: fail-fast: false matrix: - blender-version: ["4.0", "3.6", "3.3"] + blender-version: ["4.2", "3.6",] #os: [ubuntu-latest, windows-latest, macos-latest] - # FIXME Addon doesn't work on Ubuntu (don't know why) + # FIXME Addon tests doesn't work on Ubuntu (don't know why) os: [windows-latest] env: BLENDER_CACHE: ${{ github.workspace }}/.blender_releases_cache # The place where blender releases are downloaded diff --git a/.github/workflows/update_version.yml b/.github/workflows/update_version.yml deleted file mode 100644 index 70a375e..0000000 --- a/.github/workflows/update_version.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Update Version - -on: - push: - branches: [ master ] - -jobs: - - update: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 2 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - name: Get changed files - id: files - run: | - diff=$(git diff --name-only --diff-filter=AM HEAD^..HEAD | tr '\n' ',') - echo "added_modified=$diff" >> "$GITHUB_OUTPUT" - echo "Added & Modified: $diff" - diff=$(git diff --name-only --diff-filter=D HEAD^..HEAD | tr '\n' ',') - echo "removed=$diff" >> "$GITHUB_OUTPUT" - echo "Removed: $diff" - - name: Update to new Version - id: update - working-directory: .github/workflows/scripts - run: | - output=$(python update_version.py -files ${{ steps.files.outputs.added_modified }} -removed ${{ steps.files.outputs.removed }}) - output="${output//'%'/'%25'}" - output="${output//$'\n'/'%0A'}" - output="${output//$'\r'/'%0D'}" - echo "log=$output" >> $GITHUB_OUTPUT - - name: Print Log - run: echo "${{ steps.update.outputs.log }}" - - name: Update files on GitHub - uses: test-room-7/action-update-file@v1.8.0 - with: - file-path: | - ActRec/__init__.py - download_file.json - ActRec/actrec/config.py - commit-msg: Update Files - github-token: ${{ secrets.FILE_UPDATER }} diff --git a/.gitignore b/.gitignore index 8680c7f..47581a7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ __pycache__/ TextEditorExample.txt /docs/build /ActRec/packages +/ActRec/*.zip +/ActRec/lib diff --git a/ActRec/__init__.py b/ActRec/__init__.py index a925943..e626a2e 100644 --- a/ActRec/__init__.py +++ b/ActRec/__init__.py @@ -3,11 +3,11 @@ bl_info = { "name": "ActionRecorder", "author": "InamuraJIN, RivinHD", - "version": (4, 1, 2), - "blender": (3, 3, 12), + "version": (4, 2, 0), + "blender": (4, 2, 0), "location": "View 3D", "warning": "", - "docs_url": 'https://github.com/InamuraJIN/ActionRecorder/blob/master/README.md', # Documentation + "docs_url": 'https://inamurajin.github.io/ActionRecorder/', # Documentation "tracker_url": 'https://inamurajin.wixsite.com/website/post/bug-report', # Report Bug "link": 'https://twitter.com/Inamura_JIN', "category": "System" diff --git a/ActRec/actrec/__init__.py b/ActRec/actrec/__init__.py index 7be8e2f..b69a34c 100644 --- a/ActRec/actrec/__init__.py +++ b/ActRec/actrec/__init__.py @@ -1,6 +1,8 @@ # region Imports # external modules import json +import os +import shutil # blender modules import bpy @@ -10,7 +12,7 @@ # relative imports # unused import needed to give direct access to the modules from . import functions, menus, operators, panels, properties, ui_functions, uilist -from . import config, icon_manager, keymap, log, preferences, update +from . import config, icon_manager, keymap, log, preferences from .functions.shared import get_preferences from . import shared_data # endregion @@ -35,6 +37,12 @@ def on_load(dummy=None): context = bpy.context ActRec_pref = get_preferences(context) ActRec_pref.operators_list_length = 0 + + # Copy Storage.json for first startup from addon source directory to user directory + if not os.path.exists(ActRec_pref.storage_path): + source_storage_path = os.path.join(ActRec_pref.source_addon_directory, "Storage.json") + shutil.copyfile(source_storage_path, ActRec_pref.storage_path) # copy the file + # load local actions if bpy.data.filepath == "": try: @@ -70,6 +78,13 @@ def on_load(dummy=None): icon_manager.load_icons(ActRec_pref) log.logger.info("Finished: Load ActRec Data") + if bpy.app.version < (4, 2, 0): + success = functions.install_wheels(ActRec_pref) + if success: + log.logger.info("Successfully installed and loaded wheels") + else: + log.logger.error("Failed to install or load wheels") + # region Registration @@ -81,7 +96,6 @@ def register(): panels.register() uilist.register() icon_manager.register() - update.register() preferences.register() keymap.register() @@ -104,7 +118,6 @@ def unregister(): panels.unregister() uilist.unregister() icon_manager.unregister() - update.unregister() preferences.unregister() keymap.unregister() ui_functions.unregister() diff --git a/ActRec/actrec/config.py b/ActRec/actrec/config.py index 76007a2..df6c77e 100644 --- a/ActRec/actrec/config.py +++ b/ActRec/actrec/config.py @@ -4,5 +4,4 @@ check_source_url = 'https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/master/download_file.json' repo_source_url = 'https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/master/%s' release_notes_url = 'https://github.com/InamuraJIN/ActionRecorder/wiki' -version = (4, 1, 2) log_amount = 5 diff --git a/ActRec/actrec/functions/__init__.py b/ActRec/actrec/functions/__init__.py index 6310967..d8d45b3 100644 --- a/ActRec/actrec/functions/__init__.py +++ b/ActRec/actrec/functions/__init__.py @@ -59,9 +59,9 @@ execute_render_complete, enum_list_id_to_name_dict, enum_items_to_enum_prop_list, - install_packages, get_preferences, get_categorized_view_3d_modes, get_attribute, - get_attribute_default + get_attribute_default, + install_wheels ) diff --git a/ActRec/actrec/functions/shared.py b/ActRec/actrec/functions/shared.py index 3bc44a9..f2becb0 100644 --- a/ActRec/actrec/functions/shared.py +++ b/ActRec/actrec/functions/shared.py @@ -1,5 +1,6 @@ # region Imports # external modules +import importlib.util from typing import Optional, Union from contextlib import suppress from collections import defaultdict @@ -10,8 +11,10 @@ import functools import subprocess import traceback +import importlib from typing import TYPE_CHECKING from mathutils import Vector, Matrix, Color, Euler, Quaternion +from ... import __package__ as base_package # blender modules import bpy @@ -32,8 +35,6 @@ Font_analysis = object # endregion -__module__ = __package__.split(".")[0] - # region functions @@ -411,7 +412,7 @@ def run_queued_macros(context_copy: dict, action_type: str, action_id: str, star else: temp_override = context.temp_override(**context_copy) with temp_override: - ActRec_pref = context.preferences.addons[__module__].preferences + ActRec_pref = get_preferences(context) action = getattr(ActRec_pref, action_type)[action_id] play(context, action.macros, action, action_type, start) @@ -691,7 +692,7 @@ def play( action.is_playing = False -@ persistent +@persistent def execute_render_complete(dummy=None) -> None: # https://docs.blender.org/api/current/bpy.app.handlers.html """ @@ -823,87 +824,111 @@ def text_to_lines(context: Context, text: str, font: Font_analysis, limit: int, total_line_length = start + line_length total_length = total_line_length + len(psb) width = sum(characters_width[start: total_length]) + + # append to current line if width <= limit: lines[-1] += psb continue - if sum(characters_width[total_line_length: total_length]) <= limit: - lines.append(psb) - start += line_length + len(psb) - continue - start += line_length - while psb != "": - i = int(bl_math.clamp(limit / width * len(psb), 0, len(psb))) - if len(psb) != i: - if sum(characters_width[start: start + i]) <= limit: - while sum(characters_width[start: start + i]) <= limit: - i += 1 - i -= 1 - else: - while sum(characters_width[start: start + i]) >= limit: - i -= 1 - lines.append(psb[:i]) - psb = psb[i:] - start += i - width = sum(characters_width[start: total_length]) + + # create new line + start = len("".join(lines)) + lines.append(psb) + if (lines[0] == ""): lines.pop(0) return lines -def install_packages(*package_names: list[str]) -> tuple[bool, str]: +def get_preferences(context: Context) -> AR_preferences: """ - install the listed packages and ask for user permission if needed + get addon preferences of this addon, which are stored in Blender Args: - package_names list[str]: name of the package + context (Context): active blender context Returns: - tuple[bool, str]: (success, installation output) + AR_preferences: preferences of this addon """ - try: - # install package - output = subprocess.check_output( - [sys.executable, '-m', 'pip', 'install', *package_names, '--no-color'] - ).decode('utf-8').replace("\r", "") - - # get sys.path from pip after installation to update the current sys.path - output_paths = subprocess.check_output( - [sys.executable, '-m', 'site'] - ).decode('utf-8').replace("\r", "") - - # parse the output to get all sys.path paths - in_path_list = False - for line in output_paths.splitlines(): - if line.strip() == "sys.path = [": - in_path_list = True - continue - elif not in_path_list: - continue - if line.strip() == "]": - break - - path = line.strip(" \'\",\t").replace("\\\\", "\\") - if path not in sys.path: - sys.path.append(path) + return context.preferences.addons[base_package].preferences - return (True, output) - except (PermissionError, OSError, subprocess.CalledProcessError) as err: - logger.error(err) - return (False, err.output) - return (False, ":(") - - -def get_preferences(context: Context) -> AR_preferences: +def install_wheels(ActRec_pref: AR_preferences) -> bool: """ - get addon preferences of this addon, which are stored in Blender + Tires to install the required wheels for the addon and loads them into the system path. + If the wheels are already installed it will only add them to the path. + NOTE: This function should only be used for Blender prior to version 4.2 Args: context (Context): active blender context Returns: - AR_preferences: preferences of this addon - """ - return context.preferences.addons[__module__].preferences + bool: installation success + """ + # Deprecated This function should be removed if Blender 3.6 LTS is no longer supported + def load_packages(installation_path: str) -> bool: + """ + Added the installation path to the system path and tries find the packages. + + Args: + installation_path (str): The path were the packages is installed. + + Returns: + bool: found the packages successfully + """ + site_package_path = os.path.join(installation_path, "Python310", "site-packages") + if site_package_path not in sys.path: + sys.path.append(site_package_path) + found_packages = (importlib.util.find_spec("fontTools") is not None + and importlib.util.find_spec("brotli") is not None) + if not found_packages: + importlib.invalidate_caches() + found_packages = (importlib.util.find_spec("fontTools") is not None + and importlib.util.find_spec("brotli") is not None) + return found_packages + + installation_path = os.path.join(ActRec_pref.source_addon_directory, "lib") + + if load_packages(installation_path): + return True + + package_names = None + if sys.platform == "win32": + package_names = [ + "fonttools-4.54.1-cp310-cp310-win_amd64.whl", + "Brotli-1.1.0-cp310-cp310-win_amd64.whl" + ] + elif sys.platform == "darwin": + package_names = [ + "fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl", + "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl" + ] + elif sys.platform == "linux": + package_names = [ + "fonttools-4.54.1-py3-none-any.whl", + "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64." + + "manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl" + ] + + if package_names is None: + logger.error("Cannot install required packages as this platform is not supported") + return False + + wheel_path = os.path.join(ActRec_pref.source_addon_directory, "wheels") + installation_env = { + **os.environ, + "PYTHONUSERBASE": installation_path + } + package_paths = [os.path.join(wheel_path, package_name) for package_name in package_names] + try: + subprocess.run( + [sys.executable, '-m', 'pip', 'install', *package_paths, '--no-color', '--user', '--ignore-installed'], + env=installation_env + ) + except (PermissionError, OSError, subprocess.CalledProcessError) as err: + logger.error(err) + return False + if load_packages(installation_path): + return True + return False # endregion diff --git a/ActRec/actrec/functions/wrapper.py b/ActRec/actrec/functions/wrapper.py new file mode 100644 index 0000000..76c6aeb --- /dev/null +++ b/ActRec/actrec/functions/wrapper.py @@ -0,0 +1,44 @@ +# =============================================================================== +# Wrapper functions for Blender API to support multiple LTS versions of Blender. +# This script should be independent of any local import to avoid circular imports. +# +# Supported Blender versions (Updated: 2024-11-04): +# - 4.2 LTS +# - 3.6 LTS +# =============================================================================== + +# region Imports +import bpy +from bpy.app import version + +import os +# endregion + +# region Functions + + +def get_user_path(package: str, path: str = '', create: bool = False): + """ + Return a user writable directory associated with an extension. + + Args: + package (str): The __package__ of the extension. + path (str, optional): Optional subdirectory. Defaults to ''. + create (bool, optional): Treat the path as a directory and create it if its not existing. Defaults to False. + """ + fallback = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + try: + if version >= (4, 2, 0): + return bpy.utils.extension_path_user(package, path=path, create=create) + else: + return fallback # The Fallback path is also the extension user directory for Blender 3.6 LTS. + except ValueError as err: + print("ERROR ActRec: ValueError: %s" % str(err)) + if err.args[0] == "The \"package\" does not name an extension": + print("--> This error might be caused as the addon is installed the first time.") + print(" If this errors remains please try reinstalling the Add-on and report it to the developer.") + + print(" Fallback to old extension directory: %s." % fallback) + return fallback + +# endregion diff --git a/ActRec/actrec/log.py b/ActRec/actrec/log.py index ced7003..5b2d87f 100644 --- a/ActRec/actrec/log.py +++ b/ActRec/actrec/log.py @@ -9,13 +9,14 @@ # blender modules import bpy from bpy.app.handlers import persistent +import addon_utils +from .. import __package__ as base_package # relative imports from . import config +from .functions import wrapper # endregion -__module__ = __package__.split(".")[0] - # region Log system @@ -29,49 +30,61 @@ def __init__(self, count: int) -> None: Args: count (int): amount of log files which are kept simultaneously """ - dir = self.directory = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logs") + logger = logging.getLogger(base_package) + self.logger = logger + logger.setLevel(logging.DEBUG) + + self.setup_file_logging(count) + + sys.excepthook = self.exception_handler + + def setup_file_logging(self, count: int) -> None: + """ + Setup the file logging if possible. + + Args: + count (int): amount of log files which are kept simultaneously + """ + dir = self.directory = os.path.join(wrapper.get_user_path(base_package, create=True), "logs") + if not os.path.exists(dir): os.mkdir(dir) all_logs = os.listdir(dir) - log_later = [] + self.log_later = [] while len(all_logs) >= count: try: # delete oldest file os.remove(min([os.path.join(dir, filename) for filename in all_logs], key=os.path.getctime)) except PermissionError as err: - log_later.append("File is already used -> PermissionError: %s" % str(err)) + self.log_later.append("File is already used -> PermissionError: %s" % str(err)) break except FileNotFoundError as err: - log_later.append("For some reason the File doesn't exists %s" % str(err)) + self.log_later.append("For some reason the File doesn't exists %s" % str(err)) break all_logs = os.listdir(dir) - name = "" - for arg in sys.argv: - if arg.endswith(".blend"): - name = "%s_" % ".".join(os.path.basename(arg).split(".")[:-1]) - self.path = os.path.join(dir, "ActRec_%s%s.log" % (name, datetime.today().strftime('%d-%m-%Y_%H-%M-%S'))) - - logger = logging.getLogger(__module__) - self.logger = logger - logger.setLevel(logging.DEBUG) - - logger.info( - "Logging ActRec %s running on Blender %s" - % (".".join([str(x) for x in config.version]), bpy.app.version_string) - ) - for log_text in log_later: - logger.info(log_text) - - sys.excepthook = self.exception_handler + self.path = os.path.join(dir, "ActRec_%s.log" % (datetime.today().strftime('%d-%m-%Y_%H-%M-%S'))) def exception_handler(self, exc_type, exc_value, exc_tb) -> None: traceback.print_exception(exc_type, exc_value, exc_tb) self.logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_tb)) + def check_file_exists(self) -> bool: + """ + Checks if file logging is possible + + Returns: + bool: True if file logging is possible otherwise False + """ + + return hasattr(self, 'path') and self.path is not None + def detach_file(self) -> None: """ remove file of the logger """ + if not self.check_file_exists(): + return + self.file_handler.close() self.logger.removeHandler(self.file_handler) @@ -79,6 +92,9 @@ def append_file(self) -> None: """ adds a file to the logger """ + if not self.check_file_exists(): + return + file_formatter = logging.Formatter( "%(levelname)s - %(relativeCreated)d - %(filename)s:%(funcName)s - %(message)s" ) @@ -88,6 +104,25 @@ def append_file(self) -> None: self.logger.addHandler(file_handler) self.file_handler = file_handler + self.startup_log() + + def startup_log(self): + """ + Logging startup information of the logfile + """ + addon_version = (-1, -1, -1) + for mod in addon_utils.modules(): + if mod.__name__ == base_package: + addon_version = mod.bl_info.get("version", addon_version) + break + + logger.info( + "Logging ActRec %s running on Blender %s" + % (".".join([str(x) for x in addon_version]), bpy.app.version_string) + ) + for log_text in self.log_later: + logger.info(log_text) + def update_log_amount_in_config(amount: int) -> None: """ diff --git a/ActRec/actrec/operators/globals.py b/ActRec/actrec/operators/globals.py index 2259a2e..2684835 100644 --- a/ActRec/actrec/operators/globals.py +++ b/ActRec/actrec/operators/globals.py @@ -567,7 +567,7 @@ class AR_OT_global_to_local(shared.Id_based, Operator): bl_description = "Transfer the selected Action to Local-actions" bl_options = {'UNDO'} - @ classmethod + @classmethod def poll(cls, context: Context) -> bool: ActRec_pref = get_preferences(context) return len(ActRec_pref.global_actions) and len(ActRec_pref.get("global_actions.selected_ids", [])) diff --git a/ActRec/actrec/operators/macros.py b/ActRec/actrec/operators/macros.py index e12cd9b..135b3d3 100644 --- a/ActRec/actrec/operators/macros.py +++ b/ActRec/actrec/operators/macros.py @@ -497,7 +497,7 @@ class Font_analysis(): def __init__(self, font_path: str) -> None: self.path = font_path - if importlib.util.find_spec('fontTools') is None: + if not Font_analysis.is_installed(): self.use_dynamic_text = False return @@ -517,44 +517,17 @@ def __init__(self, font_path: str) -> None: @classmethod def is_installed(cls) -> bool: - - return ( - importlib.util.find_spec('fontTools') is not None - and (bpy.app.version < (3, 4, 0) or importlib.util.find_spec('brotli')) - ) - - @classmethod - def install(cls, logger: Logger) -> bool: """ - install fonttools to blender modules if not installed + Check if the fontTools and brotli package is installed through its wheels Returns: - bool: success + bool: True if both packages are installed, otherwise False """ - if bpy.app.version >= (3, 4, 0): - # Blender 3.4 uses woff2 therefore the package brotli is required - if importlib.util.find_spec('fontTools') is None or importlib.util.find_spec('brotli') is None: - success, output = functions.install_packages('fontTools', 'brotli') - if success: - logger.info(output) - else: - logger.warning(output) - if importlib.util.find_spec('fontTools') is None or importlib.util.find_spec('brotli') is None: - logger.warning("For some reason fontTools or brotli couldn't be installed :(") - return False - else: - if importlib.util.find_spec('fontTools') is None: - success, output = functions.install_packages('fontTools') - if success: - logger.info(output) - else: - logger.warning(output) - - if importlib.util.find_spec('fontTools') is None: - logger.warning("For some reason fontTools couldn't be installed :(") - return False - return True + return ( + importlib.util.find_spec('fontTools') is not None + and importlib.util.find_spec('brotli') is not None + ) def get_width_of_text(self, context: Context, text: str) -> list[float]: """ @@ -571,102 +544,11 @@ def get_width_of_text(self, context: Context, text: str) -> list[float]: total = [] font_style = context.preferences.ui_styles[0].widget for c in text: - total.append(self.s[self.t[ord(c)]].width * font_style.points/self.units_per_em) + # 0.925 is a constant for fine tuning + total.append(self.s[self.t[ord(c)]].width * font_style.points/self.units_per_em * 0.925) return total -class AR_OT_macro_multiline_support(Operator): - bl_idname = "ar.macro_multiline_support" - bl_label = "Multiline Support" - bl_options = {'INTERNAL'} - bl_description = "Adds multiline support the edit macro dialog" - - def invoke(self, context: Context, event: Event) -> set[str]: - return context.window_manager.invoke_props_dialog(self, width=350) - - def draw(self, context: Context) -> None: - ActRec_pref = get_preferences(context) - layout = self.layout - - if Font_analysis.is_installed(): - layout.label(text="Support Enabled") - return - - layout.label(text="Do you want to install multiline support?") - if bpy.app.version >= (3, 4, 0): - layout.label(text="This requires the fontTools, brotli package to be installed.") - else: - layout.label(text="This requires the fontTools package to be installed.") - row = layout.row() - if ActRec_pref.multiline_support_installing: - if bpy.app.version >= (3, 4, 0): - row.label(text="Installing fontTools, brotli...") - else: - row.label(text="Installing fontTools...") - else: - row.operator('ar.macro_install_multiline_support', text="Install") - row.prop(ActRec_pref, 'multiline_support_dont_ask') - - def execute(self, context: Context) -> set[str]: - bpy.ops.ar.macro_edit("INVOKE_DEFAULT", edit=True, multiline_asked=True) - return {'FINISHED'} - - def cancel(self, context: Context) -> None: - # Also recall when done is not clicked - bpy.ops.ar.macro_edit("INVOKE_DEFAULT", edit=True, multiline_asked=True) - - -class AR_OT_macro_install_multiline_support(Operator): - """ - Try's to install the package fonttools - to get the width of the given command and split it into multiple lines - """ - bl_idname = "ar.macro_install_multiline_support" - bl_label = "Install Multiline Support" - bl_options = {'INTERNAL'} - - success = [] - - @classmethod - def poll(cls, context: Context) -> bool: - ActRec_pref = get_preferences(context) - return not ActRec_pref.multiline_support_installing - - def invoke(self, context: Context, event: Event) -> set[str]: - def install(success: list, logger): - success.append(Font_analysis.install(logger)) - - self.success.clear() - ActRec_pref = get_preferences(context) - context.window_manager.modal_handler_add(self) - self.timer = context.window_manager.event_timer_add(0.1, window=context.window) - self.thread = threading.Thread(target=install, args=(self.success, logger), daemon=True) - self.thread.start() - ActRec_pref.multiline_support_installing = True - return {'RUNNING_MODAL'} - - def modal(self, context: Context, event: Event) -> set[str]: - - if event.type != 'TIMER': - return {'PASS_THROUGH'} - - if not self.thread.is_alive(): - return self.execute(context) - return {'RUNNING_MODAL'} - - def execute(self, context: Context) -> set[str]: - ActRec_pref = get_preferences(context) - self.thread.join() - ActRec_pref.multiline_support_installing = False - if context and context.area: - context.area.tag_redraw() - if len(self.success) and self.success[0]: - self.report({'INFO'}, "Successfully installed multiline support") - return {'FINISHED'} - self.report({'ERROR'}, "Could not install multiline support. See Log for further information.") - return {'CANCELLED'} - - class AR_OT_macro_edit(Macro_based, Operator): bl_idname = "ar.macro_edit" bl_label = "Edit" @@ -716,7 +598,7 @@ def set_command(self, value: str) -> None: if self.use_last_command: return self.lines.clear() - for line in functions.text_to_lines(bpy.context, value, AR_OT_macro_edit.font, self.width - 20): + for line in functions.text_to_lines(bpy.context, value, AR_OT_macro_edit.font, self.width): new = self.lines.add() new['text'] = line @@ -744,7 +626,7 @@ def set_last_command(self, value: str) -> None: if not self.use_last_command: return self.lines.clear() - for line in functions.text_to_lines(bpy.context, value, AR_OT_macro_edit.font, self.width - 20): + for line in functions.text_to_lines(bpy.context, value, AR_OT_macro_edit.font, self.width): new = self.lines.add() new['text'] = line @@ -776,7 +658,6 @@ def set_use_last_command(self, value: bool) -> None: last_command: StringProperty(name="Last Command", get=get_last_command, set=set_last_command) last_id: StringProperty(name="Last Id") edit: BoolProperty(default=False) - multiline_asked: BoolProperty(default=False) clear_operator: BoolProperty( name="Clear Operator", description="Delete the parameters of an operator command. Otherwise the complete command is cleared", @@ -790,7 +671,7 @@ def set_use_last_command(self, value: bool) -> None: set=set_use_last_command ) lines: CollectionProperty(type=properties.AR_macro_multiline) - width: IntProperty(default=500, name="width", description="Window width of the Popup") + width: IntProperty(default=750, name="width", description="Window width of the Popup") font = None time = 0 is_operator = False @@ -837,12 +718,6 @@ def invoke(self, context: Context, event: Event) -> set[str]: index = self.index = functions.get_local_macro_index(action, self.id, self.index) macro = action.macros[index] - if not self.multiline_asked and not Font_analysis.is_installed() and not ActRec_pref.multiline_support_dont_ask: - bpy.ops.ar.macro_multiline_support("INVOKE_DEFAULT") - self.cancel(context) - self.multiline_asked = True - return {'CANCELLED'} # Recall this Operator when handled - font_path = functions.get_font_path() if AR_OT_macro_edit.font is None or AR_OT_macro_edit.font.path != font_path: AR_OT_macro_edit.font = Font_analysis(font_path) @@ -959,7 +834,6 @@ def execute(self, context: Context) -> set[str]: def cancel(self, context: Context) -> None: self.edit = False - self.multiline_asked = False self.use_last_command = False self.clear() @@ -1155,9 +1029,7 @@ def handle_button_property( AR_OT_macro_move_up, AR_OT_macro_move_down, AR_OT_macro_edit, - AR_OT_copy_to_actrec, - AR_OT_macro_multiline_support, - AR_OT_macro_install_multiline_support + AR_OT_copy_to_actrec ] # region Registration diff --git a/ActRec/actrec/panels/main.py b/ActRec/actrec/panels/main.py index dba3a3f..5f1b4da 100644 --- a/ActRec/actrec/panels/main.py +++ b/ActRec/actrec/panels/main.py @@ -5,7 +5,6 @@ # relative imports from .. import config -from .. import update from ..log import log_sys from ..functions.shared import get_preferences # endregion @@ -36,10 +35,6 @@ class AR_PT_local(Panel): def draw(self, context: Context) -> None: ActRec_pref = get_preferences(context) layout = self.layout - if ActRec_pref.update: - box = layout.box() - box.label(text="new Version available (%s)" % ActRec_pref.version) - update.draw_update_button(box, ActRec_pref) box = layout.box() box_row = box.row() col = box_row.column() @@ -174,7 +169,6 @@ def draw_header(self, context: Context) -> None: def draw(self, context: Context) -> None: layout = self.layout - ActRec_pref = get_preferences(context) layout.operator( 'wm.url_open', text="Manual", @@ -198,23 +192,6 @@ def draw(self, context: Context) -> None: 'wm.url_open', text="Release Notes" ).url = config.release_notes_url - row = layout.row() - if ActRec_pref.update: - update.draw_update_button(row, ActRec_pref) - else: - row.operator('ar.update_check', text="Check For Updates") - if ActRec_pref.restart: - row.operator( - 'ar.show_restart_menu', - text="Restart to Finish" - ) - if ActRec_pref.version != '': - if ActRec_pref.update: - layout.label( - text="new Version available (%s)" % ActRec_pref.version - ) - else: - layout.label(text="latest Version (%s)" % ActRec_pref.version) AR_PT_help.__name__ = "AR_PT_help_%s" % space_type class AR_PT_advanced(Panel): diff --git a/ActRec/actrec/preferences.py b/ActRec/actrec/preferences.py index e7a6fbf..25a6ccb 100644 --- a/ActRec/actrec/preferences.py +++ b/ActRec/actrec/preferences.py @@ -1,7 +1,6 @@ # region Imports # external modules import os -import importlib from typing import TYPE_CHECKING # blender modules @@ -11,8 +10,10 @@ import rna_keymap_ui # relative imports -from . import properties, functions, config, update, keymap, log, shared_data +from . import properties, functions, config, keymap, log, shared_data from .log import logger, log_sys +from .. import __package__ as base_package +from .functions import wrapper if TYPE_CHECKING: def get_preferences(): return @@ -24,7 +25,7 @@ def get_preferences(): return class AR_preferences(AddonPreferences): - bl_idname = __package__.split(".")[0] + bl_idname = base_package def update_is_loaded(self, context: Context) -> None: context.scene.name = context.scene.name @@ -44,17 +45,22 @@ def set_is_loaded(self, value: bool) -> None: set=set_is_loaded ) + source_addon_directory: StringProperty( + name="source addon directory", + default=os.path.dirname(os.path.dirname(__file__)), + get=lambda self: self.bl_rna.properties['source_addon_directory'].default + ) # get the base addon directory of the source files + addon_directory: StringProperty( name="addon directory", - default=os.path.dirname(os.path.dirname(__file__)), + default=wrapper.get_user_path(base_package, create=True), get=lambda self: self.bl_rna.properties['addon_directory'].default - ) # get the base addon directory + ) # get the base addon directory for local files preference_tab: EnumProperty( items=[('settings', "Settings", ""), ('path', "Paths", ""), - ('keymap', "Keymap", ""), - ('update', "Update", "")], + ('keymap', "Keymap", "")], name="Tab", description="Switch between preference tabs" ) @@ -103,7 +109,7 @@ def set_icon_path(self, origin_path: str) -> None: icon_path: StringProperty( name="Icons Path", description="The Path to the Storage for the added Icons", - default=os.path.join(os.path.dirname(os.path.dirname(__file__)), "Icons"), + default=os.path.join(wrapper.get_user_path(base_package, create=True), "Icons"), get=get_icon_path, set=set_icon_path ) @@ -136,6 +142,25 @@ def set_icon_path(self, origin_path: str) -> None: subtype='PERCENTAGE' ) # used as slider + # update + update: BoolProperty() + restart: BoolProperty() + version: StringProperty() + auto_update: BoolProperty( + default=True, + name="Auto Update", + description="automatically search for a new update" + ) + update_progress: IntProperty( + name="Update Progress", + default=-1, + min=-1, + max=100, + soft_min=0, + soft_max=100, + subtype='PERCENTAGE' + ) # used as slider + # locals local_actions: CollectionProperty(type=properties.AR_local_actions) @@ -271,7 +296,7 @@ def set_storage_path(self, origin_path: str) -> None: storage_path: StringProperty( name="Storage Path", description="The Path to the Storage for the saved Categories", - default=os.path.join(os.path.dirname(os.path.dirname(__file__)), "Storage.json"), + default=os.path.join(wrapper.get_user_path(base_package, create=True), "Storage.json"), get=get_storage_path, set=set_storage_path ) @@ -301,21 +326,7 @@ def draw(self, context: Context) -> None: col = layout.column() row = col.row(align=True) row.prop(ActRec_pref, 'preference_tab', expand=True) - if ActRec_pref.preference_tab == 'update': - col.operator('wm.url_open', text="Release Notes").url = config.release_notes_url - row = col.row() - if ActRec_pref.update: - update.draw_update_button(row, ActRec_pref) - else: - row.operator('ar.update_check', text="Check For Updates") - if ActRec_pref.restart: - row.operator('ar.show_restart_menu', text="Restart to Finish") - if ActRec_pref.version != '': - if ActRec_pref.update: - col.label(text="A new Version is available (%s)" % ActRec_pref.version) - else: - col.label(text="You are using the latest Version (%s)" % ActRec_pref.version) - elif ActRec_pref.preference_tab == 'path': + if ActRec_pref.preference_tab == 'path': col.label(text='Action Storage Folder') row = col.row() ops = row.operator( @@ -395,17 +406,10 @@ def draw(self, context: Context) -> None: rna_keymap_ui.draw_kmi(kc.keymaps, kc, km, kmi, col2, 0) elif ActRec_pref.preference_tab == 'settings': row = col.row() - row.prop(self, 'auto_update') row.prop(self, 'autosave') row = col.row() row.prop(self, 'hide_local_text') row.prop(self, 'local_create_empty') - if importlib.util.find_spec('fontTools') is None: - row = col.row() - if self.multiline_support_installing: - row.label(text="Installing fontTools...") - else: - row.operator('ar.macro_install_multiline_support') col.separator(factor=1.5) row = col.row() row.operator('wm.url_open', text="Manual", icon='ASSET_MANAGER').url = config.manual_url diff --git a/ActRec/actrec/update.py b/ActRec/actrec/update.py deleted file mode 100644 index a9bd307..0000000 --- a/ActRec/actrec/update.py +++ /dev/null @@ -1,546 +0,0 @@ -# region Import -# external modules -from typing import Optional, Union -import requests -import json -import os -import subprocess -from collections import defaultdict -import threading -from contextlib import suppress -import sys -from typing import TYPE_CHECKING - -# blender modules -import bpy -from bpy.types import Operator, Scene, AddonPreferences, UILayout, Context, Event -from bpy.props import BoolProperty, EnumProperty -from bpy_extras.io_utils import ExportHelper -from bpy.app.handlers import persistent - -# relative imports -from . import config -from .log import logger -from .functions.shared import get_preferences -if TYPE_CHECKING: - from .preferences import AR_preferences -else: - AR_preferences = AddonPreferences -# endregion - - -__module__ = __package__.split(".")[0] - - -class Update_manager: - """manage data for update processes""" - download_list = [] - download_length = 0 - update_respond = None - update_data_chunks = defaultdict(lambda: {"chunks": b''}) - version_file = {} # used to store downloaded file from "AR_OT_update_check" - version_file_thread = None - -# region functions - - -@persistent -def on_start(dummy: Scene = None) -> None: - """ - get called on start of Blender with on_load handler and checks for available update of ActRec - opens a thread to run the process faster (the thread get closed in on_scene_update) - - Args: - dummy (Scene, optional): - needed because blender handler inputs the scene as argument for a handler function. Defaults to None. - """ - ActRec_pref = get_preferences(bpy.context) - if not (ActRec_pref.auto_update and Update_manager.version_file_thread is None): - return - t = threading.Thread(target=no_stream_download_version_file, args=[__module__], daemon=True) - t.start() - Update_manager.version_file_thread = t - - -@persistent -def on_scene_update(dummy: Scene = None) -> None: - """ - get called on the first scene update of Blender and closes the thread from on start, - which is used to check for updates and removes the functions from the handler - - Args: - dummy (bpy.type.Scene, optional): - needed because blender handler inputs the scene as argument for a handler function. Defaults to None. - """ - t = Update_manager.version_file_thread - if not (t and Update_manager.version_file.get("version", None)): - return - t.join() - bpy.app.handlers.depsgraph_update_post.remove(on_scene_update) - bpy.app.handlers.load_post.remove(on_start) - - -def check_for_update(version_file: Optional[dict]) -> tuple[bool, Union[str, tuple[int, int, int]]]: - """ - checks if a new version of ActRec is available on GitHub - - Args: - version_file (Optional[dict]): contains data about the addon version and the version of each file - - Returns: - tuple[bool, Union[str, tuple[int, int, int]]]: - [0] False for error or the latest version, True for new available version; - [1] error message or tuple of the version - """ - if version_file is None: - return (False, "No Internet Connection") - version = config.version - download_version = tuple(version_file["version"]) - if download_version > version: - return (True, download_version) - else: - return (False, version) - - -def update( - ActRec_pref: AR_preferences, - path: str, - update_respond: Optional[requests.Response], - download_chunks: dict, - download_length: int) -> Optional[bool]: - """ - runs the update process and shows the download process with a progress bar if possible - - Args: - ActRec_pref (AR_preferences): preferences of this addon - path (str): path to the file to update - update_respond (Optional[requests.Response]): open response to file, - needed if file is to large to download in one function call (chunk size 1024) - download_chunks (dict): contains all downloaded files and their data - download_length (int): the current length of the chunks, needed to show progress bar - - Returns: - Optional[bool]: the downloaded file or None if an error occurred - """ - finished_downloaded = False - progress = 0 - length = 1 - try: - if update_respond: - total_length = update_respond.headers.get('content-length', None) - if total_length is None: - length = progress = update_respond.raw._fp_bytes_read - download_chunks[path]["chunks"] = update_respond.content - update_respond.close() - finished_downloaded = True - Update_manager.update_respond = None - else: - length = int(total_length) - for chunk in update_respond.iter_content(chunk_size=1024): - if chunk: - download_chunks[path]["chunks"] += chunk - - progress = update_respond.raw._fp_bytes_read - finished_downloaded = progress == length - if finished_downloaded: - update_respond.close() - Update_manager.update_respond = None - else: - Update_manager.update_respond = requests.get( - config.repo_source_url % path, stream=True) - ActRec_pref.update_progress = int(100 * (progress / (length * download_length) + ( - download_length - len(Update_manager.download_list)) / download_length)) - if finished_downloaded: - Update_manager.download_list.pop(0) - return finished_downloaded - except Exception as err: - logger.warning("no Connection (%s)" % err) - return None - - -def install_update(ActRec_pref: AR_preferences, download_chunks: dict, version_file: dict) -> None: - """ - installs all downloaded files successively and removes old files if needed - + cleans up all unused data - - Args: - ActRec_pref (AR_preferences): preferences of this addon - download_chunks (dict): contains all downloaded files and their data - version_file (dict): contains data about the addon version, the version of each file and the open request - """ - for path in download_chunks: - # remove ActRec/ from path, because the Add-on is inside of another directory on GitHub - relative_path = path.replace("\\", "/").split("/", 1)[1] - absolute_path = os.path.join(ActRec_pref.addon_directory, relative_path) - absolute_directory = os.path.dirname(absolute_path) - if not os.path.exists(absolute_directory): - os.makedirs(absolute_directory, exist_ok=True) - with open(absolute_path, 'w', encoding='utf-8') as ar_file: - ar_file.write(download_chunks[path]["chunks"].decode('utf-8')) - for path in version_file['remove']: - # remove ActRec/ from path, because the Add-on is inside of another directory on GitHub - relative_path = path.replace("\\", "/").split("/", 1)[1] - remove_path = os.path.join(ActRec_pref.addon_directory, relative_path) - if not os.path.exists(remove_path): - continue - for root, dirs, files in os.walk(remove_path, topdown=False): - for name in files: - os.remove(os.path.join(root, name)) - for name in dirs: - os.rmdir(os.path.join(root, name)) - version = tuple(ActRec_pref.version.split(".")) - download_chunks.clear() - version_file.clear() - logger.info("Updated Action Recorder to Version: %s" % str(version)) - - -def start_get_version_file() -> bool: - """ - starts the request process to get the version file, which is needed to only update the changed files - - Returns: - bool: success of the process - """ - try: - Update_manager.version_file['respond'] = requests.get( - config.check_source_url, stream=True) - Update_manager.version_file['chunk'] = b'' - logger.info("Start Download: version_file") - return True - except Exception as err: - logger.warning("no Connection (%s)" % err) - return False - - -def get_version_file(res: requests.Response) -> Union[bool, dict, None]: - """ - downloads the data of the version file and needed to be called again if the download process isn't finished - - Args: - res (requests.Response): response that was created by start_get_version_file - - Returns: - Union[bool, dict, None]: - [None] error; - [True] needed to be called again; - [dict] data of the version file in JSON-format - """ - try: - total_length = res.headers.get('content-length', None) - if total_length is None: - logger.info("Finished Download: version_file") - content = res.content - res.close() - return json.loads(content) - - for chunk in res.iter_content(chunk_size=1024): - Update_manager.version_file['chunk'] += chunk - length = res.raw._fp_bytes_read - if int(total_length) == length: - res.close() - logger.info("Finished Download: version_file") - return json.loads(Update_manager.version_file['chunk']) - return True - except Exception as err: - logger.warning("no Connection (%s)" % err) - res.close() - return None - - -def apply_version_file_result( - ActRec_pref: AR_preferences, - version_file: dict, - update: tuple[bool, Union[tuple, str]]) -> None: - """ - updates the version in the addon preferences if needed and closes the open request from version file - - Args: - ActRec_pref (AR_preferences): preferences of this addon - version_file (dict): contains data about the addon version, the version of each file and the open request - update (tuple[bool, Union[tuple, str]]): - [0] update is available; - [1] version of the addon in str or tuple format - """ - ActRec_pref.update = update[0] - if not update[0]: - res = version_file.get('respond') - if res: - res.close() - version_file.clear() - if isinstance(update[1], str): - ActRec_pref.version = update[1] - else: - ActRec_pref.version = ".".join(map(str, update[1])) - - -def get_download_list(version_file: dict) -> Optional[list]: - """ - creates a list of which files needed to be downloaded to install the update - - Args: - version_file (dict): contains data about the addon version, the version of each file and the open request - - Returns: - Optional[list]: - [None] no paths are written; - [list] list of the paths that needed to be downloaded - """ - download_files = version_file["files"] - if download_files is None: - return None - download_list = [] - version = config.version - for key in download_files: - if tuple(download_files[key]) > version: - download_list.append(key) - return download_list - - -def no_stream_download_version_file(module_name: str) -> None: - """ - downloads the version file without needed to be called again. Is faster but stops Blender from executing other code. - - Args: - module_name (str): name of the addon to get the addon preferences - """ - try: - logger.info("Start Download: version_file") - res = requests.get(config.check_source_url) - logger.info("Finished Download: version_file") - Update_manager.version_file = json.loads(res.content) - version_file = Update_manager.version_file - update = check_for_update(version_file) - ActRec_pref = bpy.context.preferences.addons[module_name].preferences - apply_version_file_result(ActRec_pref, version_file, update) - except Exception as err: - logger.warning("no Connection (%s)" % err) -# endregion functions - -# region UI functions - - -def draw_update_button(layout: UILayout, ActRec_pref: AR_preferences) -> None: - """ - draws the update button and show a progress bar when files get downloaded - - Args: - layout (UILayout): context where to draw the button - ActRec_pref (AR_preferences): preferences of this addon - """ - if ActRec_pref.update_progress >= 0: - row = layout.row() - row.enabled = False - row.prop(ActRec_pref, 'update_progress', text="Progress", slider=True) - else: - layout.operator('ar.update', text='Update') -# endregion - -# region Operator - - -class AR_OT_update_check(Operator): - bl_idname = "ar.update_check" - bl_label = "Check for Update" - bl_description = "check for available update" - - def invoke(self, context: Context, event: Event) -> set[str]: - res = start_get_version_file() - if res: - self.timer = context.window_manager.event_timer_add(0.1) - context.window_manager.modal_handler_add(self) - return {'RUNNING_MODAL'} - self.report({'WARNING'}, "No Internet Connection") - return {'CANCELLED'} - - def modal(self, context: Context, event: Event) -> set[str]: - version_file = get_version_file(Update_manager.version_file['respond']) - if isinstance(version_file, dict) or version_file is None: - Update_manager.version_file = version_file - return self.execute(context) - return {'PASS_THROUGH'} - - def execute(self, context: Context) -> set[str]: - version_file = Update_manager.version_file - if not version_file: - return {'CANCELLED'} - if version_file.get('respond'): - return {'RUNNING_MODAL'} - update = check_for_update(version_file) - ActRec_pref = get_preferences(context) - apply_version_file_result(ActRec_pref, version_file, update) - context.window_manager.event_timer_remove(self.timer) - return {"FINISHED"} - - def cancel(self, context: Context) -> None: - if Update_manager.version_file: - res = Update_manager.version_file.get('respond') - if res: - res.close() - Update_manager.version_file.clear() - context.window_manager.event_timer_remove(self.timer) - - -class AR_OT_update(Operator): - bl_idname = "ar.update" - bl_label = "Update" - bl_description = "install the new version" - - @classmethod - def poll(cls, context: Context) -> bool: - ActRec_pref = get_preferences(context) - return ActRec_pref.update - - def invoke(self, context: Context, event: Event) -> set[str]: - Update_manager.download_list = get_download_list( - Update_manager.version_file) - Update_manager.download_length = len(Update_manager.download_list) - self.timer = context.window_manager.event_timer_add( - 0.05, window=context.window) - context.window_manager.modal_handler_add(self) - return {'RUNNING_MODAL'} - - def modal(self, context: Context, event: Event) -> set[str]: - if not len(Update_manager.download_list): - return self.execute(context) - - ActRec_pref = get_preferences(context) - path = Update_manager.download_list[0] - res = update(ActRec_pref, path, Update_manager.update_respond, - Update_manager.update_data_chunks, Update_manager.download_length) - - if res is None: - self.report({'WARNING'}, "No Internet Connection") - return {'CANCELLED'} - - context.area.tag_redraw() - return {'PASS_THROUGH'} - - def execute(self, context: Context) -> set[str]: - if not (Update_manager.version_file and Update_manager.update_data_chunks): - return {'CANCELLED'} - ActRec_pref = get_preferences(context) - ActRec_pref.update = False - ActRec_pref.restart = True - install_update(ActRec_pref, Update_manager.update_data_chunks, - Update_manager.version_file) - ActRec_pref.update_progress = -1 - self.cancel(context) - bpy.ops.ar.show_restart_menu('INVOKE_DEFAULT') - context.area.tag_redraw() - return {"FINISHED"} - - def cancel(self, context: Context) -> None: - res = Update_manager.update_respond - if res: - res.close() - Update_manager.update_respond = None - Update_manager.download_length = 0 - Update_manager.download_list.clear() - Update_manager.update_data_chunks.clear() - context.window_manager.event_timer_remove(self.timer) - - -class AR_OT_restart(Operator, ExportHelper): - bl_idname = "ar.restart" - bl_label = "Restart Blender" - bl_description = "Restart Blender" - bl_options = {"INTERNAL"} - - save: BoolProperty(default=False) - filename_ext = ".blend" - filter_folder: BoolProperty(default=True, options={'HIDDEN'}) - filter_blender: BoolProperty(default=True, options={'HIDDEN'}) - - def invoke(self, context: Context, event: Event) -> set[str]: - if self.save and not bpy.data.filepath: - return ExportHelper.invoke(self, context, event) - else: - return self.execute(context) - - def execute(self, context: Context) -> set[str]: - ActRec_pref = get_preferences(context) - path = bpy.data.filepath - if self.save: - if not path: - path = self.filepath - if not path: - return ExportHelper.invoke(self, context, None) - bpy.ops.wm.save_mainfile(filepath=path) - ActRec_pref.restart = False - if os.path.exists(path): - args = [*sys.argv, path] - else: - args = sys.argv - subprocess.Popen(args) - bpy.ops.wm.quit_blender() - return {"FINISHED"} - - def draw(self, context: Context) -> None: - pass - - -class AR_OT_show_restart_menu(Operator): - bl_idname = "ar.show_restart_menu" - bl_label = "Restart Blender" - bl_description = "Restart Blender" - bl_options = {'REGISTER', 'UNDO'} - - restart_options: EnumProperty( - items=[("exit", "Don't Restart", "Don't restart and exit this window"), - ("save", "Save & Restart", "Save & Restart Blender"), - ("restart", "Restart", "Restart Blender")]) - - def invoke(self, context: Context, event: Event) -> set[str]: - return context.window_manager.invoke_props_dialog(self) - - def execute(self, context: Context) -> set[str]: - if self.restart_options == "save": - bpy.ops.ar.restart(save=True) - elif self.restart_options == "restart": - bpy.ops.ar.restart() - return {"FINISHED"} - - def cancel(self, context: Context) -> None: - bpy.ops.ar.show_restart_menu("INVOKE_DEFAULT") - - def draw(self, context: Context) -> None: - ActRec_pref = get_preferences(context) - layout = self.layout - if ActRec_pref.restart: - layout.label( - text="You need to restart Blender to complete the Update") - layout.prop(self, 'restart_options', expand=True) -# endregion - - -classes = [ - AR_OT_update_check, - AR_OT_update, - AR_OT_restart, - AR_OT_show_restart_menu -] - -# region Registration - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - bpy.app.handlers.load_post.append(on_start) - bpy.app.handlers.depsgraph_update_post.append(on_scene_update) - - -def unregister(): - for cls in classes: - bpy.utils.unregister_class(cls) - with suppress(Exception): - bpy.app.handlers.load_post.remove(on_start) - with suppress(Exception): - bpy.app.handlers.depsgraph_update_post.remove(on_scene_update) - Update_manager.download_list.clear() - Update_manager.download_length = 0 - Update_manager.update_data_chunks.clear() - Update_manager.update_respond = None - Update_manager.version_file.clear() - Update_manager.version_file_thread = None -# endregion diff --git a/ActRec/blender_manifest.toml b/ActRec/blender_manifest.toml new file mode 100644 index 0000000..8c0e867 --- /dev/null +++ b/ActRec/blender_manifest.toml @@ -0,0 +1,78 @@ +schema_version = "1.0.0" + +# Example of manifest file for a Blender extension +# Change the values according to your extension +id = "action_recorder" +version = "4.2.0" +name = "Action Recorder" +tagline = "Automate complex, repetitive tasks to improve efficiency" +maintainer = "InamuraJIN, RivinHD" +# Supported types: "add-on", "theme" +type = "add-on" + +# # Optional: link to documentation, support, source files, etc +website = "https://inamurajin.github.io/ActionRecorder/" + +# # Optional: tag list defined by Blender and server, see: +# # https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html +tags = ["User Interface", "System"] + +blender_version_min = "4.2.0" +# # Optional: Blender version that the extension does not support, earlier versions are supported. +# # This can be omitted and defined later on the extensions platform if an issue is found. +# blender_version_max = "5.1.0" + +# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix) +# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html +license = [ + "SPDX:GPL-3.0-or-later", +] +# # Optional: required by some licenses. +# copyright = [ +# "2002-2024 Developer Name", +# "1998 Company Name", +# ] + +# # Optional: list of supported platforms. If omitted, the extension will be available in all operating systems. +platforms = ["windows-x64", "macos-arm64", "linux-x64", "macos-x64"] +# # Other supported platforms: "windows-arm64" + +# Optional: bundle 3rd party Python modules. +# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html +wheels = [ + "./wheels/fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl", + "./wheels/fonttools-4.54.1-cp311-cp311-win_amd64.whl", + "./wheels/fonttools-4.54.1-py3-none-any.whl", + "./wheels/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", + "./wheels/Brotli-1.1.0-cp311-cp311-win_amd64.whl", + "./wheels/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", +] + +# # Optional: add-ons can list which resources they will require: +# # * files (for access of any filesystem operations) +# # * network (for internet access) +# # * clipboard (to read and/or write the system clipboard) +# # * camera (to capture photos and videos) +# # * microphone (to capture audio) +# # +# # If using network, remember to also check `bpy.app.online_access` +# # https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access +# # +# # For each permission it is important to also specify the reason why it is required. +# # Keep this a single short sentence without a period (.) at the end. +# # For longer explanations use the documentation or detail page. +[permissions] +files = "Save/Load the global actions, custom icons and log file" + +# # Optional: advanced build settings. +# # https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build +# # These are the default build excluded patterns. +# # You only need to edit them if you want different options. +[build] +paths_exclude_pattern = [ + "__pycache__/", + "/.git/", + "/*.zip", + "/Icons/", + "/logs/", +] diff --git a/ActRec/wheels/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl b/ActRec/wheels/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl new file mode 100644 index 0000000..29ff0d4 Binary files /dev/null and b/ActRec/wheels/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl differ diff --git a/ActRec/wheels/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl b/ActRec/wheels/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl new file mode 100644 index 0000000..860a205 Binary files /dev/null and b/ActRec/wheels/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl differ diff --git a/ActRec/wheels/Brotli-1.1.0-cp310-cp310-win_amd64.whl b/ActRec/wheels/Brotli-1.1.0-cp310-cp310-win_amd64.whl new file mode 100644 index 0000000..ebb6c09 Binary files /dev/null and b/ActRec/wheels/Brotli-1.1.0-cp310-cp310-win_amd64.whl differ diff --git a/ActRec/wheels/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl b/ActRec/wheels/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl new file mode 100644 index 0000000..51bb597 Binary files /dev/null and b/ActRec/wheels/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl differ diff --git a/ActRec/wheels/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/ActRec/wheels/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl new file mode 100644 index 0000000..5ee808e Binary files /dev/null and b/ActRec/wheels/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ diff --git a/ActRec/wheels/Brotli-1.1.0-cp311-cp311-win_amd64.whl b/ActRec/wheels/Brotli-1.1.0-cp311-cp311-win_amd64.whl new file mode 100644 index 0000000..c8bcf38 Binary files /dev/null and b/ActRec/wheels/Brotli-1.1.0-cp311-cp311-win_amd64.whl differ diff --git a/ActRec/wheels/fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl b/ActRec/wheels/fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl new file mode 100644 index 0000000..8876343 Binary files /dev/null and b/ActRec/wheels/fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl differ diff --git a/ActRec/wheels/fonttools-4.54.1-cp310-cp310-win_amd64.whl b/ActRec/wheels/fonttools-4.54.1-cp310-cp310-win_amd64.whl new file mode 100644 index 0000000..c838fc1 Binary files /dev/null and b/ActRec/wheels/fonttools-4.54.1-cp310-cp310-win_amd64.whl differ diff --git a/ActRec/wheels/fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl b/ActRec/wheels/fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl new file mode 100644 index 0000000..6b6fd8f Binary files /dev/null and b/ActRec/wheels/fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl differ diff --git a/ActRec/wheels/fonttools-4.54.1-cp311-cp311-win_amd64.whl b/ActRec/wheels/fonttools-4.54.1-cp311-cp311-win_amd64.whl new file mode 100644 index 0000000..8d5096e Binary files /dev/null and b/ActRec/wheels/fonttools-4.54.1-cp311-cp311-win_amd64.whl differ diff --git a/ActRec/wheels/fonttools-4.54.1-py3-none-any.whl b/ActRec/wheels/fonttools-4.54.1-py3-none-any.whl new file mode 100644 index 0000000..7ad4b94 Binary files /dev/null and b/ActRec/wheels/fonttools-4.54.1-py3-none-any.whl differ diff --git a/README.md b/README.md index 2ec2673..5e2687c 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ +# Website (Installation & Documentation) +**https://inamurajin.github.io/ActionRecorder/** + # LINK - - [Action Recorder](#action-recorder) - - [Add-ons Explained](#add-ons-explained) - - [INSTALL](#install) - - [Tutorial&Readme](#tutorialreadme) - - [Update](#update) - - [Community](#community) - - [Bug Reports and Requests](#bug-reports-and-requests) -# PLEASE! -Our workload has made it difficult for us to update and fix bugs
-New programmers are expected to be created!
-We have therefore made some major changes to the Action Recorder add-on
-For example, we have separated the Python files and added comments to make it easier for other programmers to understand
-Please Fork this add-on!

+- [Website (Installation \& Documentation)](#website-installation--documentation) +- [LINK](#link) +- [Action Recorder](#action-recorder) +- [Add-ons Explained](#add-ons-explained) +- [INSTALL](#install) +- [Old Tutorial\&Readme](#old-tutorialreadme) +- [Update](#update) +- [Community](#community) +- [Bug Reports and Requests](#bug-reports-and-requests) # Action Recorder Are you tirred of coding long intimidating coding task just for one-off modeling? Or may be you're not good coding at all !
@@ -37,7 +36,7 @@ Convert complex, repetitive tasks into one click! You have to remember is "Add" & "Play" Only! -Supported Versions: 2.83 to 2.9 +Supported Versions: 3.6 to 4.2 Simplify complex repetitive tasks, which were difficult to do with the standard “Repeat Last”(Shift+R) function of Blender, by registering multiple actions. @@ -63,7 +62,7 @@ Click the "Install Add-on" button
このページの右側に「Release」ボタンからZipファイルをダウンロードして下さい -# Tutorial&Readme +# Old Tutorial&Readme 🇯🇵[日本語](https://inamurajin.wixsite.com/website/post/tutorial_readme_jp)
※ 日本語解説の更新は停止します。前バージョンの記事で参考に出来ますが、詳しくは英語版でご覧ください diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..623c61f --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +blender --command extension build +blender --command extension build --split-platforms \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 682fdf6..a5ff690 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,7 +26,8 @@ 'sphinx.ext.napoleon', 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', - 'myst_parser' + 'myst_parser', + 'sphinx_copybutton' ] templates_path = ['_templates'] @@ -45,7 +46,7 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'sphinx_rtd_theme' +html_theme = 'furo' html_static_path = ['_static'] html_context = { "default_mode": "auto" diff --git a/docs/source/getting_started/terms_definition.md b/docs/source/getting_started/terms_definition.md index 6ca7319..13a5335 100644 --- a/docs/source/getting_started/terms_definition.md +++ b/docs/source/getting_started/terms_definition.md @@ -21,7 +21,12 @@ By default it is saved in the `Storage.json` file which is located inside the in :::{hint} By default the path to the `Storage`-File is\ -` +``` C:\Users\\AppData\Roaming\Blender Foundation\Blender\3.3\scripts\addons\ActRec\Storage.json -` +``` + +As of Blender version 4.2 the `Storage`-File is located in the extension user directory:\ +``` +C:\Users\\AppData\Roaming\Blender Foundation\Blender\4.2\extensions\.user\user_default\action_recorder\Storage.json<> +``` ::: \ No newline at end of file diff --git a/docs/source/panels/macro.md b/docs/source/panels/macro.md index 4034947..10332ca 100644 --- a/docs/source/panels/macro.md +++ b/docs/source/panels/macro.md @@ -32,7 +32,7 @@ Copies the command to the clipboard. ### Execution Context The context which the operator will be executed with. -Most of the time `Execution` is the right selection. +Most of the time `Execute` is the right selection. :::{Hint} Sometimes it is helpful to set it to `Invoke` where the operator can access user input or startup a process that will not be executed immediately. @@ -96,26 +96,4 @@ Executes all macros from the selected Action (Shortcut: `alt + .`). ### Local to Global Moves the selected Action to the Global Panel. A popup appears to select the Category to append the Action to. -The "Settings/Buttons" below `Local to Global` decided weather to `Copy` the Action (keep this Action in `Local` and move an exact copy it to the `Global` section) or `Move` (removes this action from `Local` and append it to the `Global` section). - -## Multiline Support - -:::{figure-md} -![Multiline Support Install](../images/MacroEditor_MultilineInstall.png) - -First Popup to install Multiline Support -::: - -If `Don't Ask Again` is checked it can be later installed in the Preferences - -:::{figure-md} -![Multiline Preferences](../images/Preferences_SettingsMultiline.png) - -Later install in the Preferences -::: - -If Install is pressed the Popup will change to the following:\ -![Multiline Installing](../images/MacroEditor_MultilineInstalling.png) - -After the installation finished the following appear:\ -![Multiline Installed](../images/MacroEditor_MultilineInstalled.png) \ No newline at end of file +The "Settings/Buttons" below `Local to Global` decided weather to `Copy` the Action (keep this Action in `Local` and move an exact copy it to the `Global` section) or `Move` (removes this action from `Local` and append it to the `Global` section). \ No newline at end of file diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt index cf9a675..0190c42 100644 --- a/docs/source/requirements.txt +++ b/docs/source/requirements.txt @@ -1,2 +1,3 @@ -sphinx-rtd-theme -myst-parser \ No newline at end of file +furo +myst-parser +sphinx-copybutton \ No newline at end of file diff --git a/download_file.json b/download_file.json deleted file mode 100644 index 3c88f3a..0000000 --- a/download_file.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "version": [4, 1, 2], - "files": { - "ActRec/actrec/functions/__init__.py": [4, 1, 0], - "ActRec/actrec/functions/categories.py": [4, 1, 0], - "ActRec/actrec/functions/globals.py": [4, 1, 0], - "ActRec/actrec/functions/locals.py": [4, 1, 0], - "ActRec/actrec/functions/macros.py": [4, 1, 1], - "ActRec/actrec/functions/shared.py": [4, 1, 2], - "ActRec/actrec/menus/__init__.py": [4, 0, 6], - "ActRec/actrec/menus/locals.py": [4, 1, 0], - "ActRec/actrec/menus/categories.py": [4, 1, 0], - "ActRec/actrec/operators/__init__.py": [4, 1, 0], - "ActRec/actrec/operators/categories.py": [4, 1, 0], - "ActRec/actrec/operators/globals.py": [4, 1, 2], - "ActRec/actrec/operators/helper.py": [4, 1, 0], - "ActRec/actrec/operators/locals.py": [4, 1, 0], - "ActRec/actrec/operators/macros.py": [4, 1, 2], - "ActRec/actrec/operators/preferences.py": [4, 1, 0], - "ActRec/actrec/operators/shared.py": [4, 1, 0], - "ActRec/actrec/panels/__init__.py": [4, 0, 0], - "ActRec/actrec/panels/main.py": [4, 1, 0], - "ActRec/actrec/properties/__init__.py": [4, 0, 6], - "ActRec/actrec/properties/categories.py": [4, 0, 0], - "ActRec/actrec/properties/globals.py": [4, 1, 0], - "ActRec/actrec/properties/locals.py": [4, 1, 0], - "ActRec/actrec/properties/macros.py": [4, 1, 0], - "ActRec/actrec/properties/shared.py": [4, 1, 0], - "ActRec/actrec/ui_functions/__init__.py": [4, 0, 6], - "ActRec/actrec/ui_functions/categories.py": [4, 1, 0], - "ActRec/actrec/ui_functions/globals.py": [4, 1, 0], - "ActRec/actrec/uilist/__init__.py": [4, 0, 0], - "ActRec/actrec/uilist/locals.py": [4, 1, 0], - "ActRec/actrec/uilist/macros.py": [4, 1, 0], - "ActRec/actrec/__init__.py": [4, 0, 8], - "ActRec/actrec/config.py": [4, 1, 2], - "ActRec/actrec/icon_manager.py": [4, 1, 0], - "ActRec/actrec/keymap.py": [4, 1, 2], - "ActRec/actrec/log.py": [4, 1, 0], - "ActRec/actrec/preferences.py": [4, 1, 0], - "ActRec/actrec/shared_data.py": [4, 0, 8], - "ActRec/actrec/update.py": [4, 1, 0], - "ActRec/__init__.py": [4, 1, 2] - }, - "remove": [] -} \ No newline at end of file diff --git a/update_wheels.sh b/update_wheels.sh new file mode 100644 index 0000000..c63afff --- /dev/null +++ b/update_wheels.sh @@ -0,0 +1,16 @@ +pip download fonttools --dest ./ActRec/wheels --only-binary=:all: --python-version=3.11 --platform=macosx_10_13_universal2 +pip download fonttools --dest ./ActRec/wheels --only-binary=:all: --python-version=3.11 --platform=manylinux_2_28_x86_64 +pip download fonttools --dest ./ActRec/wheels --only-binary=:all: --python-version=3.11 --platform=win_amd64 + +pip download brotli --dest ./ActRec/wheels --only-binary=:all: --python-version=3.11 --platform=macosx_10_13_universal2 +pip download brotli --dest ./ActRec/wheels --only-binary=:all: --python-version=3.11 --platform=manylinux_2_17_x86_64 +pip download brotli --dest ./ActRec/wheels --only-binary=:all: --python-version=3.11 --platform=win_amd64 + +# Deprecated: removed the command below if the blender version 3.6 LTS is no longer supported +pip download fonttools --dest ./ActRec/wheels --only-binary=:all: --python-version=3.10 --platform=macosx_10_13_universal2 +pip download fonttools --dest ./ActRec/wheels --only-binary=:all: --python-version=3.10 --platform=manylinux_2_28_x86_64 +pip download fonttools --dest ./ActRec/wheels --only-binary=:all: --python-version=3.10 --platform=win_amd64 + +pip download brotli --dest ./ActRec/wheels --only-binary=:all: --python-version=3.10 --platform=macosx_10_13_universal2 +pip download brotli --dest ./ActRec/wheels --only-binary=:all: --python-version=3.10 --platform=manylinux1_x86_64 +pip download brotli --dest ./ActRec/wheels --only-binary=:all: --python-version=3.10 --platform=win_amd64 \ No newline at end of file