update addons (for 5.2)

This commit is contained in:
Nathan
2026-07-14 14:44:58 -06:00
parent a232ea1c9c
commit e3310263f3
246 changed files with 21097 additions and 9607 deletions
@@ -2,6 +2,20 @@
All notable changes to **Maya Config Pro** are listed here. Versions match `blender_manifest.toml`.
## [1.8.3] — 2026-07-14
### Fixed
- **Keymaps:** Fixed crash on blend file load from stale addon keymap RNA handles; deferred `load_post` re-sync, skip modal maps in the addon layer, use Blenders keymap import for nested operator props.
### Changed
- **Blender support:** Added **5.2 LTS**; minimum target is now **4.5 LTS** (4.24.4 dropped).
- **Marking menu:** Loop/ring multi-select uses Blender 5.1+ operators; paint-mode submenu limited to mesh objects.
## [1.8.2] — 2026-07-08
### Fixed
- **Quad Menu:** Space pie ortho views (Left/Right/Top/etc.) now affect only the active 3D viewport, not every viewport in multi-view layouts.
## [1.8.1] — 2026-06-16
### Fixed
@@ -1,4 +1,4 @@
Maya Config Pro — Install the release ZIP (Blender 4.2+)
Maya Config Pro — Install the release ZIP (Blender 4.5+)
========================================================
This package is a Blender *extension* (blender_manifest.toml). Do not unzip
@@ -75,14 +75,14 @@ Maya Config Pro transforms Blender's interface and workflow to match Maya's fami
This is the new modern way to install Maya Config Pro - no file copying required!
#### Requirements
- Blender 4.2 LTS or newer
- Blender 4.5 LTS or newer
#### Steps
1. **Download the Extension**
- Download the MayaConfigPro ZIP file from Gumroad or your purchase location
2. **Install in Blender** (Blender 4.2+; extension with `blender_manifest.toml`)
2. **Install in Blender** (Blender 4.5+; extension with `blender_manifest.toml`)
- Open Blender
- Go to **`Edit → Preferences → Get Extensions`**
- Use the **top-right `⋯` menu → `Install from Disk`**, *or* **drag the ZIP onto the Preferences window**
@@ -242,13 +242,13 @@ For older Blender versions (pre-4.2) or if you prefer the original mod approach:
| Blender Version | Support Level | Recommended Install |
|-----------------|---------------|---------------------|
| 4.2.x LTS | Full | Extension |
| 4.3.x | Full | Extension |
| 4.4.x | Full | Extension |
| 4.5.x LTS | Full | Extension |
| 5.0.x | Full | Extension |
| 3.0.x - 4.1.x | Legacy Only | Mod Installation |
| 2.83 - 2.93 | Legacy Only | Mod Installation |
| 5.1.x | Full | Extension |
| 5.2.x LTS | Full | Extension |
| 4.2.x 4.4.x | Unsupported | — |
| 3.0.x 4.1.x | Legacy Only | Mod Installation |
| 2.83 2.93 | Legacy Only | Mod Installation |
---
@@ -262,7 +262,7 @@ For older Blender versions (pre-4.2) or if you prefer the original mod approach:
### Version 1.6 / Extension 1.7.0
- **Extension Format:** Complete port to modern Blender Extension platform
- **Multi-Version Support:** Compatible with Blender 4.2, 4.5 LTS, and 5.0+
- **Multi-Version Support:** Compatible with Blender 4.5 LTS, 5.0, 5.1, and 5.2 LTS
- **No File Copying:** Everything applies at runtime, no installation directory modifications
- **Safe Registration:** Error handling for all version-specific API differences
@@ -2,7 +2,7 @@
Maya Config Pro - Blender Extension
A Maya-like configuration, shortcuts, and UI elements for Blender.
Version: 1.8.1
Version: 1.8.3
Author: Form Affinity (Jesse Doyle)
"""
@@ -12,8 +12,8 @@ Author: Form Affinity (Jesse Doyle)
bl_info = {
"name": "Maya Config Pro",
"author": "Form Affinity (Jesse Doyle)",
"version": (1, 8, 1),
"blender": (4, 2, 0),
"version": (1, 8, 3),
"blender": (4, 5, 0),
"location": "View3D > Sidebar > Maya Config",
"description": "Maya-like configuration, shortcuts, UI panels, and keymap presets",
"warning": "",
@@ -3,12 +3,12 @@ schema_version = "1.0.0"
id = "form_affinity_maya_config_pro"
name = "Maya Config Pro"
tagline = "Maya-like configuration, shortcuts, and UI elements for Blender"
version = "1.8.1"
version = "1.8.3"
type = "add-on"
maintainer = "Form Affinity"
license = ["GPL-3.0-or-later"]
blender_version_min = "4.2.0"
blender_version_min = "4.5.0"
website = "https://github.com/FormAffinity/MayaConfigPro"
@@ -9,14 +9,14 @@ that layer without restarting.
"""
import bpy
from bl_keymap_utils.io import keymap_init_from_data
from ..utils import compat
from ..utils import keymap_fix
# Import the keyconfig data
from . import fa_hotkeys_data
# Addon keymaps: (keymap, keymap_item) and created keymaps to remove for clean sync
addon_keymaps: list = []
# Addon keymaps we created (for unregister while handles are still valid)
addon_keymap_objects: list = []
# Set from __init__.register to match this add-on's module id
@@ -24,6 +24,41 @@ _addon_id: str = "form_affinity_maya_config_pro"
# Message bus: react to keyconfig changes from Preferences
_msgbus_owner = object()
_load_post_timer = None
def _clear_keymap_tracking() -> None:
addon_keymap_objects.clear()
def _keymap_signature(km_name: str, km_args: dict) -> tuple:
return (
km_name,
km_args.get("space_type", "EMPTY"),
km_args.get("region_type", "WINDOW"),
bool(km_args.get("modal", False)),
)
def _purge_tracked_keymaps(kc) -> None:
"""Remove MCP addon keymaps without stale RNA handles (safe after file load)."""
if not kc or not hasattr(fa_hotkeys_data, "keyconfig_data"):
_clear_keymap_tracking()
return
signatures = {
_keymap_signature(name, args)
for name, args, _ in fa_hotkeys_data.keyconfig_data
if not args.get("modal", False)
}
for km in list(kc.keymaps):
sig = (km.name, km.space_type, km.region_type, km.is_modal)
if sig in signatures:
try:
kc.keymaps.remove(km)
except Exception:
pass
_clear_keymap_tracking()
def set_addon_id(addon_id: str) -> None:
@@ -62,97 +97,88 @@ def register_keymaps():
if not kc:
return
try:
if hasattr(fa_hotkeys_data, "keyconfig_data"):
for km_name, km_args, km_content in fa_hotkeys_data.keyconfig_data:
km = kc.keymaps.new(name=km_name, **km_args)
addon_keymap_objects.append(km)
if not hasattr(fa_hotkeys_data, "keyconfig_data"):
return
if "items" in km_content:
for item in km_content["items"]:
if len(item) >= 2:
idname = item[0]
keymap_item = item[1]
props = item[2] if len(item) > 2 else None
kmi = km.keymap_items.new(
idname,
type=keymap_item.get("type", "A"),
value=keymap_item.get("value", "PRESS"),
any=keymap_item.get("any", False),
shift=keymap_item.get("shift", 0),
ctrl=keymap_item.get("ctrl", 0),
alt=keymap_item.get("alt", 0),
oskey=keymap_item.get("oskey", False),
key_modifier=keymap_item.get("key_modifier", "NONE"),
repeat=keymap_item.get("repeat", False),
head=keymap_item.get("head", False),
)
if props and "properties" in props:
for prop_name, prop_value in props["properties"]:
if hasattr(kmi.properties, prop_name):
setattr(kmi.properties, prop_name, prop_value)
if props and "active" in props:
kmi.active = props["active"]
addon_keymaps.append((km, kmi))
except Exception as e:
print(f"Maya Config Pro: Could not register keymaps: {e}")
for km_name, km_args, km_content in fa_hotkeys_data.keyconfig_data:
if km_args.get("modal", False):
continue
try:
km = kc.keymaps.new(name=km_name, **km_args)
addon_keymap_objects.append(km)
keymap_init_from_data(km, km_content["items"], is_modal=False)
except Exception as e:
print(f"Maya Config Pro: Could not register keymap '{km_name}': {e}")
def unregister_keymaps():
def unregister_keymaps(*, drop_stale: bool = False) -> None:
"""Remove all MCP addon keymap items and the keymap objects we created."""
global addon_keymap_objects
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if not kc:
_clear_keymap_tracking()
return
for km, kmi in list(addon_keymaps):
try:
km.keymap_items.remove(kmi)
except Exception:
pass
addon_keymaps.clear()
if drop_stale or not addon_keymap_objects:
_purge_tracked_keymaps(kc)
return
removed_any = False
for km in reversed(addon_keymap_objects):
try:
kc.keymaps.remove(km)
removed_any = True
except Exception:
pass
addon_keymap_objects.clear()
if not removed_any:
_purge_tracked_keymaps(kc)
else:
_clear_keymap_tracking()
def sync_addon_keymaps() -> None:
def sync_addon_keymaps(*, after_file_load: bool = False) -> None:
"""
If "Auto-load FA Keymap" is on and the active preset is `fa_hotkeys`, register
the addon keymap layer; otherwise remove it. Safe to call often (switcher, prefs, msgbus).
"""
prefs = _get_prefs()
if not prefs or not getattr(prefs, "auto_load_keymap", False):
unregister_keymaps()
unregister_keymaps(drop_stale=after_file_load)
return
if is_fa_keyconfig_active():
unregister_keymaps()
unregister_keymaps(drop_stale=after_file_load)
register_keymaps()
try:
keymap_fix.patch_node_editor_alt_rmb("FA_HOTKEYS")
except Exception:
pass
else:
unregister_keymaps()
unregister_keymaps(drop_stale=after_file_load)
@bpy.app.handlers.persistent
def _mcp_load_post_sync(_a, _b) -> None:
try:
sync_addon_keymaps()
except Exception as e:
print(f"Warning: Maya Config Pro: keymap sync after load: {e}")
global _load_post_timer
# File load invalidates stored keymap RNA handles — never touch them here.
_clear_keymap_tracking()
if _load_post_timer is not None:
try:
bpy.app.timers.unregister(_load_post_timer)
except Exception:
pass
def _sync_after_load():
global _load_post_timer
_load_post_timer = None
try:
sync_addon_keymaps(after_file_load=True)
except Exception as e:
print(f"Warning: Maya Config Pro: keymap sync after load: {e}")
return None
_load_post_timer = bpy.app.timers.register(_sync_after_load, first_interval=0.0)
def _msgbus_keyconfig(_):
@@ -179,6 +205,13 @@ def register_keymap_listeners() -> None:
def unregister_keymap_listeners() -> None:
global _load_post_timer
if _load_post_timer is not None:
try:
bpy.app.timers.unregister(_load_post_timer)
except Exception:
pass
_load_post_timer = None
try:
bpy.msgbus.clear_by_owner(_msgbus_owner)
except Exception:
@@ -8,6 +8,13 @@ from ..utils import compat
from bpy.types import Menu
def _draw_multi_loop_select(layout, *, ring: bool, icon: str) -> None:
op_id = compat.mesh_multi_loop_select_op(ring=ring)
op = layout.operator(op_id, text="", icon=icon)
if op and op_id == "mesh.loop_multi_select":
op.ring = ring
# ------------------------------------------------------------------------
# Sub Menus
# ------------------------------------------------------------------------
@@ -46,10 +53,14 @@ class MCP_MT_SubMode(Menu):
layout = self.layout
layout.label(text="Mode:")
layout.separator()
layout.operator("object.mode_set", text="Sculpt", icon="SCULPTMODE_HLT").mode='SCULPT'
layout.operator("object.mode_set", text="Vertex Paint", icon="VPAINT_HLT").mode='VERTEX_PAINT'
layout.operator("object.mode_set", text="Weight Paint", icon="WPAINT_HLT").mode='WEIGHT_PAINT'
layout.operator("object.mode_set", text="Texture Paint", icon="TPAINT_HLT").mode='TEXTURE_PAINT'
obj = context.object
if obj is None or obj.type != "MESH":
layout.label(text="Requires a mesh object")
return
layout.operator("object.mode_set", text="Sculpt", icon="SCULPTMODE_HLT").mode = "SCULPT"
layout.operator("object.mode_set", text="Vertex Paint", icon="VPAINT_HLT").mode = "VERTEX_PAINT"
layout.operator("object.mode_set", text="Weight Paint", icon="WPAINT_HLT").mode = "WEIGHT_PAINT"
layout.operator("object.mode_set", text="Texture Paint", icon="TPAINT_HLT").mode = "TEXTURE_PAINT"
class MCP_MT_SubFreeze(Menu):
@@ -130,12 +141,8 @@ class MCP_MT_MarkingMenu(Menu):
op = other_menu.operator("mesh.select_all", text="", icon="SELECT_SUBTRACT")
if op:
op.action = "DESELECT"
op = other_menu.operator("mesh.loop_multi_select", text="", icon="ANCHOR_LEFT")
if op:
op.ring = False
op = other_menu.operator("mesh.loop_multi_select", text="", icon="ANCHOR_TOP")
if op:
op.ring = True
_draw_multi_loop_select(other_menu, ring=False, icon="ANCHOR_LEFT")
_draw_multi_loop_select(other_menu, ring=True, icon="ANCHOR_TOP")
other_menu.operator("mesh.select_more", text="", icon="ADD")
other_menu.operator("mesh.select_less", text="", icon="REMOVE")
@@ -8,6 +8,37 @@ from ..utils import compat
from bpy.types import Menu
def _set_active_viewport_ortho_axis(context, axis: str):
"""Apply an axis view in orthographic mode to the invoking 3D viewport only."""
area = context.area
if area is None or area.type != "VIEW_3D":
return {"CANCELLED"}
space = context.space_data
if space is None or space.type != "VIEW_3D":
space = area.spaces.active
if space is None:
return {"CANCELLED"}
region = context.region
if region is None:
region = next((r for r in area.regions if r.type == "WINDOW"), None)
if region is None:
return {"CANCELLED"}
with context.temp_override(
window=context.window,
screen=context.screen,
area=area,
region=region,
space_data=space,
):
bpy.ops.view3d.view_axis(type=axis)
space.region_3d.view_perspective = "ORTHO"
return {"FINISHED"}
# ------------------------------------------------------------------------
# Main Quad Menu
# ------------------------------------------------------------------------
@@ -56,13 +87,7 @@ class MCP_OT_LeftOrtho(bpy.types.Operator):
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.view3d.view_axis(type='LEFT')
C = bpy.context
for i, a in enumerate(C.screen.areas):
if a.type == "VIEW_3D":
space = a.spaces.active
space.region_3d.view_perspective = 'ORTHO'
return {'FINISHED'}
return _set_active_viewport_ortho_axis(context, "LEFT")
class MCP_OT_RightOrtho(bpy.types.Operator):
@@ -72,13 +97,7 @@ class MCP_OT_RightOrtho(bpy.types.Operator):
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.view3d.view_axis(type='RIGHT')
C = bpy.context
for i, a in enumerate(C.screen.areas):
if a.type == "VIEW_3D":
space = a.spaces.active
space.region_3d.view_perspective = 'ORTHO'
return {'FINISHED'}
return _set_active_viewport_ortho_axis(context, "RIGHT")
class MCP_OT_BottomOrtho(bpy.types.Operator):
@@ -88,13 +107,7 @@ class MCP_OT_BottomOrtho(bpy.types.Operator):
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.view3d.view_axis(type='BOTTOM')
C = bpy.context
for i, a in enumerate(C.screen.areas):
if a.type == "VIEW_3D":
space = a.spaces.active
space.region_3d.view_perspective = 'ORTHO'
return {'FINISHED'}
return _set_active_viewport_ortho_axis(context, "BOTTOM")
class MCP_OT_TopOrtho(bpy.types.Operator):
@@ -104,13 +117,7 @@ class MCP_OT_TopOrtho(bpy.types.Operator):
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.view3d.view_axis(type='TOP')
C = bpy.context
for i, a in enumerate(C.screen.areas):
if a.type == "VIEW_3D":
space = a.spaces.active
space.region_3d.view_perspective = 'ORTHO'
return {'FINISHED'}
return _set_active_viewport_ortho_axis(context, "TOP")
class MCP_OT_FrontOrtho(bpy.types.Operator):
@@ -120,13 +127,7 @@ class MCP_OT_FrontOrtho(bpy.types.Operator):
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.view3d.view_axis(type='FRONT')
C = bpy.context
for i, a in enumerate(C.screen.areas):
if a.type == "VIEW_3D":
space = a.spaces.active
space.region_3d.view_perspective = 'ORTHO'
return {'FINISHED'}
return _set_active_viewport_ortho_axis(context, "FRONT")
class MCP_OT_BackOrtho(bpy.types.Operator):
@@ -136,13 +137,7 @@ class MCP_OT_BackOrtho(bpy.types.Operator):
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.view3d.view_axis(type='BACK')
C = bpy.context
for i, a in enumerate(C.screen.areas):
if a.type == "VIEW_3D":
space = a.spaces.active
space.region_3d.view_perspective = 'ORTHO'
return {'FINISHED'}
return _set_active_viewport_ortho_axis(context, "BACK")
# ------------------------------------------------------------------------
@@ -1,6 +1,6 @@
"""
This module provides API compatibility functions for handling differences
between Blender 4.2, 4.4, 4.5, and 5.0.
between supported Blender versions (4.5 LTS through 5.2 LTS).
"""
import bpy
@@ -50,9 +50,9 @@ def get_tools_region_type():
In Blender 2.8+, 'TOOLS' was replaced by 'UI'.
Returns:
str: 'UI' (for all supported versions 4.2+)
str: 'UI' (for all supported versions 4.5+)
"""
# For Blender 4.2 and later, always use 'UI'
# For Blender 4.5 and later, always use 'UI'
return 'UI'
@@ -72,8 +72,16 @@ def temp_override(context, **kwargs):
# Blender 3.2+ uses temp_override
return context.temp_override(**kwargs)
else:
# Older versions - this shouldn't happen for 4.2+ minimum
# Older versions - this shouldn't happen for 4.5+ minimum
# but keeping for safety
override = context.copy()
override.update(kwargs)
return override
def mesh_multi_loop_select_op(*, ring: bool) -> str:
"""Operator id for loop (ring=False) or ring (ring=True) multi-select."""
if version.is_version_at_least(5, 1, 0):
return "mesh.select_edge_ring_multi" if ring else "mesh.select_edge_loop_multi"
return "mesh.loop_multi_select"
@@ -1,53 +1,32 @@
"""
This module provides version detection and comparison utilities for
multi-version Blender support (4.2, 4.4, 4.5, and 5.0).
Version detection and comparison utilities for Blender 4.5 LTS through 5.2 LTS.
"""
import bpy
# Version constants
VERSION_4_2 = (4, 2, 0)
VERSION_4_4 = (4, 4, 0)
VERSION_4_5 = (4, 5, 0)
VERSION_5_0 = (5, 0, 0)
VERSION_5_1 = (5, 1, 0)
VERSION_5_2 = (5, 2, 0)
def get_blender_version():
"""
Returns the current Blender version as a tuple (major, minor, patch).
Returns:
tuple: (major, minor, patch) version numbers
"""
"""Return the current Blender version as (major, minor, patch)."""
return bpy.app.version
def get_version_string():
"""
Returns the current Blender version as a string (e.g., "4.2.0").
Returns:
str: Version string in format "major.minor.patch"
"""
"""Return the current Blender version as 'major.minor.patch'."""
version = get_blender_version()
return f"{version[0]}.{version[1]}.{version[2]}"
def is_version_at_least(major, minor=0, patch=0):
"""
Check if the current Blender version is at least the specified version.
Args:
major (int): Major version number
minor (int): Minor version number (default: 0)
patch (int): Patch version number (default: 0)
Returns:
bool: True if current version >= specified version
"""
"""True when the running Blender version is >= major.minor.patch."""
current = get_blender_version()
target = (major, minor, patch)
if current[0] != target[0]:
return current[0] > target[0]
if current[1] != target[1]:
@@ -56,61 +35,55 @@ def is_version_at_least(major, minor=0, patch=0):
def is_version_less_than(major, minor=0, patch=0):
"""
Check if the current Blender version is less than the specified version.
Args:
major (int): Major version number
minor (int): Minor version number (default: 0)
patch (int): Patch version number (default: 0)
Returns:
bool: True if current version < specified version
"""
"""True when the running Blender version is < major.minor.patch."""
return not is_version_at_least(major, minor, patch)
def get_version_category():
"""
Returns the version category string for the current Blender version.
Returns:
str: '4.2', '4.4', '4.5', or '5.0' based on the current version
Return a coarse support bucket for the running Blender version.
Supported targets: 4.5, 5.0, 5.1, 5.2.
"""
version = get_blender_version()
major, minor = version[0], version[1]
if major == 4:
if minor < 4:
return '4.2'
elif minor < 5:
return '4.4'
else:
return '4.5'
elif major >= 5:
return '5.0'
else:
# Fallback for older versions
return f"{major}.{minor}"
def is_version_4_2():
"""Check if running Blender 4.2 (4.2.x only, not 4.3 or 4.4)."""
version = get_blender_version()
return version[0] == 4 and version[1] == 2
if minor < 5:
return "4.4"
return "4.5"
if major >= 5:
if minor < 1:
return "5.0"
if minor < 2:
return "5.1"
return "5.2"
return f"{major}.{minor}"
def is_version_4_4():
"""Check if running Blender 4.4 (4.4.x only, not 4.5)."""
"""True on Blender 4.4.x."""
version = get_blender_version()
return version[0] == 4 and version[1] == 4
def is_version_4_5():
"""Check if running Blender 4.5 LTS."""
"""True on Blender 4.5 LTS."""
return is_version_at_least(4, 5, 0) and is_version_less_than(5, 0, 0)
def is_version_5_0():
"""Check if running Blender 5.0 or later."""
return is_version_at_least(5, 0, 0)
"""True on Blender 5.0.x."""
version = get_blender_version()
return version[0] == 5 and version[1] == 0
def is_version_5_1():
"""True on Blender 5.1.x."""
version = get_blender_version()
return version[0] == 5 and version[1] == 1
def is_version_5_2():
"""True on Blender 5.2 LTS and newer 5.x."""
return is_version_at_least(5, 2, 0)