Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
graft preditor/resource
graft preditor/dccs
recursive-include preditor *.ui
recursive-exclude tests *
89 changes: 61 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,13 @@ a better `parent_callback`.
## Installing Qt

PrEditor is built on Qt, but uses [Qt.py](https://github.com/mottosso/Qt.py) so
you can choose to use PySide2 or PyQt5. We have elected to not directly depend
on either of these packages as if you want to use PrEditor inside of a an existing
application like Maya or Houdini, they already come with PySide2 installed. If
you are using it externally, add them to your pip install command.
you can choose to use PySide6, PySide2, PyQt6 or PyQt5. We have elected to not
directly depend on either of these packages so that you can use PrEditor inside
of existing applications like Maya or Houdini that already come with PySide
installed. If you are using it externally add them to your pip install command.

- PySide2: `pip install preditor PySide2`
- PyQt5: `pip install preditor PyQt5`
- PySide6: `pip install preditor PySide6`
- PyQt6: `pip install preditor PyQt6`

## Cli

Expand All @@ -125,35 +125,68 @@ this is only useful for windows.
## QScintilla workbox

The more mature QScintilla workbox requires a few extra dependencies that must
be passed manually. It hasn't been added to `extras_require` because we plan to
split it into its own pip module due to it requiring PyQt5 which is a little hard
to get working inside of DCC's that ship with PySide2 by default. Here is the
python 3 pip install command.
be passed manually. We have added it as pip `optional-dependencies`. QScintilla
only works with PyQt5/6 and it is a little hard to get PyQt working inside of
DCC's that ship with PySide2/6 by default. Here is the python 3 pip install command.

- `pip install preditor PyQt5, QScintilla>=2.11.4 aspell-python-py3`
- PyQt6: `pip install preditor[qsci6] PyQt6, aspell-python-py3`
- PyQt5: `pip install preditor[qsci5] PyQt5, aspell-python-py3`

The aspell-python-py3 requirement is optional to enable spell check.

You may need to set the `QT_PREFERRED_BINDING` or `QT_PREFERRED_BINDING_JSON`
[environment variable](https://github.com/mottosso/Qt.py?tab=readme-ov-file#override-preferred-choice) to ensure that PrEditor can use PyQt5/PyQt6.

# DCC Integration

## Maya

PrEditor is pre-setup to use as a Maya module. To use it, create a virtualenv
with the same python as maya, or install it using mayapy.

```
virtualenv venv_preditor
venv_preditor\Scripts\activate
pip install PrEditor
set MAYA_MODULE_PATH=c:\path\to\venv_preditor\Lib\site-packages\preditor\dccs
```
Note: Due to how maya .mod files works if you are using development installs you
can't use pip editable installs. This is due to the relative path used
`PYTHONPATH +:= ../..` in `PrEditor_maya.mod`. You can modify that to use a hard
coded file path for testing, or add a second .mod file to add the virtualenv's
`site-packages` file path as a hard coded file path.

Here are several example integrations for DCC's included in PrEditor. These
require some setup to manage installing all pip requirements. These will require
you to follow the [Setup](#setup) instructions below.

- [Maya](/preditor/dccs/maya/README.md)
- [3ds Max](/preditor/dccs/studiomax/README.md)

If you are using hab, you can simply add the path to the [preditor](/preditor) folder to your site's `distro_paths`. [See .hab.json](/preditor/dccs/.hab.json)

## Setup

PrEditor has many python pip requirements. The easiest way to get access to all
of them inside an DCC is to create a virtualenv and pip install the requirements.
You can possibly use the python included with DCC(mayapy), but this guide covers
using a system install of python.

1. Identify the minor version of python that the dcc is using. Running `sys.version_info[:2]` in the DCC returns the major and minor version of python.
2. Download and install the required version of python. Note, you likely only need to match the major and minor version of python(3.11 not 3.11.12). It's recommended that you don't use the windows store to install python as it has had issues when used to create virtualenvs.
3. Create a virtualenv using that version of python. On windows you can use `py.exe -3.11` or call the correct python.exe file. Change `-3.11` to match the major and minor version returned by step 1. Note that you should create separate venvs for a given python minor version and potentially for minor versions of Qt if you are using PyQt.
```batch
cd c:\path\to\venv\parent
py -3.11 -m virtualenv preditor_311
```
4. Use the newly created pip exe to install PrEditor and its dependencies.
* This example shows using PySide and the simple TextEdit workbox in a minimal configuration.
```batch
c:\path\to\venv\parent\preditor_311\Scripts\pip install PrEditor
```
* This example shows using QScintilla in PyQt6 for a better editing experience. Note that you need to match the PyQt version used by the DCC, This may require matching the exact version of PyQt.
```batch
c:\path\to\venv\parent\preditor_311\Scripts\pip install PrEditor[qsci6] PyQt6==6.5.3
```

### Editable install

You should skip this section unless you want to develop PrEditor's code from an git repo using python's editable pip install.

Due to how editable installs work you will need to set an environment variable
specifying the site-packages directory of the virtualenv you created in the
previous step. On windows this should be the `lib\site-packages` folder inside
of the venv you just created. Store this in the `PREDITOR_SITE`, this can be done
permanently or temporarily(via `set "PREDITOR_SITE=c:\path\to\venv\parent\preditor_311\lib\site-packages"`).

This is required because you are going to use the path to your git repo's preditor
folder in the module/plugin loading methods for the the DCC you are using, but
there is no way to automatically find the virtualenv that your random git repo
is installed in. In fact, you may have have your git repo installed into multiple
virtualenvs at once.

# Plugins

Expand Down
5 changes: 4 additions & 1 deletion preditor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,10 @@ def launch(run_workbox=False, app_id=None, name=None, standalone=False):
# it regain focus.
widget.activateWindow()
widget.raise_()
widget.setWindowState(widget.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
widget.setWindowState(
widget.windowState() & ~Qt.WindowState.WindowMinimized
| Qt.WindowState.WindowActive
)
widget.console().setFocus()
app.start()

Expand Down
8 changes: 6 additions & 2 deletions preditor/about_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,14 @@ def text(self):
pass

# Add info for all Qt5 bindings that have been imported
if 'PyQt5.QtCore' in sys.modules:
ret.append('PyQt5: {}'.format(sys.modules['PyQt5.QtCore'].PYQT_VERSION_STR))
if 'PySide6.QtCore' in sys.modules:
ret.append('PySide6: {}'.format(sys.modules['PySide6.QtCore'].qVersion()))
if 'PyQt6.QtCore' in sys.modules:
ret.append('PyQt6: {}'.format(sys.modules['PyQt6.QtCore'].PYQT_VERSION_STR))
if 'PySide2.QtCore' in sys.modules:
ret.append('PySide2: {}'.format(sys.modules['PySide2.QtCore'].qVersion()))
if 'PyQt5.QtCore' in sys.modules:
ret.append('PyQt5: {}'.format(sys.modules['PyQt5.QtCore'].PYQT_VERSION_STR))

# Add qt library paths for plugin debugging
for i, path in enumerate(QtCore.QCoreApplication.libraryPaths()):
Expand Down
10 changes: 10 additions & 0 deletions preditor/dccs/.hab.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "preditor",
"version": "0.0.dev0",
"environment": {
"append": {
"MAYA_MODULE_PATH": "{relative_root}/maya",
"ADSK_APPLICATION_PLUGINS": "{relative_root}/studiomax"
}
}
}
1 change: 0 additions & 1 deletion preditor/dccs/maya/PrEditor_maya.mod
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
+ PrEditor DEVELOPMENT .
PYTHONPATH +:= ../..
22 changes: 22 additions & 0 deletions preditor/dccs/maya/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Maya Integration

This is an example of using an Maya module to add PrEditor into Maya. This adds
a PrEditor menu with a PrEditor action in Maya's menu bar letting you open PrEditor. It
adds the excepthook so if a python exception is raised it will prompt the user
to show PrEditor. PrEditor will show all python stdout/stderr output generated
after the plugin is loaded.

# Setup

Make sure to follow these [setup instructions](/preditor/README.md#Setup) first to create the virtualenv.

Alternatively you can use [myapy's](https://help.autodesk.com/view/MAYAUL/2026/ENU/?guid=GUID-72A245EC-CDB4-46AB-BEE0-4BBBF9791627) pip to install the requirements, but a
separate virtualenv is recommended. This method should not require setting the
`PREDITOR_SITE` environment variable even if you use an editable install.

# Use

The [preditor/dccs/maya](/preditor/dccs/maya) directory is setup as a Maya Module. To load it in
maya add the full path to that directory to the `MAYA_MODULE_PATH` environment
variable. You can use `;` on windows and `:` on linux to join multiple paths together.
You will need to enable auto load for the `PrEditor_maya.py` plugin.
33 changes: 32 additions & 1 deletion preditor/dccs/maya/plug-ins/PrEditor_maya.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from __future__ import absolute_import

import os
import site
from pathlib import Path

import maya.mel
from maya import OpenMayaUI, cmds

Expand Down Expand Up @@ -35,12 +39,39 @@ def launch(ignored):
return widget


def update_site():
"""Adds a site dir to python. This makes its contents importable to python.

This includes making any editable installs located in that site-packages folder
accessible to this python instance. This does not activate the virtualenv.

If the env var `PREDITOR_SITE` is set, this path is used. Otherwise the
parent directory of preditor is used.

- `PREDITOR_SITE` is useful if you want to use an editable install of preditor
for development. This should point to a virtualenv's site-packages folder.
- Otherwise if the virtualenv has a regular pip install of preditor you can
skip setting the env var.
"""
venv_path = os.getenv("PREDITOR_SITE")
# If the env var is not defined then use the parent dir of this preditor package.
if venv_path is None:
venv_path = cmds.moduleInfo(moduleName="PrEditor", path=True)
venv_path = Path(venv_path).parent.parent.parent
venv_path = str(venv_path)

print(f"Preditor is adding python site: {venv_path}")
site.addsitedir(venv_path)


def initializePlugin(mobject): # noqa: N802
"""Initialize the script plug-in"""
global preditor_menu

# If running headless, there is no need to build a gui and create the python logger
if not headless():
update_site()

from Qt.QtWidgets import QApplication

import preditor
Expand All @@ -67,7 +98,7 @@ def initializePlugin(mobject): # noqa: N802
gmainwindow = maya.mel.eval("$temp1=$gMainWindow")
preditor_menu = cmds.menu(label="PrEditor", parent=gmainwindow, tearOff=True)
cmds.menuItem(
label="Show",
label="PrEditor",
command=launch,
sourceType="python",
image=preditor.resourcePath('img/preditor.png'),
Expand Down
32 changes: 32 additions & 0 deletions preditor/dccs/studiomax/PackageContents.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<ApplicationPackage
SchemaVersion="1.0"
AutodeskProduct="3ds Max"
ProductType="Application"
Name="PrEditor"
Description="Initialize PrEditor inside 3ds Max."
AppVersion="0.0.1"
ProductCode="{7b1e4ece-9a30-4afb-99ed-b9fb7c23e2f9}"
UpgradeCode="{2d245bb4-19cf-4f90-ae19-3ff006ed0f48}">
<CompanyDetails Name="Blur Studio" Email="opensource@blur.com"/>
<Components Description="pre-start-up scripts parts">
<RuntimeRequirements OS="Win64" Platform="3ds Max" SeriesMin="2025" SeriesMax="2026" />
<ComponentEntry ModuleName="./preditor.ms" />
</Components>
<Components Description="macroscripts parts">
<RuntimeRequirements OS="Win64" Platform="3ds Max" SeriesMin="2025" SeriesMax="2026" />
<ComponentEntry ModuleName="./PrEditor-PrEditor_Show.mcr" />
</Components>
<Components Description="menu parts">
<RuntimeRequirements OS="Win64" Platform="3ds Max" SeriesMin="2025" SeriesMax="2026" />
<ComponentEntry ModuleName="./preditor_menu.mnx" />
</Components>
<Components Description="light icon paths parts">
<RuntimeRequirements OS="Win64" Platform="3ds Max" SeriesMin="2025" SeriesMax="2026" />
<ComponentEntry AppName="lighticons" Version="1.0.0" ModuleName="../../resource/img" />
</Components>
<Components Description="dark icon paths parts">
<RuntimeRequirements OS="Win64" Platform="3ds Max" SeriesMin="2025" SeriesMax="2026" />
<ComponentEntry AppName="darkicons" Version="1.0.0" ModuleName="../../resource/img" />
</Components>
</ApplicationPackage>
8 changes: 8 additions & 0 deletions preditor/dccs/studiomax/PrEditor-PrEditor_Show.mcr
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
macroScript PrEditor_Show
category:"PrEditor"
tooltip:"PrEditor..."
IconName:"preditor.ico"
(
local preditor = python.import "preditor"
preditor.launch()
)
17 changes: 17 additions & 0 deletions preditor/dccs/studiomax/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 3ds Max Integration

This is an example of using an Autodesk Application Package to load PrEditor into
3ds Max. This adds a PrEditor item to the Scripting menu in 3ds Max's menu bar
to show PrEditor. It adds the excepthook so if a python exception is raised
it will prompt the user to show PrEditor. PrEditor will show all python stdout/stderr
output generated after the plugin is loaded.

# Setup

Make sure to follow these [setup instructions](/preditor/README.md#Setup) first to create the virtualenv.

# Use

The [preditor/dccs/studiomax](/preditor/dccs/studiomax) directory is setup as a 3ds Max Application Plugin.
To load it in 3ds Max add the full path to that directory to the `ADSK_APPLICATION_PLUGINS` environment
variable. You can use `;` on windows and `:` on linux to join multiple paths together.
16 changes: 16 additions & 0 deletions preditor/dccs/studiomax/preditor.ms
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

function configure_preditor = (
local pysite = python.import "site"
local WinEnv = dotNetClass "System.Environment"
-- If the env var PREDITOR_SITE is set, add its packages to python
local venv_path = WinEnv.GetEnvironmentVariable "PREDITOR_SITE"
if venv_path != undefined do (
print("Preditor is adding python site: " + venv_path)
pysite.addsitedir venv_path
)
-- Configure preditor, adding excepthook etc.
local preditor = python.import "preditor"
preditor.configure "studiomax"
)

configure_preditor()
7 changes: 7 additions & 0 deletions preditor/dccs/studiomax/preditor_menu.mnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<MaxMenuTransformations>
<CreateMenuSeparator MenuId="658724ec-de09-47dd-b723-918c59a28ad1" Id="9cdc28de-9e70-471c-ae5c-d86e3b7fefba" BeforeId="4e206a37-f502-4fa8-ba0e-70dd427d507f"/>
<MoveItem DestinationId="658724ec-de09-47dd-b723-918c59a28ad1" ItemId="4e206a37-f502-4fa8-ba0e-70dd427d507f"/>
<CreateMenuAction MenuId="658724ec-de09-47dd-b723-918c59a28ad1" Id="f243229b-f0f6-42b4-a94d-bebe9ef9777f" ActionId="647394-PrEditor_Show`PrEditor"/>
<MoveItem DestinationId="658724ec-de09-47dd-b723-918c59a28ad1" ItemId="9cdc28de-9e70-471c-ae5c-d86e3b7fefba" BeforeId="f243229b-f0f6-42b4-a94d-bebe9ef9777f"/>
</MaxMenuTransformations>
10 changes: 7 additions & 3 deletions preditor/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ def clear(self, stamp=False):
"""Removes the contents of the log file."""
open(self._logfile, 'w').close()
if stamp:
msg = '--------- Date: {today} Version: {version} ---------'
print(msg.format(today=datetime.datetime.today(), version=sys.version))
print(self.stamp())

def flush(self):
self._stdhandle.flush()
if self._stdhandle:
self._stdhandle.flush()

def stamp(self):
msg = '--------- Date: {today} Version: {version} ---------'
return msg.format(today=datetime.datetime.today(), version=sys.version)

def write(self, msg):
f = open(self._logfile, 'a')
Expand Down
2 changes: 1 addition & 1 deletion preditor/excepthooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class PreditorExceptHook(object):
This calls each callable in the `preditor.config.excepthooks` list any time
`sys.excepthook` is called due to an raised exception.

If `config.excepthook` is empty when installing this class, it will
If `config.excepthooks` is empty when installing this class, it will
automatically add `default_excepthooks`. You can disable this by adding `None`
to the list before this class is initialized.
"""
Expand Down
4 changes: 2 additions & 2 deletions preditor/gui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def dpi_awareness_args(cls):
Returns:
args: Extend the arguments used to intialize the QApplication.
"""
if settings.OS_TYPE == "Windows" and Qt.IsPyQt5:
if settings.OS_TYPE == "Windows" and (Qt.IsPyQt6 or Qt.IsPyQt5):
# Make Qt automatically scale based on the monitor the window is
# currently located.
return ["--platform", "windows:dpiawareness=0"]
Expand Down Expand Up @@ -157,4 +157,4 @@ def start(self):
"""Exec's the QApplication if it hasn't already been started."""
if self.app_created and self.app and not self.app_has_exec:
self.app_has_exec = True
self.app.exec_()
Qt.QtCompat.QApplication.exec_()
Loading
Loading