├── .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://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 |