Skip to content

Maya Integration

AuroraView provides seamless integration with Autodesk Maya through both Native WebView and QtWebView backends.

Installation

bash
# For Qt backend (recommended for Maya)
pip install auroraview[qt]

# Or install with mayapy
"C:\Program Files\Autodesk\Maya2024\bin\mayapy.exe" -m pip install auroraview[qt]

Quick Start

python
from auroraview import QtWebView
import maya.OpenMayaUI as omui
from qtpy import QtWidgets
import shiboken2

def maya_main_window():
    ptr = omui.MQtUtil.mainWindow()
    return shiboken2.wrapInstance(int(ptr), QtWidgets.QWidget)

# Create WebView
webview = QtWebView(
    parent=maya_main_window(),
    url="http://localhost:3000",
    width=800,
    height=600
)
webview.show()

Using Native WebView

python
from auroraview import WebView
import maya.OpenMayaUI as omui

# Get Maya main window handle
maya_hwnd = int(omui.MQtUtil.mainWindow())

# Create WebView
webview = WebView.create(
    "My Tool",
    width=800,
    height=600,
    parent=maya_hwnd,
    mode="owner"
)
webview.load_html("<h1>Hello Maya</h1>")
webview.show()

Common Errors

ImportError: cannot import name 'QtWebView'

Symptom:

python
from auroraview import QtWebView
# ImportError: cannot import name 'QtWebView' from 'auroraview'

Cause: Qt backend dependencies not installed

Diagnosis:

python
import auroraview
print(f"Qt backend available: {auroraview._HAS_QT}")
if not auroraview._HAS_QT:
    print(f"Qt import error: {auroraview._QT_IMPORT_ERROR}")

Solution 1: Install Qt dependencies

bash
# Windows
"C:\Program Files\Autodesk\Maya2024\bin\mayapy.exe" -m pip install auroraview[qt]

# macOS
/Applications/Autodesk/maya2024/Maya.app/Contents/bin/mayapy -m pip install auroraview[qt]

# Linux
/usr/autodesk/maya2024/bin/mayapy -m pip install auroraview[qt]

Solution 2: Use Native backend (no extra dependencies)

python
from auroraview import WebView
import maya.OpenMayaUI as omui

maya_hwnd = int(omui.MQtUtil.mainWindow())
webview = WebView.create("My Tool", parent=maya_hwnd, mode="owner")
webview.show()

Thread Safety

AuroraView provides automatic thread safety for Maya integration. When event handlers are called from the WebView thread, they are automatically marshaled to Maya's main thread.

Zero Configuration

Since dcc_mode="auto" is the default, AuroraView automatically detects Maya and enables thread safety. No configuration needed!

Automatic Thread Safety (Default)

Just use AuroraView normally - thread safety is automatic:

python
from auroraview import WebView
import maya.OpenMayaUI as omui

maya_hwnd = int(omui.MQtUtil.mainWindow())

# Thread safety is automatically enabled when Maya is detected
webview = WebView.create(
    "My Tool",
    parent=maya_hwnd,
    mode="owner",
    # dcc_mode="auto" is the default - no need to specify!
)

@webview.on("create_cube")
def handle_create(data):
    # Automatically runs on Maya main thread!
    import maya.cmds as cmds
    name = data.get("name", "myCube")
    result = cmds.polyCube(name=name)
    return {"ok": True, "name": result[0]}

@webview.on("get_selection")
def handle_selection(data):
    import maya.cmds as cmds
    sel = cmds.ls(selection=True)
    return {"selection": sel, "count": len(sel)}

Manual Thread Safety with Decorators

For more control, use the @dcc_thread_safe decorator:

python
from auroraview import WebView
from auroraview.utils import dcc_thread_safe, dcc_thread_safe_async

webview = WebView.create("My Tool", parent=maya_hwnd, mode="owner")

@webview.on("export_scene")
@dcc_thread_safe  # Blocks until complete, returns result
def handle_export(data):
    import maya.cmds as cmds
    path = data.get("path", "/tmp/scene.ma")
    cmds.file(rename=path)
    cmds.file(save=True, type="mayaAscii")
    return {"ok": True, "path": path}

@webview.on("refresh_viewport")
@dcc_thread_safe_async  # Fire-and-forget, returns immediately
def handle_refresh(data):
    import maya.cmds as cmds
    cmds.refresh()

Thread-Safe Wrapper for Background Threads

When calling WebView methods from Maya scripts or background threads:

python
webview = WebView.create("My Tool", parent=maya_hwnd, mode="owner")

# Get thread-safe wrapper
safe = webview.thread_safe()

# These can be called from any thread safely:
def update_ui_from_script():
    safe.eval_js("updateProgress(50)")
    safe.emit("status_changed", {"status": "processing"})

Using run_on_main_thread Directly

For one-off operations:

