Metadata-Version: 2.4
Name: blender_qt
Version: 0.4.2
Summary: Qt integration for Blender
Author: Alex Telford
License: GPL-3.0-or-later
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: THIRD_PARTY_NOTICES
Requires-Dist: numpy>=1.26
Requires-Dist: PySide6>=6.7
Provides-Extra: test
Requires-Dist: pytest>=8.0; extra == "test"
Dynamic: license-file

# blender_qt

Qt integration for Blender with support for:

- custom node-editor spaces backed by QML or `QWidget`
- viewport gizmos backed by QML or `QWidget`
- managed top-level Qt windows

Developer-oriented architecture and workaround notes live in [the developer guide](https://github.com/minimalefforttech/blender_qt/blob/main/docs/developer-guide.md).

## Demo

![Blender Qt demo](https://raw.githubusercontent.com/minimalefforttech/blender_qt/main/docs/media/demo.gif)

Stable public API is exposed from the top-level `blender_qt` package.
Modules under `blender_qt.core`, `blender_qt.qml`, `blender_qt.widgets`, and `blender_qt._internal`
should be treated as implementation details unless explicitly documented otherwise.

## Installation

```bash
pip install blender-qt
```

The PyPI package name is `blender-qt`. The top-level Python import package remains `blender_qt`.

The wheel is published for add-on authors who want to depend on Blender Qt from normal Python packaging workflows.
The package can be imported outside Blender for metadata and API discovery, but runtime features still require Blender's Python environment and modules such as `bpy`.

## Sample Extension

See the `blender_qt_sample` repository for a sample Blender extension project that uses this wheel and packages it with `beb`:

https://github.com/minimalefforttech/blender_qt_sample

That sample uses `beb` because it is a convenient way to build a Blender extension archive around the published `blender-qt` PyPI package. You do not need to use `beb` for your own Blender Qt-based add-ons unless that workflow fits your packaging needs.

## Lifecycle

Call `blender_qt.init()` before registering spaces, gizmos, or windows.
Call `blender_qt.shutdown()` when your add-on unloads.

```python
import blender_qt


def register() -> None:
    blender_qt.init()


def unregister() -> None:
    blender_qt.shutdown()
```

## Register a custom QML space

Use `register_qml_space()` to add a custom node-editor space rendered from a QML root item.

```python
from pathlib import Path

import blender_qt

TREE_TYPE = "MY_ADDON_QML_NT"
QML_PATH = Path(__file__).with_name("main.qml")

blender_qt.register_qml_space(
    tree_type=TREE_TYPE,
    label="My QML Space",
    qml_path=QML_PATH,
    icon="NODETREE",
    sidebar_category="My Tools",
)
```

### Unregister a custom space

Use `unregister_space()` with the same `tree_type`.

```python
blender_qt.unregister_space("MY_ADDON_QML_NT")
```

## Register a custom widget space

Use `register_widget_space()` when your UI is a `QWidget` tree.

```python
from PySide6 import QtWidgets
import blender_qt


class MyWidgetSurface(QtWidgets.QWidget):
    def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
        super().__init__(parent)
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(QtWidgets.QLabel("Hello from a widget space"))


blender_qt.register_widget_space(
    tree_type="MY_ADDON_WIDGET_NT",
    label="My Widget Space",
    widget_factory=MyWidgetSurface,
    icon="NODETREE",
    sidebar_category="My Tools",
)
```

Unregister it the same way:

```python
blender_qt.unregister_space("MY_ADDON_WIDGET_NT")
```

List active custom spaces:

```python
tree_types = blender_qt.registered_space_types()
```

## Register a QML viewport gizmo

Use `register_qml_viewport_gizmo()` to render a floating viewport panel from QML.

```python
from pathlib import Path

import blender_qt

blender_qt.register_qml_viewport_gizmo(
    gizmo_id="MY_ADDON_QML_GIZMO",
    qml_path=Path(__file__).with_name("gizmo.qml"),
    space_type="VIEW_3D",
    width=320,
    height=220,
    is_2d=True,
    initial_position=(80, 80),
)
```

### 3D QML gizmo

Set `is_2d=False` to anchor the panel in world space.

```python
blender_qt.register_qml_viewport_gizmo(
    gizmo_id="MY_ADDON_QML_GIZMO_3D",
    qml_path=Path(__file__).with_name("gizmo.qml"),
    space_type="VIEW_3D",
    width=320,
    height=220,
    is_2d=False,
    initial_3d_position=(0.0, 0.0, 1.0),
    billboard=True,
    use_depth_test=True,
)
```

## Register a widget viewport gizmo

Use `register_widget_viewport_gizmo()` to render a floating viewport panel from a `QWidget`.

```python
from PySide6 import QtWidgets
import blender_qt


class MyViewportPanel(QtWidgets.QWidget):
    def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
        super().__init__(parent)
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(QtWidgets.QPushButton("Action"))


blender_qt.register_widget_viewport_gizmo(
    gizmo_id="MY_ADDON_WIDGET_GIZMO",
    widget_factory=MyViewportPanel,
    label="My Widget Gizmo",
    space_type="VIEW_3D",
    width=280,
    height=180,
    is_2d=True,
    initial_position=(120, 120),
)
```

## Unregister a viewport gizmo

Use `unregister_viewport_gizmo()` with the same `gizmo_id`.

```python
blender_qt.unregister_viewport_gizmo("MY_ADDON_WIDGET_GIZMO")
```

List active gizmos:

```python
gizmo_ids = blender_qt.registered_viewport_gizmo_ids()
```

## Managed windows

Use `register_window()` for standalone top-level windows.

```python
from PySide6 import QtWidgets
import blender_qt

window = QtWidgets.QDialog()
window.setWindowTitle("My Tool")
blender_qt.register_window(window, unique=True)
```

Optional close callback:

```python
def _on_window_closed(widget: QtWidgets.QWidget) -> None:
    print("Closed", widget.objectName())


blender_qt.register_window(window, unique=True, on_close=_on_window_closed)
```

Get active managed windows:

```python
windows = blender_qt.managed_windows()
```

Close them all:

```python
blender_qt.close_managed_windows()
```

## Recommended add-on structure

```python
import blender_qt


def register() -> None:
    blender_qt.init()
    blender_qt.register_qml_space(
        tree_type="MY_ADDON_QML_NT",
        label="My Space",
        qml_path="/path/to/main.qml",
    )
    blender_qt.register_qml_viewport_gizmo(
        gizmo_id="MY_ADDON_GIZMO",
        qml_path="/path/to/gizmo.qml",
        is_2d=True,
    )


def unregister() -> None:
    blender_qt.unregister_viewport_gizmo("MY_ADDON_GIZMO")
    blender_qt.unregister_space("MY_ADDON_QML_NT")
    blender_qt.shutdown()
```

## Notes

- `tree_type` and `gizmo_id` must be unique.
- Registering a duplicate space or gizmo raises `ValueError`.
- Missing QML files raise `FileNotFoundError` during registration.
- QML viewport gizmos use `is_2d` instead of `mode`.
- QML viewport gizmos do not use a `label` argument.
- Widget viewport gizmos still accept `label`, which is used as the host window title.
- Use `VIEW_3D`, `IMAGE_EDITOR`, `NODE_EDITOR`, `CLIP_EDITOR`, or `SEQUENCE_EDITOR` for `space_type` where supported by Blender.

## Project Layout

- `blender_qt`: importable package
- `docs`: contributor documentation
- `tests`: standalone smoke tests for package metadata and public API imports

## Releases

GitHub Actions runs tests and build validation on pushes and pull requests, and publishes tagged releases to PyPI.
