├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── documentation.yml │ ├── flake8_linting.yml │ ├── scripts │ └── update_version.py │ ├── unit_test_addon.yml │ └── update_version.yml ├── .gitignore ├── .vscode ├── settings.json └── snippets.code-snippets ├── ActRec ├── Storage.json ├── __init__.py └── actrec │ ├── __init__.py │ ├── config.py │ ├── functions │ ├── __init__.py │ ├── categories.py │ ├── globals.py │ ├── locals.py │ ├── macros.py │ └── shared.py │ ├── icon_manager.py │ ├── keymap.py │ ├── log.py │ ├── menus │ ├── __init__.py │ ├── categories.py │ └── locals.py │ ├── operators │ ├── __init__.py │ ├── categories.py │ ├── globals.py │ ├── helper.py │ ├── locals.py │ ├── macros.py │ ├── preferences.py │ └── shared.py │ ├── panels │ ├── __init__.py │ └── main.py │ ├── preferences.py │ ├── properties │ ├── __init__.py │ ├── categories.py │ ├── globals.py │ ├── locals.py │ ├── macros.py │ └── shared.py │ ├── shared_data.py │ ├── ui_functions │ ├── __init__.py │ ├── categories.py │ └── globals.py │ ├── uilist │ ├── __init__.py │ ├── locals.py │ └── macros.py │ └── update.py ├── LICENSE ├── README.md ├── ReadMe_English.txt ├── ReadMe_Japanese.txt ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── style.css │ ├── _templates │ └── layout.html │ ├── conf.py │ ├── getting_started │ ├── first_action.md │ ├── installation.md │ └── terms_definition.md │ ├── images │ ├── Add_Action.svg │ ├── Add_Macro.svg │ ├── Added_Action.svg │ ├── ContextMenu_CopyButton.svg │ ├── LocalOperators.svg │ ├── MacroEdit_Context.png │ ├── MacroEdit_Context.svg │ ├── MacroEdit_Operator.png │ ├── MacroEdit_Operator.svg │ ├── MacroEditorOperators.svg │ ├── MacroEditor_MultilineInstall.png │ ├── MacroEditor_MultilineInstall.svg │ ├── MacroEditor_MultilineInstalled.png │ ├── MacroEditor_MultilineInstalling.png │ ├── Preferences_SettingsMultiline.png │ ├── RecordingPhases.svg │ ├── Simple_Macro.svg │ ├── Tab_ActionRecorder.png │ └── terms.svg │ ├── index.rst │ ├── panels │ ├── local.md │ └── macro.md │ └── requirements.txt ├── download_file.json └── testing ├── requirements.txt ├── test_addon.py └── unit ├── __init__.py ├── functions ├── __init__.py └── test_shared.py ├── helper.py ├── test_icon_manager.py └── test_src_data └── icon_manager ├── test_icon_jpg.jpg ├── test_icon_png1.png ├── test_icon_png2.png └── test_icon_svg.svg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report any Error or unlogical behavior. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Version Information:** 27 | - Add-on Version [e.g. 2.1.4] 28 | - Blender Version [e.g. 3.0.1] 29 | - OS: [e.g. Windows] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this Add-on 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. Use Images as needed. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about the add-on 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master", Development ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master", Development ] 20 | schedule: 21 | - cron: '31 21 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Sphinx documentation to Pages 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | pages: 10 | runs-on: ubuntu-20.04 11 | environment: 12 | name: github-pages 13 | url: ${{ steps.deployment.outputs.page_url }} 14 | permissions: 15 | pages: write 16 | id-token: write 17 | steps: 18 | - id: deployment 19 | uses: sphinx-notes/pages@v3 20 | with: 21 | documentation_path: ./docs/source 22 | requirements_path: ./docs/source/requirements.txt -------------------------------------------------------------------------------- /.github/workflows/flake8_linting.yml: -------------------------------------------------------------------------------- 1 | name: flake8 Lint 2 | 3 | on: 4 | push: 5 | branches: [master, Development] 6 | pull_request: 7 | branches: [master, Development] 8 | 9 | jobs: 10 | flake8-lint: 11 | runs-on: ubuntu-latest 12 | name: Lint 13 | steps: 14 | - name: Check out source repository 15 | uses: actions/checkout@v4 16 | - name: Set up Python environment 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: "3.11" 20 | - name: flake8 Lint 21 | uses: py-actions/flake8@v2 22 | with: 23 | ignore: "E121,E123,E126,E226,E24,E704,F722,F821,F401,W503,W504" 24 | max-line-length: "120" 25 | path: "ActRec" -------------------------------------------------------------------------------- /.github/workflows/scripts/update_version.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import json 4 | import re 5 | 6 | 7 | def collapse_json(text, list_length=4): 8 | for length in range(list_length): 9 | re_pattern = r'\[' + (r'\s*(.+)\s*,' * length)[:-1] + r'\]' 10 | re_repl = r'[' + ''.join(r'\{}, '.format(i+1) for i in range(length))[:-2] + r']' 11 | 12 | text = re.sub(re_pattern, re_repl, text) 13 | 14 | return text 15 | 16 | 17 | if __name__ == "__main__": 18 | parser = argparse.ArgumentParser() 19 | 20 | def parse_list(text): 21 | return text.split(",") 22 | parser.add_argument("-files", type=parse_list, default=[], nargs='?', const=[]) 23 | parser.add_argument("-removed", type=parse_list, default=[], nargs='?', const=[]) 24 | args = parser.parse_args() 25 | 26 | addon_directory = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 27 | version = (0, 0, 0) 28 | with open(os.path.join(addon_directory, "ActRec/__init__.py"), 'r', encoding='utf-8') as file: 29 | for line in file.readlines(): 30 | if "version" in line: 31 | version = eval("%s)" % line.split(":")[1].split(")")[0].strip()) 32 | break 33 | with open(os.path.join(addon_directory, "ActRec/actrec/config.py"), 'r', encoding='utf-8') as file: 34 | for line in file.readlines(): 35 | if "version" in line: 36 | check_version = eval(line.split("=")[1].strip()) 37 | if check_version > version: 38 | version = check_version 39 | break 40 | 41 | print("Update to Version %s\nFiles: %s\nRemoved: %s" % (version, args.files, args.removed)) 42 | 43 | version = list(version) 44 | with open(os.path.join(addon_directory, "download_file.json"), 'r', encoding='utf-8') as download_file: 45 | data = json.loads(download_file.read()) 46 | data_files = data["files"] 47 | for file in args.files: 48 | if data_files.get(file, None): 49 | data_files[file] = version 50 | data_remove = data["remove"] 51 | for file in args.removed: 52 | if file not in data_remove and file.startswith("ActRec/"): 53 | data_remove.append(file) 54 | data["version"] = version 55 | with open(os.path.join(addon_directory, "download_file.json"), 'w', encoding='utf-8') as download_file: 56 | download_file.write(collapse_json(json.dumps(data, ensure_ascii=False, indent=4))) 57 | 58 | lines = [] 59 | with open(os.path.join(addon_directory, "ActRec/__init__.py"), 'r', encoding='utf-8') as file: 60 | for line in file.read().splitlines(): 61 | if "version" in line: 62 | split = line.split(": ") 63 | sub_split = split[1].split(")") 64 | line = "%s: %s%s" % (split[0], str(tuple(version)), sub_split[-1]) 65 | lines.append(line) 66 | with open(os.path.join(addon_directory, "ActRec/__init__.py"), 'w', encoding='utf-8') as file: 67 | file.write("\n".join(lines)) 68 | file.write("\n") 69 | 70 | lines = [] 71 | with open(os.path.join(addon_directory, "ActRec/actrec/config.py"), 'r', encoding='utf-8') as file: 72 | for line in file.read().splitlines(): 73 | if "version" in line: 74 | split = line.split("=") 75 | line = "version = %s" % str(tuple(version)) 76 | lines.append(line) 77 | with open(os.path.join(addon_directory, "ActRec/actrec/config.py"), 'w', encoding='utf-8') as file: 78 | file.write("\n".join(lines)) 79 | file.write("\n") 80 | -------------------------------------------------------------------------------- /.github/workflows/unit_test_addon.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/nangtani/blender-addon-tester/blob/master/.github/workflows/test-fake-addon-example-from-local-wheel.yml 2 | name: test-addon 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | branches: [master, Development] 8 | pull_request: 9 | branches: [master, Development] 10 | 11 | jobs: 12 | build: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | blender-version: ["4.0", "3.6", "3.3"] 18 | #os: [ubuntu-latest, windows-latest, macos-latest] 19 | # FIXME Addon doesn't work on Ubuntu (don't know why) 20 | os: [windows-latest] 21 | env: 22 | BLENDER_CACHE: ${{ github.workspace }}/.blender_releases_cache # The place where blender releases are downloaded 23 | BLENDER_VERSION: ${{ matrix.blender-version }} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Set up Python 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: "3.10" 31 | 32 | - name: Cache Blender release download 33 | uses: actions/cache@v4 34 | with: 35 | path: ${{ env.BLENDER_CACHE }} 36 | key: ${{ matrix.os }}-blender-${{ matrix.blender-version }} 37 | 38 | - name: Setup Environment 39 | run: pip install -r testing/requirements.txt 40 | 41 | - name: Test Blender ${{ matrix.blender-version }} x ${{ matrix.os}} 42 | run: python testing/test_addon.py ActRec ${{ matrix.blender-version }} unit 43 | -------------------------------------------------------------------------------- /.github/workflows/update_version.yml: -------------------------------------------------------------------------------- 1 | name: Update Version 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | 9 | update: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 2 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.10" 21 | - name: Get changed files 22 | id: files 23 | run: | 24 | diff=$(git diff --name-only --diff-filter=AM HEAD^..HEAD | tr '\n' ',') 25 | echo "added_modified=$diff" >> "$GITHUB_OUTPUT" 26 | echo "Added & Modified: $diff" 27 | diff=$(git diff --name-only --diff-filter=D HEAD^..HEAD | tr '\n' ',') 28 | echo "removed=$diff" >> "$GITHUB_OUTPUT" 29 | echo "Removed: $diff" 30 | - name: Update to new Version 31 | id: update 32 | working-directory: .github/workflows/scripts 33 | run: | 34 | output=$(python update_version.py -files ${{ steps.files.outputs.added_modified }} -removed ${{ steps.files.outputs.removed }}) 35 | output="${output//'%'/'%25'}" 36 | output="${output//$'\n'/'%0A'}" 37 | output="${output//$'\r'/'%0D'}" 38 | echo "log=$output" >> $GITHUB_OUTPUT 39 | - name: Print Log 40 | run: echo "${{ steps.update.outputs.log }}" 41 | - name: Update files on GitHub 42 | uses: test-room-7/action-update-file@v1.8.0 43 | with: 44 | file-path: | 45 | ActRec/__init__.py 46 | download_file.json 47 | ActRec/actrec/config.py 48 | commit-msg: Update Files 49 | github-token: ${{ secrets.FILE_UPDATER }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | /ActRec/logs 3 | /ActRec/keymap.json 4 | /ActRec/Icons 5 | TextEditorExample.txt 6 | /docs/build 7 | /ActRec/packages 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "actrec", 4 | "autosave", 5 | "COLLAPSEMENU", 6 | "depsgraph", 7 | "DOPESHEET", 8 | "endcharacter", 9 | "Endnumber", 10 | "endregion", 11 | "excepthook", 12 | "FILEBROWSER", 13 | "fonttools", 14 | "Icontable", 15 | "idname", 16 | "keyconfigs", 17 | "keymapitem", 18 | "keymaps", 19 | "levelname", 20 | "mainfile", 21 | "mathutils", 22 | "oskey", 23 | "propname", 24 | "recategorize", 25 | "SORTTIME", 26 | "Startnumber", 27 | "Stepnumber", 28 | "TEXTEDIT", 29 | "texteditor", 30 | "topdown", 31 | "TRIA", 32 | "uilist", 33 | "WHEELDOWNMOUSE", 34 | "WHEELUPMOUSE" 35 | ], 36 | "flake8.args": [ 37 | "--max-line-length=120", 38 | "--ignore=E121,E123,E126,E226,E24,E704,F722,F821,F401,W503,W504" 39 | ], 40 | "autopep8.args": [ 41 | "--max-line-length", 42 | "120", 43 | "--experimental" 44 | ], 45 | "github-actions.workflows.pinned.workflows": [ 46 | ".github/workflows/unit_test_addon.yml" 47 | ] 48 | } 49 | 50 | // ActRec_pref (bpy.types.Preferences): preferences of this addon 51 | // context (bpy.types.Context): active blender context 52 | // default Blender property getter -------------------------------------------------------------------------------- /.vscode/snippets.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | // Place your ActRec workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 7 | // Placeholders with the same ids are connected. 8 | // Example: 9 | // "Print to console": { 10 | // "scope": "javascript,typescript", 11 | // "prefix": "log", 12 | // "body": [ 13 | // "console.log('$1');", 14 | // "$2" 15 | // ], 16 | // "description": "Log output to console" 17 | // } 18 | 19 | 20 | "AR_preferences": { 21 | "prefix":["AR_preferences"], 22 | "body": [ 23 | "ActRec_pref = functions.get_preferences(context)" 24 | ], 25 | "description": "Instert ActRec_pref Prefeneces from functions" 26 | }, 27 | 28 | "Registration": { 29 | "prefix":["Registration"], 30 | "body": [ 31 | "# region Registration", 32 | "def register():", 33 | " for cls in classes:", 34 | " bpy.utils.register_class(cls)", 35 | "", 36 | "def unregister():", 37 | " for cls in classes:", 38 | " bpy.utils.unregister_class(cls)", 39 | "# endregion" 40 | ], 41 | "description": "Insert Standart classes registration" 42 | } 43 | } -------------------------------------------------------------------------------- /ActRec/__init__.py: -------------------------------------------------------------------------------- 1 | from . import actrec 2 | 3 | bl_info = { 4 | "name": "ActionRecorder", 5 | "author": "InamuraJIN, RivinHD", 6 | "version": (4, 1, 2), 7 | "blender": (3, 3, 12), 8 | "location": "View 3D", 9 | "warning": "", 10 | "docs_url": 'https://github.com/InamuraJIN/ActionRecorder/blob/master/README.md', # Documentation 11 | "tracker_url": 'https://inamurajin.wixsite.com/website/post/bug-report', # Report Bug 12 | "link": 'https://twitter.com/Inamura_JIN', 13 | "category": "System" 14 | } 15 | 16 | 17 | def register(): 18 | actrec.register() 19 | 20 | 21 | def unregister(): 22 | actrec.unregister() 23 | -------------------------------------------------------------------------------- /ActRec/actrec/__init__.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # external modules 3 | import json 4 | 5 | # blender modules 6 | import bpy 7 | from bpy.app.handlers import persistent 8 | from bpy.props import PointerProperty 9 | 10 | # relative imports 11 | # unused import needed to give direct access to the modules 12 | from . import functions, menus, operators, panels, properties, ui_functions, uilist 13 | from . import config, icon_manager, keymap, log, preferences, update 14 | from .functions.shared import get_preferences 15 | from . import shared_data 16 | # endregion 17 | 18 | 19 | @persistent 20 | def check_on_load(dummy=None): 21 | """ 22 | used to load global actions if on_load wasn't called 23 | """ 24 | if check_on_load in bpy.app.handlers.depsgraph_update_post: 25 | bpy.app.handlers.depsgraph_update_post.remove(check_on_load) 26 | if shared_data.data_loaded: 27 | return 28 | on_load() 29 | 30 | 31 | @persistent 32 | def on_load(dummy=None): 33 | shared_data.data_loaded = True 34 | log.logger.info("Start: Load ActRec Data") 35 | context = bpy.context 36 | ActRec_pref = get_preferences(context) 37 | ActRec_pref.operators_list_length = 0 38 | # load local actions 39 | if bpy.data.filepath == "": 40 | try: 41 | context.scene.ar.local = "{}" 42 | except AttributeError as err: 43 | log.logger.info(err) 44 | # load old local action data 45 | elif context.scene.ar.local == "{}" and context.scene.get('ar_local', None): 46 | try: 47 | data = [] 48 | old_data = json.loads(context.scene.get('ar_local')) 49 | for i, old_action in enumerate(old_data[0]['Commands'], 1): 50 | data.append({ 51 | "label": old_action['cname'], 52 | "macros": [{ 53 | "label": old_macro['cname'], 54 | "command": old_macro['macro'], 55 | "active": old_macro['active'], 56 | "icon": old_macro['icon'] 57 | } for old_macro in old_data[i]['Commands']], 58 | "icon": old_action['icon'] 59 | }) 60 | context.scene.ar.local = json.dumps(data) 61 | except json.JSONDecodeError as err: 62 | log.logger.info("old scene-data couldn't be parsed (%s)" % err) 63 | functions.load_local_action(ActRec_pref, json.loads(context.scene.ar.local)) 64 | 65 | # update paths 66 | ActRec_pref.storage_path = ActRec_pref.storage_path 67 | ActRec_pref.icon_path = ActRec_pref.icon_path 68 | 69 | functions.load(ActRec_pref) 70 | icon_manager.load_icons(ActRec_pref) 71 | log.logger.info("Finished: Load ActRec Data") 72 | 73 | # region Registration 74 | 75 | 76 | def register(): 77 | log.log_sys.append_file() 78 | properties.register() 79 | menus.register() 80 | operators.register() 81 | panels.register() 82 | uilist.register() 83 | icon_manager.register() 84 | update.register() 85 | preferences.register() 86 | keymap.register() 87 | 88 | handlers = bpy.app.handlers 89 | handlers.render_complete.append(functions.execute_render_complete) 90 | handlers.depsgraph_update_post.append(functions.track_scene) 91 | handlers.load_post.append(on_load) 92 | handlers.depsgraph_update_post.append(check_on_load) 93 | 94 | bpy.types.Scene.ar = PointerProperty(type=properties.AR_scene_data) 95 | 96 | shared_data.data_loaded = False 97 | log.logger.info("Registered Action Recorder") 98 | 99 | 100 | def unregister(): 101 | properties.unregister() 102 | menus.unregister() 103 | operators.unregister() 104 | panels.unregister() 105 | uilist.unregister() 106 | icon_manager.unregister() 107 | update.unregister() 108 | preferences.unregister() 109 | keymap.unregister() 110 | ui_functions.unregister() 111 | 112 | handlers = bpy.app.handlers 113 | handlers.render_complete.remove(functions.execute_render_complete) 114 | handlers.depsgraph_update_post.remove(functions.track_scene) 115 | handlers.load_post.remove(on_load) 116 | if check_on_load in bpy.app.handlers.depsgraph_update_post: 117 | handlers.depsgraph_update_post.remove(check_on_load) 118 | 119 | del bpy.types.Scene.ar 120 | log.logger.info("Unregistered Action Recorder") 121 | log.log_sys.detach_file() 122 | # endregion 123 | -------------------------------------------------------------------------------- /ActRec/actrec/config.py: -------------------------------------------------------------------------------- 1 | manual_url = 'https://github.com/InamuraJIN/ActionRecorder/blob/master/README.md' 2 | hint_url = 'https://inamurajin.wixsite.com/website/post/hint-help' 3 | bug_report_url = 'https://inamurajin.wixsite.com/website/post/bug-report' 4 | check_source_url = 'https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/master/download_file.json' 5 | repo_source_url = 'https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/master/%s' 6 | release_notes_url = 'https://github.com/InamuraJIN/ActionRecorder/wiki' 7 | version = (4, 1, 2) 8 | log_amount = 5 9 | -------------------------------------------------------------------------------- /ActRec/actrec/functions/__init__.py: -------------------------------------------------------------------------------- 1 | """only relative import from intra-modules: ui, shared_data""" 2 | 3 | from .categories import ( 4 | read_category_visibility, 5 | get_category_id 6 | ) 7 | 8 | from .globals import ( 9 | save, 10 | load, 11 | import_global_from_dict, 12 | get_global_action_id, 13 | get_global_action_ids, 14 | add_empty_action_keymap, 15 | is_action_keymap_empty, 16 | get_action_keymap, 17 | remove_action_keymap, 18 | get_all_action_keymaps 19 | ) 20 | 21 | from .locals import ( 22 | save_local_to_scene, 23 | get_local_action_index, 24 | load_local_action, 25 | local_action_to_text, 26 | remove_local_action_from_text 27 | ) 28 | 29 | from .macros import ( 30 | get_local_macro_index, 31 | add_report_as_macro, 32 | get_report_text, 33 | split_context_report, 34 | create_object_copy, 35 | improve_context_report, 36 | split_operator_report, 37 | evaluate_operator, 38 | improve_operator_report, 39 | dict_to_kwarg_str, 40 | track_scene, 41 | merge_report_tracked, 42 | compare_op_dict, 43 | convert_value_to_python 44 | ) 45 | 46 | from .shared import ( 47 | check_for_duplicates, 48 | add_data_to_collection, 49 | insert_to_collection, 50 | swap_collection_items, 51 | property_to_python, 52 | apply_data_to_item, 53 | get_name_of_command, 54 | update_command, 55 | play, 56 | get_font_path, 57 | split_and_keep, 58 | text_to_lines, 59 | execute_render_complete, 60 | enum_list_id_to_name_dict, 61 | enum_items_to_enum_prop_list, 62 | install_packages, 63 | get_preferences, 64 | get_categorized_view_3d_modes, 65 | get_attribute, 66 | get_attribute_default 67 | ) 68 | -------------------------------------------------------------------------------- /ActRec/actrec/functions/categories.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # external modules 3 | from typing import Optional, TYPE_CHECKING 4 | 5 | 6 | # blender modules 7 | from bpy.types import AddonPreferences 8 | 9 | # relative imports 10 | if TYPE_CHECKING: 11 | from ..preferences import AR_preferences 12 | else: 13 | AR_preferences = AddonPreferences 14 | # endregion 15 | 16 | 17 | # region functions 18 | 19 | 20 | def get_category_id(ActRec_pref: AR_preferences, id: str, index: int) -> str: 21 | """ 22 | get category id based on id (check for existence) or index 23 | fallback to selected category if no match occurred 24 | 25 | Args: 26 | ActRec_pref (AR_preferences): preferences of this addon 27 | id (str): id to check 28 | index (int): index of the category 29 | 30 | Returns: 31 | str: id of the category, fallback to selected category if not found 32 | """ 33 | if ActRec_pref.categories.find(id) != -1: 34 | return id 35 | 36 | if index >= 0 and len(ActRec_pref.categories) > index: 37 | return ActRec_pref.categories[index].id 38 | else: 39 | return ActRec_pref.selected_category 40 | 41 | 42 | def read_category_visibility(ActRec_pref: AR_preferences, id: str) -> Optional[list]: 43 | """ 44 | get all areas and modes where the category with the given id is visible 45 | 46 | Args: 47 | ActRec_pref (AR_preferences): preferences of this addon 48 | id (str): id of the category 49 | 50 | Returns: 51 | Optional[list]: dict on success, None on fail 52 | """ 53 | visibility = [] 54 | category = ActRec_pref.categories.get(id, None) 55 | if not category: 56 | return 57 | for area in category.areas: 58 | for mode in area.modes: 59 | visibility.append((area.type, mode.type)) 60 | if len(area.modes) == 0: 61 | visibility.append((area.type, 'all')) 62 | return visibility 63 | # endregion 64 | -------------------------------------------------------------------------------- /ActRec/actrec/functions/globals.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # external modules 3 | import json 4 | import os 5 | from typing import Union, TYPE_CHECKING, Iterable 6 | 7 | # blender modules 8 | import bpy 9 | from bpy.types import AddonPreferences, Context, KeyMapItem, KeyMap 10 | 11 | # relative imports 12 | from ..log import logger 13 | from .. import ui_functions, keymap 14 | from . import shared 15 | if TYPE_CHECKING: 16 | from ..preferences import AR_preferences 17 | else: 18 | AR_preferences = AddonPreferences 19 | # endregion 20 | 21 | 22 | # region Functions 23 | 24 | 25 | def save(ActRec_pref: AR_preferences) -> None: 26 | """ 27 | save the global actions and categories to the storage file 28 | 29 | Args: 30 | ActRec_pref (AR_preferences): preferences of this addon 31 | """ 32 | data = {} 33 | data['categories'] = shared.property_to_python( 34 | ActRec_pref.categories, 35 | exclude=[ 36 | "name", 37 | "selected", 38 | "actions.name", 39 | "areas.name", 40 | "areas.modes.name" 41 | ] 42 | ) 43 | data['actions'] = shared.property_to_python( 44 | ActRec_pref.global_actions, 45 | exclude=[ 46 | "name", 47 | "selected", 48 | "alert", 49 | "execution_mode", 50 | "macros.name", 51 | "macros.is_available", 52 | "macros.is_playing", 53 | "macros.alert", 54 | "is_playing" 55 | ] 56 | ) 57 | with open(ActRec_pref.storage_path, 'w', encoding='utf-8') as storage_file: 58 | json.dump(data, storage_file, ensure_ascii=False, indent=2) 59 | logger.info('saved global actions') 60 | 61 | 62 | def load(ActRec_pref: AR_preferences) -> bool: 63 | """ 64 | load the global actions and categories from the storage file 65 | 66 | Args: 67 | ActRec_pref (AR_preferences): preferences of this addon 68 | 69 | Returns: 70 | bool: success 71 | """ 72 | if not os.path.exists(ActRec_pref.storage_path): 73 | return False 74 | with open(ActRec_pref.storage_path, 'r', encoding='utf-8') as storage_file: 75 | text = storage_file.read() 76 | if not text: 77 | text = "{}" 78 | data = json.loads(text) 79 | logger.info('load global actions') 80 | # cleanup 81 | for i in range(len(ActRec_pref.categories)): 82 | ui_functions.unregister_category(ActRec_pref, i) 83 | ActRec_pref.categories.clear() 84 | ActRec_pref.global_actions.clear() 85 | # load data 86 | if data: 87 | import_global_from_dict(ActRec_pref, data) 88 | return True 89 | return False 90 | 91 | 92 | def import_global_from_dict(ActRec_pref: AR_preferences, data: dict) -> None: 93 | """ 94 | import the global actions and categories from a dict 95 | 96 | Args: 97 | ActRec_pref (AR_preferences): preferences of this addon 98 | data (dict): dict to use 99 | """ 100 | existing_category_len = len(ActRec_pref.categories) 101 | value = data.get('categories', None) 102 | if value: 103 | shared.apply_data_to_item(ActRec_pref.categories, value) 104 | value = data.get('actions', None) 105 | if value: 106 | shared.apply_data_to_item(ActRec_pref.global_actions, value) 107 | 108 | for i in range(existing_category_len, len(ActRec_pref.categories)): 109 | ui_functions.register_category(ActRec_pref, i) 110 | if len(ActRec_pref.categories): 111 | ActRec_pref.categories[0].selected = True 112 | if len(ActRec_pref.global_actions): 113 | ActRec_pref.global_actions[0].selected = True 114 | 115 | 116 | def get_global_action_id(ActRec_pref: AR_preferences, id: str, index: int) -> Union[str, None]: 117 | """ 118 | get global action id based on id (check for existence) or index 119 | 120 | Args: 121 | ActRec_pref (AR_preferences): preferences of this addon 122 | id (str): id to check 123 | index (int): index of action 124 | 125 | Returns: 126 | Union[str, None]: str: action id; None: fail 127 | """ 128 | if ActRec_pref.global_actions.find(id) != -1: 129 | return id 130 | if index >= 0 and len(ActRec_pref.global_actions) > index: 131 | return ActRec_pref.global_actions[index].id 132 | else: 133 | return None 134 | 135 | 136 | def get_global_action_ids(ActRec_pref: AR_preferences, id: str, index: int) -> list: 137 | """ 138 | get global action is inside a list or selected global actions if not found 139 | 140 | Args: 141 | ActRec_pref (AR_preferences): preferences of this addon 142 | id (str): id to check 143 | index (int): index of action 144 | 145 | Returns: 146 | list: list with ids of actions 147 | """ 148 | id = get_global_action_id(ActRec_pref, id, index) 149 | if id is None: 150 | return ActRec_pref.get("global_actions.selected_ids", []) 151 | return [id] 152 | 153 | 154 | def add_empty_action_keymap(id: str, km: KeyMap) -> KeyMapItem: 155 | """ 156 | adds an empty keymap for a global action 157 | 158 | Args: 159 | id (str): id of the action 160 | context (Context): active blender context 161 | 162 | Returns: 163 | KeyMapItem: created keymap or found keymap of action 164 | """ 165 | logger.info("add empty action") 166 | kmi = get_action_keymap(id, km) 167 | if kmi is None: 168 | kmi = km.keymap_items.new( 169 | "ar.global_execute_action", 170 | "NONE", 171 | "PRESS", 172 | head=True 173 | ) 174 | kmi.properties.id = id 175 | return kmi 176 | 177 | 178 | def get_action_keymap(id: str, km: KeyMap) -> Union[KeyMapItem, None]: 179 | """ 180 | get the keymap of the action with the given id 181 | 182 | Args: 183 | id (str): id of the action 184 | context (Context): active blender context 185 | 186 | Returns: 187 | Union[KeyMapItem, None]: KeyMapItem on success; None on fail 188 | """ 189 | for kmi in km.keymap_items: 190 | if kmi.idname == "ar.global_execute_action" and kmi.properties.id == id: 191 | return kmi 192 | return None 193 | 194 | 195 | def is_action_keymap_empty(kmi: KeyMapItem) -> bool: 196 | """ 197 | checks is the given keymapitem is empty 198 | 199 | Args: 200 | kmi (KeyMapItem): keymapitem to check 201 | 202 | Returns: 203 | bool: is empty 204 | """ 205 | return kmi.type == "NONE" 206 | 207 | 208 | def remove_action_keymap(id: str, km: KeyMap) -> None: 209 | """ 210 | removes the keymapitem for the action with the given id 211 | 212 | Args: 213 | id (str): id of the action 214 | context (Context): active blender context 215 | """ 216 | kmi = get_action_keymap(id, km) 217 | km.keymap_items.remove(kmi) 218 | 219 | 220 | def get_all_action_keymaps(km: KeyMap) -> Iterable[KeyMapItem]: 221 | return filter(lambda x: x.idname == "ar.global_execute_action", km.keymap_items) 222 | # endregion 223 | -------------------------------------------------------------------------------- /ActRec/actrec/functions/locals.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # external modules 3 | import json 4 | from typing import TYPE_CHECKING 5 | 6 | # blender modules 7 | import bpy 8 | from bpy.app.handlers import persistent 9 | from bpy.types import AddonPreferences, Scene, PropertyGroup 10 | 11 | # relative imports 12 | from .. import shared_data 13 | from . import shared 14 | from .shared import get_preferences 15 | if TYPE_CHECKING: 16 | from ..preferences import AR_preferences 17 | from ..properties.locals import AR_local_actions 18 | else: 19 | AR_preferences = AddonPreferences 20 | AR_local_actions = PropertyGroup 21 | # endregion 22 | 23 | 24 | # region Functions 25 | 26 | 27 | def save_local_to_scene(ActRec_pref: AR_preferences, scene: Scene) -> None: 28 | """ 29 | saves all local actions to the given scene 30 | 31 | Args: 32 | ActRec_pref (AR_preferences): preferences of this addon 33 | scene (Scene): Blender scene to write to 34 | """ 35 | scene.ar.local = json.dumps(shared.property_to_python(ActRec_pref.local_actions)) 36 | 37 | 38 | def load_local_action(ActRec_pref: AR_preferences, data: list) -> None: 39 | """ 40 | load the given data to the local actions 41 | 42 | Args: 43 | ActRec_pref (AR_preferences): preferences of this addon 44 | data (list): data to apply 45 | """ 46 | actions = ActRec_pref.local_actions 47 | actions.clear() 48 | for value in data: 49 | shared.add_data_to_collection(actions, value) 50 | 51 | 52 | def local_action_to_text(action: AR_local_actions, text_name: str = None) -> None: 53 | """ 54 | write the local action and it's macro to the TextEditor 55 | 56 | Args: 57 | action (AR_local_actions): action to write 58 | text_name (str, optional): name of the written text. Defaults to None. 59 | """ 60 | if text_name is None: 61 | text_name = action.label 62 | texts = bpy.data.texts 63 | if texts.find(text_name) == -1: 64 | texts.new(text_name) 65 | text = texts[text_name] 66 | text.clear() 67 | text.write( 68 | "###ActRec_pref### id: '%s', icon: %i\n%s" % ( 69 | action.id, action.icon, "\n".join( 70 | ["%s # id: '%s', label: '%s', icon: %i, active: %s, is_available: %s" % ( 71 | macro.command, macro.id, macro.label, macro.icon, macro.active, macro.is_available 72 | ) for macro in action.macros] 73 | ) 74 | ) 75 | ) 76 | 77 | 78 | def remove_local_action_from_text(action: AR_local_actions, text_name: str = None) -> None: 79 | """ 80 | remove the local action from the TextEditor 81 | 82 | Args: 83 | action (AR_local_actions): action to remove 84 | text_name (str, optional): name of the text to remove. Defaults to None. 85 | """ 86 | if text_name is None: 87 | text_name = action.label 88 | texts = bpy.data.texts 89 | text_index = texts.find(text_name) 90 | if text_index != -1: 91 | texts.remove(texts[text_index]) 92 | return 93 | id = action.id 94 | for text in texts: 95 | if text.lines[0].body.strip().startswith("###ActRec_pref### id: '%s'" % id): 96 | texts.remove(text) 97 | return 98 | 99 | 100 | def get_local_action_index(ActRec_pref: AR_preferences, id: str, index: int) -> int: 101 | """ 102 | get local action index based on the given id or index (checks if index is in range) 103 | 104 | Args: 105 | ActRec_pref (AR_preferences): preferences of this addon 106 | id (str): id to get index from 107 | index (int): index for fallback 108 | 109 | Returns: 110 | int: valid index of a local actions or active local action index on fallback 111 | """ 112 | action = ActRec_pref.local_actions.find(id) 113 | if action != -1: 114 | return action 115 | if index >= 0 and len(ActRec_pref.local_actions) > index: # fallback to input index 116 | return index 117 | else: 118 | return ActRec_pref.active_local_action_index # fallback to selection 119 | 120 | 121 | # endregion 122 | -------------------------------------------------------------------------------- /ActRec/actrec/icon_manager.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # external modules 3 | import os 4 | from typing import TYPE_CHECKING 5 | 6 | # blender modules 7 | import bpy 8 | import bpy.utils.previews 9 | from bpy.types import Operator, PropertyGroup, AddonPreferences, Context, Event 10 | from bpy.props import IntProperty, StringProperty, BoolProperty, CollectionProperty 11 | from bpy_extras.io_utils import ImportHelper 12 | from bpy.utils.previews import ImagePreviewCollection 13 | 14 | # relative imports 15 | from .log import logger 16 | from .functions.shared import get_preferences 17 | from . import functions 18 | if TYPE_CHECKING: 19 | from .preferences import AR_preferences 20 | else: 21 | AR_preferences = AddonPreferences 22 | # endregion 23 | 24 | preview_collections = {} 25 | 26 | # region functions 27 | 28 | 29 | def get_icons_name_map() -> dict: 30 | """ 31 | get all default icons of Blender as dict with {name: value} except the icon 'NONE' (value: 0) 32 | 33 | Returns: 34 | dict: {name of icon: value of icon} 35 | """ 36 | return {item.name: item.value 37 | for item in bpy.types.UILayout.bl_rna.functions["prop"].parameters["icon"].enum_items[1:]} 38 | 39 | 40 | def get_icons_value_map() -> dict: 41 | """ 42 | get all default icons of Blender as dict with {value: name} except the icon 'NONE' (value: 0) 43 | 44 | Returns: 45 | dict: {value of icon: name of icon} 46 | """ 47 | return {item.value: item.name 48 | for item in bpy.types.UILayout.bl_rna.functions["prop"].parameters["icon"].enum_items[1:]} 49 | 50 | 51 | def get_custom_icon_name_map() -> dict: 52 | """ 53 | get all custom icons as dict with {name: value} except the icon 'NONE' (value: 0) 54 | 55 | Returns: 56 | dict: {name of icon: value of icon} 57 | """ 58 | return {key: item.icon_id for key, item in preview_collections['ar_custom'].items()} 59 | 60 | 61 | def get_custom_icons_value_map() -> dict: 62 | """ 63 | get all custom icons as dict with {value: name} except the icon 'NONE' (value: 0) 64 | 65 | Returns: 66 | dict: {value of icon: name of icon} 67 | """ 68 | return {item.icon_id: key for key, item in preview_collections['ar_custom'].items()} 69 | 70 | 71 | def load_icons(ActRec_pref: AR_preferences) -> None: 72 | """ 73 | loads all saved icons from the icon folder, which can be located by the user. 74 | the icon are saved as png and with their icon name 75 | supported blender image formats https://docs.blender.org/manual/en/latest/files/media/image_formats.html 76 | 77 | Args: 78 | ActRec_pref (AR_preferences): preferences of this addon 79 | """ 80 | directory = ActRec_pref.icon_path 81 | for icon in os.listdir(directory): 82 | filepath = os.path.join(directory, icon) 83 | if not (os.path.exists(filepath) and os.path.isfile(filepath)): 84 | continue 85 | register_icon( 86 | preview_collections['ar_custom'], 87 | "AR_%s" % ".".join(icon.split(".")[:-1]), filepath, True 88 | ) 89 | 90 | 91 | def load_icon(ActRec_pref: AR_preferences, filepath: str, only_new: bool = False) -> None: 92 | """ 93 | load image form filepath as custom addon icon and resize to 32x32 (Blender icon size) 94 | supported blender image formats https://docs.blender.org/manual/en/latest/files/media/image_formats.html 95 | 96 | Args: 97 | ActRec_pref (AR_preferences): preferences of this addon 98 | filepath (str): filepath to the image file 99 | only_new (bool, optional): if icon is already register by name, it won't be registered again. Defaults to False. 100 | """ 101 | # uses Blender image to convert to other image format and resize to icon size 102 | image = bpy.data.images.load(filepath) 103 | image.scale(32, 32) 104 | name = os.path.splitext(image.name)[0] # name without file extension 105 | # image.name has format included 106 | internal_path = os.path.join(ActRec_pref.icon_path, image.name) 107 | image.save_render(internal_path) # save Blender image to inside the icon folder 108 | register_icon(preview_collections['ar_custom'], "AR_%s" % name, internal_path, only_new) 109 | bpy.data.images.remove(image) 110 | 111 | 112 | def register_icon( 113 | preview_collection: ImagePreviewCollection, 114 | name: str, 115 | filepath: str, 116 | only_new: bool) -> None: 117 | """ 118 | adds image form filepath to the addon icon collection with a custom name 119 | 120 | Args: 121 | preview_collection (ImagePreviewCollection): collection to add icon to 122 | name (str): name of the icon 123 | filepath (str): filepath to the image file 124 | only_new (bool): if icon is already register by name, it won't be registered again. 125 | """ 126 | if only_new and not (name in preview_collection) or not only_new: 127 | name = functions.check_for_duplicates(preview_collection, name) 128 | preview_collection.load(name, filepath, 'IMAGE', force_reload=True) 129 | logger.info("Custom Icon <%s> registered" % name) 130 | 131 | 132 | def unregister_icon(preview_collection: ImagePreviewCollection, name: str) -> None: 133 | """ 134 | deletes icon by name from given collection if possible 135 | 136 | Args: 137 | preview_collection (ImagePreviewCollection): collection to add icon to 138 | name (str): name of the icon 139 | """ 140 | if name in preview_collection: 141 | del preview_collection[name] 142 | # endregion 143 | 144 | # region Operators 145 | 146 | 147 | class Icontable(Operator): 148 | bl_label = "Icons" 149 | bl_description = "Press to select an Icon" 150 | 151 | search: StringProperty( 152 | name="Icon Search", 153 | description="search Icon by name", 154 | options={'TEXTEDIT_UPDATE'} 155 | ) 156 | default_icon_value: IntProperty( 157 | name="Default Icon", 158 | description="Default icon that get set when clear is pressed", 159 | default=0 160 | ) 161 | reuse: BoolProperty( 162 | name="Reuse", 163 | description="Reuse the last selected icon" 164 | ) 165 | 166 | def draw(self, context: Context) -> None: 167 | ActRec_pref = get_preferences(context) 168 | layout = self.layout 169 | box = layout.box() 170 | row = box.row() 171 | row.label(text="Selected Icon:") 172 | row.label(text=" ", icon_value=ActRec_pref.selected_icon) 173 | row.prop(self, 'search', text='Search:') 174 | row.operator('ar.icon_selector', 175 | text="Clear Icon").icon = self.default_icon_value 176 | box = layout.box() 177 | grid_flow = box.grid_flow(row_major=True, columns=35, 178 | even_columns=True, even_rows=True, align=True) 179 | for icon_name, value in get_icons_name_map().items(): 180 | human_name = icon_name.lower().replace("_", " ") 181 | if self.search == '' or self.search.lower() in human_name: 182 | grid_flow.operator('ar.icon_selector', text="", 183 | icon_value=value).icon = value 184 | box = layout.box() 185 | row = box.row().split(factor=0.5) 186 | row.label(text="Custom Icons") 187 | row2 = row.row() 188 | row2.operator('ar.add_custom_icon', text="Add Custom Icon", 189 | icon='PLUS').activate_pop_up = self.bl_idname 190 | row2.operator('ar.delete_custom_icon', text="Delete", icon='TRASH') 191 | grid_flow = box.grid_flow(row_major=True, columns=35, 192 | even_columns=True, even_rows=True, align=True) 193 | for icon_name, value in get_custom_icon_name_map().items(): 194 | human_name = icon_name.lower().replace("_", " ") 195 | if self.search == '' or self.search.lower() in human_name: 196 | grid_flow.operator('ar.icon_selector', text="", 197 | icon_value=value).icon = value 198 | 199 | def check(self, context: Context) -> bool: 200 | return True 201 | 202 | 203 | class AR_OT_icon_selector(Operator): 204 | bl_idname = "ar.icon_selector" 205 | bl_label = "Icon" 206 | bl_options = {'REGISTER', 'INTERNAL'} 207 | bl_description = "Select the Icon" 208 | 209 | icon: IntProperty(default=0) # Icon: NONE 210 | 211 | def execute(self, context: Context) -> set[str]: 212 | ActRec_pref = get_preferences(context) 213 | ActRec_pref.selected_icon = self.icon 214 | return {"FINISHED"} 215 | 216 | 217 | class AR_OT_add_custom_icon(Operator, ImportHelper): 218 | bl_idname = "ar.add_custom_icon" 219 | bl_label = "Add Custom Icon" 220 | bl_description = "Adds a custom Icon" 221 | 222 | filter_image: BoolProperty(default=True, options={'HIDDEN'}) 223 | filter_folder: BoolProperty(default=True, options={'HIDDEN'}) 224 | activate_pop_up: StringProperty(default="") 225 | 226 | def execute(self, context: Context) -> set[str]: 227 | ActRec_pref = get_preferences(context) 228 | # supported blender image formats https://docs.blender.org/manual/en/latest/files/media/image_formats.html 229 | if os.path.isfile(self.filepath) and self.filepath.lower().endswith(tuple(bpy.path.extensions_image)): 230 | load_icon(ActRec_pref, self.filepath) 231 | else: 232 | self.report( 233 | {'ERROR'}, 'The selected File is not an Image or an Image Format supported by Blender') 234 | if self.activate_pop_up != "": 235 | exec("bpy.ops.%s%s" % ( 236 | ".".join(self.activate_pop_up.split("_OT_")).lower(), 237 | "('INVOKE_DEFAULT', reuse= True)" 238 | )) 239 | return {"FINISHED"} 240 | 241 | def cancel(self, context: Context) -> None: 242 | if self.activate_pop_up != "": 243 | exec("bpy.ops.%s%s" % ( 244 | ".".join(self.activate_pop_up.split("_OT_")).lower(), 245 | "('INVOKE_DEFAULT', reuse= True)" 246 | )) 247 | 248 | 249 | class AR_OT_delete_custom_icon(Operator): 250 | bl_idname = "ar.delete_custom_icon" 251 | bl_label = "Delete Icon" 252 | bl_description = "Delete a custom Icon" 253 | 254 | def get_select_all(self) -> bool: 255 | return self.get("select_all", False) 256 | 257 | def set_select_all(self, value: bool) -> None: 258 | self["select_all"] = value 259 | for icon in self.icons: 260 | icon["select_all"] = value 261 | 262 | class AR_icon(PropertyGroup): 263 | def get_selected(self) -> None: 264 | return self.get("selected", False) or self.get("select_all", False) 265 | 266 | def set_selected(self, value: bool) -> None: 267 | if not self.get("select_all", False): 268 | self["selected"] = value 269 | 270 | icon_id: IntProperty() 271 | icon_name: StringProperty() 272 | selected: BoolProperty(default=False, name='Select', get=get_selected, set=set_selected) 273 | 274 | icons: CollectionProperty(type=AR_icon) 275 | select_all: BoolProperty( 276 | name="All Icons", 277 | description="Select all Icons", 278 | get=get_select_all, 279 | set=set_select_all 280 | ) 281 | 282 | @classmethod 283 | def poll(cls, context: Context) -> bool: 284 | return len(preview_collections['ar_custom']) 285 | 286 | def invoke(self, context: Context, event: Event) -> set[str]: 287 | coll = self.icons 288 | coll.clear() 289 | icon_list = list(preview_collections['ar_custom']) 290 | icon_list_values = [ 291 | icon.icon_id for icon in preview_collections['ar_custom'].values()] 292 | for i in range(len(icon_list)): 293 | new = coll.add() 294 | new.icon_id = icon_list_values[i] 295 | new.icon_name = icon_list[i] 296 | return context.window_manager.invoke_props_dialog(self) 297 | 298 | def execute(self, context: Context) -> set[str]: 299 | ActRec_pref = get_preferences(context) 300 | for ele in self.icons: 301 | if not (ele.selected or self.select_all): 302 | continue 303 | icon_path = ele.icon_name[3:] 304 | filenames = os.listdir(ActRec_pref.icon_path) 305 | names = [os.path.splitext(os.path.basename(path))[0] for path in filenames] 306 | if icon_path in names: 307 | os.remove(os.path.join(ActRec_pref.icon_path, filenames[names.index(icon_path)])) 308 | unregister_icon(preview_collections['ar_custom'], ele.icon_name) 309 | return {"FINISHED"} 310 | 311 | def draw(self, context: Context) -> None: 312 | layout = self.layout 313 | layout.prop(self, 'select_all') 314 | box = layout.box() 315 | coll = self.icons 316 | for ele in coll: 317 | row = box.row() 318 | row.prop(ele, 'selected', text='') 319 | row.label(text=ele.icon_name[3:], icon_value=ele.icon_id) 320 | # endregion 321 | 322 | 323 | classes = [ 324 | AR_OT_icon_selector, 325 | AR_OT_add_custom_icon, 326 | AR_OT_delete_custom_icon.AR_icon, 327 | AR_OT_delete_custom_icon 328 | ] 329 | 330 | # region Registration 331 | 332 | 333 | def register(): 334 | for cls in classes: 335 | bpy.utils.register_class(cls) 336 | preview_collections['ar_custom'] = bpy.utils.previews.new() 337 | 338 | 339 | def unregister(): 340 | for cls in classes: 341 | bpy.utils.unregister_class(cls) 342 | for preview_collection in preview_collections.values(): 343 | bpy.utils.previews.remove(preview_collection) 344 | preview_collections.clear() 345 | # endregion 346 | -------------------------------------------------------------------------------- /ActRec/actrec/keymap.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # external modules 3 | import os 4 | import json 5 | from collections import defaultdict 6 | 7 | # blender modules 8 | import bpy 9 | from bpy.types import KeyMapItems, KeyMap 10 | 11 | # relative imports 12 | from .log import logger 13 | # endregion 14 | 15 | keymaps = {} 16 | keymap_items = defaultdict(list) 17 | 18 | # region functions 19 | 20 | 21 | def load_action_keymap_data(data: list, items: KeyMapItems) -> None: 22 | """ 23 | applies action keymap data in JSON format to Blender 24 | 25 | Args: 26 | data (dict): Keymap in JSON Format 27 | items (KeyMapItems): Keymap items to register data to 28 | """ 29 | if not data: 30 | return 31 | for key in data.get('keymap', []): 32 | kmi = items.new( 33 | "ar.global_execute_action", key['type'], 34 | key['value'], 35 | any=key['any'], 36 | shift=key['shift'], 37 | ctrl=key['ctrl'], 38 | alt=key['alt'], 39 | oskey=key['oskey'], 40 | key_modifier=key['key_modifier'], 41 | repeat=key['repeat'], 42 | head=True 43 | ) 44 | kmi.properties.id = key['id'] 45 | kmi.active = key['active'] 46 | kmi.map_type = key['map_type'] 47 | 48 | 49 | def append_keymap(data: dict, export_action_ids: list, km: KeyMap) -> None: 50 | """ 51 | Appends the given keymap to the data dict with on the key 'keymap' 52 | wich holds a dict of the values 'id, active, type, value, 53 | any, shift, ctrl, alt, oskey, key_modifier, repeat, map_type'. 54 | 55 | Args: 56 | data (dict): The dict where the keymap gets appended. 57 | export_action_ids (list): The ids of the action that get exported i.e. are selected for export 58 | km (KeyMap): The keymap to append. 59 | """ 60 | for kmi in km.keymap_items: 61 | if kmi.idname == "ar.global_execute_action" and kmi.properties.id in export_action_ids: 62 | data['keymap'].append({ 63 | 'id': kmi.properties['id'], 64 | 'active': kmi.active, 65 | 'type': kmi.type, 66 | 'value': kmi.value, 67 | 'any': kmi.any, 68 | 'shift': kmi.shift, 69 | 'ctrl': kmi.ctrl, 70 | 'alt': kmi.alt, 71 | 'oskey': kmi.oskey, 72 | 'key_modifier': kmi.key_modifier, 73 | 'repeat': kmi.repeat, 74 | 'map_type': kmi.map_type 75 | }) 76 | 77 | # endregion 78 | 79 | # region Registration 80 | 81 | 82 | def register(): 83 | addon = bpy.context.window_manager.keyconfigs.addon 84 | if addon: 85 | keymaps['temp'] = addon.keymaps.new(name='ActionButtons') 86 | km = addon.keymaps.new(name='Screen') 87 | keymaps['default'] = km 88 | save_items = keymap_items['default'] 89 | items = km.keymap_items 90 | # operators 91 | kmi = items.new("ar.macro_add", 'COMMA', 'PRESS', alt=True) 92 | save_items.append(kmi) 93 | kmi = items.new("ar.local_play", 'PERIOD', 'PRESS', alt=True) 94 | save_items.append(kmi) 95 | kmi = items.new( 96 | "ar.local_selection_up", 97 | 'WHEELUPMOUSE', 98 | 'PRESS', 99 | shift=True, 100 | alt=True 101 | ) 102 | save_items.append(kmi) 103 | kmi = items.new( 104 | "ar.local_selection_down", 105 | 'WHEELDOWNMOUSE', 106 | 'PRESS', 107 | shift=True, 108 | alt=True 109 | ) 110 | save_items.append(kmi) 111 | # menu 112 | kmi = items.new("wm.call_menu_pie", 'A', 'PRESS', shift=True, alt=True) 113 | kmi.properties.name = 'AR_MT_action_pie' 114 | save_items.append(kmi) 115 | kmi = items.new("wm.call_menu", 'C', 'PRESS', shift=True, alt=True) 116 | kmi.properties.name = 'AR_MT_Categories' 117 | save_items.append(kmi) 118 | 119 | 120 | def unregister(): 121 | addon = bpy.context.window_manager.keyconfigs.addon 122 | if not addon: 123 | return 124 | for km in keymaps.values(): 125 | if keymaps.get(km.name): 126 | addon.keymaps.remove(km) 127 | keymaps.clear() 128 | keymap_items.clear() 129 | # endregion 130 | -------------------------------------------------------------------------------- /ActRec/actrec/log.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # external modules 3 | import os 4 | import logging 5 | import traceback 6 | import sys 7 | from datetime import datetime 8 | 9 | # blender modules 10 | import bpy 11 | from bpy.app.handlers import persistent 12 | 13 | # relative imports 14 | from . import config 15 | # endregion 16 | 17 | __module__ = __package__.split(".")[0] 18 | 19 | # region Log system 20 | 21 | 22 | class Log_system: 23 | """logging system for the addon""" 24 | 25 | def __init__(self, count: int) -> None: 26 | """ 27 | creates a log object which unregister with blender 28 | 29 | Args: 30 | count (int): amount of log files which are kept simultaneously 31 | """ 32 | dir = self.directory = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logs") 33 | if not os.path.exists(dir): 34 | os.mkdir(dir) 35 | all_logs = os.listdir(dir) 36 | log_later = [] 37 | while len(all_logs) >= count: 38 | try: 39 | # delete oldest file 40 | os.remove(min([os.path.join(dir, filename) for filename in all_logs], key=os.path.getctime)) 41 | except PermissionError as err: 42 | log_later.append("File is already used -> PermissionError: %s" % str(err)) 43 | break 44 | except FileNotFoundError as err: 45 | log_later.append("For some reason the File doesn't exists %s" % str(err)) 46 | break 47 | all_logs = os.listdir(dir) 48 | name = "" 49 | for arg in sys.argv: 50 | if arg.endswith(".blend"): 51 | name = "%s_" % ".".join(os.path.basename(arg).split(".")[:-1]) 52 | self.path = os.path.join(dir, "ActRec_%s%s.log" % (name, datetime.today().strftime('%d-%m-%Y_%H-%M-%S'))) 53 | 54 | logger = logging.getLogger(__module__) 55 | self.logger = logger 56 | logger.setLevel(logging.DEBUG) 57 | 58 | logger.info( 59 | "Logging ActRec %s running on Blender %s" 60 | % (".".join([str(x) for x in config.version]), bpy.app.version_string) 61 | ) 62 | for log_text in log_later: 63 | logger.info(log_text) 64 | 65 | sys.excepthook = self.exception_handler 66 | 67 | def exception_handler(self, exc_type, exc_value, exc_tb) -> None: 68 | traceback.print_exception(exc_type, exc_value, exc_tb) 69 | self.logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_tb)) 70 | 71 | def detach_file(self) -> None: 72 | """ 73 | remove file of the logger 74 | """ 75 | self.file_handler.close() 76 | self.logger.removeHandler(self.file_handler) 77 | 78 | def append_file(self) -> None: 79 | """ 80 | adds a file to the logger 81 | """ 82 | file_formatter = logging.Formatter( 83 | "%(levelname)s - %(relativeCreated)d - %(filename)s:%(funcName)s - %(message)s" 84 | ) 85 | file_handler = logging.FileHandler(self.path, mode='a', encoding='utf-8', delay=True) 86 | file_handler.setLevel(self.logger.level) 87 | file_handler.setFormatter(file_formatter) 88 | self.logger.addHandler(file_handler) 89 | self.file_handler = file_handler 90 | 91 | 92 | def update_log_amount_in_config(amount: int) -> None: 93 | """ 94 | writes given amount as log amount into the config file 95 | 96 | Args: 97 | amount (int): log amount 98 | """ 99 | if config.log_amount == amount: 100 | return 101 | 102 | path = os.path.join(os.path.dirname(__file__), "config.py") 103 | with open(path, 'r', encoding='utf-8') as file: 104 | lines = file.readlines() 105 | 106 | for i, line in enumerate(lines): 107 | if not line.startswith('log_amount'): 108 | continue 109 | lines[i] = "log_amount = %i\n" % amount 110 | break 111 | else: 112 | lines.append("log_amount = %i\n" % amount) 113 | 114 | with open(path, 'w', encoding='utf-8') as file: 115 | file.writelines(lines) 116 | 117 | 118 | # creates logger 119 | log_amount = 5 120 | if hasattr(config, 'log_amount'): 121 | log_amount = config.log_amount 122 | log_sys = Log_system(log_amount) 123 | logger = log_sys.logger 124 | # endregion 125 | -------------------------------------------------------------------------------- /ActRec/actrec/menus/__init__.py: -------------------------------------------------------------------------------- 1 | # region Registration 2 | def register(): 3 | from .locals import register as reg 4 | reg() 5 | from .categories import register as reg 6 | reg() 7 | 8 | 9 | def unregister(): 10 | from .locals import unregister as unreg 11 | unreg() 12 | from .categories import unregister as unreg 13 | unreg() 14 | # endregion 15 | -------------------------------------------------------------------------------- /ActRec/actrec/menus/categories.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # blender modules 3 | import bpy 4 | from bpy.types import Menu, Context 5 | 6 | # relative imports 7 | from ..functions.shared import get_preferences 8 | from .. import ui_functions 9 | # endregion 10 | 11 | # region Menus 12 | 13 | 14 | class AR_MT_Categories(Menu): 15 | bl_label = "Categories" 16 | bl_idname = "AR_MT_Categories" 17 | 18 | @classmethod 19 | def poll(cls, context: Context) -> bool: 20 | return (context.area is not None) 21 | 22 | def draw(self, context: Context) -> None: 23 | layout = self.layout 24 | ActRec_pref = get_preferences(context) 25 | if not ActRec_pref.is_loaded: # loads the actions if not already done 26 | ActRec_pref.is_loaded = True 27 | for index, category in enumerate(ui_functions.get_visible_categories(ActRec_pref, context)): 28 | layout.menu("AR_MT_category_%s" % index, text=category.label) 29 | # endregion 30 | 31 | 32 | def register(): 33 | bpy.utils.register_class(AR_MT_Categories) 34 | 35 | 36 | def unregister(): 37 | bpy.utils.unregister_class(AR_MT_Categories) 38 | -------------------------------------------------------------------------------- /ActRec/actrec/menus/locals.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # external modules 3 | from contextlib import suppress 4 | 5 | # blender modules 6 | import bpy 7 | from bpy.types import Menu, Context 8 | 9 | # relative imports 10 | from .. import keymap 11 | from ..functions.shared import get_preferences 12 | from .. import functions 13 | # endregion 14 | 15 | 16 | # region Menus 17 | 18 | 19 | class AR_MT_action_pie(Menu): 20 | bl_idname = "AR_MT_action_pie" 21 | bl_label = "ActRec Pie Menu" 22 | 23 | def draw(self, context: Context) -> None: 24 | ActRec_pref = get_preferences(context) 25 | pie = self.layout.menu_pie() 26 | actions = ActRec_pref.local_actions 27 | for i in range(len(actions)): 28 | if i >= 8: 29 | break 30 | action = actions[i] 31 | ops = pie.operator("ar.local_play", text=actions[i].label, icon_value=action.icon if action.icon else 101) 32 | ops.id = action.id 33 | ops.index = i 34 | 35 | # based on 36 | # https://docs.blender.org/api/current/bpy.types.Menu.html?highlight=menu_draw#extending-the-button-context-menu 37 | 38 | 39 | def menu_draw(self, context: Context) -> None: 40 | layout = self.layout 41 | layout.separator() 42 | layout.operator("ar.copy_to_actrec") 43 | button_prop = getattr(context, "button_prop", None) 44 | if button_prop and hasattr(button_prop, 'is_array') and button_prop.is_array: 45 | layout.operator("ar.copy_to_actrec", text="Copy to Action Recorder (Single)").copy_single = True 46 | button_operator = getattr(context, "button_operator", None) 47 | if button_operator and button_operator.bl_rna.identifier == "AR_OT_global_execute_action": 48 | km = context.window_manager.keyconfigs.user.keymaps['Screen'] 49 | kmi = km.keymap_items.find_from_operator( 50 | "ar.global_execute_action", 51 | properties=button_operator 52 | ) 53 | if kmi: 54 | layout.operator("ar.remove_ar_shortcut").id = button_operator.id 55 | else: 56 | layout.operator("ar.add_ar_shortcut").id = button_operator.id 57 | layout.operator("ar.global_edit").id = button_operator.id 58 | 59 | 60 | class WM_MT_button_context(Menu): 61 | bl_label = "Unused" 62 | 63 | def draw(self, context: Context) -> None: 64 | pass 65 | # endregion 66 | 67 | 68 | classes = [ 69 | AR_MT_action_pie 70 | ] 71 | internal_classes = [ 72 | WM_MT_button_context 73 | ] 74 | 75 | # region Registration 76 | 77 | 78 | def register(): 79 | for cls in classes: 80 | bpy.utils.register_class(cls) 81 | 82 | for cls in internal_classes: 83 | with suppress(Exception): 84 | bpy.utils.register_class(cls) 85 | with suppress(Exception): 86 | bpy.types.WM_MT_button_context.append(menu_draw) 87 | 88 | 89 | def unregister(): 90 | for cls in classes: 91 | bpy.utils.unregister_class(cls) 92 | 93 | with suppress(Exception): 94 | bpy.types.WM_MT_button_context.remove(menu_draw) 95 | for cls in internal_classes: 96 | with suppress(Exception): 97 | bpy.utils.unregister_class(cls) 98 | # endregion 99 | -------------------------------------------------------------------------------- /ActRec/actrec/operators/__init__.py: -------------------------------------------------------------------------------- 1 | """only relative import from intra-modules: functions, ui, properties""" 2 | 3 | from .categories import ( 4 | AR_OT_category_add, 5 | AR_OT_category_edit, 6 | AR_OT_category_interface, 7 | AR_OT_category_apply_visibility, 8 | AR_OT_category_delete_visibility, 9 | AR_OT_category_delete 10 | ) 11 | 12 | from .globals import ( 13 | AR_OT_global_recategorize_action, 14 | AR_OT_global_import, 15 | AR_OT_global_import_settings 16 | ) 17 | 18 | from .locals import ( 19 | AR_OT_local_to_global 20 | ) 21 | 22 | from .helper import ( 23 | AR_OT_helper_object_to_collection 24 | ) 25 | 26 | from .preferences import ( 27 | AR_OT_preferences_directory_selector, 28 | AR_OT_preferences_recover_directory 29 | ) 30 | 31 | from .shared import ( 32 | AR_OT_check_ctrl 33 | ) 34 | 35 | # region Registration 36 | 37 | 38 | def register(): 39 | from .categories import register as reg 40 | reg() 41 | from .globals import register as reg 42 | reg() 43 | from .locals import register as reg 44 | reg() 45 | from .macros import register as reg 46 | reg() 47 | from .helper import register as reg 48 | reg() 49 | from .preferences import register as reg 50 | reg() 51 | from .shared import register as reg 52 | reg() 53 | 54 | 55 | def unregister(): 56 | from .categories import unregister as unreg 57 | unreg() 58 | from .globals import unregister as unreg 59 | unreg() 60 | from .locals import unregister as unreg 61 | unreg() 62 | from .macros import unregister as unreg 63 | unreg() 64 | from .helper import unregister as unreg 65 | unreg() 66 | from .preferences import unregister as unreg 67 | unreg() 68 | from .shared import unregister as unreg 69 | unreg() 70 | # endregion 71 | -------------------------------------------------------------------------------- /ActRec/actrec/operators/helper.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # blender modules 3 | import bpy 4 | from bpy.types import Operator, Context 5 | # endregion 6 | 7 | 8 | # Why Helper Operator? 9 | # Helper Operator are used to mimic Operator that are executed on an object or need specific user interaction to work 10 | 11 | # region Operators 12 | 13 | 14 | class AR_OT_helper_object_to_collection(Operator): 15 | bl_idname = "ar.helper_object_to_collection" 16 | bl_label = "Object to Collection" 17 | bl_options = {'INTERNAL', 'REGISTER', 'UNDO'} 18 | 19 | def execute(self, context: Context) -> set[str]: 20 | active_coll = context.collection 21 | for obj in context.objects: 22 | for coll in obj.users_collection: 23 | coll.objects.unlink(obj) 24 | active_coll.objects.link(obj) 25 | return {'FINISHED'} 26 | # endregion 27 | 28 | 29 | classes = [ 30 | AR_OT_helper_object_to_collection 31 | ] 32 | 33 | # region Registration 34 | 35 | 36 | def register(): 37 | for cls in classes: 38 | bpy.utils.register_class(cls) 39 | 40 | 41 | def unregister(): 42 | for cls in classes: 43 | bpy.utils.unregister_class(cls) 44 | # endregion 45 | -------------------------------------------------------------------------------- /ActRec/actrec/operators/preferences.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # external modules 3 | import os 4 | import sys 5 | import subprocess 6 | 7 | # blender modules 8 | import bpy 9 | from bpy.types import Context, Event, Operator 10 | from bpy.props import StringProperty 11 | from bpy_extras.io_utils import ExportHelper 12 | 13 | # relative imports 14 | from ..log import logger 15 | from ..functions.shared import get_preferences 16 | # endregion 17 | 18 | 19 | # region Operator 20 | 21 | 22 | class AR_OT_preferences_directory_selector(Operator, ExportHelper): 23 | bl_idname = "ar.preferences_directory_selector" 24 | bl_label = "Select Directory" 25 | bl_description = " " 26 | bl_options = {'REGISTER', 'INTERNAL'} 27 | 28 | filename_ext = "" 29 | filename = "" 30 | filepath = "" 31 | filter_glob: bpy.props.StringProperty( 32 | default="", 33 | options={'HIDDEN'}, 34 | maxlen=255, # Max internal buffer length, longer would be clamped. 35 | ) 36 | use_filter_folder = True 37 | directory: bpy.props.StringProperty(name="Folder Path", maxlen=1024, default="") 38 | 39 | preference_name: StringProperty(options={'HIDDEN'}) 40 | path_extension: StringProperty(options={'HIDDEN'}) 41 | 42 | def invoke(self, context: Context, event: Event) -> set[str]: 43 | super().invoke(context, event) 44 | self.filepath = "" 45 | return {'RUNNING_MODAL'} 46 | 47 | def execute(self, context: Context) -> set[str]: 48 | ActRec_pref = get_preferences(bpy.context) 49 | user_path = self.properties.directory 50 | if (not os.path.isdir(user_path)): 51 | msg = "Please select a directory not a file\n" + user_path 52 | self.report({'ERROR'}, msg) 53 | return {'CANCELLED'} 54 | ActRec_pref = get_preferences(context) 55 | setattr(ActRec_pref, self.preference_name, os.path.join(user_path, self.path_extension)) 56 | return {'FINISHED'} 57 | 58 | 59 | class AR_OT_preferences_recover_directory(Operator): 60 | bl_idname = "ar.preferences_recover_directory" 61 | bl_label = "Recover Standard Directory" 62 | bl_description = "Recover the standard Storage directory" 63 | bl_options = {'REGISTER', 'INTERNAL'} 64 | 65 | preference_name: StringProperty(options={'HIDDEN'}) 66 | path_extension: StringProperty(options={'HIDDEN'}) 67 | 68 | def execute(self, context: Context) -> set[str]: 69 | ActRec_pref = get_preferences(context) 70 | setattr(ActRec_pref, self.preference_name, os.path.join(ActRec_pref.addon_directory, self.path_extension)) 71 | return {'FINISHED'} 72 | 73 | 74 | class AR_OT_preferences_open_explorer(Operator): 75 | bl_idname = "ar.preferences_open_explorer" 76 | bl_label = "Open Explorer" 77 | bl_description = "Open the Explorer with the given path" 78 | bl_options = {'REGISTER', 'INTERNAL'} 79 | 80 | path: StringProperty(name="Path", description="Open the explorer with the given path") 81 | 82 | def open_file_in_explorer(self, path: str) -> None: 83 | """ 84 | opens the file in the os file explorer 85 | 86 | Args: 87 | path (str): path to file to open 88 | """ 89 | if sys.platform == "win32": 90 | subprocess.call(["explorer", "/select,", path]) 91 | elif sys.platform == "darwin": # Mac OS X 92 | subprocess.call(["open", "-R", path]) 93 | else: # Linux 94 | subprocess.call(["xdg-open", os.path.dirname(path)]) 95 | 96 | def open_directory_in_explorer(self, directory: str) -> None: 97 | """ 98 | open the directory in the os file explorer 99 | 100 | Args: 101 | directory (str): path to directory to open 102 | """ 103 | if sys.platform == "win32": 104 | os.startfile(directory) 105 | else: 106 | opener = "open" if sys.platform == "darwin" else "xdg-open" 107 | subprocess.call([opener, directory]) 108 | 109 | def execute(self, context: Context) -> set[str]: 110 | self.path = os.path.normpath(self.path) 111 | if os.path.isdir(self.path): 112 | self.open_directory_in_explorer(self.path) 113 | elif os.path.isfile(self.path): 114 | try: 115 | self.open_file_in_explorer(self.path) 116 | except Exception as err: 117 | self.open_directory_in_explorer(os.path.dirname(self.path)) 118 | logger.info("Fallback to show directory: %s" % err) 119 | return {'FINISHED'} 120 | # endregion 121 | 122 | 123 | classes = [ 124 | AR_OT_preferences_directory_selector, 125 | AR_OT_preferences_recover_directory, 126 | AR_OT_preferences_open_explorer 127 | ] 128 | 129 | # region Registration 130 | 131 | 132 | def register(): 133 | for cls in classes: 134 | bpy.utils.register_class(cls) 135 | 136 | 137 | def unregister(): 138 | for cls in classes: 139 | bpy.utils.unregister_class(cls) 140 | # endregion 141 | -------------------------------------------------------------------------------- /ActRec/actrec/operators/shared.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # blender modules 3 | import bpy 4 | from bpy.types import Operator, Context, Event 5 | from bpy.props import StringProperty, IntProperty 6 | # endregion 7 | 8 | 9 | # region Operators 10 | 11 | 12 | class AR_OT_check_ctrl(Operator): 13 | bl_idname = "ar.check_ctrl" 14 | bl_label = "Check Ctrl" 15 | bl_options = {'INTERNAL'} 16 | 17 | def invoke(self, context: Context, event: Event) -> set[str]: 18 | if event.ctrl: 19 | return {"FINISHED"} 20 | return {"CANCELLED"} 21 | 22 | def execute(self, context: Context) -> set[str]: 23 | return {"FINISHED"} 24 | 25 | 26 | class Id_based(Operator): 27 | id: StringProperty(name="id", description="id of the action (1. indicator)") 28 | index: IntProperty(name="index", description="index of the action (2. indicator)", default=-1) 29 | 30 | def clear(self) -> None: 31 | self.id = "" 32 | self.index = -1 33 | 34 | 35 | class AR_OT_copy_text(Operator): 36 | bl_idname = "ar.copy_text" 37 | bl_label = "Copy Text" 38 | bl_description = "Loads the given text in the clipboard" 39 | bl_options = {"INTERNAL"} 40 | 41 | text: StringProperty(name="copy text", description="Text to copy to clipboard") 42 | 43 | @classmethod 44 | def poll(cls, context: Context) -> bool: 45 | return context.window_manager is not None 46 | 47 | def execute(self, context: Context) -> set[str]: 48 | context.window_manager.clipboard = self.text 49 | return {"FINISHED"} 50 | 51 | # endregion 52 | 53 | 54 | classes = [ 55 | AR_OT_check_ctrl, 56 | AR_OT_copy_text 57 | ] 58 | 59 | # region Registration 60 | 61 | 62 | def register(): 63 | for cls in classes: 64 | bpy.utils.register_class(cls) 65 | 66 | 67 | def unregister(): 68 | for cls in classes: 69 | bpy.utils.unregister_class(cls) 70 | # endregion 71 | -------------------------------------------------------------------------------- /ActRec/actrec/panels/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import ( 2 | ui_space_types, 3 | panel_factory, 4 | ) 5 | 6 | # region Registration 7 | 8 | 9 | def register(): 10 | from .main import register as reg 11 | reg() 12 | 13 | 14 | def unregister(): 15 | from .main import unregister as unreg 16 | unreg() 17 | # endregion 18 | -------------------------------------------------------------------------------- /ActRec/actrec/panels/main.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # blender modules 3 | import bpy 4 | from bpy.types import Panel, Context 5 | 6 | # relative imports 7 | from .. import config 8 | from .. import update 9 | from ..log import log_sys 10 | from ..functions.shared import get_preferences 11 | # endregion 12 | 13 | classes = [] 14 | ui_space_types = ['CLIP_EDITOR', 'NODE_EDITOR', 'TEXT_EDITOR', 'SEQUENCE_EDITOR', 'NLA_EDITOR', 15 | 'DOPESHEET_EDITOR', 'VIEW_3D', 'GRAPH_EDITOR', 'IMAGE_EDITOR'] # blender spaces with UI region 16 | 17 | # region Panels 18 | 19 | 20 | def panel_factory(space_type: str): 21 | """ 22 | create panels for every space type with UI 23 | 24 | Args: 25 | space_type (str): valid space type of blender which has a UI region 26 | """ 27 | 28 | class AR_PT_local(Panel): 29 | bl_space_type = space_type 30 | bl_region_type = 'UI' 31 | bl_category = 'Action Recorder' 32 | bl_label = 'Local Actions' 33 | bl_idname = "AR_PT_local_%s" % space_type 34 | bl_order = 0 35 | 36 | def draw(self, context: Context) -> None: 37 | ActRec_pref = get_preferences(context) 38 | layout = self.layout 39 | if ActRec_pref.update: 40 | box = layout.box() 41 | box.label(text="new Version available (%s)" % ActRec_pref.version) 42 | update.draw_update_button(box, ActRec_pref) 43 | box = layout.box() 44 | box_row = box.row() 45 | col = box_row.column() 46 | col.template_list('AR_UL_locals', '', ActRec_pref, 'local_actions', 47 | ActRec_pref, 'active_local_action_index', rows=4, sort_lock=True) 48 | col = box_row.column() 49 | col2 = col.column(align=True) 50 | col2.operator("ar.local_add", text='', icon='ADD') 51 | col2.operator("ar.local_remove", text='', icon='REMOVE') 52 | col2 = col.column(align=True) 53 | col2.operator("ar.local_move_up", text='', icon='TRIA_UP') 54 | col2.operator("ar.local_move_down", text='', icon='TRIA_DOWN') 55 | AR_PT_local.__name__ = "AR_PT_local_%s" % space_type 56 | 57 | class AR_PT_macro(Panel): 58 | bl_space_type = space_type 59 | bl_region_type = 'UI' 60 | bl_category = 'Action Recorder' 61 | bl_label = 'Macro Editor' 62 | bl_idname = "AR_PT_macro_%s" % space_type 63 | bl_order = 1 64 | 65 | @classmethod 66 | def poll(cls, context: Context) -> bool: 67 | ActRec_pref = get_preferences(context) 68 | return len(ActRec_pref.local_actions) 69 | 70 | def draw(self, context: Context) -> None: 71 | ActRec_pref = get_preferences(context) 72 | layout = self.layout 73 | box = layout.box() 74 | box_row = box.row() 75 | col = box_row.column() 76 | selected_action = ActRec_pref.local_actions[ActRec_pref.active_local_action_index] 77 | col.template_list( 78 | 'AR_UL_macros', 79 | '', 80 | selected_action, 81 | 'macros', 82 | selected_action, 83 | 'active_macro_index', 84 | rows=4, 85 | sort_lock=True 86 | ) 87 | col = box_row.column() 88 | col.active = not selected_action.is_playing 89 | if not ActRec_pref.local_record_macros: 90 | col2 = col.column(align=True) 91 | col2.operator("ar.macro_add", text='', icon='ADD') 92 | col2.operator("ar.macro_add_event", text='', icon='MODIFIER') 93 | col2.operator("ar.macro_remove", text='', icon='REMOVE') 94 | col2 = col.column(align=True) 95 | col2.operator("ar.macro_move_up", text='', icon='TRIA_UP') 96 | col2.operator("ar.macro_move_down", text='', icon='TRIA_DOWN') 97 | row = layout.row() 98 | row.active = not selected_action.is_playing 99 | if ActRec_pref.local_record_macros: 100 | row.scale_y = 2 101 | row.operator("ar.local_record", text='Stop') 102 | else: 103 | row2 = row.row(align=True) 104 | row2.operator("ar.local_record", text='Record', icon='REC') 105 | row2.operator("ar.local_clear", text='Clear') 106 | col = layout.column() 107 | row = col.row() 108 | row.scale_y = 2 109 | row.operator("ar.local_play", text="Playing..." if selected_action.is_playing else "Play") 110 | col.operator("ar.local_to_global", text='Local to Global') 111 | row = col.row(align=True) 112 | row.enabled = bpy.ops.ar.local_to_global.poll() 113 | row.prop(ActRec_pref, 'local_to_global_mode', expand=True) 114 | AR_PT_macro.__name__ = "AR_PT_macro_%s" % space_type 115 | 116 | class AR_PT_global(Panel): 117 | bl_space_type = space_type 118 | bl_region_type = 'UI' 119 | bl_category = 'Action Recorder' 120 | bl_label = 'Global Actions' 121 | bl_idname = "AR_PT_global_%s" % space_type 122 | bl_order = 2 123 | 124 | def draw_header(self, context: Context) -> None: 125 | ActRec_pref = get_preferences(context) 126 | layout = self.layout 127 | row = layout.row(align=True) 128 | row.prop( 129 | ActRec_pref, 130 | 'global_hide_menu', 131 | icon='COLLAPSEMENU', 132 | text="", 133 | emboss=True 134 | ) 135 | 136 | def draw(self, context: Context) -> None: 137 | ActRec_pref = get_preferences(context) 138 | if not ActRec_pref.is_loaded: # loads the actions if not already done 139 | ActRec_pref.is_loaded = True 140 | layout = self.layout 141 | if not ActRec_pref.global_hide_menu: 142 | col = layout.column() 143 | row = col.row() 144 | row.scale_y = 2 145 | row.operator("ar.global_to_local", text='Global to Local') 146 | row = col.row(align=True) 147 | row.enabled = bpy.ops.ar.global_to_local.poll() 148 | row.prop(ActRec_pref, 'global_to_local_mode', expand=True) 149 | row = layout.row().split(factor=0.4) 150 | row.label(text='Buttons') 151 | row2 = row.row(align=True) 152 | row2.operator("ar.global_move_up", text='', icon='TRIA_UP') 153 | row2.operator("ar.global_move_down", text='', icon='TRIA_DOWN') 154 | row2.operator( 155 | "ar.global_recategorize_action", 156 | text='', 157 | icon='PRESET' 158 | ) 159 | row2.operator("ar.global_remove", text='', icon='TRASH') 160 | AR_PT_global.__name__ = "AR_PT_global_%s" % space_type 161 | 162 | class AR_PT_help(Panel): 163 | bl_space_type = space_type 164 | bl_region_type = 'UI' 165 | bl_category = 'Action Recorder' 166 | bl_label = 'Help' 167 | bl_idname = "AR_PT_help_%s" % space_type 168 | bl_options = {'DEFAULT_CLOSED'} 169 | bl_order = 3 170 | 171 | def draw_header(self, context: Context) -> None: 172 | layout = self.layout 173 | layout.label(icon='INFO') 174 | 175 | def draw(self, context: Context) -> None: 176 | layout = self.layout 177 | ActRec_pref = get_preferences(context) 178 | layout.operator( 179 | 'wm.url_open', 180 | text="Manual", 181 | icon='ASSET_MANAGER' 182 | ).url = config.manual_url 183 | layout.operator( 184 | 'wm.url_open', 185 | text="Hint", 186 | icon='HELP' 187 | ).url = config.hint_url 188 | layout.operator( 189 | 'ar.preferences_open_explorer', 190 | text="Open Log" 191 | ).path = log_sys.path 192 | layout.operator( 193 | 'wm.url_open', 194 | text="Bug Report", 195 | icon='URL' 196 | ).url = config.bug_report_url 197 | layout.operator( 198 | 'wm.url_open', 199 | text="Release Notes" 200 | ).url = config.release_notes_url 201 | row = layout.row() 202 | if ActRec_pref.update: 203 | update.draw_update_button(row, ActRec_pref) 204 | else: 205 | row.operator('ar.update_check', text="Check For Updates") 206 | if ActRec_pref.restart: 207 | row.operator( 208 | 'ar.show_restart_menu', 209 | text="Restart to Finish" 210 | ) 211 | if ActRec_pref.version != '': 212 | if ActRec_pref.update: 213 | layout.label( 214 | text="new Version available (%s)" % ActRec_pref.version 215 | ) 216 | else: 217 | layout.label(text="latest Version (%s)" % ActRec_pref.version) 218 | AR_PT_help.__name__ = "AR_PT_help_%s" % space_type 219 | 220 | class AR_PT_advanced(Panel): 221 | bl_space_type = space_type 222 | bl_region_type = 'UI' 223 | bl_category = 'Action Recorder' 224 | bl_label = 'Advanced' 225 | bl_idname = "AR_PT_advanced_%s" % space_type 226 | bl_options = {'DEFAULT_CLOSED'} 227 | bl_order = 4 228 | 229 | def draw(self, context: Context) -> None: 230 | ActRec_pref = get_preferences(context) 231 | layout = self.layout 232 | col = layout.column() 233 | col.label(text="Category", icon='GROUP') 234 | row = col.row(align=True) 235 | row.label(text='') 236 | row2 = row.row(align=True) 237 | row2.scale_x = 1.5 238 | row2.operator("ar.category_move_up", text='', icon='TRIA_UP') 239 | row2.operator("ar.category_move_down", text='', icon='TRIA_DOWN') 240 | row2.operator("ar.category_add", text='', icon='ADD') 241 | row2.operator("ar.category_delete", text='', icon='TRASH') 242 | row.label(text='') 243 | row = col.row(align=False) 244 | row.operator("ar.category_edit", text='Edit') 245 | row.prop( 246 | ActRec_pref, 247 | 'show_all_categories', 248 | text="", 249 | icon='RESTRICT_VIEW_OFF' if ActRec_pref.show_all_categories else 'RESTRICT_VIEW_ON' 250 | ) 251 | col.label(text="Data Management", icon='FILE_FOLDER') 252 | col.operator("ar.global_import", text='Import') 253 | col.operator("ar.global_export", text='Export') 254 | col.label(text="Storage File Settings", icon="FOLDER_REDIRECT") 255 | row = col.row() 256 | row.label(text="AutoSave") 257 | row.prop( 258 | ActRec_pref, 259 | 'autosave', 260 | toggle=True, 261 | text="On" if ActRec_pref.autosave else "Off" 262 | ) 263 | col.operator("ar.global_save", text='Save to File') 264 | col.operator("ar.global_load", text='Load from File') 265 | col.label(text="Local Settings") 266 | row = col.row(align=True) 267 | row.operator("ar.local_load", text='Load Local Actions') 268 | row.prop( 269 | ActRec_pref, 270 | 'hide_local_text', 271 | text="", 272 | toggle=True, 273 | icon="HIDE_ON" if ActRec_pref.hide_local_text else "HIDE_OFF" 274 | ) 275 | col.prop( 276 | ActRec_pref, 277 | 'local_create_empty', 278 | text="Create Empty Macro on Error" 279 | ) 280 | AR_PT_advanced.__name__ = "AR_PT_advanced_%s" % space_type 281 | 282 | global classes 283 | classes += [ 284 | AR_PT_local, 285 | AR_PT_macro, 286 | AR_PT_global, 287 | AR_PT_help, 288 | AR_PT_advanced 289 | ] 290 | # endregion 291 | 292 | 293 | # region Registration 294 | for space in ui_space_types: 295 | panel_factory(space) 296 | 297 | 298 | def register(): 299 | for cls in classes: 300 | bpy.utils.register_class(cls) 301 | 302 | 303 | def unregister(): 304 | for cls in classes: 305 | bpy.utils.unregister_class(cls) 306 | # endregion 307 | -------------------------------------------------------------------------------- /ActRec/actrec/properties/__init__.py: -------------------------------------------------------------------------------- 1 | """used by intra-modules: preferences, operators""" 2 | 3 | from .categories import ( 4 | AR_category 5 | ) 6 | 7 | from .globals import ( 8 | AR_global_actions, 9 | AR_global_import_category, 10 | AR_global_export_categories 11 | ) 12 | 13 | from .locals import ( 14 | AR_local_actions, 15 | AR_local_load_text 16 | ) 17 | 18 | from .macros import ( 19 | AR_macro_multiline, 20 | AR_event_object_name 21 | ) 22 | 23 | from .shared import ( 24 | Id_based, 25 | AR_macro, 26 | AR_action, 27 | AR_scene_data 28 | ) 29 | 30 | # region Registration 31 | 32 | 33 | def register(): 34 | from .shared import register as reg 35 | reg() 36 | from .categories import register as reg 37 | reg() 38 | from .globals import register as reg 39 | reg() 40 | from .locals import register as reg 41 | reg() 42 | from .macros import register as reg 43 | reg() 44 | 45 | 46 | def unregister(): 47 | from .shared import unregister as unreg 48 | unreg() 49 | from .categories import unregister as unreg 50 | unreg() 51 | from .globals import unregister as unreg 52 | unreg() 53 | from .locals import unregister as unreg 54 | unreg() 55 | from .macros import unregister as unreg 56 | unreg() 57 | # endregion 58 | -------------------------------------------------------------------------------- /ActRec/actrec/properties/categories.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # blender modules 3 | import bpy 4 | from bpy.types import PropertyGroup 5 | from bpy.props import BoolProperty, StringProperty, CollectionProperty 6 | 7 | # relative imports 8 | from . import shared 9 | from ..functions.shared import get_preferences 10 | # endregion 11 | 12 | # region PropertyGroups 13 | 14 | 15 | class AR_category_modes(PropertyGroup): 16 | def get_name(self) -> str: 17 | """ 18 | getter of name, same as type 19 | 20 | Returns: 21 | str: type of the category 22 | """ 23 | # self['name'] needed because of Blender default implementation 24 | self['name'] = self.type 25 | return self['name'] 26 | # needed for easier access to the mode of the category 27 | name: StringProperty(get=get_name) 28 | type: StringProperty() 29 | 30 | 31 | class AR_category_areas(PropertyGroup): 32 | def get_name(self) -> str: 33 | """ 34 | getter of name, same as type 35 | 36 | Returns: 37 | str: type of the category 38 | """ 39 | # self['name'] needed because of Blender default implementation 40 | self['name'] = self.type 41 | return self['name'] 42 | 43 | # needed for easier access to the types of the category 44 | name: StringProperty(get=get_name) 45 | type: StringProperty() 46 | modes: CollectionProperty(type=AR_category_modes) 47 | 48 | 49 | class AR_category_actions(shared.Id_based, PropertyGroup): # holds id's of actions 50 | pass 51 | 52 | 53 | class AR_category(shared.Id_based, PropertyGroup): 54 | def get_selected(self) -> bool: 55 | """ 56 | default Blender property getter 57 | 58 | Returns: 59 | bool: selection state of the category 60 | """ 61 | return self.get("selected", False) 62 | 63 | def set_selected(self, value: bool): 64 | """ 65 | set the category as active, False will not change anything 66 | 67 | Args: 68 | value (bool): state of category 69 | """ 70 | ActRec_pref = get_preferences(bpy.context) 71 | selected_id = ActRec_pref.get("categories.selected_id", "") 72 | # implementation similar to a UIList (only one selection of all can be active) 73 | if value: 74 | ActRec_pref["categories.selected_id"] = self.id 75 | self['selected'] = value 76 | category = ActRec_pref.categories.get(selected_id, None) 77 | if category: 78 | category.selected = False 79 | elif selected_id != self.id: 80 | self['selected'] = value 81 | 82 | label: StringProperty() 83 | selected: BoolProperty(description='Select this Category', 84 | name='Select', get=get_selected, set=set_selected) 85 | actions: CollectionProperty(type=AR_category_actions) 86 | areas: CollectionProperty(type=AR_category_areas) 87 | # endregion 88 | 89 | 90 | classes = [ 91 | AR_category_modes, 92 | AR_category_areas, 93 | AR_category_actions, 94 | AR_category 95 | ] 96 | 97 | # region Registration 98 | 99 | 100 | def register(): 101 | for cls in classes: 102 | bpy.utils.register_class(cls) 103 | 104 | 105 | def unregister(): 106 | for cls in classes: 107 | bpy.utils.unregister_class(cls) 108 | # endregion 109 | -------------------------------------------------------------------------------- /ActRec/actrec/properties/globals.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # blender modules 3 | import bpy 4 | from bpy.types import PropertyGroup 5 | from bpy.props import BoolProperty, StringProperty, CollectionProperty, EnumProperty 6 | 7 | # relative Imports 8 | from . import shared 9 | from .. import functions 10 | from ..functions.shared import get_preferences 11 | # endregion 12 | 13 | # region PropertyGroups 14 | 15 | 16 | class AR_global_actions(shared.AR_action, PropertyGroup): 17 | 18 | def get_selected(self) -> bool: 19 | """ 20 | default Blender property getter 21 | 22 | Returns: 23 | bool: selected state of action 24 | """ 25 | return self.get("selected", False) 26 | 27 | def set_selected(self, value: bool) -> None: 28 | """ 29 | set selected macro or if ctrl is pressed multiple macros can be selected 30 | if ctrl is not pressed all selected macros get deselected except the new selected. 31 | 32 | Args: 33 | value (bool): state of selection 34 | """ 35 | ActRec_pref = get_preferences(bpy.context) 36 | selected_ids = list(ActRec_pref.get("global_actions.selected_ids", [])) 37 | # implementation similar to a UIList (only one selection of all can be active), 38 | # with extra multi selection by pressing ctrl 39 | value |= (len(selected_ids) > 1) # used as bool or 40 | if not value: 41 | self['selected'] = False 42 | return 43 | 44 | # uses check_ctrl operator to check for ctrl event 45 | ctrl_value = bpy.ops.ar.check_ctrl('INVOKE_DEFAULT') 46 | # {'CANCELLED'} == ctrl is not pressed 47 | if selected_ids and ctrl_value == {'CANCELLED'}: 48 | ActRec_pref["global_actions.selected_ids"] = [] 49 | for selected_id in selected_ids: 50 | action = ActRec_pref.global_actions.get(selected_id, None) 51 | if action is None: 52 | continue 53 | action.selected = (not bool(action)) 54 | selected_ids.clear() 55 | selected_ids.append(self.id) 56 | ActRec_pref["global_actions.selected_ids"] = selected_ids 57 | self['selected'] = value 58 | 59 | selected: BoolProperty( 60 | default=False, 61 | set=set_selected, 62 | get=get_selected, 63 | description="Select this Action Button\nuse ctrl to select multiple", 64 | name='Select' 65 | ) 66 | 67 | 68 | class AR_global_import_action(PropertyGroup): 69 | def get_use(self) -> bool: 70 | """ 71 | get state whether the action will be used to import 72 | with extra check if the category of this action is also selected for import 73 | 74 | Returns: 75 | bool: action import state 76 | """ 77 | return self.get('use', True) and self.get('category.use', True) 78 | 79 | def set_use(self, value: bool) -> None: 80 | """ 81 | set state whether the action will be used to import 82 | 83 | Args: 84 | value (bool): action import state 85 | """ 86 | if self.get('category.use', True): 87 | self['use'] = value 88 | 89 | label: StringProperty() 90 | identifier: StringProperty() 91 | use: BoolProperty( 92 | default=True, 93 | name="Import Action", 94 | description="Decide whether to import the action", 95 | get=get_use, 96 | set=set_use 97 | ) 98 | shortcut: StringProperty() 99 | 100 | 101 | class AR_global_import_category(PropertyGroup): 102 | def get_use(self) -> bool: 103 | """ 104 | get state whether the category will be used to import 105 | 106 | Returns: 107 | bool: category import state 108 | """ 109 | return self.get("use", True) 110 | 111 | def set_use(self, value: bool) -> None: 112 | """ 113 | set state whether the category will be used to import 114 | 115 | Args: 116 | value (bool): category import state 117 | """ 118 | self['use'] = value 119 | # needed for the action to check if there category is imported 120 | for action in self.actions: 121 | action['category.use'] = value 122 | 123 | label: StringProperty() 124 | identifier: StringProperty() 125 | actions: CollectionProperty(type=AR_global_import_action) 126 | show: BoolProperty(default=True) 127 | use: BoolProperty( 128 | default=True, 129 | name="Import Category", 130 | description="Decide whether to import the category", 131 | get=get_use, 132 | set=set_use 133 | ) 134 | 135 | 136 | class AR_global_export_action(shared.Id_based, PropertyGroup): 137 | def get_use(self) -> bool: 138 | """ 139 | get state whether the action will be used to export 140 | with extra check if the category of this action is also selected for export or export_all is active 141 | 142 | Returns: 143 | bool: action export state 144 | """ 145 | return self.get("use", True) and self.get('category.use', True) or self.get('export_all', False) 146 | 147 | def set_use(self, value: bool) -> None: 148 | """ 149 | set state whether the action will be used to export 150 | 151 | Args: 152 | value (bool): action export state 153 | """ 154 | if self.get('category.use', True) and not self.get('export_all', False): 155 | self['use'] = value 156 | 157 | label: StringProperty() 158 | use: BoolProperty( 159 | default=True, 160 | name="Export Action", 161 | description="Decide whether to export the action", 162 | get=get_use, 163 | set=set_use 164 | ) 165 | shortcut: StringProperty() 166 | 167 | 168 | class AR_global_export_categories(shared.Id_based, PropertyGroup): 169 | def get_use(self) -> bool: 170 | """ 171 | get state whether the category will be used to export or export_all is active 172 | 173 | Returns: 174 | bool: category export state 175 | """ 176 | return self.get("use", True) or self.get("export_all", False) 177 | 178 | def set_use(self, value: bool) -> None: 179 | """ 180 | set state whether the category will be used to export 181 | 182 | Args: 183 | value (bool): category export state 184 | """ 185 | if self.get("export_all", False): 186 | return 187 | self['use'] = value 188 | for action in self.actions: 189 | action['category.use'] = value 190 | 191 | label: StringProperty() 192 | actions: CollectionProperty(type=AR_global_export_action) 193 | show: BoolProperty(default=True) 194 | use: BoolProperty( 195 | default=True, 196 | name="Export Category", 197 | description="Decide whether to export the category", 198 | get=get_use, 199 | set=set_use 200 | ) 201 | # endregion 202 | 203 | 204 | classes = [ 205 | AR_global_actions, 206 | AR_global_import_action, 207 | AR_global_import_category, 208 | AR_global_export_action, 209 | AR_global_export_categories 210 | ] 211 | 212 | # region Registration 213 | 214 | 215 | def register(): 216 | for cls in classes: 217 | bpy.utils.register_class(cls) 218 | 219 | 220 | def unregister(): 221 | for cls in classes: 222 | bpy.utils.unregister_class(cls) 223 | # endregion 224 | -------------------------------------------------------------------------------- /ActRec/actrec/properties/locals.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # blender modules 3 | import bpy 4 | from bpy.types import PropertyGroup 5 | from bpy.props import BoolProperty, StringProperty, IntProperty, EnumProperty 6 | 7 | # relative Imports 8 | from . import shared 9 | from ..functions import get_preferences 10 | from .. import functions 11 | # endregion 12 | 13 | # region PropertyGroups 14 | 15 | 16 | class AR_local_actions(shared.AR_action, PropertyGroup): 17 | def get_active_macro_index(self) -> int: 18 | """ 19 | get the active index of the local macro. 20 | If the index is out of range the last index of all macros is passed on. 21 | 22 | Returns: 23 | int: macro index 24 | """ 25 | value = self.get('active_macro_index', 0) 26 | macros_length = len(self.macros) 27 | return value if value < macros_length else macros_length - 1 28 | 29 | def set_active_macro_index(self, value: int) -> None: 30 | """ 31 | sets the active index of the local macro. 32 | if value is out of range the last index of the macros is passed on. 33 | 34 | Args: 35 | value (int): index of the active macro 36 | """ 37 | macros_length = len(self.macros) 38 | value = value if value < macros_length else macros_length - 1 39 | self['active_macro_index'] = value if value >= 0 else macros_length - 1 40 | 41 | active_macro_index: IntProperty( 42 | name="Select", 43 | min=0, 44 | get=get_active_macro_index, 45 | set=set_active_macro_index 46 | ) 47 | 48 | 49 | class AR_local_load_text(PropertyGroup): 50 | name: StringProperty() 51 | apply: BoolProperty(default=False) 52 | # endregion 53 | 54 | 55 | classes = [ 56 | AR_local_actions, 57 | AR_local_load_text 58 | ] 59 | 60 | # region Registration 61 | 62 | 63 | def register(): 64 | for cls in classes: 65 | bpy.utils.register_class(cls) 66 | 67 | 68 | def unregister(): 69 | for cls in classes: 70 | bpy.utils.unregister_class(cls) 71 | # endregion 72 | -------------------------------------------------------------------------------- /ActRec/actrec/properties/macros.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # blender modules 3 | import bpy 4 | from bpy.types import PropertyGroup 5 | from bpy.props import StringProperty, BoolProperty 6 | # endregion 7 | 8 | 9 | class AR_macro_multiline(PropertyGroup): 10 | def get_text(self) -> str: 11 | """ 12 | default Blender property getter 13 | 14 | Returns: 15 | str: text of the multiline macro 16 | """ 17 | return self.get('text', '') 18 | 19 | def set_text(self, value: str) -> None: 20 | """ 21 | set the text of the multiline macro and update to True 22 | 23 | Args: 24 | value (str): text for the multiline macro 25 | """ 26 | self['text'] = value 27 | self['update'] = True 28 | 29 | def get_update(self) -> bool: 30 | """ 31 | reset update back to false but the previous value will be passed on 32 | 33 | Returns: 34 | bool: state of update 35 | """ 36 | value = self.get('update', False) 37 | self['update'] = False 38 | return value 39 | text: StringProperty(get=get_text, set=set_text) 40 | update: BoolProperty(get=get_update) 41 | 42 | 43 | class AR_event_object_name(PropertyGroup): 44 | name: StringProperty( 45 | name="Object", 46 | description="Choose an Object which get select when this Event is played", 47 | default="" 48 | ) 49 | 50 | 51 | classes = [ 52 | AR_macro_multiline, 53 | AR_event_object_name 54 | ] 55 | 56 | # region Registration 57 | 58 | 59 | def register(): 60 | for cls in classes: 61 | bpy.utils.register_class(cls) 62 | 63 | 64 | def unregister(): 65 | for cls in classes: 66 | bpy.utils.unregister_class(cls) 67 | # endregion 68 | -------------------------------------------------------------------------------- /ActRec/actrec/properties/shared.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # external modules 3 | import uuid 4 | 5 | # blender modules 6 | import bpy 7 | from bpy.types import PropertyGroup, Context 8 | from bpy.props import StringProperty, IntProperty, CollectionProperty, BoolProperty, EnumProperty 9 | 10 | # relative imports 11 | from .. import functions 12 | from ..functions.shared import get_preferences 13 | from ..icon_manager import get_icons_name_map, get_icons_value_map, get_custom_icon_name_map, get_custom_icons_value_map 14 | # endregion 15 | 16 | # region PropertyGroups 17 | 18 | 19 | class Id_based: 20 | def get_id(self) -> str: 21 | """ 22 | get id as UUID, 23 | generates new UUID if id is not set 24 | 25 | Returns: 26 | str: UUID in hex format 27 | """ 28 | self['name'] = self.get('name', uuid.uuid1().hex) 29 | return self['name'] 30 | 31 | def set_id(self, value: str) -> None: 32 | """ 33 | Create a UUID from a string of 32 hexadecimal digits 34 | 35 | Args: 36 | value (str): preferred UUID in hex format 37 | 38 | Raises: 39 | ValueError: unparsable value into UUID get raised 40 | """ 41 | try: 42 | self['name'] = uuid.UUID(value).hex 43 | except ValueError as err: 44 | raise ValueError("%s with %s" % (err, value)) 45 | 46 | # needed for easier access to the types of the category 47 | # id and name are the same, because CollectionProperty use property 'name' as key 48 | name: StringProperty(get=get_id) 49 | # create id by calling get-function of id 50 | id: StringProperty(get=get_id, set=set_id) 51 | 52 | 53 | class Alert_system: 54 | def get_alert(self) -> bool: 55 | """ 56 | default Blender property getter 57 | 58 | Returns: 59 | bool: alert state 60 | """ 61 | return self.get('alert', False) 62 | 63 | def set_alert(self, value: bool) -> None: 64 | """ 65 | automatically reset the alert after 1 second to false again, 66 | because alert is only shown temporarily in the UI 67 | 68 | Args: 69 | value (bool): change alert state 70 | """ 71 | self['alert'] = value 72 | if value: 73 | def reset() -> None: 74 | self['alert'] = False 75 | bpy.app.timers.register(reset, first_interval=1, persistent=True) 76 | 77 | def update_alert(self, context: Context) -> None: 78 | """ 79 | redraw the area to show the alert change in the UI 80 | 81 | Args: 82 | context (Context): active blender context 83 | """ 84 | if hasattr(context, 'area') and context.area: 85 | context.area.tag_redraw() 86 | 87 | alert: BoolProperty( 88 | default=False, 89 | description="Internal use", 90 | get=get_alert, 91 | set=set_alert, 92 | update=update_alert 93 | ) 94 | 95 | 96 | class Icon_system: 97 | def get_icon(self) -> str: 98 | icons = get_icons_name_map() 99 | icons.update(get_custom_icon_name_map()) 100 | return icons.get(self.icon_name, self.get("icon", 0)) 101 | 102 | def set_icon(self, value: int) -> None: 103 | icons = get_icons_value_map() 104 | icons.update(get_custom_icons_value_map()) 105 | self["icon"] = value 106 | self["icon_name"] = icons.get(value, "NONE") 107 | 108 | def get_icon_name(self) -> str: 109 | return self.get("icon_name", "NONE") 110 | 111 | def set_icon_name(self, value: str) -> None: 112 | self["icon_name"] = value 113 | # Icon NONE: Global: BLANK1 (101), Local: MESH_PLANE (286) 114 | icon: IntProperty(default=0, set=set_icon, get=get_icon) 115 | icon_name: StringProperty(default='NONE', set=set_icon_name, get=get_icon_name) 116 | 117 | 118 | class AR_macro(Id_based, Alert_system, Icon_system, PropertyGroup): 119 | def get_active(self) -> bool: 120 | """ 121 | default Blender property getter with extra check if the macro is available 122 | 123 | Returns: 124 | bool: state of macro, true if active, always false if macro is not available 125 | """ 126 | return self.get('active', True) and self.is_available 127 | 128 | def set_active(self, value: bool) -> None: 129 | """ 130 | set the active state if macro is available and macro recording is turned off 131 | if the value change it is written to the local scene data if autosave is active 132 | 133 | Args: 134 | value (bool): state of macro 135 | """ 136 | if not self.is_available or self.is_playing: 137 | return 138 | context = bpy.context 139 | ActRec_pref = get_preferences(context) 140 | if not ActRec_pref.local_record_macros: 141 | if self.get('active', True) != value: 142 | functions.save_local_to_scene(ActRec_pref, context.scene) 143 | self['active'] = value 144 | 145 | def get_command(self) -> str: 146 | """ 147 | default Blender property getter 148 | 149 | Returns: 150 | str: command of macro 151 | """ 152 | return self.get("command", "") 153 | 154 | def set_command(self, value: str) -> None: 155 | """ 156 | sets the macro command and updates it with the running Blender version 157 | if the command isn't found in the running Blender it will be marked as not available 158 | 159 | Args: 160 | value (str): command to set to 161 | """ 162 | res = functions.update_command(value) 163 | self['command'] = res if isinstance(res, str) else value 164 | self['is_available'] = res is not None 165 | 166 | def get_is_available(self) -> bool: 167 | """ 168 | default Blender property getter 169 | 170 | Returns: 171 | bool: state if macro is available 172 | """ 173 | return self.get('is_available', True) 174 | 175 | label: StringProperty() 176 | command: StringProperty(get=get_command, set=set_command) 177 | active: BoolProperty( 178 | default=True, 179 | description='Toggles Macro on and off.', 180 | get=get_active, 181 | set=set_active 182 | ) 183 | is_available: BoolProperty(default=True, get=get_is_available) 184 | ui_type: StringProperty(default="") 185 | operator_execution_context: EnumProperty( 186 | items=[ # https://docs.blender.org/api/current/bpy.ops.html#execution-context 187 | ("EXEC_DEFAULT", "Execute", "The operator get executed immediately"), 188 | ("INVOKE_DEFAULT", "Invoke", "The operator can wait for user input") 189 | ], 190 | default="EXEC_DEFAULT", 191 | name="Execution Context", 192 | description="""Choose the execution behavior of the operator (only applies to operator commands) 193 | The operator can be executed immediately or invoked where the operator can wait for user input 194 | 195 | HINT: Sometimes it helps to change to Invoke to get the expected behavior""" 196 | ) 197 | is_playing: BoolProperty( 198 | default=False, 199 | description="Indicates whether the parent action executes its macros" 200 | ) 201 | 202 | 203 | class AR_action(Id_based, Alert_system, Icon_system): 204 | 205 | def get_is_playing(self): 206 | return self.get("is_playing", False) 207 | 208 | def set_is_playing(self, value): 209 | self["is_playing"] = value 210 | for macro in self.macros: 211 | macro.is_playing = value 212 | 213 | label: StringProperty() 214 | description: StringProperty(default="Play this Action Button") 215 | macros: CollectionProperty(type=AR_macro) 216 | execution_mode: EnumProperty( 217 | items=[("INDIVIDUAL", "Individual", 218 | """Performs the current action on all selected objects individually. 219 | Therefore, the action is executed as many times as there are selected objects.""", 220 | "STICKY_UVS_DISABLE", 0), 221 | ("GROUP", "Group", 222 | "Performs the current action on all selected objects without separating them (Default Behavior)", 223 | "STICKY_UVS_LOC", 1)], 224 | name="Execution Mode", 225 | description="Choses to perform the current actions on the selected objects individually or as a group", 226 | default="GROUP" 227 | ) 228 | is_playing: BoolProperty( 229 | default=False, 230 | description="Indicates whether the action executes its macros", 231 | get=get_is_playing, 232 | set=set_is_playing 233 | ) 234 | 235 | 236 | class AR_scene_data(PropertyGroup): # as Scene PointerProperty 237 | local: StringProperty( 238 | name="Local", 239 | description='Scene Backup-Data of AddonPreference.local_actions (json format)', 240 | default='{}' 241 | ) 242 | record_undo_end: BoolProperty( 243 | name="Undo End", 244 | description="Used to get the undo step before the record started to compare the undo steps (INTERNAL)", 245 | default=False 246 | ) 247 | # endregion 248 | 249 | 250 | classes = [ 251 | AR_macro, 252 | AR_scene_data, 253 | # AR_keymap 254 | ] 255 | 256 | # region Registration 257 | 258 | 259 | def register(): 260 | for cls in classes: 261 | bpy.utils.register_class(cls) 262 | 263 | 264 | def unregister(): 265 | for cls in classes: 266 | bpy.utils.unregister_class(cls) 267 | # endregion 268 | -------------------------------------------------------------------------------- /ActRec/actrec/shared_data.py: -------------------------------------------------------------------------------- 1 | # only mutable types define immutable with BlenderProperties 2 | render_complete_macros = [] 3 | 4 | tracked_actions = [] 5 | 6 | data_loaded = False 7 | -------------------------------------------------------------------------------- /ActRec/actrec/ui_functions/__init__.py: -------------------------------------------------------------------------------- 1 | """no imports from other intra-modules 2 | used by intra-modules: functions, operators""" 3 | 4 | from .categories import ( 5 | register_category, 6 | unregister_category, 7 | category_visible, 8 | get_visible_categories 9 | ) 10 | 11 | from .globals import ( 12 | draw_global_action, 13 | draw_simple_global_action 14 | ) 15 | 16 | 17 | # region Registration 18 | def unregister(): 19 | from .categories import unregister as unreg 20 | unreg() 21 | # endregion 22 | -------------------------------------------------------------------------------- /ActRec/actrec/ui_functions/categories.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # externals modules 3 | from contextlib import suppress 4 | from typing import TYPE_CHECKING 5 | 6 | # blender modules 7 | import bpy 8 | from bpy.types import Panel, Menu, Context, AddonPreferences, PropertyGroup 9 | 10 | # relative imports 11 | from . import globals 12 | from .. import panels 13 | from ..functions.shared import get_preferences 14 | from ..log import logger 15 | if TYPE_CHECKING: 16 | from ..preferences import AR_preferences 17 | from ..properties.categories import AR_category 18 | else: 19 | AR_preferences = AddonPreferences 20 | AR_category = PropertyGroup 21 | # endregion 22 | 23 | classes = [] 24 | space_mode_attribute = { 25 | 'IMAGE_EDITOR': 'ui_mode', 26 | 'NODE_EDITOR': 'texture_type', 27 | 'SEQUENCE_EDITOR': 'view_type', 28 | 'CLIP_EDITOR': 'mode', 29 | 'DOPESHEET_EDITOR': 'ui_mode' 30 | } 31 | 32 | 33 | def category_visible( 34 | ActRec_pref: AR_preferences, 35 | context: Context, 36 | category: 'AR_category') -> bool: 37 | """ 38 | checks if category is visible based on the given context 39 | 40 | Args: 41 | ActRec_pref (AR_preferences): preferences of this addon 42 | context (Context): active blender context 43 | category (AR_category): category to check 44 | 45 | Returns: 46 | bool: true if category is visible 47 | """ 48 | if ActRec_pref.show_all_categories or not len(category.areas): 49 | return True 50 | if context.area is None: 51 | return False 52 | area_type = context.area.ui_type 53 | area_space = context.area.type 54 | for area in category.areas: 55 | if area.type != area_type: 56 | continue 57 | if len(area.modes) == 0: 58 | return True 59 | if area_space == 'VIEW_3D': 60 | mode = "" 61 | if context.object: 62 | mode = context.object.mode 63 | else: 64 | mode = getattr( 65 | context.space_data, 66 | space_mode_attribute[area_space]) 67 | return mode in set(mode.type for mode in area.modes) 68 | return False 69 | 70 | 71 | def get_visible_categories(ActRec_pref: AR_preferences, context: Context) -> list['AR_category']: 72 | """ 73 | get list of all visible categories 74 | 75 | Args: 76 | ActRec_pref (AR_preferences): preferences of this addon 77 | context (Context): active blender context 78 | 79 | Returns: 80 | list[AR_category]: list of all visible categories 81 | """ 82 | return [category for category in ActRec_pref.categories if category_visible(ActRec_pref, context, category)] 83 | 84 | 85 | def register_category(ActRec_pref: AR_preferences, index: int) -> None: 86 | """ 87 | register a category based on the index in all spaces (panels.ui_space_types) 88 | 89 | Args: 90 | ActRec_pref (AR_preferences): preferences of this addon 91 | index (int): index of category to register 92 | """ 93 | register_unregister_category(index) 94 | 95 | 96 | def unregister_category(ActRec_pref: AR_preferences, index: int) -> None: 97 | """ 98 | unregister a category based on the index in all spaces (panels.ui_space_types) 99 | 100 | Args: 101 | ActRec_pref (AR_preferences): preferences of this addon 102 | index (int): index of category to unregister 103 | """ 104 | register_unregister_category(index, register=False) 105 | 106 | 107 | def register_unregister_category( 108 | index: int, 109 | space_types: list[str] = panels.ui_space_types, 110 | register: bool = True) -> None: 111 | """ 112 | register or unregister a single category in all given spaces 113 | 114 | Args: 115 | index (int): index of the category 116 | space_types (list[str], optional): list of spaces to unregister the category from. 117 | Defaults to panels.ui_space_types. 118 | register (bool, optional): true: register category; false: unregister category. Defaults to True. 119 | """ 120 | for spaceType in space_types: 121 | class AR_PT_category(Panel): 122 | bl_space_type = spaceType 123 | bl_region_type = 'UI' 124 | bl_category = 'Action Recorder' 125 | bl_label = ' ' 126 | bl_idname = "AR_PT_category_%s_%s" % (index, spaceType) 127 | bl_parent_id = "AR_PT_global_%s" % spaceType 128 | bl_order = index + 1 129 | bl_options = {"INSTANCED", "DEFAULT_CLOSED"} 130 | 131 | @classmethod 132 | def poll(self, context: Context) -> bool: 133 | ActRec_pref = get_preferences(context) 134 | index = int(self.bl_idname.split("_")[3]) 135 | return index < len(get_visible_categories(ActRec_pref, context)) 136 | 137 | def draw_header(self, context: Context) -> None: 138 | ActRec_pref = get_preferences(context) 139 | index = int(self.bl_idname.split("_")[3]) 140 | category = get_visible_categories(ActRec_pref, context)[index] 141 | layout = self.layout 142 | row = layout.row() 143 | row.prop(category, 'selected', text='', 144 | icon='LAYER_ACTIVE' if category.selected else 'LAYER_USED', emboss=False) 145 | row.label(text=category.label) 146 | 147 | def draw(self, context: Context) -> None: 148 | ActRec_pref = get_preferences(context) 149 | index = int(self.bl_idname.split("_")[3]) 150 | category = get_visible_categories(ActRec_pref, context)[index] 151 | layout = self.layout 152 | col = layout.column() 153 | for id in [x.id for x in category.actions]: 154 | globals.draw_global_action(col, ActRec_pref, id) 155 | AR_PT_category.__name__ = "AR_PT_category_%s_%s" % (index, spaceType) 156 | 157 | if register: 158 | try: 159 | bpy.utils.register_class(AR_PT_category) 160 | classes.append(AR_PT_category) 161 | except RuntimeError as err: 162 | logger.error("Couldn't register Panel :(\n(%s)", err) 163 | else: 164 | with suppress(Exception): 165 | if hasattr(bpy.types, AR_PT_category.__name__): 166 | panel = getattr(bpy.types, AR_PT_category.__name__) 167 | bpy.utils.unregister_class(panel) 168 | classes.remove(panel) 169 | 170 | class AR_MT_category(Menu): 171 | bl_idname = "AR_MT_category_%s" % index 172 | bl_label = "Category" 173 | 174 | @classmethod 175 | def poll(self, context: Context) -> bool: 176 | ActRec_pref = get_preferences(context) 177 | index = int(self.bl_idname.split("_")[3]) 178 | return index < len(get_visible_categories(ActRec_pref, context)) 179 | 180 | def draw(self, context: Context) -> None: 181 | ActRec_pref = get_preferences(context) 182 | index = int(self.bl_idname.split("_")[3]) 183 | category = get_visible_categories(ActRec_pref, context)[index] 184 | layout = self.layout 185 | col = layout.column() 186 | for id in [x.id for x in category.actions]: 187 | globals.draw_simple_global_action(col, ActRec_pref, id) 188 | AR_MT_category.__name__ = "AR_MT_category_%s" % index 189 | 190 | if register: 191 | try: 192 | bpy.utils.register_class(AR_MT_category) 193 | classes.append(AR_MT_category) 194 | except RuntimeError as err: 195 | logger.error("Couldn't register Menu :(\n(%s)", err) 196 | else: 197 | with suppress(Exception): 198 | if hasattr(bpy.types, AR_MT_category.__name__): 199 | menu = getattr(bpy.types, AR_MT_category.__name__) 200 | bpy.utils.unregister_class(menu) 201 | classes.remove(menu) 202 | 203 | ActRec_pref = get_preferences(bpy.context) 204 | if ActRec_pref.selected_category == '' and len(ActRec_pref.categories): 205 | ActRec_pref.categories[0].selected = True 206 | 207 | 208 | def unregister(): 209 | for cls in classes: 210 | bpy.utils.unregister_class(cls) 211 | classes.clear() 212 | -------------------------------------------------------------------------------- /ActRec/actrec/ui_functions/globals.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # external modules 3 | from typing import TYPE_CHECKING 4 | 5 | # blender modules 6 | import bpy 7 | from bpy.types import UILayout, AddonPreferences 8 | 9 | # relative imports 10 | if TYPE_CHECKING: 11 | from ..preferences import AR_preferences 12 | else: 13 | AR_preferences = AddonPreferences 14 | # endregion 15 | 16 | # region UI functions 17 | 18 | 19 | def draw_global_action(layout: UILayout, ActRec_pref: AR_preferences, id: str) -> None: 20 | """ 21 | draws row of global action button. 22 | 23 | Args: 24 | layout (UILayout): UI context of Blender 25 | ActRec_pref (AR_preferences): preferences of this addon 26 | id (str): UUID of the action, use action.id the get the UUID 27 | """ 28 | action = ActRec_pref.global_actions[id] 29 | row = layout.row(align=True) 30 | row.alert = action.alert 31 | row.prop( 32 | action, 33 | 'selected', 34 | toggle=1, 35 | icon='LAYER_ACTIVE' if action.selected else 'LAYER_USED', 36 | text="", 37 | event=True 38 | ) 39 | op = row.operator( 40 | "ar.global_icon", 41 | text="", 42 | icon_value=action.icon if action.icon else 101 43 | ) 44 | op.id = id 45 | op = row.operator("ar.global_execute_action", text=action.label) 46 | op.id = id 47 | row.prop(action, 'execution_mode', text="", icon_only=True) 48 | 49 | 50 | def draw_simple_global_action(layout: UILayout, ActRec_pref: AR_preferences, id: str) -> None: 51 | """ 52 | draws row of global action button but only the operator 53 | 54 | Args: 55 | layout (UILayout): UI context of Blender 56 | ActRec_pref (AR_preferences): preferences of this addon 57 | id (str): UUID of the action, use action.id the get the UUID 58 | """ 59 | action = ActRec_pref.global_actions[id] 60 | row = layout.row() 61 | row.alert = action.alert 62 | execution_mode_name = row.enum_item_name(action, 'execution_mode', action.execution_mode) 63 | op = row.operator( 64 | "ar.global_execute_action", 65 | text="%s [%s]" % (action.label, execution_mode_name), 66 | icon_value=action.icon if action.icon else 101 67 | ) 68 | op.id = id 69 | # endregion 70 | -------------------------------------------------------------------------------- /ActRec/actrec/uilist/__init__.py: -------------------------------------------------------------------------------- 1 | from .locals import ( 2 | AR_UL_locals 3 | ) 4 | 5 | from .macros import ( 6 | AR_UL_macros 7 | ) 8 | 9 | # region Registration 10 | 11 | 12 | def register(): 13 | from .locals import register as reg 14 | reg() 15 | from .macros import register as reg 16 | reg() 17 | 18 | 19 | def unregister(): 20 | from .locals import unregister as unreg 21 | unreg() 22 | from .macros import unregister as unreg 23 | unreg() 24 | # endregion 25 | -------------------------------------------------------------------------------- /ActRec/actrec/uilist/locals.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # blender modules 3 | import bpy 4 | from bpy.types import UIList 5 | # endregion 6 | 7 | # region UIList 8 | 9 | 10 | class AR_UL_locals(UIList): 11 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index) -> None: 12 | self.use_filter_show = False 13 | self.use_filter_sort_lock = True 14 | row = layout.row(align=True) 15 | row.alert = item.alert 16 | ops = row.operator( 17 | "ar.local_icon", 18 | text="", 19 | icon_value=item.icon if item.icon else 286, 20 | emboss=False 21 | ) 22 | ops.id = item.id 23 | ops.index = index 24 | col = row.column() 25 | col.ui_units_x = 0.5 26 | row.prop(item, 'label', text='', emboss=False) 27 | row.prop(item, 'execution_mode', text="", icon_only=True) 28 | # endregion 29 | 30 | 31 | classes = [ 32 | AR_UL_locals 33 | ] 34 | 35 | # region Registration 36 | 37 | 38 | def register(): 39 | for cls in classes: 40 | bpy.utils.register_class(cls) 41 | 42 | 43 | def unregister(): 44 | for cls in classes: 45 | bpy.utils.unregister_class(cls) 46 | # endregion 47 | -------------------------------------------------------------------------------- /ActRec/actrec/uilist/macros.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | # blender modules 3 | import bpy 4 | from bpy.types import UIList 5 | # endregion 6 | 7 | # region UIList 8 | 9 | 10 | class AR_UL_macros(UIList): 11 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index) -> None: 12 | self.use_filter_show = False 13 | self.use_filter_sort_lock = True 14 | row = layout.row(align=True) 15 | row.alert = item.alert 16 | row.prop(item, 'active', text="") 17 | ops = row.operator("ar.macro_edit", text=item.label, emboss=False) 18 | ops.id = item.id 19 | ops.index = index 20 | # endregion 21 | 22 | 23 | classes = [ 24 | AR_UL_macros 25 | ] 26 | 27 | # region Registration 28 | 29 | 30 | def register(): 31 | for cls in classes: 32 | bpy.utils.register_class(cls) 33 | 34 | 35 | def unregister(): 36 | for cls in classes: 37 | bpy.utils.unregister_class(cls) 38 | # endregion 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LINK 2 | - [Action Recorder](#action-recorder) 3 | - [Add-ons Explained](#add-ons-explained) 4 | - [INSTALL](#install) 5 | - [Tutorial&Readme](#tutorialreadme) 6 | - [Update](#update) 7 | - [Community](#community) 8 | - [Bug Reports and Requests](#bug-reports-and-requests) 9 | # PLEASE! 10 | Our workload has made it difficult for us to update and fix bugs
11 | New programmers are expected to be created!
12 | We have therefore made some major changes to the Action Recorder add-on
13 | For example, we have separated the Python files and added comments to make it easier for other programmers to understand
14 | Please Fork this add-on!

15 | 16 | # Action Recorder 17 | Are you tirred of coding long intimidating coding task just for one-off modeling? Or may be you're not good coding at all !
18 | Introducing "ActionRecorder" !
19 | 20 | ActRec is an add-on developed to streamline your work in Blender
21 | 22 | This add-on is similar to Affinity Photo and Photoshop's "Action"
23 | This add-on has been reborn! 24 | **Command Recorder --> Action Recorder** 25 | 26 | This add-on is available for Free!
27 | But, It takes a lot of coffee to develop an Add-on! 28 | [![](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://paypal.me/InamuraJIN?locale.x=ja_JP) 29 | 30 | The update button has been moved!
31 | :warning: Be sure to use this button
32 | 33 | 34 | # Add-ons Explained 35 | 36 | Convert complex, repetitive tasks into one click! 37 | 38 | You have to remember is "Add" & "Play" Only! 39 | 40 | Supported Versions: 2.83 to 2.9 41 | 42 | Simplify complex repetitive tasks, which were difficult to do with the standard “Repeat Last”(Shift+R) function of Blender, by registering multiple actions. 43 | 44 | You can register, edit and play back various tasks as you wish 45 | 46 | For example, with the touch of a button, you can do routine tasks such as adding instant coffee, milk and sugar while boiling water and adding hot water! 47 | 48 | (I've thought about it a few times, but unfortunately this add-on only allows you to register work within Blender.) 49 |


50 | 51 | # INSTALL
52 |
53 | On the right side of this page, there is a "Release Link"
54 | Download the Zip file from this button
55 | The next step is the same as the normal add-on installation
56 | 57 | Header Menu ->Edit > Preferences > Add-ons > Install
58 |
59 | 60 | Select the Zip file you downloaded
61 |
62 | Click the "Install Add-on" button
63 | 64 | このページの右側に「Release」ボタンからZipファイルをダウンロードして下さい 65 | 66 | # Tutorial&Readme 67 | 🇯🇵[日本語](https://inamurajin.wixsite.com/website/post/tutorial_readme_jp)
68 | ※ 日本語解説の更新は停止します。前バージョンの記事で参考に出来ますが、詳しくは英語版でご覧ください 69 | 70 | 🇺🇸[English](https://inamurajin.wixsite.com/website/post/tutorial_readme_en) 71 |


72 | 73 | 74 | 75 | 76 | # Update 77 | 78 | https://github.com/InamuraJIN/ActionRecorder/wiki 79 |


80 | 81 | 82 | 83 | 84 | # Community 85 | Discord
86 | https://discord.gg/XTWbkFx 87 |


88 | 89 | 90 | 91 | # Bug Reports and Requests 92 | - Discord
93 | https://discord.gg/XTWbkFx

94 | - Twitter
95 | https://twitter.com/Inamura_JIN

96 | - GitHub
97 | https://github.com/InamuraJIN/ActionRecorder/issues

98 |





99 | thank you 100 | -------------------------------------------------------------------------------- /ReadMe_English.txt: -------------------------------------------------------------------------------- 1 | product guide 2 | 3 | 4 | ■CommandRecorder 5 | https://eachpath.booth.pm/items/1123127 6 | The action registration function is familiar with The DCC tool! 7 | This is a versatile add-on that the moderator thinks and programmer implements! 8 | CommandRecorder has fuction of records and plays. 9 | Watch the video for installation and usage! 10 | https://youtu.be/7UtFRyxm-EU 11 | 12 | 13 | ■Post 14 | https://satahiko.booth.pm/items/1229821 15 | ■Vessel 16 | https://satahiko.booth.pm/items/1123123 17 | 18 | 19 | 20 | 21 | 22 | 23 | About CommandRecorder 24 | ▪ This is a feature that has not been introduced in the video. 25 | 26 | 27 | You can switch the display of command history. 28 | ●Standard: Normal command display 29 | ●Extend:Displays the command in detail 30 | 31 | 32 | 33 | 34 | ■Notes 35 | 36 | 37 | ●Commands stored in the command list are passed on only for the project. 38 | After you register and save to action button, you can use it for other projects too. 39 | 40 | 41 | ●About the commands that need to be selected 42 | For example, you can register and execute loop cuts, but you cannot specify the application location. 43 | Commands with important selection scope and mouse position tend to be unintended behavior. 44 | 45 | 46 | ·About Application once or each 47 | Currently, we are dealing with the execution of each application in object mode, bone editing mode and pause mode. 48 | 49 | 50 | ● If you switch between Japanese and other languages in the middle of recording, it will be down. 51 | 52 | 53 | 54 | 55 | 56 | 57 | 〇About all products 58 | ■Terms of Use 59 | 60 | 61 | Article 1 Definitions 62 | The meanings of the terms listed in the following items shall be as prescribed respectively in those items. 63 | (1) The terms of use (hereinafter referred to as "the terms") apply to all people using this service (hereinafter referred to as "Service") provided by BuuGraphic (hereinafter referred to as "Producer"). 64 | (2) Secondary works 65 | A work means that created by translating, arranging, or transforming, or dramatizing, cinematizing or others. 66 | (3) Modified products 67 | A work created by modifying, excising or others does not fall under the category of a secondary work. 68 | (4) Secondary Creation 69 | This term refers to modifications, secondary works, and other works created based on works. 70 | 71 | 72 | Article 2 About Use 73 | By using this Service, users comply with the Terms and Conditions. also if users use this Service, users agree to the terms and conditions of this Service. 74 | 75 | 76 | Article 3 Prohibited matters 77 | Use of this Service shall prohibit the following acts (including acts inducing or preparing them) 78 | (1) Reproduction and transfer of this Service to a third party without authorization from BuuGraphic 79 | (2) Handling and distributing the Services and the secondary works of the Services by third parties, except where individual permissions are obtained from BuuGraphics. 80 | (3) All or part of this Service shall be incorporated for the purpose of assignment or sale to third parties. 81 | 82 | 83 | Article 4 Disclaimer 84 | (1) Both these Terms and Services may be updated without notice. Please check the latest version at the following URL. 85 | https://docs.google.com/document/d/15ktxBGEo31Ya63_vtXTljndoPHmvWvJeZaw1e_b-bXs/edit 86 | (2) The copyright of the materials provided in this service belongs to BuuGraphics. 87 | (3) The producer shall not be responsible for any damage or trouble caused by this service. 88 | 89 | 90 | Article 5 Response to breach of rules 91 | (1) If any violation of this Agreement is found, we will contact the Violator of the Agreement. Malicious offenders may be charged a fee without prior notice. 92 | 93 | 94 | Article 6 Applicable Acts, Courts of Jurisdiction, Languages, and Other 95 | (1) This Code shall be governed by the Japanese law and the Osaka District Court shall be the exclusive jurisdiction of the first instance. 96 | (2) This agreement is based on Japanese and is always interpreted only in Japanese. 97 | (3) All rights other than those stated in this Service Description are reserved in BuuGraphic. 98 | 99 | 100 | If you have any questions or comments, please contact BuuGraphics. 101 | Twitter:https://twitter.com/BuuGraphic 102 | Gmail:buugraphic@gmail.com -------------------------------------------------------------------------------- /ReadMe_Japanese.txt: -------------------------------------------------------------------------------- 1 | 〇BuuGraphic制作商品案内 2 | ■CommandRecorder 3 | https://eachpath.booth.pm/items/1123127 4 | DCCツールではお馴染みのアクション登録機能! 5 | モデラーが考え、プログラマーが実装した汎用性の高いアドオンです! 6 | CommandRecorderは操作を録画・再生します 7 | インストール方法や使い方については動画をご覧ください! 8 | https://youtu.be/7UtFRyxm-EU 9 | 10 | 11 | 12 | 13 | ■ポスト 14 | https://satahiko.booth.pm/items/1229821 15 | ■ツボ 16 | https://satahiko.booth.pm/items/1123123 17 | 18 | 19 | 20 | 21 | 〇CommandRecorderについて 22 | ■動画内で紹介し切れていない機能です 23 | ==================================================================== 24 | コマンド履歴の表示の切り替えが出来ます 25 | ●Standard:通常のコマンド表示です 26 | ●Extend:コマンドを詳細に表示します 27 | 28 | 29 | 30 | 31 | ■注意点 32 | ==================================================================== 33 | ●コマンドリストに保存されたコマンドは、そのプロジェクトでのみ引き継がれます 34 | アクションボタンへ登録して保存することで他のプロジェクトでも使用できます 35 | 36 | 37 | ●選択が必要なコマンドについて 38 | 例えばループカット等、登録・実行はできますが、適用位置については指定が出来ません 39 | 選択範囲やマウスの位置が重要なコマンドは、意図しない挙動になりやすいです 40 | 41 | 42 | ●まとめて適用・それぞれ適用について 43 | 現在、それぞれ適用についてはオブジェクトモード・ボーンエディットモード・ポーズモードでの実行に対応しております 44 | 45 | 46 | ●日本語とその他言語を録画途中で切り替えると落ちます。 47 | 48 | 49 | 50 | 51 | 52 | 53 | 〇全制作物について 54 | ■利用規約 55 | ==================================================================== 56 | 第1条 定義 57 | 次の各号に掲げる用語の意義は、当該各号に定めるところによります。 58 | (1)利用規約(以下「本規約」という)は、BuuGraphic(以下「制作者」という)が提供するサービス(以下「本サービス」という)にて、本サービスを利用する方全て(以下「ユーザー」という)に適用されるものとします。 59 | (2)二次的著作物 60 | 著作物を翻訳し、編曲し、若しくは変形し、または脚色し、映画化し、その他翻案することにより創作した著作物をいいます。 61 | (3)改変物 62 | 著作物を変更、切除その他改変して作成したものであって、二次的著作物に該当しないものをいいます。 63 | (4)二次創作物 64 | 改変物および二次的著作物、その他著作物に依拠して作成された著作物を総称したものをいいます。 65 | 66 | 67 | 第2条 ご利用について 68 | 本サービスを利用するにあたりユーザーは本規約を遵守するものとし、ユーザーが本サービスを利用する場合、本規約の内容を承諾したものとします。 69 | 70 | 71 | 第3条 禁止事項 72 | 本サービスのご利用について、以下に定める行為(それらを誘発する行為や準備行為も含む)を禁止します 73 | (1)本サービスをBuuGraphicに無断で転載および第三者へ譲渡すること。 74 | (2)本サービスおよび本サービスの二次創作物を第三者が自由に扱える状態で配布する行為。ただし、BuuGraphicより個別の許可を得ている場合を除く。 75 | (3)第三者への譲渡・販売を目的として本サービスの全部または一部を組み込む行為。 76 | 77 | 78 | 第4条 免責事項 79 | (1)本規約・本サービス共に予告なく更新される場合があります。最新版の確認は以下URLよりお願いします。 80 | https://docs.google.com/document/d/1O-FCxxuSw5A0yBnyPXh15mo6HjwGfkkIcVwE7hFNwNo/edit 81 | (2)本サービスで提供している素材の著作権はBuuGraphicに帰属します。 82 | (3)本サービスによって生じた損害・トラブル等に対する責任を制作者は一切負わないものとします。 83 | 84 | 85 | 第5条 規約違反への対応 86 | (1)本規約への違反が発覚した際は規約違反者に連絡を行います。悪質な違反者に対しては事前連絡なく使用料の請求をする場合があります。 87 | 88 | 89 | 第6条 準拠法・管轄裁判所・言語・その他 90 | (1)本規約は日本法を準拠法とし、大阪地方裁判所を第一審の専属管轄裁判所とします。 91 | (2)本規約は日本語によるものを正本とし、常に日本語のみにより解釈されます。 92 | (3)本サービスに関する本規約に記述のない全ての権利は、BuuGraphicに留保いたします。 93 | 94 | 95 | ご不明な点やご意見等ございましたらBuuGraphicまでご連絡をお願い致します。 96 | Twitterアカウント:https://twitter.com/BuuGraphic 97 | Gmail:buugraphic@gmail.com 98 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/_static/style.css: -------------------------------------------------------------------------------- 1 | .wy-nav-content { 2 | max-width: 1000px !important; 3 | } -------------------------------------------------------------------------------- /docs/source/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | {% block extrahead %} 3 | 4 | {% endblock %} -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | from __future__ import absolute_import 10 | from docutils import nodes 11 | from docutils.parsers.rst import Directive, directives 12 | import os 13 | import sys 14 | from datetime import date 15 | 16 | 17 | project = 'Action Recorder' 18 | copyright = f'{date.today().year}, InamuraJIN, RivinHD' 19 | author = 'InamuraJIN, RivinHD' 20 | release = '4.1.0' 21 | 22 | # -- General configuration --------------------------------------------------- 23 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 24 | 25 | extensions = [ 26 | 'sphinx.ext.napoleon', 27 | 'sphinx.ext.autodoc', 28 | 'sphinx.ext.autosummary', 29 | 'myst_parser' 30 | ] 31 | 32 | templates_path = ['_templates'] 33 | exclude_patterns = [] 34 | 35 | print(os.path.abspath("../../")) 36 | sys.path.insert(0, os.path.abspath("../../")) 37 | napoleon_google_docstring = True 38 | myst_enable_extensions = [ 39 | "colon_fence", 40 | "deflist", 41 | "attrs_inline" 42 | ] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 47 | 48 | html_theme = 'sphinx_rtd_theme' 49 | html_static_path = ['_static'] 50 | html_context = { 51 | "default_mode": "auto" 52 | } 53 | 54 | 55 | # Video Embedded 56 | # -*- coding: utf-8 -*- 57 | # From https://gist.github.com/dbrgn/2922648 58 | """ 59 | ReST directive for embedding Youtube and Vimeo videos. 60 | There are two directives added: ``youtube`` and ``vimeo``. The only 61 | argument is the video id of the video to include. 62 | Both directives have three optional arguments: ``height``, ``width`` 63 | and ``align``. Default height is 281 and default width is 500. 64 | Example:: 65 | .. youtube:: anwy2MPT5RE 66 | :height: 315 67 | :width: 560 68 | :align: left 69 | :copyright: (c) 2012 by Danilo Bargen. 70 | :license: BSD 3-clause 71 | """ 72 | 73 | 74 | def align(argument): 75 | """Conversion function for the "align" option.""" 76 | return directives.choice(argument, ('left', 'center', 'right')) 77 | 78 | 79 | class IframeVideo(Directive): 80 | has_content = False 81 | required_arguments = 1 82 | optional_arguments = 0 83 | final_argument_whitespace = False 84 | option_spec = { 85 | 'height': directives.nonnegative_int, 86 | 'width': directives.nonnegative_int, 87 | 'align': align, 88 | } 89 | default_width = 500 90 | default_height = 281 91 | 92 | def run(self): 93 | self.options['video_id'] = directives.uri(self.arguments[0]) 94 | if not self.options.get('width'): 95 | self.options['width'] = self.default_width 96 | if not self.options.get('height'): 97 | self.options['height'] = self.default_height 98 | if not self.options.get('align'): 99 | self.options['align'] = 'left' 100 | return [nodes.raw('', self.html % self.options, format='html')] 101 | 102 | 103 | class Youtube(IframeVideo): 104 | html = '' 108 | 109 | 110 | class Vimeo(IframeVideo): 111 | html = '' 115 | 116 | 117 | def setup(builder): 118 | directives.register_directive('youtube', Youtube) 119 | directives.register_directive('vimeo', Vimeo) 120 | -------------------------------------------------------------------------------- /docs/source/getting_started/first_action.md: -------------------------------------------------------------------------------- 1 | # First Action 2 | ## Adding Actions 3 | :::{figure-md} 4 | ![Add Action](../images/Add_Action.svg) 5 | 6 | To add an Action press the `+`-Button. 7 | ::: 8 | 9 | :::{figure-md} 10 | ![Added Action](../images/Added_Action.svg) 11 | 12 | A new Action appears in the list. Which consist of 3 Parts. 13 | ::: 14 | 15 | **1. Icon** 16 | : By clicking on the Icon it can be changed to customize the look of the action. 17 | 18 | **2. Label** 19 | : By double-click on the Label it can be changed and by default it will be `Untitled`. 20 | 21 | **3. Execution Mode** 22 | : This can be changed to execute between `Group`-Execution and `Individual`-Execution. 23 | - **Group**: Performs the current action on all selected objects without separating them (Default Behavior) 24 | - **Individual**: Performs the current action on all selected objects individually. Therefore, the action is executed as many times as there are selected objects. 25 | 26 | ## Adding Macros 27 | :::{figure-md} 28 | ![Add Macro](../images/Add_Macro.svg) 29 | 30 | To add a Macro 4 Options are available. 31 | ::: 32 | 33 | **➕ Add** 34 | : This will add the latest used command as a Macro. 35 | If the setting `Create Empty on Error` is checked it will create an empty Macro if no command is available. (Shortcut: `alt + ,`) 36 | 37 | **🔧 Event** 38 | : This will show a list of possible events. E.g. with the `Clipboard`-Event the content of the Clipboard is imported as a Macro. Also see [Event List](../panels/macro.md#event-list) 39 | 40 | **🔴 Record** 41 | : This will record all commands that you execute until the `Stop`-Button is pressed, which appears after record is pressed. 42 | 43 | **Context Menu** 44 | : ![Copy To Action Recorder Button in the Context Menu](../images/ContextMenu_CopyButton.svg){align=right} By right-click on a property or button the context menu appears with a new option: `Copy To Action Recorder`. This will add the right-clicked property/button as a Macro. 45 | 46 | 47 | ![Alt text](../images/Simple_Macro.svg)\ 48 | We now have a Macro. This can be `activated` and `deactivated` through the check-box. By double-click on the label the Macro can be edited through a dialog. -------------------------------------------------------------------------------- /docs/source/getting_started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 |
4 | On the right side of this page, there is a "Release" section
5 | Click on the text "Action Recorder X.X.X" in the "Release" section
6 | Download the Zip file from this button which is located on the bottom under "Assets"
7 | The next step is the same as the normal add-on installation
8 | 9 | Header Menu > Edit > Preferences > Add-ons > Install
10 |
11 | 12 | Select the Zip file you downloaded
13 |
14 | Click the "Install Add-on" button
15 | 16 | Finished! Now you can see a Tab "Action Recorder" in the UI (by pressing `N`)![Tab ActionRecorder](../images/Tab_ActionRecorder.png) -------------------------------------------------------------------------------- /docs/source/getting_started/terms_definition.md: -------------------------------------------------------------------------------- 1 | # Terms & Definition 2 | ![Alt text](../images/terms.svg) 3 | 4 | ## Action 5 | Consist of multiple Macros. 6 | 7 | ## Macro 8 | Stores the commands to be executed. 9 | Mostly they store Blender Operations such as Operator (bpy.ops.xx.xx) or Context changes (bpy.context.xx.xx = xx). 10 | But they can also contain regular python code. 11 | 12 | ## Local 13 | Available only in the project file. 14 | The selected action can be executed with the `Play`-Button (Shortcut: `Alt + .`) 15 | Direct editing of macros is possible. 16 | If you want to edit the Global macro, move it to Local and then edit it. 17 | 18 | ## Global 19 | It can be shared and used with other project files as well. 20 | By default it is saved in the `Storage.json` file which is located inside the install directory of the Add-on. 21 | 22 | :::{hint} 23 | By default the path to the `Storage`-File is\ 24 | ` 25 | C:\Users\\AppData\Roaming\Blender Foundation\Blender\3.3\scripts\addons\ActRec\Storage.json 26 | ` 27 | ::: -------------------------------------------------------------------------------- /docs/source/images/Add_Action.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 57 | -------------------------------------------------------------------------------- /docs/source/images/Add_Macro.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 230 | -------------------------------------------------------------------------------- /docs/source/images/MacroEdit_Context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/53a59586af570a5e141ba64f580378bc413199d3/docs/source/images/MacroEdit_Context.png -------------------------------------------------------------------------------- /docs/source/images/MacroEdit_Operator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/53a59586af570a5e141ba64f580378bc413199d3/docs/source/images/MacroEdit_Operator.png -------------------------------------------------------------------------------- /docs/source/images/MacroEditor_MultilineInstall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/53a59586af570a5e141ba64f580378bc413199d3/docs/source/images/MacroEditor_MultilineInstall.png -------------------------------------------------------------------------------- /docs/source/images/MacroEditor_MultilineInstalled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/53a59586af570a5e141ba64f580378bc413199d3/docs/source/images/MacroEditor_MultilineInstalled.png -------------------------------------------------------------------------------- /docs/source/images/MacroEditor_MultilineInstalling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/53a59586af570a5e141ba64f580378bc413199d3/docs/source/images/MacroEditor_MultilineInstalling.png -------------------------------------------------------------------------------- /docs/source/images/Preferences_SettingsMultiline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/53a59586af570a5e141ba64f580378bc413199d3/docs/source/images/Preferences_SettingsMultiline.png -------------------------------------------------------------------------------- /docs/source/images/Tab_ActionRecorder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/53a59586af570a5e141ba64f580378bc413199d3/docs/source/images/Tab_ActionRecorder.png -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Action Recorder documentation master file, created by 2 | sphinx-quickstart on Sat Sep 16 21:38:57 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Action Recorder documentation! 7 | =========================================== 8 | 9 | .. youtube:: OA0vjP7D4Ec 10 | :height: 315 11 | :width: 560 12 | :align: left 13 | 14 | 15 | | Are you tired of coding long intimidating coding task just for one-off modeling? 16 | | Or may be you're not good coding at all ! 17 | | Introducing "ActionRecorder" ! 18 | 19 | | ActRec is an add-on developed to streamline your work in Blender 20 | 21 | | This add-on is similar to Affinity Photo and Photoshop's "Action" 22 | | This add-on has been reborn! Command Recorder --> Action Recorder 23 | 24 | | This add-on is available for Free! 25 | | But, It takes a lot of coffee to develop an Add-on! |DonateLink|_ 26 | 27 | .. |DonateLink| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif 28 | .. _DonateLink: https://paypal.me/InamuraJIN?locale.x=ja_JP 29 | 30 | -------------------- 31 | Quick Start Tutorial 32 | -------------------- 33 | 34 | .. youtube:: FqXxuMHuktc 35 | :height: 315 36 | :width: 560 37 | :align: left 38 | 39 | 40 | .. toctree:: 41 | :maxdepth: 2 42 | :hidden: 43 | :caption: GETTING STARTED 44 | 45 | Home 46 | getting_started/installation 47 | getting_started/terms_definition 48 | getting_started/first_action 49 | 50 | .. toctree:: 51 | :maxdepth: 2 52 | :hidden: 53 | :caption: PANELS 54 | 55 | panels/local 56 | panels/macro 57 | -------------------------------------------------------------------------------- /docs/source/panels/local.md: -------------------------------------------------------------------------------- 1 | # Local Action 2 | 3 | ## Action 4 | :::{figure-md} 5 | ![Added Action](../images/Added_Action.svg) 6 | 7 | A local Action consist of 3 Parts. 8 | ::: 9 | 10 | **1. Icon** 11 | : By clicking on the Icon it can be changed to customize the look of the Action. 12 | 13 | **2. Label** 14 | : By double-click on the Label it can be changed and by default it will be `Untitled`. 15 | 16 | **3. Execution Mode** 17 | : This can be changed to execute between `Group`-Execution and `Individual`-Execution. 18 | - **Group**: Performs the current action on all selected objects without separating them (Default Behavior) 19 | - **Individual**: Performs the current action on all selected objects individually. Therefore, the action is executed as many times as there are selected objects. 20 | 21 | ## Operations 22 | ![Local Operations](../images/LocalOperators.svg) 23 | 24 | ### Add 25 | Adds a new Local Action to the project and select it. 26 | 27 | ### Remove 28 | Remove to selected Local Action. 29 | 30 | ### Move Up 31 | Move to selected Local Action one position up in the list and swap it with the next upper Action. 32 | 33 | ### Move Down 34 | Move to selected Local Action one position down in the list and swap it with the next lower Action. 35 | 36 | ### Selection Up 37 | Sets the selection to the next upper Action. 38 | :::{attention} 39 | Only available through shortcut. Default is `Shift + Alt + Wheel Up` 40 | ::: 41 | 42 | ### Selection Down 43 | Sets the selection to the next lower Action. 44 | :::{attention} 45 | Only available through shortcut. Default is `Shift + Alt + Wheel Down` 46 | ::: 47 | -------------------------------------------------------------------------------- /docs/source/panels/macro.md: -------------------------------------------------------------------------------- 1 | # Macro Editor 2 | ## Macro 3 | ![Simple Macro](../images/Simple_Macro.svg) 4 | 5 | This is a Macro it can be `activated` and `deactivated` through the checkbox and won't be executed if `deactivated`.\ 6 | By double clicking on the label the Macro can be edited through a dialog. 7 | 8 | ## Editing Macros 9 | Double click on the Macro to open the editing dialog. 10 | 11 | The dialog provides to different styles for operators commands or context changes. 12 | 13 | **Operators** 14 | 15 | ![Editing Macros Operators](../images/MacroEdit_Operator.png) 16 | 17 | **Context** 18 | 19 | ![Editing Macros Context](../images/MacroEdit_Context.png) 20 | 21 | ### Label 22 | The text that is displayed for this macro. 23 | 24 | ### Command 25 | The actual python code that will be executed for this macro. 26 | 27 | ### Properties 28 | The area/windows where this Macro will be executed. 29 | 30 | ### Copy Text 31 | Copies the command to the clipboard. 32 | 33 | ### Execution Context 34 | The context which the operator will be executed with. 35 | Most of the time `Execution` is the right selection. 36 | 37 | :::{Hint} 38 | 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. 39 | ::: 40 | 41 | ### Copy Previous 42 | Is a toggle button which will try to load the action executed before the recorded one if active. 43 | 44 | ### Clear Operator 45 | Removes all properties from an operator. 46 | E.g. `bpy.ops.transform.translate(value=(1,1,1), ...)` becomes `bpy.ops.transform.translate()`. 47 | 48 | ## Operations 49 | ![Macro Editor Operations](../images/MacroEditorOperators.svg) 50 | 51 | ### Add 52 | 53 | Adds the last used command as a Macro. 54 | If the `Create Empty on Error` setting is checked, an empty Macro will be created if no command is available (Shortcut: `alt + ,`). 55 | On the first run a popup will appear asking if you want to enable [Multiline Support](../panels/macro.md#multiline-support). 56 | 57 | ### Add Event 58 | 59 | This will show a list of possible events that can be used to create more suitable actions. 60 | 61 | #### Event List 62 | :::{table} 63 | :widths: auto 64 | | Event | Description | 65 | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | 66 | | Clipboard | Adds a new Macro with content of the Clipboard to the selected Action | 67 | | Timer | Wait the specified time and then continue playing the Action | 68 | | Render Complete | Execute the blow Macros after a render completed rendering
**Note**: No alters will be shown | 69 | | Loop | Loop the below Macros the given amount of time.
**Note**: EndLoop is need to mark the end of the loop otherwise this event is skipped | 70 | | EndLoop | Marks the end of the loop block | 71 | | Select Object | Gives the option to select a specific Object in the Scene | 72 | | Run Script | Select a text from the Texteditor that is saved internally and will be executed | 73 | ::: 74 | 75 | ### Remove 76 | 77 | Remove the selected Macro 78 | 79 | ### Move Up 80 | 81 | Moves the selected Macro one position up 82 | 83 | ### Move Down 84 | 85 | Moves the selected Macro one position down 86 | 87 | ### Record 88 | When pressed the Action Recorder switches to recording mode and tries to catch every operation you do in Blender until the `Stop`-Button is pressed. 89 | The recorded operation will automatically be added as macros. 90 | 91 | ### Clear 92 | Removes all macros from the selected Action. 93 | 94 | ### Play 95 | Executes all macros from the selected Action (Shortcut: `alt + .`). 96 | 97 | ### Local to Global 98 | Moves the selected Action to the Global Panel. A popup appears to select the Category to append the Action to. 99 | 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). 100 | 101 | ## Multiline Support 102 | 103 | :::{figure-md} 104 | ![Multiline Support Install](../images/MacroEditor_MultilineInstall.png) 105 | 106 | First Popup to install Multiline Support 107 | ::: 108 | 109 | If `Don't Ask Again` is checked it can be later installed in the Preferences 110 | 111 | :::{figure-md} 112 | ![Multiline Preferences](../images/Preferences_SettingsMultiline.png) 113 | 114 | Later install in the Preferences 115 | ::: 116 | 117 | If Install is pressed the Popup will change to the following:\ 118 | ![Multiline Installing](../images/MacroEditor_MultilineInstalling.png) 119 | 120 | After the installation finished the following appear:\ 121 | ![Multiline Installed](../images/MacroEditor_MultilineInstalled.png) -------------------------------------------------------------------------------- /docs/source/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-rtd-theme 2 | myst-parser -------------------------------------------------------------------------------- /download_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": [4, 1, 2], 3 | "files": { 4 | "ActRec/actrec/functions/__init__.py": [4, 1, 0], 5 | "ActRec/actrec/functions/categories.py": [4, 1, 0], 6 | "ActRec/actrec/functions/globals.py": [4, 1, 0], 7 | "ActRec/actrec/functions/locals.py": [4, 1, 0], 8 | "ActRec/actrec/functions/macros.py": [4, 1, 1], 9 | "ActRec/actrec/functions/shared.py": [4, 1, 2], 10 | "ActRec/actrec/menus/__init__.py": [4, 0, 6], 11 | "ActRec/actrec/menus/locals.py": [4, 1, 0], 12 | "ActRec/actrec/menus/categories.py": [4, 1, 0], 13 | "ActRec/actrec/operators/__init__.py": [4, 1, 0], 14 | "ActRec/actrec/operators/categories.py": [4, 1, 0], 15 | "ActRec/actrec/operators/globals.py": [4, 1, 2], 16 | "ActRec/actrec/operators/helper.py": [4, 1, 0], 17 | "ActRec/actrec/operators/locals.py": [4, 1, 0], 18 | "ActRec/actrec/operators/macros.py": [4, 1, 2], 19 | "ActRec/actrec/operators/preferences.py": [4, 1, 0], 20 | "ActRec/actrec/operators/shared.py": [4, 1, 0], 21 | "ActRec/actrec/panels/__init__.py": [4, 0, 0], 22 | "ActRec/actrec/panels/main.py": [4, 1, 0], 23 | "ActRec/actrec/properties/__init__.py": [4, 0, 6], 24 | "ActRec/actrec/properties/categories.py": [4, 0, 0], 25 | "ActRec/actrec/properties/globals.py": [4, 1, 0], 26 | "ActRec/actrec/properties/locals.py": [4, 1, 0], 27 | "ActRec/actrec/properties/macros.py": [4, 1, 0], 28 | "ActRec/actrec/properties/shared.py": [4, 1, 0], 29 | "ActRec/actrec/ui_functions/__init__.py": [4, 0, 6], 30 | "ActRec/actrec/ui_functions/categories.py": [4, 1, 0], 31 | "ActRec/actrec/ui_functions/globals.py": [4, 1, 0], 32 | "ActRec/actrec/uilist/__init__.py": [4, 0, 0], 33 | "ActRec/actrec/uilist/locals.py": [4, 1, 0], 34 | "ActRec/actrec/uilist/macros.py": [4, 1, 0], 35 | "ActRec/actrec/__init__.py": [4, 0, 8], 36 | "ActRec/actrec/config.py": [4, 1, 2], 37 | "ActRec/actrec/icon_manager.py": [4, 1, 0], 38 | "ActRec/actrec/keymap.py": [4, 1, 2], 39 | "ActRec/actrec/log.py": [4, 1, 0], 40 | "ActRec/actrec/preferences.py": [4, 1, 0], 41 | "ActRec/actrec/shared_data.py": [4, 0, 8], 42 | "ActRec/actrec/update.py": [4, 1, 0], 43 | "ActRec/__init__.py": [4, 1, 2] 44 | }, 45 | "remove": [] 46 | } -------------------------------------------------------------------------------- /testing/requirements.txt: -------------------------------------------------------------------------------- 1 | blender-addon-tester @ git+https://github.com/nangtani/blender-addon-tester.git -------------------------------------------------------------------------------- /testing/test_addon.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | try: 4 | import blender_addon_tester as BAT 5 | except Exception as e: 6 | print(e) 7 | sys.exit(1) 8 | 9 | 10 | def main(): 11 | if len(sys.argv) > 1: 12 | addon = sys.argv[1] 13 | else: 14 | addon = "ActRec" 15 | if len(sys.argv) > 2: 16 | blender_rev = sys.argv[2] 17 | else: 18 | blender_rev = "3.3" # LTS 19 | 20 | if len(sys.argv) > 3: 21 | test_format = sys.argv[3] 22 | else: 23 | test_format = "unit" 24 | 25 | config = { 26 | "tests": os.path.join("testing", test_format) 27 | } 28 | 29 | try: 30 | exit_val = BAT.test_blender_addon(addon_path=addon, blender_revision=blender_rev, config=config) 31 | except Exception as e: 32 | print(e) 33 | exit_val = 1 34 | sys.exit(exit_val) 35 | 36 | 37 | if __name__ == "__main__": 38 | main() 39 | -------------------------------------------------------------------------------- /testing/unit/__init__.py: -------------------------------------------------------------------------------- 1 | from . import helper 2 | from . import test_icon_manager 3 | -------------------------------------------------------------------------------- /testing/unit/functions/__init__.py: -------------------------------------------------------------------------------- 1 | from . import test_shared 2 | -------------------------------------------------------------------------------- /testing/unit/functions/test_shared.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ActRec.actrec.functions import shared 3 | import bpy 4 | from .. import helper 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "check_list, name, output", 9 | [ 10 | ([], "test", "test"), 11 | (["test"], "test", "test.001"), 12 | (["test", "test.001"], "test", "test.002"), 13 | (["test", "test.001", "test.002"], "test", "test.003"), 14 | (["test", "Ho", "something", "this", "there"], "name", "name"), 15 | ([], "", ""), 16 | ([""], "name", "name") 17 | ] 18 | ) 19 | def test_check_for_duplicates(check_list, name, output): 20 | assert shared.check_for_duplicates(check_list, name) == output 21 | 22 | 23 | @pytest.fixture(scope="function") 24 | def clear_load_global(request): 25 | pref = shared.get_preferences(bpy.context) 26 | pref.global_actions.clear() 27 | helper.load_global_actions_test_data(pref) 28 | return helper.get_pref_data(request.param) 29 | 30 | 31 | @pytest.mark.parametrize( 32 | "clear_load_global, exclude, output", 33 | [ 34 | ('global_actions["c7a1f271164611eca91770c94ef23b30"].macros["c7a3dcba164611ecaaec70c94ef23b30"]', [], 35 | { 36 | "name": "c7a3dcba164611ecaaec70c94ef23b30", 37 | "id": "c7a3dcba164611ecaaec70c94ef23b30", 38 | "label": "Delete", 39 | "command": "bpy.ops.object.delete(use_global=False)", 40 | "active": True, 41 | "icon": 0, 42 | "icon_name": "NONE", 43 | "is_available": True, 44 | "is_playing": False, 45 | "ui_type": "", 46 | "alert": False, 47 | "operator_execution_context": "EXEC_DEFAULT" 48 | }), 49 | ('global_actions["c7a40353164611ecbaad70c94ef23b30"]', 50 | ["name", "selected", "alert", "macros.name", "macros.is_available", 51 | "macros.alert", "macros.is_playing", "is_playing"], 52 | { 53 | "id": "c7a40353164611ecbaad70c94ef23b30", 54 | "label": "Subd Smooth", 55 | "macros": [ 56 | { 57 | "id": "c7a40354164611ecb05c70c94ef23b30", 58 | "label": "Subdivision Set", 59 | "command": "bpy.ops.object.subdivision_set(level=1, relative=False)", 60 | "active": True, 61 | "icon": 0, 62 | "icon_name": "NONE", 63 | "ui_type": "", 64 | "operator_execution_context": "EXEC_DEFAULT" 65 | }, 66 | { 67 | "id": "c7a40355164611ecb9cd70c94ef23b30", 68 | "label": "Shade Smooth", 69 | "command": "bpy.ops.object.shade_smooth()", 70 | "active": True, 71 | "icon": 0, 72 | "icon_name": "NONE", 73 | "ui_type": "", 74 | "operator_execution_context": "EXEC_DEFAULT" 75 | }, 76 | { 77 | "id": "c7a42aa4164611ecba6570c94ef23b30", 78 | "label": "Auto Smooth = True", 79 | "command": "bpy.context.object.data.use_auto_smooth = True", 80 | "active": True, 81 | "icon": 0, 82 | "icon_name": "NONE", 83 | "ui_type": "", 84 | "operator_execution_context": "EXEC_DEFAULT" 85 | }, 86 | { 87 | "id": "c7a6be1e164611ec8ede70c94ef23b30", 88 | "label": "Auto Smooth Angle = 3.14159", 89 | "command": "bpy.context.object.data.auto_smooth_angle = 3.14159", 90 | "active": True, 91 | "icon": 0, 92 | "icon_name": "NONE", 93 | "ui_type": "", 94 | "operator_execution_context": "EXEC_DEFAULT" 95 | } 96 | ], 97 | "icon": 127, 98 | "icon_name": "NODE_MATERIAL", 99 | "execution_mode": "GROUP", 100 | "description": "Play this Action Button" 101 | })], 102 | indirect=["clear_load_global"] 103 | ) 104 | def test_property_to_python(clear_load_global, exclude, output): 105 | data = shared.property_to_python(clear_load_global, exclude) 106 | assert data == output 107 | 108 | 109 | @ pytest.fixture(scope="function") 110 | def apply_data(request): 111 | pref = shared.get_preferences(bpy.context) 112 | pref.global_actions.clear() 113 | helper.load_global_actions_test_data(pref) 114 | if request.param == 'global_actions["c7a1f271164611eca91770c94ef23b30"]': 115 | pref.global_actions["c7a1f271164611eca91770c94ef23b30"].macros.clear() 116 | return helper.get_pref_data(request.param) 117 | 118 | 119 | @ pytest.mark.parametrize( 120 | "apply_data, data", 121 | [('global_actions["c7a1f271164611eca91770c94ef23b30"].macros["c7a3dcba164611ecaaec70c94ef23b30"]', 122 | {"id": "c7a3dcba164611ecaaec70c94ef23b30", "label": "Something", 123 | "command": "bpy.ops.object.delete(use_global=False)", "active": False, "icon": 15, "ui_type": ""}), 124 | ('global_actions["c7a1f271164611eca91770c94ef23b30"]', 125 | {"id": "c7a1f271164611eca91770c94ef23b30", "label": "Something", 126 | "macros": 127 | [{"id": "c7a3dcba164611ecaaec70c94ef23b30", "label": "Delete", 128 | "command": "bpy.ops.object.delete(use_global=False)", "active": False, "icon": 26, "ui_type": ""}], 129 | "icon": 7})], 130 | indirect=["apply_data"] 131 | ) 132 | def test_apply_data_to_item(apply_data, data): 133 | shared.apply_data_to_item(apply_data, data) 134 | assert helper.compare_with_dict(apply_data, data) 135 | 136 | 137 | @ pytest.mark.parametrize("collection, data", 138 | [(bpy.context.preferences.addons['cycles'].preferences.devices, 139 | {'name': "test", 'id': "TT", 'use': False, 'type': "OPTIX"})] 140 | ) 141 | def test_add_data_to_collection(collection, data): 142 | length = len(collection) 143 | name = data['name'] 144 | shared.add_data_to_collection(collection, data) 145 | index = collection.find(name) 146 | assert length + 1 == len(collection) 147 | assert index != -1 148 | assert helper.compare_with_dict(collection[name], data) 149 | collection.remove(index) 150 | 151 | 152 | @pytest.mark.parametrize( 153 | "clear_load_global, index, data", 154 | [ 155 | ('global_actions["c7a1f271164611eca91770c94ef23b30"].macros', 0, 156 | { 157 | "id": "c7a759ec164611ecb07c70c94ef23b30", 158 | "label": "Toggle Edit Mode", 159 | "command": "bpy.ops.object.editmode_toggle()", 160 | "active": True, 161 | "icon": 0, 162 | "ui_type": "" 163 | } 164 | ), 165 | ("global_actions", 1, 166 | { 167 | "id": "c7a759ee164611ecb84c70c94ef23b30", 168 | "label": "Merge", 169 | "macros": [ 170 | { 171 | "id": "c7a759ef164611eca84970c94ef23b30", 172 | "label": "Resize", 173 | "command": "bpy.ops.transform.resize(value=(0, 0, 0))", 174 | "active": True, 175 | "icon": 0, 176 | "ui_type": "" 177 | }, 178 | { 179 | "id": "c7a759f0164611ec84fd70c94ef23b30", 180 | "label": "Merge by Distance", 181 | "command": "bpy.ops.mesh.remove_doubles()", 182 | "active": True, 183 | "icon": 0, 184 | "ui_type": "" 185 | } 186 | ], 187 | "icon": 608 188 | }) 189 | ], 190 | indirect=["clear_load_global"] 191 | ) 192 | def test_insert_to_collection(clear_load_global, index, data): 193 | shared.insert_to_collection(clear_load_global, index, data) 194 | if index >= len(clear_load_global): 195 | index = len(clear_load_global) - 1 196 | assert helper.compare_with_dict(clear_load_global[index], data) 197 | 198 | 199 | @pytest.mark.parametrize( 200 | "clear_load_global, index1, index2, output1, output2", 201 | [("global_actions", 0, 0, 202 | { 203 | "id": "c7a1f271164611eca91770c94ef23b30", 204 | "label": "Delete", 205 | "macros": [ 206 | { 207 | "id": "c7a3dcba164611ecaaec70c94ef23b30", 208 | "label": "Delete", 209 | "command": "bpy.ops.object.delete(use_global=False)", 210 | "active": True, 211 | "icon": 0, 212 | "ui_type": "" 213 | } 214 | ], 215 | "icon": 3 216 | }, 217 | { 218 | "id": "c7a1f271164611eca91770c94ef23b30", 219 | "label": "Delete", 220 | "macros": [ 221 | { 222 | "id": "c7a3dcba164611ecaaec70c94ef23b30", 223 | "label": "Delete", 224 | "command": "bpy.ops.object.delete(use_global=False)", 225 | "active": True, 226 | "icon": 0, 227 | "ui_type": "" 228 | } 229 | ], 230 | "icon": 3 231 | }), 232 | ("global_actions", 0, 2, 233 | { 234 | "id": "c7a6be1f164611ec9a5570c94ef23b30", 235 | "label": "Align_X", 236 | "macros": [ 237 | { 238 | "id": "c7a6e499164611ec927970c94ef23b30", 239 | "label": "Only Locations = True", 240 | "command": "bpy.context.scene.tool_settings.use_transform_pivot_point_align = True", 241 | "active": True, 242 | "icon": 0, 243 | "ui_type": "" 244 | }, 245 | { 246 | "id": "c7a6e49a164611ec9f1370c94ef23b30", 247 | "label": "Resize", 248 | "command": "bpy.ops.transform.resize(value=(1, 0, 1))", 249 | "active": True, 250 | "icon": 0, 251 | "ui_type": "" 252 | }, 253 | { 254 | "id": "c7a6e49b164611ecadb070c94ef23b30", 255 | "label": "Only Locations = False", 256 | "command": "bpy.context.scene.tool_settings.use_transform_pivot_point_align = False", 257 | "active": True, 258 | "icon": 0, 259 | "ui_type": "" 260 | } 261 | ], 262 | "icon": 0 263 | }, 264 | { 265 | "id": "c7a1f271164611eca91770c94ef23b30", 266 | "label": "Delete", 267 | "macros": [ 268 | { 269 | "id": "c7a3dcba164611ecaaec70c94ef23b30", 270 | "label": "Delete", 271 | "command": "bpy.ops.object.delete(use_global=False)", 272 | "active": True, 273 | "icon": 0, 274 | "ui_type": "" 275 | } 276 | ], 277 | "icon": 3 278 | }), 279 | ("global_actions", 0, 5, 280 | { 281 | "id": "c7a6be1f164611ec9a5570c94ef23b30", 282 | "label": "Align_X", 283 | "macros": [ 284 | { 285 | "id": "c7a6e499164611ec927970c94ef23b30", 286 | "label": "Only Locations = True", 287 | "command": "bpy.context.scene.tool_settings.use_transform_pivot_point_align = True", 288 | "active": True, 289 | "icon": 0, 290 | "ui_type": "" 291 | }, 292 | { 293 | "id": "c7a6e49a164611ec9f1370c94ef23b30", 294 | "label": "Resize", 295 | "command": "bpy.ops.transform.resize(value=(1, 0, 1))", 296 | "active": True, 297 | "icon": 0, 298 | "ui_type": "" 299 | }, 300 | { 301 | "id": "c7a6e49b164611ecadb070c94ef23b30", 302 | "label": "Only Locations = False", 303 | "command": "bpy.context.scene.tool_settings.use_transform_pivot_point_align = False", 304 | "active": True, 305 | "icon": 0, 306 | "ui_type": "" 307 | } 308 | ], 309 | "icon": 0 310 | }, 311 | { 312 | "id": "c7a1f271164611eca91770c94ef23b30", 313 | "label": "Delete", 314 | "macros": [ 315 | { 316 | "id": "c7a3dcba164611ecaaec70c94ef23b30", 317 | "label": "Delete", 318 | "command": "bpy.ops.object.delete(use_global=False)", 319 | "active": True, 320 | "icon": 0, 321 | "ui_type": "" 322 | } 323 | ], 324 | "icon": 3 325 | }) 326 | ], 327 | indirect=["clear_load_global"] 328 | ) 329 | def test_swap_collection_items(clear_load_global, index1, index2, output1, output2): 330 | shared.swap_collection_items(clear_load_global, index1, index2) 331 | if index1 >= len(clear_load_global): 332 | index1 = len(clear_load_global) - 1 333 | if index2 >= len(clear_load_global): 334 | index2 = len(clear_load_global) - 1 335 | assert helper.compare_with_dict(clear_load_global[index1], output1) 336 | assert helper.compare_with_dict(clear_load_global[index2], output2) 337 | -------------------------------------------------------------------------------- /testing/unit/helper.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from ActRec.actrec.functions.shared import get_preferences 3 | 4 | 5 | def compare_with_dict(obj, compare_dict): 6 | for key, value in compare_dict.items(): 7 | if isinstance(value, dict): 8 | check = compare_with_dict(getattr(obj, key), value) 9 | elif isinstance(value, list): 10 | check = all(compare_with_dict(getattr(obj, key)[i], x) for i, x in enumerate(value)) 11 | else: 12 | check = getattr(obj, key) == value 13 | if not check: 14 | print(key, value, getattr(obj, key)) 15 | return check 16 | return True 17 | 18 | 19 | def get_pref_data(param): 20 | pref = get_preferences(bpy.context) 21 | params = param.split(".") 22 | prop = pref 23 | for param in params: 24 | att_sp = param.split("[") 25 | prop = getattr(prop, att_sp[0]) 26 | if len(att_sp) == 2: 27 | prop = prop[att_sp[1][:-1].replace('"', "")] # remove ] 28 | return prop 29 | 30 | 31 | def load_global_actions_test_data(pref): 32 | action = pref.global_actions.add() 33 | action.id = "c7a1f271164611eca91770c94ef23b30" 34 | action.label = "Delete" 35 | macro = action.macros.add() 36 | macro.id = "c7a3dcba164611ecaaec70c94ef23b30" 37 | macro.label = "Delete" 38 | macro.command = "bpy.ops.object.delete(use_global=False)" 39 | action.icon = 3 40 | 41 | action = pref.global_actions.add() 42 | action.id = "c7a40353164611ecbaad70c94ef23b30" 43 | action.label = "Subd Smooth" 44 | macro = action.macros.add() 45 | macro.id = "c7a40354164611ecb05c70c94ef23b30" 46 | macro.label = "Subdivision Set" 47 | macro.command = "bpy.ops.object.subdivision_set(level=1, relative=False)" 48 | macro = action.macros.add() 49 | macro.id = "c7a40355164611ecb9cd70c94ef23b30" 50 | macro.label = "Shade Smooth" 51 | macro.command = "bpy.ops.object.shade_smooth()" 52 | macro = action.macros.add() 53 | macro.id = "c7a42aa4164611ecba6570c94ef23b30" 54 | macro.label = "Auto Smooth = True" 55 | macro.command = "bpy.context.object.data.use_auto_smooth = True" 56 | macro = action.macros.add() 57 | macro.id = "c7a6be1e164611ec8ede70c94ef23b30" 58 | macro.label = "Auto Smooth Angle = 3.14159" 59 | macro.command = "bpy.context.object.data.auto_smooth_angle = 3.14159" 60 | action.icon = 127 61 | 62 | action = pref.global_actions.add() 63 | action.id = "c7a6be1f164611ec9a5570c94ef23b30" 64 | action.label = "Align_X" 65 | macro = action.macros.add() 66 | macro.id = "c7a6e499164611ec927970c94ef23b30" 67 | macro.label = "Only Locations = True" 68 | macro.command = "bpy.context.scene.tool_settings.use_transform_pivot_point_align = True" 69 | macro = action.macros.add() 70 | macro.id = "c7a6e49a164611ec9f1370c94ef23b30" 71 | macro.label = "Resize" 72 | macro.command = "bpy.ops.transform.resize(value=(1, 0, 1))" 73 | macro = action.macros.add() 74 | macro.id = "c7a6e49b164611ecadb070c94ef23b30" 75 | macro.label = "Only Locations = False" 76 | macro.command = "bpy.context.scene.tool_settings.use_transform_pivot_point_align = False" 77 | -------------------------------------------------------------------------------- /testing/unit/test_icon_manager.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ActRec.actrec import icon_manager 3 | import os 4 | import bpy 5 | from ActRec.actrec.functions.shared import get_preferences 6 | 7 | """ 8 | @pytest.mark.parametrize( 9 | ("input, output", 10 | [ 11 | (212, 212), 12 | ("sefsfse", 101), 13 | (None, 101), 14 | ("TRASH", 21) 15 | ] 16 | ) 17 | )""" 18 | 19 | 20 | def test_get_icons_value_map(): 21 | assert 0 not in icon_manager.get_icons_value_map().keys() 22 | 23 | 24 | def test_get_icons_name_map(): 25 | assert 'NONE' not in icon_manager.get_icons_name_map().keys() 26 | 27 | 28 | def test_get_custom_icon_name_map(): 29 | icon_manager.get_custom_icon_name_map() 30 | 31 | 32 | def test_get_custom_icons_value_map(): 33 | icon_manager.get_custom_icons_value_map() 34 | 35 | 36 | @pytest.mark.parametrize( 37 | "file, name, only_new, success", 38 | [ 39 | ("test_icon_png1.png", "AR_test_icon_png1", False, True), 40 | ("test_icon_png2.png", "AR_test_icon_png2", False, True), 41 | ("test_icon_jpg.jpg", "AR_test_icon_jpg", False, True), 42 | ("test_icon_png1.png", "AR_test_icon_png1.001", False, True), 43 | ("test_icon_jpg.jpg", "AR_test_icon_jpg", True, True) 44 | ] 45 | ) 46 | def test_load_icon(file, name, only_new, success): 47 | # include register_icon testing 48 | # don't know why preview couldn't be registered, therefore manual 49 | dirpath = "test_src_data\\icon_manager" 50 | path = os.path.join(os.path.dirname(__file__), dirpath, file) 51 | pref = get_preferences(bpy.context) 52 | pref.icon_path = os.path.dirname(__file__) 53 | icon_manager.load_icon(pref, path, only_new) 54 | assert (name in list(icon_manager.preview_collections['ar_custom'])) == success 55 | -------------------------------------------------------------------------------- /testing/unit/test_src_data/icon_manager/test_icon_jpg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/53a59586af570a5e141ba64f580378bc413199d3/testing/unit/test_src_data/icon_manager/test_icon_jpg.jpg -------------------------------------------------------------------------------- /testing/unit/test_src_data/icon_manager/test_icon_png1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/53a59586af570a5e141ba64f580378bc413199d3/testing/unit/test_src_data/icon_manager/test_icon_png1.png -------------------------------------------------------------------------------- /testing/unit/test_src_data/icon_manager/test_icon_png2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InamuraJIN/ActionRecorder/53a59586af570a5e141ba64f580378bc413199d3/testing/unit/test_src_data/icon_manager/test_icon_png2.png -------------------------------------------------------------------------------- /testing/unit/test_src_data/icon_manager/test_icon_svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------