python
from auroraview.utils import run_on_main_thread, run_on_main_thread_sync

# Fire-and-forget (non-blocking)
def create_sphere():
    import maya.cmds as cmds
    cmds.polySphere()

run_on_main_thread(create_sphere)

# Blocking with return value
def get_scene_path():
    import maya.cmds as cmds
    return cmds.file(q=True, sceneName=True)

scene = run_on_main_thread_sync(get_scene_path)
print(f"Current scene: {scene}")

Threading Model

❌ WRONG: Using show_async()

python
# DON'T DO THIS - Will cause Maya to freeze!
webview = WebView.create("My Tool", parent=hwnd, mode="owner")
webview.load_html(html)
webview.show_async()  # Creates window in background thread

Why this fails:

  1. show_async() creates the WebView in a background thread
  2. The window is parented to Maya's main window
  3. Windows GUI thread affinity: Child/owned windows must be created in the same thread as their parent
  4. Result: Maya freezes

✅ CORRECT: Using show() with scriptJob

python
import maya.cmds as cmds
import __main__

webview = WebView.create("My Tool", parent=hwnd, mode="owner")
webview.load_html(html)

# Store in __main__ for scriptJob access
__main__.my_webview = webview

# Create scriptJob to process events
def process_events():
    if hasattr(__main__, 'my_webview'):
        should_close = __main__.my_webview.process_events()
        if should_close:
            # Cleanup
            if hasattr(__main__, 'my_webview_timer'):
                cmds.scriptJob(kill=__main__.my_webview_timer)
                del __main__.my_webview_timer
            del __main__.my_webview

# Create timer BEFORE showing window
timer_id = cmds.scriptJob(event=["idle", process_events])
__main__.my_webview_timer = timer_id

# Show window (non-blocking in embedded mode)
webview.show()

Why this works:

  1. WebView is created in Maya's main thread
  2. show() in embedded mode is non-blocking
  3. scriptJob calls process_events() periodically
  4. Result: Maya stays responsive

Parent Modes

python
# Owner mode (recommended)
webview = WebView.create("My Tool", parent=hwnd, mode="owner")
# - Uses GWLP_HWNDPARENT (owned window)
# - Safer for cross-thread scenarios
# - Window can be moved independently

# Child mode (advanced)
webview = WebView.create("My Tool", parent=hwnd, mode="child")
# - Uses WS_CHILD style
# - Requires same-thread creation
# - Window is clipped to parent bounds

Dockable Panel

python
from auroraview import QtWebView
from qtpy.QtWidgets import QDockWidget
from qtpy.QtCore import Qt
import maya.OpenMayaUI as omui
from qtpy import QtWidgets
import shiboken2

def maya_main_window():
    ptr = omui.MQtUtil.mainWindow()
    return shiboken2.wrapInstance(int(ptr), QtWidgets.QWidget)

# Create dock widget
main_win = maya_main_window()
dock = QDockWidget("My Tool", main_win)

# Create WebView
webview = QtWebView(parent=dock)
webview.load_url("http://localhost:3000")

# Set as dock widget content
dock.setWidget(webview)
main_win.addDockWidget(Qt.RightDockWidgetArea, dock)

webview.show()

API Binding Example

python
from auroraview import QtWebView
import maya.cmds as cmds

class MayaAPI:
    def get_selection(self) -> dict:
        """Get current selection"""
        sel = cmds.ls(selection=True)
        return {"selection": sel, "count": len(sel)}

    def select_objects(self, names: list = None) -> dict:
        """Select objects by name"""
        names = names or []
        cmds.select(names, replace=True)
        return {"ok": True, "selected": names}

    def create_cube(self, name: str = "cube") -> dict:
        """Create a polygon cube"""
        result = cmds.polyCube(name=name)
        return {"ok": True, "name": result[0]}

# Create WebView with API
webview = QtWebView(
    parent=maya_main_window(),
    url="http://localhost:3000"
)
webview.bind_api(MayaAPI())
webview.show()
javascript
// JavaScript side
const sel = await auroraview.api.get_selection();
console.log('Selected:', sel.selection);

await auroraview.api.create_cube({ name: 'myCube' });
await auroraview.api.select_objects({ names: ['myCube'] });

Selection Sync Example

python
from auroraview import QtWebView
import maya.cmds as cmds
from maya.api import OpenMaya as om

class OutlinerTool(QtWebView):
    def __init__(self, parent=None):
        super().__init__(parent=parent, width=300, height=600)
        self.load_url("http://localhost:3000")
        self._setup_callbacks()

    def _setup_callbacks(self):
        # Maya selection changed callback
        self.callback_id = om.MEventMessage.addEventCallback(
            "SelectionChanged",
            self._on_maya_selection_changed
        )

        # JavaScript selection event
        @self.on("select_objects")
        def handle_select(data):
            names = data.get("names", [])
            cmds.select(names, replace=True)

    def _on_maya_selection_changed(self, *args):
        sel = cmds.ls(selection=True)
        self.emit("selection_changed", {"selection": sel})

    def closeEvent(self, event):
        om.MMessage.removeCallback(self.callback_id)
        super().closeEvent(event)

