diff --git a/README.md b/README.md
index 4272f4e..1718755 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[Installation/Usage](#installationusage) | [Docs](https://nxt-dev.github.io/) | [Contributing](CONTRIBUTING.md) | [Licensing](LICENSE)
# Installation/Usage
-**To Use NXT please use the [NXT Standalone](#nxt-standalone) or [DCC plugin zip.](#maya-plugin)**
+**To Use NXT please use the [NXT Standalone](#nxt-standalone) or [DCC plugin zip.](#DCC-Plugins)**
Only clone this repo if you're [contributing](CONTRIBUTING.md) to the NXT codebase.
@@ -24,32 +24,14 @@ Our releases are hosted on [PyPi](https://pypi.org/project/nxt-editor/).
- Update:
- `pip install -U nxt-editor`
-### Blender addon:
-- Install:
- 1. Download Blender addon (nxt_blender.zip) [latest release](https://github.com/nxt-dev/nxt_editor/releases/latest)
- 2. Extract and follow `README.md` inside [nxt_blender](nxt_editor/integration/blender/README.md) instructions (also included in the download)
-- Launch:
- 1. Load the `nxt_blender` Addon (Edit > Preferences > Add-ons)
- 2. Navigate the newly created NXT menu and select Open Editor.
-- Update:
- - Automatically: NXT > Update NXT
- - By Hand: `/path/to/python.exe -m pip install -U nxt-editor`
- - Relaunch Blender after
-
+### DCC Plugins
-### Maya plugin:
+Each of our supported DCC's get a zip file on our [latest release](https://github.com/nxt-dev/nxt_editor/releases/latest)
-- Install:
- 1. Download the maya module(`nxt_maya.zip`) from the [latest release](https://github.com/nxt-dev/nxt_editor/releases/latest)
- 2. Follow the [nxt_maya](nxt_editor/integration/maya/README.md) instructions (also included in the download)
-- Launch:
- 1. Load `nxt_maya` plugin in Maya
- 2. Select the `nxt` menu from the menus at the top of Maya
- 3. Click `Open Editor`
-- Update:
- 1. Download the `nxt_maya` zip from the [latest release](https://github.com/nxt-dev/nxt_editor/releases/latest)
- 2. Extract the zip and replace the existing `nxt_maya` files with the newly extracted files.
- 3. Re-launch Maya
+Each one contains a `README.md` inside to explain how to install/update them.
+- [nxt_maya](nxt_editor/integration/maya/README.md)
+- [nxt_blender](nxt_editor/integration/blender/README.md)
+- [nxt_unreal](nxt_editor/integration/unreal/README.md)
diff --git a/build/make_maya_plugin.nxt b/build/make_maya_plugin.nxt
index f29d579..e9847b6 100644
--- a/build/make_maya_plugin.nxt
+++ b/build/make_maya_plugin.nxt
@@ -170,7 +170,11 @@
"os.makedirs('${mod_folder}/scripts/Qt')",
"",
"with open('${mod_folder}/scripts/Qt/__init__.py', 'w+') as fp:",
- " fp.write(result.content)"
+ " if isinstance(result.content, str):",
+ " fp.write(result.content)",
+ " else:",
+ " fp.write(result.content.decode())",
+ ""
]
}
}
diff --git a/build/make_unreal_plugin.nxt b/build/make_unreal_plugin.nxt
new file mode 100644
index 0000000..6e02431
--- /dev/null
+++ b/build/make_unreal_plugin.nxt
@@ -0,0 +1,60 @@
+{
+ "version": "1.17",
+ "alias": "make_unreal_plugin",
+ "color": "#879fda",
+ "mute": false,
+ "solo": false,
+ "meta_data": {
+ "positions": {
+ "/make_plugin": [
+ -144.0,
+ -49.0
+ ]
+ }
+ },
+ "nodes": {
+ "/": {
+ "code": [
+ "import os",
+ "import shutil"
+ ]
+ },
+ "/make_plugin": {
+ "start_point": true,
+ "attrs": {
+ "icon_path": {
+ "type": "raw",
+ "value": "${file::../nxt_editor/resources/icons/nxt_128.png}"
+ },
+ "result_dir": {
+ "type": "raw",
+ "value": "${path::nxt_unreal}"
+ },
+ "unreal_integration_dir": {
+ "type": "raw",
+ "value": "${file::../nxt_editor/integration/unreal}"
+ },
+ "uplugin_path": {
+ "type": "raw",
+ "value": "${file::${unreal_integration_dir}/nxt_unreal.uplugin}"
+ }
+ },
+ "code": [
+ "if os.path.exists('${result_dir}'):",
+ " shutil.rmtree('${result_dir}')",
+ "shutil.copytree('${unreal_integration_dir}', '${result_dir}')",
+ "resources_dir = '${result_dir}/Resources'",
+ "os.makedirs(resources_dir)",
+ "target_icon_path = os.path.join(resources_dir, 'Icon128.png')",
+ "shutil.copyfile('${icon_path}', target_icon_path)",
+ "leftover_init = os.path.join(self.result_dir, '__init__.py')",
+ "leftover_pycache = os.path.join(self.result_dir, '__pycache__')",
+ "os.remove(leftover_init)",
+ "try:",
+ " shutil.rmtree(leftover_pycache)",
+ "except:",
+ " pass"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/build/release.nxt b/build/release.nxt
index e21a2dc..be60d4d 100644
--- a/build/release.nxt
+++ b/build/release.nxt
@@ -5,6 +5,7 @@
"mute": false,
"solo": false,
"references": [
+ "make_unreal_plugin.nxt",
"make_maya_plugin.nxt",
"make_blender_plugin.nxt",
"../../nxt/build/release.nxt"
@@ -23,8 +24,8 @@
},
"positions": {
"/CreateRelease": [
- 2460.0,
- 60.0
+ 2861.9022362347496,
+ 69.87218527196421
],
"/GitClone": [
-1000.0,
@@ -59,8 +60,8 @@
420.0
],
"/ReleaseLoop": [
- 2420.0,
- 0.0
+ 2824.2201296032613,
+ 7.554291903452558
],
"/build_maya_plugin": [
1800.0,
@@ -78,6 +79,10 @@
1820.0,
0.0
],
+ "/make_plugin": [
+ 2465.5358761299212,
+ 1.3083409543444589
+ ],
"/versions": [
140.0,
0.0
@@ -85,6 +90,10 @@
"/zip_blender_addon": [
2140.0,
80.0
+ ],
+ "/zip_blender_addon2": [
+ 2460.556716567386,
+ 151.71085853406586
]
},
"collapse": {
@@ -128,6 +137,7 @@
"DraftRelease",
"UploadBlenderAddon",
"UploadMayaPlugin",
+ "UploadUnrealPlugin",
"OpenReleaseURL"
]
},
@@ -157,8 +167,21 @@
}
}
},
+ "/CreateRelease/UploadUnrealPlugin": {
+ "instance": "/GitUpload",
+ "attrs": {
+ "asset_path": {
+ "type": "raw",
+ "value": "${/make_plugin/zip_unreal_plugin.zip_path}"
+ },
+ "content_type": {
+ "type": "raw",
+ "value": "application/zip"
+ }
+ }
+ },
"/ReleaseLoop": {
- "execute_in": "/make_addon",
+ "execute_in": "/make_plugin",
"attrs": {
"release_types": {
"type": "tuple",
@@ -224,6 +247,34 @@
"shutil.rmtree('${mod_folder}')"
]
},
+ "/make_plugin": {
+ "start_point": false,
+ "execute_in": "/make_addon",
+ "child_order": [
+ "zip_unreal_plugin"
+ ],
+ "attrs": {
+ "result_dir": {
+ "value": "${path::${release_dir}/nxt_unreal}"
+ }
+ }
+ },
+ "/make_plugin/zip_unreal_plugin": {
+ "attrs": {
+ "zip_name": {
+ "type": "raw",
+ "value": "${result_dir}"
+ },
+ "zip_path": {
+ "type": "raw",
+ "value": "${zip_name}.zip"
+ }
+ },
+ "code": [
+ "self.zip_path = shutil.make_archive('${zip_name}', 'zip', '${result_dir}')",
+ "shutil.rmtree('${result_dir}')"
+ ]
+ },
"/versions": {
"attrs": {
"EDITOR": {
diff --git a/nxt_editor/__init__.py b/nxt_editor/__init__.py
index c96437e..a9d778a 100644
--- a/nxt_editor/__init__.py
+++ b/nxt_editor/__init__.py
@@ -78,6 +78,7 @@ def make_resources(qrc_path=None, result_path=None):
from nxt_editor import qresources
except ImportError:
make_resources()
+ from nxt_editor import qresources
def _new_qapp():
diff --git a/nxt_editor/connection_graphics_item.py b/nxt_editor/connection_graphics_item.py
index a68df04..526a621 100644
--- a/nxt_editor/connection_graphics_item.py
+++ b/nxt_editor/connection_graphics_item.py
@@ -10,6 +10,7 @@
from . import colors
from nxt import nxt_path, nxt_node
import nxt_editor
+from nxt_editor.node_graphics_item import MIN_LOD
logger = logging.getLogger(nxt_editor.LOGGER_NAME)
@@ -98,9 +99,19 @@ def rebuild_line(self):
self.update()
def paint(self, painter, option, widget):
- painter.setRenderHints(QtGui.QPainter.Antialiasing |
- QtGui.QPainter.SmoothPixmapTransform)
- pen = QtGui.QPen(self.color, self.thickness, self.pen_style)
+ lod = QtWidgets.QStyleOptionGraphicsItem.levelOfDetailFromTransform(
+ painter.worldTransform())
+ if lod > MIN_LOD:
+ painter.setRenderHints(QtGui.QPainter.Antialiasing |
+ QtGui.QPainter.SmoothPixmapTransform)
+ thick_mult = 1
+ pen_style = self.pen_style
+ else:
+ painter.setRenderHints(False)
+ thick_mult = 3
+ pen_style = QtCore.Qt.PenStyle.SolidLine
+ pen = QtGui.QPen(self.color, self.thickness * thick_mult,
+ self.pen_style)
# if self.tgt_path in self.model.selection:
# pen.setColor(colors.SELECTED)
# elif self.is_hovered:
diff --git a/nxt_editor/dockwidgets/code_editor.py b/nxt_editor/dockwidgets/code_editor.py
index 6c7476a..7ed7aef 100644
--- a/nxt_editor/dockwidgets/code_editor.py
+++ b/nxt_editor/dockwidgets/code_editor.py
@@ -319,19 +319,26 @@ def update_editor(self, node_list=()):
# for faster updates. And avoid that early exit check at the top.
get_code = self.stage_model.get_node_code_string
code_string = get_code(self.node_path,
- self.stage_model.data_state,
- self.stage_model.comp_layer)
+ self.stage_model.data_state,
+ self.stage_model.comp_layer)
cached_state = self.stage_model.data_state == DATA_STATE.CACHED
+ self.actual_display_state = DATA_STATE.RAW
if code_string and cached_state:
self.actual_display_state = DATA_STATE.CACHED
elif not code_string and cached_state:
self.actual_display_state = DATA_STATE.RAW
code_string = get_code(self.node_path, DATA_STATE.RAW,
- self.stage_model.comp_layer)
+ self.stage_model.comp_layer)
+ else:
+ self.actual_display_state = self.stage_model.data_state
if self.editing_active:
- self.overlay_widget.main_color = self.overlay_widget.base_color
+ self.overlay_widget.hide()
+ elif self.code_is_local:
+ self.overlay_widget.main_color = None
+ self.overlay_widget.show()
else:
self.overlay_widget.main_color = self.overlay_widget.ext_color
+ self.overlay_widget.show()
self.overlay_widget.update()
self.editor.verticalScrollBar().blockSignals(True)
self.cached_code_lines = code_string.split('\n')
@@ -397,11 +404,6 @@ def update_code_is_local(self):
self.editor.current_line_highlight = not is_local
self.overlay_widget.main_color = self.overlay_widget.base_color
self.update_background()
- if self.editing_active or is_local:
- self.overlay_widget.hide()
- else:
- self.overlay_widget.main_color = self.overlay_widget.ext_color
- self.overlay_widget.show()
self.code_is_local = is_local
def localize_code(self):
@@ -1280,26 +1282,34 @@ def __init__(self, parent=None):
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
self.data_state = ''
+ self.click_msg = 'Double Click To Edit'
def paintEvent(self, event):
painter = QtGui.QPainter()
painter.begin(self)
+ painter.setFont(QtGui.QFont("Roboto", 14))
+ font_metrics = QtGui.QFontMetrics(painter.font())
painter.setRenderHint(QtGui.QPainter.Antialiasing)
# actual_display_state
code_editor = self._parent.ce_widget
model = code_editor.stage_model
self.data_state = code_editor.actual_display_state
painter.setPen(QtCore.Qt.white)
- font_matrics = QtGui.QFontMetrics(painter.font())
- offset = font_matrics.boundingRect(self.data_state).width()
- offset += painter.font().pointSize()
+ # Draw top right data state text
+ offset = font_metrics.boundingRect(self.data_state).width()
+ offset += painter.font().pointSize() * 1.5
painter.drawText(self.rect().right() - offset,
painter.font().pointSize() * 1.5, self.data_state)
+ # Draw center message text
+ msg_offset = font_metrics.boundingRect(self.click_msg).width()
+ msg_offset += painter.font().pointSize()
+ painter.drawText(self.rect().center().x() - (msg_offset*.5),
+ self.rect().center().y(), self.click_msg)
painter.setCompositionMode(QtGui.QPainter.CompositionMode_Darken)
-
path = QtGui.QPainterPath()
path.addRoundedRect(QtCore.QRectF(self.rect()), 9, 9)
- painter.fillPath(path, QtGui.QBrush(self.main_color))
+ if self.main_color:
+ painter.fillPath(path, QtGui.QBrush(self.main_color))
painter.setCompositionMode(QtGui.QPainter.CompositionMode_Screen)
display_is_raw = self.data_state == DATA_STATE.RAW
mode_is_cache = model.data_state == DATA_STATE.CACHED
diff --git a/nxt_editor/dockwidgets/widget_builder.py b/nxt_editor/dockwidgets/widget_builder.py
index 7541916..79bde18 100644
--- a/nxt_editor/dockwidgets/widget_builder.py
+++ b/nxt_editor/dockwidgets/widget_builder.py
@@ -1,6 +1,10 @@
# Built-in
import logging
import ast
+try:
+ from collections.abc import Iterable
+except ImportError:
+ from collections import Iterable
# External
from Qt import QtWidgets
@@ -18,6 +22,7 @@
from nxt_editor import user_dir
from nxt_editor.dockwidgets.dock_widget_base import DockWidgetBase
from nxt import DATA_STATE, nxt_path
+from nxt import nxt_node
logger = logging.getLogger(nxt_editor.LOGGER_NAME)
@@ -39,6 +44,7 @@ def __init__(self, graph_model=None, title='Workflow Tools', parent=None,
minimum_width=minimum_width,
minimum_height=minimum_height)
+ self.updating = False
self.setObjectName('Workflow Tools')
self.default_title = title
@@ -262,6 +268,8 @@ def get_window_title(self):
return title or None
def update_window(self, changed_paths=None):
+ if self.updating:
+ return
if not self.isVisible():
return
@@ -271,7 +279,9 @@ def update_window(self, changed_paths=None):
return
update = False if changed_paths else True
- for path in (changed_paths or []):
+ if not isinstance(changed_paths, Iterable):
+ changed_paths = []
+ for path in changed_paths:
node_path, _ = nxt_path.path_attr_partition(path)
is_widget = get_widget_type(node_path, self.stage_model)
if is_widget:
@@ -285,7 +295,7 @@ def update_window(self, changed_paths=None):
break
if not update:
return
-
+ self.updating = True
# window title
title = self.get_window_title()
self.setWindowTitle(title or self.default_title)
@@ -310,6 +320,7 @@ def update_window(self, changed_paths=None):
widget=self.window_frame,
items=None,
parent=self)
+ self.updating = False
def set_window_style(self):
background_color = self.stage_model.get_node_attr_value(
@@ -881,15 +892,22 @@ def execute_node_path(self, node_path, attr_name):
# filter out widget nodes
filtered_nodes = []
- widget_descendants = []
+ dont_run = []
for path in descendants:
widget_type = get_widget_type(path, self.stage_model)
if widget_type:
des = self.stage_model.comp_layer.descendants(path,
ordered=True)
- widget_descendants += des
- if not widget_type and path not in widget_descendants:
- filtered_nodes.append(path)
+ dont_run += des
+ if not widget_type and path not in dont_run:
+ enabled = self.stage_model.get_node_enabled(path)
+ anc_enabled = self.stage_model.get_node_ancestor_enabled(path)
+ if enabled and anc_enabled:
+ filtered_nodes.append(path)
+ else:
+ des = self.stage_model.comp_layer.descendants(path,
+ ordered=True)
+ dont_run += [path] + des
node_paths = [exec_path] + filtered_nodes
if user_dir.user_prefs.get(RECOMP_PREF, True):
diff --git a/nxt_editor/integration/__init__.py b/nxt_editor/integration/__init__.py
index 020fa48..5b768b2 100644
--- a/nxt_editor/integration/__init__.py
+++ b/nxt_editor/integration/__init__.py
@@ -1,5 +1,6 @@
import os
-import shutil
+import sys
+import subprocess
import importlib
@@ -32,29 +33,41 @@ def _safe_import_package(package_name, global_name=None):
def _install_and_import_package(self, module_name, package_name=None,
global_name=None):
+ """Calls a subprocess to pip install the given package name and then
+ attempts to import the new package.
+
+ :param module_name: Desired module to import after install
+ :param package_name: pip package name
+ :param global_name: Global name to access the module if different
+ than the module name.
+ :raises: subprocess.CalledProcessError
+ :return: bool
+ """
if package_name is None:
package_name = module_name
if global_name is None:
global_name = module_name
- args = ['install', package_name]
- self.ensure_pip()
- import pip
- if hasattr(pip, 'main'):
- pip.main(['install', package_name])
- else:
- pip._internal.main(['install', package_name])
+ environ_copy = dict(os.environ)
+ environ_copy["PYTHONNOUSERSITE"] = "1"
+ subprocess.run([sys.executable, "-m", "pip", "install",
+ package_name], check=True, env=environ_copy)
+
success = self._safe_import_package(package_name=package_name,
global_name=global_name)
return success
- def _update_package(self, package_name):
- args = ['install', '-U', package_name]
- self.ensure_pip()
- import pip
- if hasattr(pip, 'main'):
- pip.main(args)
- else:
- pip._internal.main(args)
+ @staticmethod
+ def _update_package(package_name):
+ """Calls a subprocess to pip update the given package name.
+
+ :param package_name: pip package name
+ :raises: subprocess.CalledProcessError
+ :return: None
+ """
+ environ_copy = dict(os.environ)
+ environ_copy["PYTHONNOUSERSITE"] = "1"
+ subprocess.run([sys.executable, "-m", "pip", "install", "-U",
+ package_name], check=True, env=environ_copy)
print('Please restart your DCC or Python interpreter')
def check_for_nxt_core(self, install=False):
@@ -81,51 +94,39 @@ def check_for_nxt_editor(self, install=False):
print('Failed to import and/or install nxt-editor')
return success
- @staticmethod
- def ensure_pip():
- try:
- import pip
- except ImportError:
- import ensurepip
- ensurepip.bootstrap()
- os.environ.pop('PIP_REQ_TRACKER', None)
-
def update(self):
if self.check_for_nxt_core():
self._update_package('nxt-core')
if self.check_for_nxt_editor():
self._update_package('nxt-editor')
+ @staticmethod
+ def _uninstall_package(package_name):
+ """Calls a subprocess to pip uninstall the given package name. Will
+ NOT prompt the user to confrim uninstall.
+
+ :param package_name: pip package name
+ :raises: subprocess.CalledProcessError
+ :return: None
+ """
+ environ_copy = dict(os.environ)
+ environ_copy["PYTHONNOUSERSITE"] = "1"
+ subprocess.run([sys.executable, "-m", "pip", "uninstall",
+ package_name, '-y'], check=True, env=environ_copy)
+
+ def uninstall(self):
+ if self.check_for_nxt_core():
+ self._uninstall_package('nxt-core')
+ if self.check_for_nxt_editor():
+ self._uninstall_package('nxt-editor')
+ # print('Please restart your DCC or Python interpreter')
-class Blender(NxtIntegration):
- def __init__(self):
- super(Blender, self).__init__(name='blender')
- import bpy
- b_major, b_minor, b_patch = bpy.app.version
- if b_major != 2 or b_minor < 80:
- raise RuntimeError('Blender version is not compatible with this '
- 'version of nxt.')
- user_dir = os.path.expanduser('~/AppData/Roaming/Blender '
- 'Foundation/Blender/'
- '{}.{}'.format(b_major, b_minor))
- self.user_dir = user_dir
- nxt_modules = os.path.join(user_dir, 'scripts/addons/modules')
- self.modules_dir = nxt_modules.replace(os.sep, '/')
+ def launch_nxt(self):
+ raise NotImplementedError('Your DCC needs it own nxt launch method.')
- @classmethod
- def setup(cls):
- self = cls()
- import bpy
- bpy.ops.preferences.addon_disable(module='nxt_' + self.name)
- addons_dir = os.path.join(self.user_dir, 'scripts/addons')
- integration_filepath = self.get_integration_filepath()
- shutil.copy(integration_filepath, addons_dir)
- bpy.ops.preferences.addon_enable(module='nxt_' + self.name)
+ def quit_nxt(self):
+ raise NotImplementedError('Your DCC needs it own nxt quit method.')
- @classmethod
- def update(cls):
- self = cls()
- og_cwd = os.getcwd()
- os.chdir(self.modules_dir)
- super(Blender, self).update()
- os.chdir(og_cwd)
+ def create_context(self):
+ raise NotImplementedError('Your DCC needs it own method of creating a '
+ 'context.')
diff --git a/nxt_editor/integration/blender/README.md b/nxt_editor/integration/blender/README.md
index e7b2c04..d9215be 100644
--- a/nxt_editor/integration/blender/README.md
+++ b/nxt_editor/integration/blender/README.md
@@ -4,22 +4,54 @@ This is a Blender addon for nxt. Note that it will access the internet to instal
### By hand (if you're familiar with pip)
1. Locate the path to blenders Python interpreter
- - In Blender, you can run `sys.exec_prefix` to find the folder containing the Python executable.
+ - In Blender, you can run `sys.exec_prefix` to find the folder containing the Python executable
2. Open Terminal or CMD (If you're using Windows)
-3. Run: `/path/to/python.exe -m pip install nxt-editor`
+3. Run: `/path/to/blender/python.exe -m pip install nxt-editor`
4. Start Blender
5. Open the addon manager (Edit > Preferences > Add-ons)
-6. Click "Install" and select the `nxt_blender.py` file provided with this addon zip.
-7. To launch NXT navigate the newly created `NXT` menu and select `Open Editor`.
+6. Click "Install" and select the `nxt_blender.py` file provided with this addon zip
+7. To launch NXT navigate the newly created `NXT` menu and select `Open Editor`
-_Note: If you install NXT Blender this way the "Update" button may not work in the NXT menu._
### Automated
-1. Open Blender's script editor.
-2. Drag and drop `blender_installer.py` into the script editor and click the play button or press `Alt+P` (default run script hotkey)
-3. The installation may take a minute or two, during this time Blender will be unresponsive.
- - Optionally open the System Console window before running the script, so you can see what's happening.
+1. Open the addon manager (Edit > Preferences > Add-ons)
+2. Click "Install" and select the `nxt_blender.py` file provided with this addon zip
+3. Enable the `NXT Blender` and twirl down the addon preferences
+3. Click `Install NXT dependencies`
+ - The installation may take a minute or two, during this time Blender will be unresponsive
+ - Optionally open the console window before running the script, so you can see what's happening
+4. Restart Blender
# Usage
-- Ensure the `NXT Blender` addon is enabled.
-- To launch NXT navigate the newly created `NXT` menu and select `Open Editor`.
\ No newline at end of file
+- Ensure the `NXT Blender` addon is enabled
+- To launch NXT navigate the newly created `NXT` menu and select `Open Editor`
+
+# Updating
+### By hand (if you're familiar with pip)
+1. In terminal or cmd run: `/path/to/blender/python.exe -m pip install -U nxt-editor nxt`
+2. Restart Blender
+
+### Automated
+1. Open the addon manager (Edit > Preferences > Add-ons)
+3. Locate the `NXT Blender` and twirl down the addon preferences
+3. Click `Update NXT dependencies`
+4. Restart Blender
+
+_or_
+
+1. Navigate to the NXT menu
+2. Click `Update NXT`
+3. Restart Blender
+
+# Uninstall
+### By hand (if you're familiar with pip)
+1. Open the addon manager (Edit > Preferences > Add-ons)
+2. Locate the `NXT Blender` and twirl down the addon preferences
+3. Click 'Remove'
+1. In terminal or cmd run: `/path/to/blender/python.exe -m pip uninstall nxt-editor nxt -y`
+
+### Automated
+1. Open the addon manager (Edit > Preferences > Add-ons)
+3. Locate the `NXT Blender` and twirl down the addon preferences
+3. Click `Uninstall NXT dependencies`
+3. When that finishes, click the 'Remove' button
\ No newline at end of file
diff --git a/nxt_editor/integration/blender/__init__.py b/nxt_editor/integration/blender/__init__.py
new file mode 100644
index 0000000..09f83bb
--- /dev/null
+++ b/nxt_editor/integration/blender/__init__.py
@@ -0,0 +1,96 @@
+# Builtin
+import os
+import shutil
+import sys
+import atexit
+
+# External
+import bpy
+from Qt import QtCore, QtWidgets
+
+# Internal
+from nxt.constants import NXT_DCC_ENV_VAR
+from nxt_editor.integration import NxtIntegration
+import nxt_editor
+
+__NXT_INTEGRATION__ = None
+
+
+class Blender(NxtIntegration):
+ def __init__(self):
+ super(Blender, self).__init__(name='blender')
+ b_major, b_minor, b_patch = bpy.app.version
+ if b_major != 2 or b_minor < 80:
+ raise RuntimeError('Blender version is not compatible with this '
+ 'version of nxt.')
+ addons_dir = bpy.utils.user_resource('SCRIPTS', 'addons')
+ self.addons_dir = addons_dir.replace(os.sep, '/')
+ self.instance = None
+ self.nxt_qapp = QtWidgets.QApplication.instance()
+
+ @classmethod
+ def setup(cls):
+ self = cls()
+ bpy.ops.preferences.addon_disable(module='nxt_' + self.name)
+ integration_filepath = self.get_integration_filepath()
+ shutil.copy(integration_filepath, self.addons_dir)
+ bpy.ops.preferences.addon_enable(module='nxt_' + self.name)
+
+ @classmethod
+ def update(cls):
+ self = cls()
+ og_cwd = os.getcwd()
+ super(Blender, self).update()
+ os.chdir(og_cwd)
+ addon_file = os.path.join(os.path.dirname(__file__), 'nxt_blender.py')
+ shutil.copy(addon_file, self.addons_dir)
+
+ @classmethod
+ def launch_nxt(cls):
+ self = cls()
+ os.environ[NXT_DCC_ENV_VAR] = 'blender'
+ global __NXT_INTEGRATION__
+ if not __NXT_INTEGRATION__:
+ __NXT_INTEGRATION__ = self
+ else:
+ self = __NXT_INTEGRATION__
+ if self.instance:
+ self.instance.show()
+ return
+ if not self.nxt_qapp:
+ self.nxt_qapp = nxt_editor._new_qapp()
+ nxt_win = nxt_editor.show_new_editor(start_rpc=False)
+ else:
+ nxt_win = nxt_editor.show_new_editor(start_rpc=False)
+ if 'win32' in sys.platform:
+ # gives nxt it's own entry on taskbar
+ nxt_win.setWindowFlags(QtCore.Qt.Window)
+
+ def unregister_nxt():
+ self.instance = None
+ if self.nxt_qapp:
+ self.nxt_qapp.quit()
+ self.nxt_qapp = None
+
+ nxt_win.close_signal.connect(unregister_nxt)
+ nxt_win.show()
+ atexit.register(nxt_win.close)
+ self.instance = nxt_win
+ return self
+
+ def quit_nxt(self):
+ if self.instance:
+ self.instance.close()
+ atexit.unregister(self.instance.close)
+ if self.nxt_qapp:
+ self.nxt_qapp.quit()
+ global __NXT_INTEGRATION__
+ __NXT_INTEGRATION__ = None
+
+ def create_context(self):
+ placeholder_txt = 'Blender {}.{}'.format(*bpy.app.version)
+ args = ['-noaudio', '--background', '--python']
+ self.instance.create_remote_context(placeholder_txt,
+ interpreter_exe=bpy.app.binary_path,
+ exe_script_args=args)
+
diff --git a/nxt_editor/integration/blender/blender_installer.py b/nxt_editor/integration/blender/blender_installer.py
deleted file mode 100644
index 878e601..0000000
--- a/nxt_editor/integration/blender/blender_installer.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import os
-import sys
-
-try:
- import pip
- _ = pip.main
- _ = pip._internal.main
-
-except (ImportError, AttributeError):
- import ensurepip
- ensurepip.bootstrap()
- os.environ.pop('PIP_REQ_TRACKER', None)
-
-try:
- import nxt_editor
-except ImportError:
- import bpy
- b_major, b_minor, b_patch = bpy.app.version
- user_dir = os.path.expanduser('~/AppData/Roaming/Blender '
- 'Foundation/Blender/'
- '{}.{}'.format(b_major, b_minor))
- t = os.path.join(user_dir, 'scripts/addons/modules')
- t = t.replace(os.sep, '/')
- args = ['install', 'D:/Projects/nxt_editor', '--target', t]
- if hasattr(pip, 'main'):
- pip.main(args)
- else:
- pip._internal.main(args)
- # The next time blender starts it will see this path automatically
- cwd = os.getcwd()
- os.chdir(t)
- sys.path.append(t)
- os.chdir(cwd)
-
-# Install nxt_blender addon
-from nxt_editor import integration
-integration.Blender.setup()
diff --git a/nxt_editor/integration/blender/nxt_blender.py b/nxt_editor/integration/blender/nxt_blender.py
index 7e43d8d..f99857c 100644
--- a/nxt_editor/integration/blender/nxt_blender.py
+++ b/nxt_editor/integration/blender/nxt_blender.py
@@ -1,28 +1,41 @@
+"""
+Loosely based on the example addon from this repo:
+https://github.com/robertguetzkow/blender-python-examples
+"""
# Builtin
import os
import sys
+import subprocess
# External
-from Qt import QtCore, QtWidgets
import bpy
-# Internal
-from nxt.constants import NXT_DCC_ENV_VAR
-from nxt_editor.constants import NXT_WEBSITE
-import nxt_editor.main_window
-import nxt_editor
-os.environ[NXT_DCC_ENV_VAR] = 'blender'
+try:
+ # External
+ from Qt import QtCore, QtWidgets
+ # Internal
+ from nxt_editor.constants import NXT_WEBSITE
+ from nxt_editor.integration import blender
+ nxt_installed = True
+except ImportError:
+ nxt_installed = False
+ NXT_WEBSITE = 'https://nxt-dev.github.io/'
+
+nxt_package_name = 'nxt-editor'
bl_info = {
"name": "NXT Blender",
"blender": (2, 80, 0),
- "version": (0, 1, 0),
+ "version": (0, 2, 0),
"location": "NXT > Open Editor",
"wiki_url": "https://nxt-dev.github.io/",
"tracker_url": "https://github.com/nxt-dev/nxt_editor/issues",
"category": "nxt",
- "warning": "This is an experimental version of nxt_blender. Save early, "
- "save often."
+ "description": "NXT is a general purpose code compositor designed for "
+ "rigging, scene assembly, and automation. (This is an "
+ "experimental version of nxt_blender. Save "
+ "early, save often.)",
+ "warning": "This addon requires installation of dependencies."
}
@@ -38,45 +51,32 @@ class BLENDER_PLUGIN_VERSION(object):
VERSION = VERSION_STR
-__NXT_INSTANCE__ = None
-__NXT_CREATED_QAPP__ = None
-
-
-class OpenNxtEditor(bpy.types.Operator):
- bl_label = "Open NXT Editor"
- bl_idname = "nxt.nxt_editor"
+class CreateBlenderContext(bpy.types.Operator):
+ bl_label = "Create Remote Blender NXT Context"
+ bl_idname = "nxt.create_blender_context"
def execute(self, context):
- global __NXT_INSTANCE__
- global __NXT_CREATED_QAPP__
- if __NXT_INSTANCE__:
- __NXT_INSTANCE__.show()
- return
- if not __NXT_CREATED_QAPP__:
- nxt_win = nxt_editor.launch_editor()
+ global nxt_installed
+ if nxt_installed:
+ b = blender.__NXT_INTEGRATION__
+ if not b:
+ b = blender.Blender.launch_nxt()
+ b.create_context()
else:
- nxt_win = nxt_editor.show_new_editor()
- if 'win32' in sys.platform:
- # gives nxt it's own entry on taskbar
- nxt_win.setWindowFlags(QtCore.Qt.Window)
-
- def unregister_nxt():
- global __NXT_INSTANCE__
- __NXT_INSTANCE__ = None
-
- nxt_win.close_signal.connect(unregister_nxt)
- nxt_win.show()
- __NXT_INSTANCE__ = nxt_win
+ show_dependency_warning()
return {'FINISHED'}
-class UpdateNxt(bpy.types.Operator):
- bl_label = "Update NXT"
- bl_idname = "nxt.nxt_update"
+class OpenNxtEditor(bpy.types.Operator):
+ bl_label = "Open NXT Editor"
+ bl_idname = "nxt.nxt_editor"
def execute(self, context):
- import nxt_editor.integration
- nxt_editor.integration.Blender.update()
+ global nxt_installed
+ if nxt_installed:
+ blender.Blender.launch_nxt()
+ else:
+ show_dependency_warning()
return {'FINISHED'}
@@ -97,8 +97,11 @@ def draw(self, context):
layout = self.layout
layout.operator("nxt.nxt_editor", text="Open Editor")
layout.separator()
- layout.operator("nxt.nxt_update", text="Update NXT (Requires Blender "
- "Restart)")
+ layout.operator("nxt.nxt_update_dependencies",
+ text="Update NXT (Requires Blender Restart)")
+ layout.separator()
+ layout.operator('nxt.create_blender_context', text='Create Blender '
+ 'Context')
layout.separator()
layout.operator("nxt.nxt_about", text="About")
@@ -106,34 +109,147 @@ def menu_draw(self, context):
self.layout.menu("TOPBAR_MT_nxt")
-nxt_menu_operators = (TOPBAR_MT_nxt, OpenNxtEditor)
+class NxtInstallDependencies(bpy.types.Operator):
+ bl_idname = 'nxt.nxt_install_dependencies'
+ bl_label = "Install NXT dependencies"
+ bl_description = ("Downloads and installs the required python packages "
+ "for NXT. Internet connection is required. "
+ "Blender may have to be started with elevated "
+ "permissions in order to install the package. "
+ "Alternatively you can pip install nxt-editor into your "
+ "Blender Python environment.")
+ bl_options = {"REGISTER", "INTERNAL"}
+
+ @classmethod
+ def poll(cls, context):
+ global nxt_installed
+ return not nxt_installed
+
+ def execute(self, context):
+ success = False
+ environ_copy = dict(os.environ)
+ environ_copy["PYTHONNOUSERSITE"] = "1"
+ pkg = 'nxt-editor'
+ try:
+ subprocess.run([sys.executable, "-m", "pip", "install", pkg],
+ check=True, env=environ_copy)
+ except subprocess.CalledProcessError as e:
+ self.report({"ERROR"}, str(e))
+ return {"CANCELLED"}
+ if not success:
+ self.report({"INFO"}, 'Please restart Blender to '
+ 'finish installing NXT.')
+ return {"FINISHED"}
+
+
+class NxtUpdateDependencies(bpy.types.Operator):
+ bl_idname = 'nxt.nxt_update_dependencies'
+ bl_label = "Update NXT dependencies"
+ bl_description = ("Downloads and updates the required python packages "
+ "for NXT. Internet connection is required. "
+ "Blender may have to be started with elevated "
+ "permissions in order to install the package. "
+ "Alternatively you can pip install -U nxt-editor into "
+ "your Blender Python environment.")
+ bl_options = {"REGISTER", "INTERNAL"}
+
+ @classmethod
+ def poll(cls, context):
+ global nxt_installed
+ return nxt_installed
+
+ def execute(self, context):
+ try:
+ blender.Blender._update_package('nxt-editor')
+ except subprocess.CalledProcessError as e:
+ self.report({"ERROR"}, str(e))
+ return {"CANCELLED"}
+ self.report({"INFO"}, 'Please restart Blender to '
+ 'finish updating NXT.')
+ return {"FINISHED"}
+
+
+class NxtUninstallDependencies(bpy.types.Operator):
+ bl_idname = 'nxt.nxt_uninstall_dependencies'
+ bl_label = "Uninstall NXT dependencies"
+ bl_description = ("Uninstalls the NXT Python packages. "
+ "Blender may have to be started with elevated "
+ "permissions in order to install the package. "
+ "Alternatively you can pip uninstall nxt-editor from "
+ "your Blender Python environment.")
+ bl_options = {"REGISTER", "INTERNAL"}
+
+ @classmethod
+ def poll(cls, context):
+ global nxt_installed
+ return nxt_installed
+
+ def execute(self, context):
+ try:
+ blender.Blender().uninstall()
+ except subprocess.CalledProcessError as e:
+ self.report({"ERROR"}, str(e))
+ return {"CANCELLED"}
+ self.report({"INFO"}, 'Please restart Blender to '
+ 'finish uninstalling NXT dependencies.')
+ return {"FINISHED"}
+
+
+class NxtDependenciesPreferences(bpy.types.AddonPreferences):
+ bl_idname = __name__
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator(NxtInstallDependencies.bl_idname, icon="PLUGIN")
+ layout.operator(NxtUpdateDependencies.bl_idname, icon="SCRIPT")
+ layout.operator(NxtUninstallDependencies.bl_idname, icon="PANEL_CLOSE")
+
+
+def show_dependency_warning():
+
+ def draw(self, context):
+ layout = self.layout
+ lines = [
+ f"Please install the missing dependencies for the NXT add-on.",
+ "1. Open the preferences (Edit > Preferences > Add-ons).",
+ f"2. Search for the \"{bl_info.get('name')}\" add-on.",
+ "3. Open the details section of the add-on.",
+ f"4. Click on the \"{NxtInstallDependencies.bl_label}\" button.",
+ "This will download and install the missing Python packages. "
+ "You man need to start Blender with elevated permissions",
+ f"Alternatively you can pip install \"{nxt_package_name}\" into "
+ f"your Blender Python environment."
+ ]
+
+ for line in lines:
+ layout.label(text=line)
+ bpy.context.window_manager.popup_menu(draw, title='NXT Warning!',
+ icon="ERROR")
+
+
+nxt_operators = (TOPBAR_MT_nxt, OpenNxtEditor, NxtUpdateDependencies,
+ NxtUninstallDependencies, NxtDependenciesPreferences,
+ NxtInstallDependencies, CreateBlenderContext)
def register():
- global __NXT_CREATED_QAPP__
- existing = QtWidgets.QApplication.instance()
- if existing:
- __NXT_CREATED_QAPP__ = False
- else:
- __NXT_CREATED_QAPP__ = True
- nxt_editor._new_qapp()
- bpy.utils.register_class(TOPBAR_MT_nxt)
- bpy.utils.register_class(OpenNxtEditor)
+ global nxt_installed
+ for cls in nxt_operators:
+ bpy.utils.register_class(cls)
bpy.utils.register_class(AboutNxt)
- bpy.utils.register_class(UpdateNxt)
bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR_MT_nxt.menu_draw)
def unregister():
- global __NXT_CREATED_QAPP__
- if __NXT_CREATED_QAPP__:
- QtWidgets.QApplication.instance().quit()
- __NXT_CREATED_QAPP__ = False
+ try:
+ if blender.__NXT_INTEGRATION__:
+ blender.__NXT_INTEGRATION__.quit_nxt()
+ except NameError:
+ pass
bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR_MT_nxt.menu_draw)
- bpy.utils.unregister_class(TOPBAR_MT_nxt)
- bpy.utils.unregister_class(OpenNxtEditor)
+ for cls in nxt_operators:
+ bpy.utils.unregister_class(cls)
bpy.utils.unregister_class(AboutNxt)
- bpy.utils.unregister_class(UpdateNxt)
if __name__ == "__main__":
diff --git a/nxt_editor/integration/unreal/Content/Python/init_unreal.py b/nxt_editor/integration/unreal/Content/Python/init_unreal.py
new file mode 100644
index 0000000..95e915f
--- /dev/null
+++ b/nxt_editor/integration/unreal/Content/Python/init_unreal.py
@@ -0,0 +1,94 @@
+import os
+import sys
+import unreal
+import subprocess
+
+
+def is_nxt_available():
+ try:
+ from nxt_editor.integration.unreal import launch_nxt_in_ue
+ return True
+ except:
+ return False
+
+def get_python_exc_path():
+ exc_name = 'python'
+ if sys.platform == 'win32':
+ exc_name = 'python.exe'
+
+ real_prefix = os.path.realpath(sys.prefix)
+ return os.path.join(real_prefix, exc_name)
+
+def install_nxt_to_interpreter():
+ subprocess.check_call([get_python_exc_path(), '-m', 'pip',
+ 'install', 'nxt-editor'])
+ unreal.log_warning("Please restart the editor for nxt menu options.")
+
+def update_installed_nxt():
+ subprocess.check_call([get_python_exc_path(), '-m', 'pip',
+ 'install', '--upgrade', 'nxt-editor', 'nxt-core'])
+
+def uninstall_nxt_from_interpreter():
+ subprocess.check_call([get_python_exc_path(), '-m', 'pip',
+ 'uninstall', '-y', 'nxt-editor', 'nxt-core'])
+ unreal.log_warning("Nxt menu will refresh next editor launch.")
+
+def make_open_editor_entry():
+ entry = unreal.ToolMenuEntry(name='Open Editor',
+ type=unreal.MultiBlockType.MENU_ENTRY)
+ entry.set_label('Open Editor')
+ launch_command = "from nxt_editor.integration.unreal import launch_nxt_in_ue; launch_nxt_in_ue()"
+ entry.set_string_command(unreal.ToolMenuStringCommandType.PYTHON, 'Python',
+ string=launch_command)
+ return entry
+
+def make_install_entry():
+ entry = unreal.ToolMenuEntry(name='Install Package',
+ type=unreal.MultiBlockType.MENU_ENTRY)
+ entry.set_label('Install nxt package to active python')
+ entry.set_string_command(unreal.ToolMenuStringCommandType.PYTHON, 'Python',
+ string='install_nxt_to_interpreter()')
+ return entry
+
+def make_update_entry():
+ entry = unreal.ToolMenuEntry(name='Update nxt package',
+ type=unreal.MultiBlockType.MENU_ENTRY)
+ entry.set_label('Update nxt python package')
+ entry.set_string_command(unreal.ToolMenuStringCommandType.PYTHON, 'Python',
+ string='update_installed_nxt()')
+ return entry
+
+def make_uninstall_entry():
+ entry = unreal.ToolMenuEntry(name='Uninstall Package',
+ type=unreal.MultiBlockType.MENU_ENTRY)
+ entry.set_label('Uninstall nxt package from active python')
+ entry.set_string_command(unreal.ToolMenuStringCommandType.PYTHON, 'Python',
+ string='uninstall_nxt_from_interpreter()')
+ return entry
+
+
+def make_or_find_nxt_menu():
+ menus = unreal.ToolMenus.get()
+ nxt_menu = menus.find_menu("LevelEditor.MainMenu.NxtMenu")
+ if nxt_menu:
+ return nxt_menu
+ main_menu = menus.find_menu("LevelEditor.MainMenu")
+ if not main_menu:
+ raise ValueError("Cannot find main menu")
+ nxt_menu = main_menu.add_sub_menu(main_menu.get_name(), "nxt-section",
+ "NxtMenu", "nxt", "The nxt graph editor")
+ return nxt_menu
+
+def refresh_nxt_menu():
+ nxt_menu = make_or_find_nxt_menu()
+ if is_nxt_available():
+ nxt_menu.add_menu_entry("nxt-section", make_open_editor_entry())
+ nxt_menu.add_menu_entry("nxt-section", make_update_entry())
+ nxt_menu.add_menu_entry("nxt-section", make_uninstall_entry())
+ else:
+ nxt_menu.add_menu_entry("nxt-section", make_install_entry())
+ menus = unreal.ToolMenus.get()
+ menus.refresh_all_widgets()
+
+if __name__ == '__main__':
+ refresh_nxt_menu()
diff --git a/nxt_editor/integration/unreal/README.md b/nxt_editor/integration/unreal/README.md
new file mode 100644
index 0000000..d5f9269
--- /dev/null
+++ b/nxt_editor/integration/unreal/README.md
@@ -0,0 +1,18 @@
+# Installation
+**This is an experimental version of nxt_unreal. Save early, save often.**
+This is an Unreal plugin to connect to the nxt python package. The nxt python package will be downloaded from the internet as part of this installation.
+
+1. Move this plugin either into your project or engine's plugin directory.
+2. Ensure that the python editor scripting plugin is activated.
+3. Activate the plugin in the engine plugin browser. Exit the engine.
+4. Install the nxt python package
+ - __automated -__ From unreal editor top menu option "nxt", select "Install nxt package to active python"
+ - __by hand -__ Locate engine python(`sys.prefix`) in commmand prompt or terminal and run: `python -m pip install nxt-editor` (Note on windows this will be `python.exe`)
+4. Restart Editor
+
+# Launch
+From the top menu "nxt", select "Open Editor" to start the nxt editor.
+
+# Update
+- __automated -__ From nxt menu select "Update nxt package"
+- __by hand -__ From command line of engine python run `python -m pip install --upgrade nxt-editor`
diff --git a/nxt_editor/integration/unreal/__init__.py b/nxt_editor/integration/unreal/__init__.py
new file mode 100644
index 0000000..063c64b
--- /dev/null
+++ b/nxt_editor/integration/unreal/__init__.py
@@ -0,0 +1,34 @@
+# Built-in
+import os
+import atexit
+
+# External
+import unreal
+from Qt import QtWidgets
+
+# Internal
+from nxt.constants import NXT_DCC_ENV_VAR
+import nxt_editor
+
+
+global __NXT_WINDOW
+__NXT_WINDOW = None
+
+def launch_nxt_in_ue():
+ os.environ[NXT_DCC_ENV_VAR] = 'unreal'
+ existing = QtWidgets.QApplication.instance()
+ if existing:
+ unreal.log('Found existing QApp')
+ else:
+ unreal.log('Building new QApp for nxt')
+ existing = nxt_editor._new_qapp()
+
+ global __NXT_WINDOW
+ if __NXT_WINDOW:
+ __NXT_WINDOW.show()
+ __NXT_WINDOW.raise_()
+ else:
+ __NXT_WINDOW = nxt_editor.show_new_editor()
+
+ __NXT_WINDOW.close_signal.connect(existing.exit)
+ atexit.register(__NXT_WINDOW.close)
\ No newline at end of file
diff --git a/nxt_editor/integration/unreal/nxt_unreal.uplugin b/nxt_editor/integration/unreal/nxt_unreal.uplugin
new file mode 100644
index 0000000..f933456
--- /dev/null
+++ b/nxt_editor/integration/unreal/nxt_unreal.uplugin
@@ -0,0 +1,17 @@
+{
+ "FileVersion": 3,
+ "Version": 1,
+ "VersionName": "0.1.0",
+ "FriendlyName": "Nxt Unreal",
+ "Description": "Unreal editor connections to the nxt python module.",
+ "Category": "Scripting",
+ "CreatedBy": "The nxt Authors",
+ "CreatedByURL": "https://nxt-dev.github.io",
+ "DocsURL": "https://nxt-dev.github.io/tutorials",
+ "MarketplaceURL": "",
+ "SupportURL": "https://github.com/nxt-dev/nxt_editor/discussions",
+ "CanContainContent": true,
+ "IsBetaVersion": true,
+ "IsExperimentalVersion": false,
+ "Installed": false
+}
\ No newline at end of file
diff --git a/nxt_editor/main_window.py b/nxt_editor/main_window.py
index 8427945..f802a16 100644
--- a/nxt_editor/main_window.py
+++ b/nxt_editor/main_window.py
@@ -327,6 +327,12 @@ def handle_rpc_tailing_signals(self, state):
self.rpc_log_tail.new_text.disconnect(raw_write_func)
self.rpc_log_tail.new_text.disconnect(rich_write_func)
+ def event(self, event):
+ if event.type() == QtCore.QEvent.WindowDeactivate:
+ self._held_keys = []
+ self.zoom_keys_down = False
+ return super(MainWindow, self).event(event)
+
@staticmethod
def set_waiting_cursor(state=True):
if state:
@@ -337,15 +343,15 @@ def set_waiting_cursor(state=True):
@staticmethod
def create_remote_context(place_holder_text='',
interpreter_exe=sys.executable,
- context_graph=None):
+ context_graph=None, exe_script_args=()):
cur_context = nxt.remote.contexts.get_current_context_exe_name()
pop_up = QtWidgets.QDialog()
pop_up.setWindowTitle('Create context for "{}"'.format(cur_context))
v_layout = QtWidgets.QVBoxLayout()
pop_up.setLayout(v_layout)
label = QtWidgets.QPlainTextEdit()
- info = ('Create context for "{}" your host '
- 'python interpreter\n'
+ info = ('Create remote context for your host '
+ 'Python interpreter/DCC\n'
'Type your desired name in the box below '
'and click create.'.format(cur_context))
label.setPlainText(info)
@@ -358,6 +364,7 @@ def create_remote_context(place_holder_text='',
v_layout.addLayout(h_layout)
name = QtWidgets.QLineEdit()
name.setPlaceholderText(str(place_holder_text))
+ name.setText(str(place_holder_text))
create_button = QtWidgets.QPushButton('Create!')
h_layout.addWidget(name)
h_layout.addWidget(create_button)
@@ -366,7 +373,8 @@ def do_create():
try:
nxt.create_context(name.text(),
interpreter_exe=interpreter_exe,
- context_graph=context_graph)
+ context_graph=context_graph,
+ exe_script_args=exe_script_args)
pop_up.close()
except (IOError, NameError) as e:
info = str(e)
diff --git a/nxt_editor/node_graphics_item.py b/nxt_editor/node_graphics_item.py
index e448210..58f160e 100644
--- a/nxt_editor/node_graphics_item.py
+++ b/nxt_editor/node_graphics_item.py
@@ -9,6 +9,7 @@
from Qt import QtWidgets
from Qt import QtGui
from Qt import QtCore
+from PySide2 import __version_info__ as qt_version
# Internal
import nxt_editor
@@ -17,12 +18,23 @@
from . import colors
from nxt.stage import INTERNAL_ATTRS
from .label_edit import NameEditDialog
+from .user_dir import USER_PREF, user_prefs
logger = logging.getLogger(nxt_editor.LOGGER_NAME)
+MIN_LOD = user_prefs.get(USER_PREF.LOD, .4)
-class NodeGraphicsItem(QtWidgets.QGraphicsItem):
+_pyside_version = qt_version
+
+
+if _pyside_version[1] < 11:
+ graphic_type = QtWidgets.QGraphicsItem
+else:
+ graphic_type = QtWidgets.QGraphicsObject
+
+
+class NodeGraphicsItem(graphic_type):
"""The graphics item used to represent nodes in the graph. Contains
instances of NodeGraphicsPlug for each attribute on the associated node.
Contains functionality for arranging children into stacks.
@@ -44,11 +56,12 @@ def __init__(self, model, node_path, view):
# item settings
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
- #self.setCacheMode(QtWidgets.QGraphicsItem.ItemCoordinateCache)
+
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable |
QtWidgets.QGraphicsItem.ItemIsFocusable |
QtWidgets.QGraphicsItem.ItemIsSelectable |
- QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges)
+ QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges |
+ QtWidgets.QGraphicsItem.ItemNegativeZStacksBehindParent)
self.setAcceptHoverEvents(True)
# draw settings
@@ -85,6 +98,99 @@ def __init__(self, model, node_path, view):
# draw node
self.update_from_model()
+ # Setup groups
+ # In
+ self.in_anim_group = QtCore.QParallelAnimationGroup()
+ self.in_anim_group.finished.connect(self.finished_anim)
+ # Out
+ self.out_anim_group = QtCore.QParallelAnimationGroup()
+ self.out_anim_group.finished.connect(self.finished_anim)
+
+ def _setup_anim_properties(self):
+ # Position anim property
+ self.pos_anim = QtCore.QPropertyAnimation(self, b"pos", self)
+ # Set graphics effect
+ effect = QtWidgets.QGraphicsOpacityEffect(self)
+ effect.setOpacity(1)
+ self.setGraphicsEffect(effect)
+ # Opacity anim property
+ self.opacity_anim = QtCore.QPropertyAnimation(effect, b"opacity",
+ effect)
+ # Lower power caching
+ self.setCacheMode(QtWidgets.QGraphicsItem.ItemCoordinateCache)
+
+ def setup_in_anim(self):
+ self._setup_anim_properties()
+ self.in_anim_group.addAnimation(self.pos_anim)
+ self.in_anim_group.addAnimation(self.opacity_anim)
+
+ def setup_out_anim(self):
+ self._setup_anim_properties()
+ self.out_anim_group.addAnimation(self.pos_anim)
+ self.out_anim_group.addAnimation(self.opacity_anim)
+
+ def finished_anim(self):
+ self.setGraphicsEffect(None)
+ self.in_anim_group.clear()
+ self.out_anim_group.clear()
+ self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
+ self.view.update()
+ self.view._animating.remove(self)
+
+ def get_is_animating(self):
+ i = self.in_anim_group.State.Running == self.in_anim_group.state()
+ o = self.out_anim_group.State.Running == self.out_anim_group.state()
+ return i or o
+
+ def anim_into_place(self, end_pos):
+ if self.get_is_animating():
+ return
+ if end_pos == self.pos():
+ return
+ self.view._animating.append(self)
+ if self.view.do_animations:
+ self.setup_in_anim()
+ else:
+ self.setPos(end_pos)
+ self.in_anim_group.finished.emit()
+ return
+
+ self.opacity_anim.setStartValue(0)
+ self.opacity_anim.setEndValue(1)
+ self.opacity_anim.setDuration(80)
+
+ curve = QtCore.QEasingCurve(QtCore.QEasingCurve.OutBack)
+ curve.setAmplitude(.8)
+ self.pos_anim.setEasingCurve(curve)
+
+ self.pos_anim.setDuration(100)
+ self.pos_anim.setEndValue(end_pos)
+ self.in_anim_group.start()
+
+ def anim_out(self):
+ if self.get_is_animating():
+ return
+ self.view._animating.append(self)
+ if self.view.do_animations:
+ self.setup_out_anim()
+ else:
+ self.out_anim_group.finished.emit()
+ return
+ self.setCacheMode(QtWidgets.QGraphicsItem.ItemCoordinateCache)
+ self.opacity_anim.setStartValue(1)
+ self.opacity_anim.setEndValue(0)
+ self.opacity_anim.setDuration(80)
+
+ self.pos_anim.setDuration(80)
+ self.pos_anim.setEasingCurve(QtCore.QEasingCurve.Linear)
+ x_move = self.stack_offset * -1 * .5
+ if not self.parentItem() or not self.parentItem().parentItem():
+ y_move = 0.0
+ else:
+ y_move = (self.parentItem().boundingRect().height() * -1.0) * .5
+ self.pos_anim.setEndValue(QtCore.QPointF(x_move, y_move))
+ self.out_anim_group.start()
+
def update_color(self):
layers = self.model.get_layers_with_opinion(self.node_path)
n_colors = []
@@ -160,7 +266,7 @@ def itemChange(self, change, value):
ml = QtWidgets.QApplication.mouseButtons() == QtCore.Qt.LeftButton
shift = QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ShiftModifier
force_snap = self.view.alignment_actions.snap_action.isChecked()
- if (ml & shift) or force_snap:
+ if (ml & shift) or force_snap and not self.get_is_animating():
value = self.closest_grid_point(value)
return value
@@ -176,16 +282,22 @@ def paint(self, painter, option, widget):
"""Override of QtWidgets.QGraphicsItem paint. Handles all visuals of the Node. Split up into 3
functions for organization.
"""
- painter.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing | QtGui.QPainter.SmoothPixmapTransform)
- self.draw_title(painter)
- self.draw_attributes(painter)
- self.draw_border(painter)
+ lod = QtWidgets.QStyleOptionGraphicsItem.levelOfDetailFromTransform(painter.worldTransform())
+ if lod > MIN_LOD:
+ painter.setRenderHints(QtGui.QPainter.Antialiasing |
+ QtGui.QPainter.TextAntialiasing |
+ QtGui.QPainter.SmoothPixmapTransform)
+ else:
+ painter.setRenderHints(False)
+ self.draw_title(painter, lod)
+ self.draw_attributes(painter, lod)
+ self.draw_border(painter, lod)
def closest_grid_point(self, position):
snapped_pos = self.model.snap_pos_to_grid((position.x(), position.y()))
return QtCore.QPointF(*snapped_pos)
- def draw_border(self, painter):
+ def draw_border(self, painter, lod=1.):
"""Draws border, called exclusively by paint.
:param painter: painter from paint.
@@ -213,7 +325,7 @@ def draw_border(self, painter):
self.get_selection_rect().width() - 2,
self.get_selection_rect().height() - 2))
- def draw_title(self, painter):
+ def draw_title(self, painter, lod=1.):
"""Draw title of the node. Called exclusively in paint.
:param painter: painter from paint.
@@ -307,76 +419,92 @@ def draw_title(self, painter):
if self.exec_out_plug:
self.scene().removeItem(self.exec_out_plug)
self.exec_out_plug = None
-
- # draw attr dots
- offset = -6
- for fill in self.attr_dots:
- painter.setBrush(QtCore.Qt.white)
- if fill:
+ if lod > MIN_LOD:
+ # draw attr dots
+ offset = -6
+ for fill in self.attr_dots:
painter.setBrush(QtCore.Qt.white)
- else:
- painter.setBrush(QtCore.Qt.NoBrush)
- dots_color = QtGui.QColor(QtCore.Qt.white).darker(self.dim_factor)
- painter.setPen(QtGui.QPen(dots_color, 0.5))
- dot_x = self.max_width - 15
- dot_y = (self.title_rect_height / 2) + offset
- painter.drawEllipse(QtCore.QPointF(dot_x, dot_y), 2, 2)
- offset += 6
+ if fill:
+ painter.setBrush(QtCore.Qt.white)
+ else:
+ painter.setBrush(QtCore.Qt.NoBrush)
+ dots_color = QtGui.QColor(QtCore.Qt.white).darker(self.dim_factor)
+ painter.setPen(QtGui.QPen(dots_color, 0.5))
+ dot_x = self.max_width - 15
+ dot_y = (self.title_rect_height / 2) + offset
+ painter.drawEllipse(QtCore.QPointF(dot_x, dot_y), 2, 2)
+ offset += 6
# draw title
- painter.setPen(QtGui.QColor(QtCore.Qt.white).darker(self.dim_factor))
- if not self.node_enabled:
- painter.setPen(QtGui.QColor(QtCore.Qt.white).darker(150))
+
painter.setFont(self.title_font)
title_str = nxt_path.node_name_from_node_path(self.node_path)
font_metrics = QtGui.QFontMetrics(self.title_font)
width = self.max_width - 40
if self.error_list:
width -= 20
- title = font_metrics.elidedText(title_str, QtCore.Qt.ElideRight, width)
- painter.drawText(15, 0, self.max_width - 15, self.title_rect_height,
- QtCore.Qt.AlignVCenter, title)
-
- # draw error
- if self.error_list:
- pos = QtCore.QPointF(self.max_width-45, self.title_rect_height/4)
- error_item = ErrorItem(font=QtGui.QFont('Roboto', 16, 75),
- pos=pos, text='!')
- error_item.setParentItem(self)
- error_item.setZValue(50)
- self.error_item = error_item
+ if lod > MIN_LOD:
+ painter.setPen(
+ QtGui.QColor(QtCore.Qt.white).darker(self.dim_factor))
+ if not self.node_enabled:
+ painter.setPen(QtGui.QColor(QtCore.Qt.white).darker(150))
+ title = font_metrics.elidedText(title_str,
+ QtCore.Qt.ElideRight, width)
+ painter.drawText(15, 0, self.max_width - 15, self.title_rect_height,
+ QtCore.Qt.AlignVCenter, title)
else:
- if self.error_item:
- self.scene().removeItem(self.error_item)
- self.error_item.deleteLater()
- self.error_item = None
+ painter.setBrush(QtGui.QColor(QtCore.Qt.white).darker(self.dim_factor))
+ if not self.node_enabled:
+ painter.setBrush(QtGui.QColor(QtCore.Qt.white).darker(150))
+ proxy_rect = font_metrics.boundingRect(title_str)
+ r_width = proxy_rect.width() * .8
+ height = proxy_rect.height()
+ painter.drawRect(15, height * .8,
+ min(r_width, width), height * .2)
+
+ if lod > MIN_LOD:
+ # draw error
+ if self.error_list:
+ pos = QtCore.QPointF(self.max_width-45, self.title_rect_height/4)
+ error_item = ErrorItem(font=QtGui.QFont('Roboto', 16, 75),
+ pos=pos, text='!')
+ error_item.setParentItem(self)
+ error_item.setZValue(50)
+ self.error_item = error_item
+ else:
+ if self.error_item:
+ self.scene().removeItem(self.error_item)
+ self.error_item.deleteLater()
+ self.error_item = None
+ # draw collapse state arrow
for arrow in self.collapse_arrows:
self.scene().removeItem(arrow)
- self.collapse_arrows = []
- # TODO calculation needed arrows should be done outside drawing
- # draw collapse state arrow
- if self.collapse_state:
- des_colors = self.model.get_descendant_colors(self.node_path)
- filled = self.model.has_children(self.node_path)
- if not filled:
- des_colors = [QtCore.Qt.white]
- elif not des_colors:
- disp = self.model.comp_layer
- des_colors = [self.model.get_node_color(self.node_path, disp)]
- i = 0
- num = len(des_colors)
- for c in des_colors:
- arrow = CollapseArrow(self, filled=filled, color=c)
- arrow_width = arrow.width * 1.1
- center_offset = (arrow_width * (num * .5) - arrow_width * .5)
- cur_offset = (i * arrow_width)
- pos = ((self.max_width * .5) + center_offset - cur_offset)
- arrow.setPos(pos, self.title_rect_height)
- self.collapse_arrows += [arrow]
- i += 1
-
- def draw_attributes(self, painter):
+ if lod > MIN_LOD:
+ self.collapse_arrows = []
+ # TODO calculation needed arrows should be done outside drawing
+
+ if self.collapse_state:
+ des_colors = self.model.get_descendant_colors(self.node_path)
+ filled = self.model.has_children(self.node_path)
+ if not filled:
+ des_colors = [QtCore.Qt.white]
+ elif not des_colors:
+ disp = self.model.comp_layer
+ des_colors = [self.model.get_node_color(self.node_path, disp)]
+ i = 0
+ num = len(des_colors)
+ for c in des_colors:
+ arrow = CollapseArrow(self, filled=filled, color=c)
+ arrow_width = arrow.width * 1.1
+ center_offset = (arrow_width * (num * .5) - arrow_width * .5)
+ cur_offset = (i * arrow_width)
+ pos = ((self.max_width * .5) + center_offset - cur_offset)
+ arrow.setPos(pos, self.boundingRect().height())
+ self.collapse_arrows += [arrow]
+ i += 1
+
+ def draw_attributes(self, painter, lod=1.):
"""Draw attributes for this node. Called exclusively by paint.
:param painter: painter from paint.
@@ -395,44 +523,60 @@ def draw_attributes(self, painter):
self._attr_plug_graphics.setdefault(attr_name, {})
attr_plug_graphics = self._attr_plug_graphics[attr_name]
current_in_plug = attr_plug_graphics.get('in_plug')
- in_pos = self.get_attr_in_pos(attr_name, scene=False)
- if current_in_plug:
- current_in_plug.setPos(in_pos)
- current_in_plug.color = target_color
- current_in_plug.update()
- else:
- in_plug = NodeGraphicsPlug(pos=in_pos,
- radius=self.ATTR_PLUG_RADIUS,
- color=target_color,
- attr_name_represented=attr_name,
- is_input=True)
- attr_plug_graphics['in_plug'] = in_plug
- in_plug.setParentItem(self)
+ if lod > MIN_LOD:
+ in_pos = self.get_attr_in_pos(attr_name, scene=False)
+ if current_in_plug:
+ current_in_plug.show()
+ current_in_plug.setPos(in_pos)
+ current_in_plug.color = target_color
+ current_in_plug.update()
+ else:
+ current_in_plug = NodeGraphicsPlug(pos=in_pos,
+ radius=self.ATTR_PLUG_RADIUS,
+ color=target_color,
+ attr_name_represented=attr_name,
+ is_input=True)
+ attr_plug_graphics['in_plug'] = current_in_plug
+ current_in_plug.setParentItem(self)
+ elif current_in_plug:
+ current_in_plug.hide()
current_out_plug = attr_plug_graphics.get('out_plug')
- out_pos = self.get_attr_out_pos(attr_name, scene=False)
- if current_out_plug:
- current_out_plug.setPos(out_pos)
- current_out_plug.color = target_color
- current_out_plug.update()
- else:
- out_plug = NodeGraphicsPlug(pos=out_pos,
- radius=self.ATTR_PLUG_RADIUS,
- color=target_color,
- attr_name_represented=attr_name,
- is_input=False)
- attr_plug_graphics['out_plug'] = out_plug
- out_plug.setParentItem(self)
+ if lod > MIN_LOD:
+ out_pos = self.get_attr_out_pos(attr_name, scene=False)
+ if current_out_plug:
+ current_out_plug.show()
+ current_out_plug.setPos(out_pos)
+ current_out_plug.color = target_color
+ current_out_plug.update()
+ else:
+ out_plug = NodeGraphicsPlug(pos=out_pos,
+ radius=self.ATTR_PLUG_RADIUS,
+ color=target_color,
+ attr_name_represented=attr_name,
+ is_input=False)
+ attr_plug_graphics['out_plug'] = out_plug
+ out_plug.setParentItem(self)
+ elif current_out_plug:
+ current_out_plug.hide()
# draw attr_name
- painter.setPen(attr_details['title_color'])
- painter.setFont(attr_details['title_font'])
rect = attr_details['bg_rect']
+ painter.setFont(attr_details['title_font'])
font_metrics = QtGui.QFontMetrics(self.attr_font)
title = font_metrics.elidedText(attr_name, QtCore.Qt.ElideRight,
self.max_width - 20)
- painter.drawText(rect.x() + 10, rect.y() - 1, rect.width(),
- rect.height(), QtCore.Qt.AlignVCenter, title)
+ if lod > MIN_LOD:
+ painter.setPen(attr_details['title_color'])
+ painter.drawText(rect.x() + 10, rect.y() - 1, rect.width(),
+ rect.height(), QtCore.Qt.AlignVCenter, title)
+ else:
+ proxy_rect = font_metrics.boundingRect(title)
+ height = proxy_rect.height()
+ width = proxy_rect.width()
+ painter.setBrush(attr_details['title_color'].darker(150))
+ painter.drawRect(rect.x() + 10, rect.y() + height*.8,
+ width, height*.2)
def calculate_attribute_draw_details(self):
"""Calculate position of all known attr names. Details stored in
@@ -721,9 +865,8 @@ def arrange_descendants(self):
children_paths = self.model.get_children(self.node_path, ordered=True,
include_implied=True)
prev_y = 0
- offset_z = len(children_paths)
prev_child = None
- index = 0
+ index = 1
for child_path in children_paths:
child = self.view.get_node_graphic(child_path)
if not child:
@@ -737,11 +880,9 @@ def arrange_descendants(self):
else:
y = self.get_selection_rect().height()
y += prev_y
- child.setPos(self.stack_offset, y)
- child.setZValue(offset_z)
-
+ new_pos = QtCore.QPointF(self.stack_offset, y)
+ child.anim_into_place(new_pos)
prev_y = y
- offset_z -= 1
child.arrange_descendants()
prev_child = child
index += 1
@@ -824,7 +965,14 @@ def boundingRect(self):
def paint(self, painter, option, widget):
"""Override of QtWidgets.QGraphicsItem paint. Handles all visuals of the Plug."""
- painter.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing | QtGui.QPainter.SmoothPixmapTransform)
+ lod = QtWidgets.QStyleOptionGraphicsItem.levelOfDetailFromTransform(
+ painter.worldTransform())
+ if lod > MIN_LOD:
+ painter.setRenderHints(QtGui.QPainter.Antialiasing |
+ QtGui.QPainter.TextAntialiasing |
+ QtGui.QPainter.SmoothPixmapTransform)
+ else:
+ painter.setRenderHints(False)
if self.is_hovered:
painter.setPen(QtGui.QPen(QtCore.Qt.white, self.hover_width, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
else:
@@ -836,7 +984,7 @@ def paint(self, painter, option, widget):
# create triangle
polygon = QtGui.QPolygonF()
step_angle = 120
- for i in range(4):
+ for i in [0, 1, 2, 3]:
step = step_angle * i
x = self.radius * 1.2 * math.cos(math.radians(step))
y = self.radius * 1.2 * math.sin(math.radians(step))
@@ -853,8 +1001,6 @@ def paint(self, painter, option, widget):
painter.setBrush(QtCore.Qt.green)
painter.drawPolygon(polygon)
- painter.setPen(QtCore.Qt.black)
- painter.setFont(QtGui.QFont('Roboto', 12))
elif self.is_break:
painter.drawRect(self.radius * -1, self.radius * -1, self.radius * 2, self.radius * 2)
else:
@@ -931,6 +1077,7 @@ def __init__(self, parent=None, filled=False, color=None):
is_str = isinstance(self.color, str)
if is_str:
self.color = QtGui.QColor(self.color)
+ self.setZValue(30)
def boundingRect(self):
"""Override of QtWidgets.QGraphicsItem boundingRect. If this rectangle
@@ -946,18 +1093,35 @@ def paint(self, painter, option, widget):
if self.filled:
brush = QtGui.QBrush(self.color)
painter.setBrush(brush)
+ if self.color.lightness() < 100:
+ pen_color = QtGui.QColor(self.color.lighter(300))
+ else:
+ pen_color = QtGui.QColor(self.color.darker(300))
+ pen = QtGui.QPen(pen_color)
+ pen.setJoinStyle(QtCore.Qt.RoundJoin)
painter.setPen(QtCore.Qt.NoPen)
else:
- painter.setBrush(QtCore.Qt.NoBrush)
- painter.setPen(QtCore.Qt.white)
+ brush = QtGui.QBrush(QtCore.Qt.white, QtCore.Qt.Dense6Pattern)
+ painter.setBrush(brush)
+ pen = QtGui.QPen(QtCore.Qt.white)
+ pen.setStyle(QtCore.Qt.DotLine)
+ painter.setPen(pen)
# draw triangle
points = [
- QtCore.QPointF(0-(self.width*.5), 0),
+ QtCore.QPointF(0 - (self.width * .5), 0),
QtCore.QPointF(self.width*.5, 0),
QtCore.QPointF(0, self.height)
]
painter.drawPolygon(points)
+ # Draw outline
+ if self.filled:
+ painter.setBrush(QtCore.Qt.NoBrush)
+ painter.setPen(pen)
+ painter.drawLine(0 - (self.width * .5), 0,
+ 0, self.height)
+ painter.drawLine(0, self.height,
+ self.width * .5, 0)
def itemChange(self, change, value):
"""Override of QtWidgets.QGraphicsItem itemChange."""
diff --git a/nxt_editor/resources/icons/nxt_128.png b/nxt_editor/resources/icons/nxt_128.png
new file mode 100644
index 0000000..16f8006
Binary files /dev/null and b/nxt_editor/resources/icons/nxt_128.png differ
diff --git a/nxt_editor/stage_model.py b/nxt_editor/stage_model.py
index f5d38f3..2716e7c 100644
--- a/nxt_editor/stage_model.py
+++ b/nxt_editor/stage_model.py
@@ -748,7 +748,7 @@ def get_descendant_colors(self, base_path):
base_node = self.comp_layer.lookup(base_path)
if base_node is None:
return layer_colors
- layers = [self.get_node_source_layer(base_path, self.comp_layer)]
+ layers = []
des = self.get_descendants(base_path, self.comp_layer, True)
for d in des:
node = self.comp_layer.lookup(d)
@@ -2073,7 +2073,6 @@ def clear_breakpoints(self, layer=None):
layer = layer or self.top_layer
cmd = ClearBreakpoints(model=self, layer_path=layer.real_path)
self.undo_stack.push(cmd)
- self.nodes_changed.emit(cmd.prev_breaks)
self.breaks_changed.emit([])
def _add_breakpoint(self, node_path, layer):
@@ -2544,14 +2543,15 @@ def execute_snippet(self, code_string, node_path, rt_layer=None,
logger.warning('No code to execute!')
return
self.about_to_execute.emit(True)
- rt = rt_layer
+ rt_layer = rt_layer or self.current_rt_layer
np = [node_path]
- valid, bad_paths = self.validate_runtime_layer(rt_layer=rt,
+ valid, bad_paths = self.validate_runtime_layer(rt_layer=rt_layer,
node_paths=np)
if not rt_layer or not valid:
new_rt = self.prompt_runtime_rebuild(must_rebuild=bool(bad_paths))
if new_rt:
rt_layer = new_rt
+ self.current_rt_layer = new_rt
elif not rt_layer:
return
if not rt_layer or not hasattr(rt_layer, '_console'):
@@ -2866,7 +2866,7 @@ def can_build_run(self):
return True
def setup_build(self, node_paths, rt_layer=None):
- # Reset timer vars
+ # Reset once_sec_timer vars
self.build_start_time = time.time()
self.build_paused_time = .0
self.last_step_time = .0
@@ -2927,7 +2927,7 @@ def step_build(self):
pause_delta = self.build_paused_time - time.time()
self.build_start_time -= pause_delta
# Always reset the paused time as a build step is the same as paused
- # in regard to the build timer
+ # in regard to the build once_sec_timer
self.build_paused_time = .0
if not self.can_build_run():
logger.error("Cannot step execution. Build is not ready.")
diff --git a/nxt_editor/stage_view.py b/nxt_editor/stage_view.py
index 14c9fef..b27b473 100644
--- a/nxt_editor/stage_view.py
+++ b/nxt_editor/stage_view.py
@@ -12,10 +12,11 @@
# Interal
import nxt_editor
from nxt import nxt_node, tokens
-from nxt_editor.node_graphics_item import NodeGraphicsItem, NodeGraphicsPlug
+from nxt_editor.node_graphics_item import (NodeGraphicsItem, NodeGraphicsPlug,
+ _pyside_version)
from nxt_editor.connection_graphics_item import AttrConnectionGraphic
from nxt_editor.commands import *
-
+from .user_dir import USER_PREF, user_prefs
logger = logging.getLogger(nxt_editor.LOGGER_NAME)
@@ -38,6 +39,18 @@ class StageView(QtWidgets.QGraphicsView):
def __init__(self, model, parent=None):
super(StageView, self).__init__(parent=parent)
self.main_window = parent
+ self._do_anim_pref = user_prefs.get(USER_PREF.ANIMATION, True)
+ if _pyside_version[1] < 11:
+ self._do_anim_pref = False
+ self.do_animations = self._do_anim_pref
+ self.once_sec_timer = QtCore.QTimer(self)
+ self.once_sec_timer.timeout.connect(self.calculate_fps)
+ self.frames = 0
+ self.fps = 0
+ self.once_sec_timer.setInterval(1000)
+ if user_prefs.get(USER_PREF.FPS, True):
+ self.once_sec_timer.start()
+ self._animating = []
# EXEC ACTIONS
self.exec_actions = parent.execute_actions
self.addActions(self.exec_actions.actions())
@@ -64,9 +77,11 @@ def __init__(self, model, parent=None):
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.horizontalScrollBar().setValue(0)
self.verticalScrollBar().setValue(0)
-
+ self.setOptimizationFlag(self.DontSavePainterState, enabled=True)
+ self.setOptimizationFlag(self.DontAdjustForAntialiasing, enabled=True)
# scene
- self.setScene(QtWidgets.QGraphicsScene())
+ self._scene = QtWidgets.QGraphicsScene()
+ self.setScene(self._scene)
# TODO Currently setting scene rect and never changing it. We hope for expanding graphs in the future.
self.scene().setSceneRect(QtCore.QRect(-5000, -5000, 10000, 10000))
# rubber band
@@ -132,23 +147,38 @@ def __init__(self, model, parent=None):
# filepath HUD
self.filepath_label = HUDItem(text=None)
self.filepath_label.setFont(QtGui.QFont("Roboto Mono", 8))
- self.filepath_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
+ self.filepath_label.setAlignment(QtCore.Qt.AlignLeft |
+ QtCore.Qt.AlignTop)
self.update_filepath()
self.hud_layout.addWidget(self.filepath_label, 0, 0)
# resolved HUD
- self.resolved_label = HUDItem(text='resolved',
- fade_time=1000,
- start_color=QtGui.QColor(255, 255, 255, 255),
- end_color=QtGui.QColor(255, 255, 255, 100))
+ self.resolved_label = HUDItem(text='resolved', fade_time=1000)
self.resolved_label.setFont(QtGui.QFont("Roboto", 12, weight=75))
- self.resolved_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTop)
+ self.resolved_label.setAlignment(QtCore.Qt.AlignRight |
+ QtCore.Qt.AlignTop)
self.hud_layout.addWidget(self.resolved_label, 0, 3)
+ self.fps_label = HUDItem(text='resolved', fade_time=0)
+ self.fps_label.setFont(QtGui.QFont("Roboto", 12, weight=75))
+ self.fps_label.setAlignment(QtCore.Qt.AlignRight |
+ QtCore.Qt.AlignBottom)
+ if user_prefs.get(USER_PREF.FPS, True):
+ self.hud_layout.addWidget(self.fps_label, 1, 3)
+
self.SEL_ADD_MODIFIERS = QtCore.Qt.ShiftModifier | QtCore.Qt.ControlModifier
self.SEL_TOGGLE_MODIFIERS = QtCore.Qt.KeyboardModifiers(QtCore.Qt.ShiftModifier)
self.SEL_RMV_MODIFIERS = QtCore.Qt.KeyboardModifiers(QtCore.Qt.ControlModifier)
+ def calculate_fps(self):
+ self.fps = (self.frames + self.fps) * .5
+ self.fps_label.setText(str(round(self.fps)))
+ self.frames = 0
+
+ def drawForeground(self, painter, rect):
+ super(StageView, self).drawForeground(painter, rect)
+ self.frames += 1
+
def focusInEvent(self, event):
super(StageView, self).focusInEvent(event)
@@ -294,7 +324,10 @@ def draw_graph(self, dirty):
else:
node_paths = self.model.get_descendants(nxt_path.WORLD,
include_implied=True)
+ og_do_anims = self.do_animations
+ self.do_animations = False
self.handle_nodes_changed(node_paths)
+ self.do_animations = og_do_anims
def draw_node(self, node_path):
graphic = self.get_node_graphic(node_path)
@@ -707,9 +740,6 @@ def mousePressEvent(self, event):
# start panning action
self.panning = True
self._previous_mouse_pos = None
- for node in self._node_graphics.values():
- coord_cache = QtWidgets.QGraphicsItem.ItemCoordinateCache
- node.setCacheMode(coord_cache)
event.accept()
return
@@ -918,9 +948,6 @@ def mouseReleaseEvent(self, event):
if self.panning and event.button() == QtCore.Qt.MiddleButton:
self._previous_mouse_pos = None
self.panning = False
- for node in self._node_graphics.values():
- pretty_mode = QtWidgets.QGraphicsItem.DeviceCoordinateCache
- node.setCacheMode(pretty_mode)
self._current_pan_distance = 0.0
event.accept()
return
@@ -944,39 +971,28 @@ def mouseDoubleClickEvent(self, event):
item.collapse_node()
def wheelEvent(self, event):
- num_degrees = event.delta() / 8.
- num_steps = num_degrees / 15.
- self._num_scheduled_scalings += num_steps
- if self._num_scheduled_scalings * num_steps < 0.:
- self._num_scheduled_scalings = num_steps
- anim = QtCore.QTimeLine(duration=150, parent=self)
- anim.setUpdateInterval(20)
- anim.valueChanged.connect(self.scaling_time)
- anim.finished.connect(self.anim_finished)
self._view_pos = event.pos()
self._scene_pos = self.mapToScene(self._view_pos)
- anim.start()
- event.accept()
- return
- def scaling_time(self, f):
- zoom_pref_key = user_dir.USER_PREF.ZOOM_MULT
- pref_mult = user_dir.user_prefs.get(zoom_pref_key, 1.0)
- mult = 150 / pref_mult
- factor = 1.0 + self._num_scheduled_scalings / mult
- if 0 < factor < 1:
+ try:
+ new_scale = event.delta() * .001 + 1.0
+ except AttributeError:
+ new_scale = 1.1
+
+ if 0 < new_scale < 1:
if self.scale_factor > self._scale_minimum:
- self.scale(factor, factor)
+ self.scale(new_scale, new_scale)
self._center_view()
-
else:
self.scale(1, 1)
- elif factor > 1:
+ elif new_scale > 1:
if self.scale_factor < self._scale_maximum:
- self.scale(factor, factor)
+ self.scale(new_scale, new_scale)
self._center_view()
else:
self.scale(1, 1)
+ event.accept()
+ return
def _center_view(self):
view_center = self.mapToScene(self.viewport().rect().center())
@@ -1134,6 +1150,7 @@ def on_model_selection_changed(self, new_selection):
def handle_nodes_changed(self, node_paths):
updated_paths = []
roots_hit = set()
+ new_nodes = []
for path in node_paths:
if path == nxt_path.WORLD:
# never draw world node.
@@ -1216,6 +1233,10 @@ def handle_node_move(self, node_path, pos):
node_item.setPos(pos[0], pos[1])
def handle_collapse_changed(self, node_paths):
+ while self._animating:
+ QtWidgets.QApplication.processEvents()
+ og_do_anims = self.do_animations
+ self.do_animations = self._do_anim_pref
comp_layer = self.model.comp_layer
roots_hit = set()
for path in node_paths:
@@ -1231,7 +1252,8 @@ def handle_collapse_changed(self, node_paths):
include_implied=True)
for child_path in children:
self.remove_node_graphic(child_path)
- roots_hit.add(nxt_path.get_root_path(path))
+ if children:
+ roots_hit.add(nxt_path.get_root_path(path))
else:
descendants = self.model.get_descendants(path,
include_implied=True)
@@ -1241,6 +1263,7 @@ def handle_collapse_changed(self, node_paths):
if not root_graphic:
continue
root_graphic.arrange_descendants()
+ self.do_animations = og_do_anims
def remove_node_graphic(self, node_path):
if node_path not in self._node_graphics:
@@ -1249,13 +1272,12 @@ def remove_node_graphic(self, node_path):
if not graphic:
return
self.remove_node_connection_graphics(node_path)
- self.scene().removeItem(graphic)
- # because node graphics are parented to one another, removing parent
- # implicitly removes descendants.
- for key in list(self._node_graphics.keys()):
- if nxt_path.is_ancestor(key, node_path):
- self._node_graphics.pop(key)
- self.remove_node_connection_graphics(key)
+
+ def handle_del():
+ self.scene().removeItem(graphic)
+
+ graphic.out_anim_group.finished.connect(handle_del)
+ graphic.anim_out()
def get_node_graphic(self, name):
return self._node_graphics.get(name, None)
diff --git a/nxt_editor/user_dir.py b/nxt_editor/user_dir.py
index 57fae5d..26cbd87 100644
--- a/nxt_editor/user_dir.py
+++ b/nxt_editor/user_dir.py
@@ -68,6 +68,9 @@ class USER_PREF():
TREE_INDENT = 'layer_tree_indent'
FIND_REP_NODE_PATTERNS = 'find_replace_nodes_patterns'
FIND_REP_ATTRS = 'find_replace_attrs'
+ FPS = 'fps'
+ LOD = 'lod'
+ ANIMATION = 'animation'
class EDITOR_CACHE():
diff --git a/nxt_editor/version.json b/nxt_editor/version.json
index 9dfb411..0845990 100644
--- a/nxt_editor/version.json
+++ b/nxt_editor/version.json
@@ -1,7 +1,7 @@
{
"EDITOR": {
"MAJOR": 3,
- "MINOR": 6,
- "PATCH": 2
+ "MINOR": 7,
+ "PATCH": 0
}
}