# Usage
tool = OutlinerTool(parent=maya_main_window())
tool.show()

Complete Example

python
import maya.cmds as cmds
import maya.OpenMayaUI as omui
from auroraview import WebView
from shiboken2 import wrapInstance
from qtpy.QtWidgets import QWidget

# Get Maya main window
main_window_ptr = omui.MQtUtil.mainWindow()
maya_window = wrapInstance(int(main_window_ptr), QWidget)
hwnd = int(maya_window.winId())

# Create WebView
webview = WebView.create(
    "My Tool",
    width=800,
    height=600,
    parent=hwnd,
    mode="owner"
)

# Load content
webview.load_html("<h1>Hello Maya</h1>")

# Register event handlers
@webview.on("my_event")
def handle_event(data):
    print(f"Event received: {data}")

# Store in __main__
import __main__
__main__.my_webview = webview

# Create event processor
def process_events():
    if hasattr(__main__, 'my_webview'):
        should_close = __main__.my_webview.process_events()
        if should_close:
            if hasattr(__main__, 'my_webview_timer'):
                cmds.scriptJob(kill=__main__.my_webview_timer)
                del __main__.my_webview_timer
            del __main__.my_webview

# Create timer
timer_id = cmds.scriptJob(event=["idle", process_events])
__main__.my_webview_timer = timer_id

# Show window
webview.show()
print(f"WebView shown (timer ID: {timer_id})")

userSetup.py Integration

Add to your userSetup.py:

python
import maya.cmds as cmds

def launch_my_tool():
    from auroraview import QtWebView
    import maya.OpenMayaUI as omui
    from qtpy import QtWidgets
    import shiboken2

    def maya_main_window():
        ptr = omui.MQtUtil.mainWindow()
        return shiboken2.wrapInstance(int(ptr), QtWidgets.QWidget)

    webview = QtWebView(
        parent=maya_main_window(),
        url="http://localhost:3000"
    )
    webview.show()
    return webview

# Register menu item
cmds.evalDeferred("""
import maya.cmds as cmds
if cmds.menu('myToolMenu', exists=True):
    cmds.deleteUI('myToolMenu')
cmds.menu('myToolMenu', label='My Tool', parent='MayaWindow')
cmds.menuItem(label='Launch Tool', command='launch_my_tool()')
""")

WebView2 Pre-warming

QtWebView automatically pre-warms WebView2 for faster subsequent creation:

python
from auroraview.integration.qt import WebViewPool

# Explicit pre-warm at Maya startup (optional)
WebViewPool.prewarm()

# Check status
if WebViewPool.has_prewarmed():
    print(f"Pre-warm took {WebViewPool.get_prewarm_time():.2f}s")

Performance Tips

Optimize scriptJob Frequency

python
# Option 1: Use "idle" event (called very frequently)
timer_id = cmds.scriptJob(event=["idle", process_events])

# Option 2: Use QTimer for more control
from qtpy.QtCore import QTimer
timer = QTimer()
timer.timeout.connect(process_events)
timer.start(16)  # ~60 FPS

Batch Maya Commands

When handling events from JavaScript, batch Maya commands:

python
@webview.on("create_objects")
def handle_create(data):
    def _do_create():
        for obj_type in data['objects']:
            if obj_type == 'cube':
                cmds.polyCube()
            elif obj_type == 'sphere':
                cmds.polySphere()
    
    import maya.utils as mutils
    mutils.executeDeferred(_do_create)

Troubleshooting

Maya freezes when opening WebView

Cause: Using show_async() instead of show()

Solution: Use show() with scriptJob pattern

WebView window doesn't respond to clicks

Cause: Not calling process_events() periodically

Solution: Create scriptJob to call process_events() on idle events

Window closes immediately

Cause: WebView object is garbage collected

Solution: Store WebView in __main__ or a global variable

python
import __main__
__main__.my_webview = webview  # Keeps it alive

Events from JavaScript not received

Cause: Event handlers registered after show()

Solution: Register event handlers before calling show()

python
# Register handlers FIRST
@webview.on("my_event")
def handle_event(data):
    print(data)

# THEN show
webview.show()

Cleanup

Manual Cleanup

python
# Kill the timer
if hasattr(__main__, 'my_webview_timer'):
    cmds.scriptJob(kill=__main__.my_webview_timer)
    del __main__.my_webview_timer

# Delete the WebView
if hasattr(__main__, 'my_webview'):
    del __main__.my_webview

Automatic Cleanup

The process_events() function automatically cleans up when the user closes the window.

Released under the MIT License.