diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f743eb2..28edd54 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,14 +85,9 @@ Conda is best installed via [miniconda](https://docs.conda.io/en/latest/minicond ``` ## Dependencies -- Python 2.7 +- [Python 3.9](https://www.python.org/downloads/release/python-390/) - [Qt.py](https://github.com/mottosso/Qt.py) - - [PySide2](https://wiki.qt.io/Qt_for_Python) 5.6 (Python 2) - - `pip install -e ` - -- Python 3.7 - - [Qt.py](https://github.com/mottosso/Qt.py) - - [PySide2](https://wiki.qt.io/Qt_for_Python) 5.11.1 (Python 3) + - [PySide6](https://doc.qt.io/qtforpython-6/gettingstarted.html) - `pip install -e ` ## Changelog syntax diff --git a/LICENSE b/LICENSE index be2edfa..d6a9913 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015-2020 The nxt Authors +Copyright (c) 2015-2025 The nxt Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, diff --git a/README.md b/README.md index a251c64..6bf21d5 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ Only clone this repo if you're [contributing](CONTRIBUTING.md) to the NXT codeba
#### Requirements -- Python >= [2.7.*](https://www.python.org/download/releases/2.7) <= [3.7.*](https://www.python.org/download/releases/3.7) -- We strongly recommend using a Python [virtual environment](https://docs.python.org/3.7/tutorial/venv.html) +- Python >= [3.9.*](https://www.python.org/downloads/release/python-390/) +- We strongly recommend using a Python [virtual environment](https://docs.python.org/3/library/venv.html) *[Requirements for contributors](CONTRIBUTING.md#python-environment)* diff --git a/build/release_footer.md b/build/release_footer.md index b4e7bb2..825cf84 100644 --- a/build/release_footer.md +++ b/build/release_footer.md @@ -6,13 +6,13 @@ This release includes backwards compatibility for graph versions as old as `0.45 # Installation Types Each described installation is self contained, and produces a working nxt. ## Pip Installation -From a Python(2 or 3) environment run the following command: +From a Python 3 environment run the following command: `pip install nxt-editor` **Python Dependancies** - [nxt-core](https://github.com/nxt-dev/nxt) - [Qt.py](https://github.com/mottosso/Qt.py) -- [pyside2](https://doc.qt.io/qtforpython/index.html) - - **Windows Only** Note that pyside2 is not available for python2.7 by default on windows([details](https://wiki.qt.io/Qt_for_Python/Considerations#Missing_Windows_.2F_Python_2.7_release)). For instructions on using conda to build an environment to satifsy these dependencies please see [CONTRIBUTING.md](https://github.com/nxt-dev/nxt/blob/release/CONTRIBUTING.md#python-environment) +- [PySide6](https://doc.qt.io/qtforpython-6/gettingstarted.html) + ## Blender (2.8 and newer) Installation 1. Download Blender addon (nxt_blender.zip) @@ -23,6 +23,6 @@ From a Python(2 or 3) environment run the following command: - By Hand: `/path/to/python.exe -m pip install -U nxt-editor` -## Maya(2019-2020) Installation/Update +## Maya(2019-2025) Installation/Update 1. Download Maya module(nxt_maya.zip) 2. Extract and follow `README.md` inside diff --git a/nxt_editor/__init__.py b/nxt_editor/__init__.py index cf2523c..ca824ca 100644 --- a/nxt_editor/__init__.py +++ b/nxt_editor/__init__.py @@ -36,10 +36,7 @@ class StringSignaler(QtCore.QObject): def make_resources(qrc_path=None, result_path=None): - import PySide2 - pyside_dir = os.path.dirname(PySide2.__file__) - full_pyside2rcc_path = os.path.join(pyside_dir, 'pyside2-rcc') - full_rcc_path = os.path.join(pyside_dir, 'rcc') + import subprocess this_dir = os.path.dirname(os.path.realpath(__file__)) if not qrc_path: qrc_path = os.path.join(this_dir, 'resources/resources.qrc') @@ -47,37 +44,13 @@ def make_resources(qrc_path=None, result_path=None): result_path = os.path.join(this_dir, 'qresources.py') msg = 'First launch nxt resource generation from {} to {}' logger.info(msg.format(qrc_path, result_path)) - import subprocess - ver = ['-py2'] - if sys.version_info[0] == 3: - ver += ['-py3'] - args = [qrc_path] + ver + ['-o', result_path] - try: - subprocess.check_call(['pyside2-rcc'] + args) - except: - pass - else: - return + args = [qrc_path, '-o', result_path, '-g', 'python'] try: - subprocess.check_call([full_pyside2rcc_path] + args) + subprocess.call(['pyside6-rcc'] + args) except: - pass - else: - return - try: - subprocess.check_call([full_rcc_path, '-g', 'python', qrc_path, - '-o', result_path], cwd=pyside_dir) - except: - pass - else: - return - try: - subprocess.check_call(['rcc', '-g', 'python', qrc_path, - '-o', result_path], cwd=pyside_dir) - except: - raise Exception("Failed to generate UI resources using pyside2 rcc!" - " Reinstalling pyside2 may fix the problem. If you " + raise Exception("Failed to generate UI resources using PySide rcc!" + " Reinstalling PySide6 may fix the problem. If you " "know how to use rcc please build from: \"{}\" and " "output to \"{}\"".format(qrc_path, result_path)) else: @@ -111,7 +84,7 @@ def _new_qapp(): return app -def launch_editor(paths=None, start_rpc=True): +def launch_editor(paths=None, start_rpc=False): """Launch an instance of the editor. Will attach to existing QApp if found, otherwise will create and open one. """ @@ -120,6 +93,8 @@ def launch_editor(paths=None, start_rpc=True): app = existing else: app = _new_qapp() + from nxt_editor.dialogs import UpgradePrefsDialogue + UpgradePrefsDialogue.confirm_upgrade_if_possible() instance = show_new_editor(paths, start_rpc) app.setActiveWindow(instance) if not existing: @@ -127,11 +102,14 @@ def launch_editor(paths=None, start_rpc=True): return instance -def show_new_editor(paths=None, start_rpc=True): +def show_new_editor(paths=None, start_rpc=False): path = None - if paths is not None: + if paths and isinstance(paths, list): path = paths[0] paths.pop(0) + elif isinstance(paths, str): + path = paths + paths = [] else: paths = [] # Deferred import since main window relies on us diff --git a/nxt_editor/constants.py b/nxt_editor/constants.py index 51c6174..66a3d48 100644 --- a/nxt_editor/constants.py +++ b/nxt_editor/constants.py @@ -22,11 +22,13 @@ class EDITOR_VERSION(object): class FONTS(object): - DEFAULT_FAMILY = 'RobotoMono-Regular' + DEFAULT_FAMILY = 'Roboto Mono' DEFAULT_SIZE = 10 -_pref_dir_name = str(EDITOR_VERSION.MAJOR) -PREF_DIR = os.path.join(USER_DIR, 'prefs', _pref_dir_name) +PREF_DIR_INT = EDITOR_VERSION.MAJOR +PREF_DIR_NAME = 'prefs' +_pref_dir_num = str(PREF_DIR_INT) +PREF_DIR = os.path.join(USER_DIR, PREF_DIR_NAME, _pref_dir_num) NXT_WEBSITE = 'https://nxt-dev.github.io/' diff --git a/nxt_editor/dialogs.py b/nxt_editor/dialogs.py index c6653bc..f56f41a 100644 --- a/nxt_editor/dialogs.py +++ b/nxt_editor/dialogs.py @@ -297,7 +297,7 @@ def build_widgets(self): self.save_details_button.released.connect(self.on_save_details) self.detail_buttons_layout = QtWidgets.QHBoxLayout() - self.detail_buttons_layout.addStretch(streth=1) + self.detail_buttons_layout.addStretch(1) self.detail_buttons_layout.addWidget(self.save_details_button) self.detail_buttons_layout.addWidget(self.copy_details_button) @@ -314,7 +314,7 @@ def build_widgets(self): self.top_right_layout = QtWidgets.QVBoxLayout() self.top_right_layout.addWidget(self.text_label) self.top_right_layout.addWidget(self.info_label) - self.top_right_layout.addStretch(streth=1) + self.top_right_layout.addStretch(1) self.top_right_layout.addLayout(self.buttons_layout) self.top_layout = QtWidgets.QHBoxLayout() self.top_layout.addWidget(self.icon) @@ -373,14 +373,16 @@ def show_message(cls, text, info, details=None): class NxtConfirmDialog(QtWidgets.QMessageBox): + Ok = QtWidgets.QMessageBox.StandardButton.Ok + Cancel = QtWidgets.QMessageBox.StandardButton.Cancel def __init__(self, text='Title', info='Confirm something!', button_text=None, icon=QtWidgets.QMessageBox.Icon.Question): """Simple message box used for user confirmation :param text: Title text :param info: Main info text :param button_text: Custom button text dict: - {QtWidgets.QMessageBox.Ok: 'Custom Ok Text', - QtWidgets.QMessageBox.Cancel: 'Custom Cancel Text'} + {QtWidgets.QMessageBox.StandardButton.Ok: 'Custom Ok Text', + QtWidgets.QMessageBox.StandardButton.Cancel: 'Custom Cancel Text'} """ super(NxtConfirmDialog, self).__init__() self.setText(text) @@ -389,6 +391,7 @@ def __init__(self, text='Title', info='Confirm something!', self.setIcon(icon) self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) self.setStandardButtons(self.Ok | self.Cancel) + self.setWindowTitle(text) if button_text: self.setButtonText(self.Ok, button_text.get(self.Ok, 'Ok')) self.setButtonText(self.Cancel, button_text.get(self.Cancel, @@ -408,6 +411,31 @@ def show_message(cls, *args, **kwargs): return False +class UpgradePrefsDialogue(NxtConfirmDialog): + def __int__(self, title_text, info, button_text): + super(UpgradePrefsDialogue, self).__init__(text=title_text, + info=info, + button_text=button_text) + + @classmethod + def confirm_upgrade_if_possible(cls): + + if not user_dir.UPGRADABLE_PREFS: + return + from_version = user_dir.UPGRADE_PREFS_FROM_VERSION + title_text = f'Copy version {from_version} Preferences?' + button_text = { + NxtConfirmDialog.Ok: f'Copy v{from_version} prefs', + NxtConfirmDialog.Cancel: 'Use default preferences' + } + i = ('Would you like to copy preferences from an older version of NXT?' + '\nSome things like the window layout may not be preserved.') + do_upgrade = super().show_message(text=title_text, info=i, + button_text=button_text) + if do_upgrade: + user_dir.upgrade_prefs(user_dir.UPGRADABLE_PREFS) + + class UnsavedLayersDialogue(QtWidgets.QDialog): @classmethod def save_before_exit(cls, stage_models, main_window): diff --git a/nxt_editor/dockwidgets/build_view.py b/nxt_editor/dockwidgets/build_view.py index 3d21643..768e214 100644 --- a/nxt_editor/dockwidgets/build_view.py +++ b/nxt_editor/dockwidgets/build_view.py @@ -315,7 +315,7 @@ class BuildTable(QtWidgets.QTableView): """ def __init__(self): super(BuildTable, self).__init__() - self.setSelectionMode(self.NoSelection) + self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) self.horizontalHeader().hide() self.verticalHeader().hide() self.break_delegate = LetterCheckboxDelegeate('B') @@ -342,7 +342,7 @@ def setModel(self, model): header = self.horizontalHeader() header.setStretchLastSection(False) header.setDefaultSectionSize(28) - header.setSectionResizeMode(header.Fixed) + header.setSectionResizeMode(QtWidgets.QHeaderView.Fixed) if header.count(): column = BuildModel.PATH_COLUMN header.setSectionResizeMode(column, QtWidgets.QHeaderView.Stretch) diff --git a/nxt_editor/dockwidgets/code_editor.py b/nxt_editor/dockwidgets/code_editor.py index 0005dda..dd1b42d 100644 --- a/nxt_editor/dockwidgets/code_editor.py +++ b/nxt_editor/dockwidgets/code_editor.py @@ -778,7 +778,7 @@ def focusOutEvent(self, event): return QtWidgets.QPlainTextEdit.focusOutEvent(self, event) def wheelEvent(self, event): - delta = event.delta() + delta = event.angleDelta().y() / 8 if event.modifiers() == QtCore.Qt.ControlModifier: if delta > 0: self.set_font_size(delta=0.5) @@ -1271,7 +1271,7 @@ def paintEvent(self, event): def get_width(self): count = self.editor.blockCount() - width = self.fontMetrics().width(str(count)) + 10 + width = self.fontMetrics().horizontalAdvance(str(count)) + 10 return width def update_width(self): diff --git a/nxt_editor/dockwidgets/find_rep.py b/nxt_editor/dockwidgets/find_rep.py index bd94f67..23aaa9c 100644 --- a/nxt_editor/dockwidgets/find_rep.py +++ b/nxt_editor/dockwidgets/find_rep.py @@ -196,7 +196,7 @@ def setModel(self, model): super(SearchResultsTree, self).setModel(model) header = self.header() header.setStretchLastSection(False) - header.setSectionResizeMode(header.ResizeToContents) + header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) if self.model(): self.model().modelReset.connect(self.expandAll) diff --git a/nxt_editor/dockwidgets/layer_manager.py b/nxt_editor/dockwidgets/layer_manager.py index c65c9a1..3c2aeea 100644 --- a/nxt_editor/dockwidgets/layer_manager.py +++ b/nxt_editor/dockwidgets/layer_manager.py @@ -19,7 +19,7 @@ class LayerManager(DockWidgetBase): """Interactive tree view of the layers in the open graph. """ - def __init__(self, title='Layer Manger', parent=None): + def __init__(self, title='Layer Manager', parent=None): super(LayerManager, self).__init__(title=title, parent=parent, minimum_width=100) @@ -100,7 +100,7 @@ def setModel(self, model): header = self.header() header.setStretchLastSection(False) header.setDefaultSectionSize(LayerTreeView.SIZE) - header.setSectionResizeMode(header.Fixed) + header.setSectionResizeMode(QtWidgets.QHeaderView.Fixed) if header.count(): header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) self.hideColumn(LayerModel.TARGET_COLUMN) diff --git a/nxt_editor/dockwidgets/output_log.py b/nxt_editor/dockwidgets/output_log.py index b5ec502..0ec8508 100644 --- a/nxt_editor/dockwidgets/output_log.py +++ b/nxt_editor/dockwidgets/output_log.py @@ -205,7 +205,7 @@ def __init__(self, graph_model=None, parent=None): self.buttons_layout = QtWidgets.QHBoxLayout() self.buttons_layout.addWidget(self.log_filter_button) - self.buttons_layout.addStretch(stretch=1) + self.buttons_layout.addStretch(1) self.buttons_layout.addWidget(self.clear_rich_button) self.rich_output_layout = QtWidgets.QVBoxLayout() diff --git a/nxt_editor/dockwidgets/property_editor.py b/nxt_editor/dockwidgets/property_editor.py index 2f80f38..2c114e0 100644 --- a/nxt_editor/dockwidgets/property_editor.py +++ b/nxt_editor/dockwidgets/property_editor.py @@ -12,7 +12,7 @@ QtCore.QStringListModel except AttributeError: del QtCore - from PySide2 import QtCore + from PySide6 import QtCore # Internal from nxt_editor import user_dir diff --git a/nxt_editor/dockwidgets/syntax.py b/nxt_editor/dockwidgets/syntax.py index 246c23f..4d8d25f 100644 --- a/nxt_editor/dockwidgets/syntax.py +++ b/nxt_editor/dockwidgets/syntax.py @@ -39,15 +39,15 @@ class PythonHighlighter(QSyntaxHighlighter): # Comparison '==', '!=', '<', '<=', '>', '>=', # Arithmetic - '\+', '-', '\*', '/', '//', '\%', '\*\*', + r'\+', '-', r'\*', '/', '//', r'\%', r'\*\*', # In-place - '\+=', '-=', '\*=', '/=', '\%=', + r'\+=', '-=', r'\*=', '/=', r'\%=', # Bitwise - '\^', '\|', '\&', '\~', '>>', '<<' + r'\^', r'\|', r'\&', r'\~', '>>', '<<' ] # Python braces - braces = ['\{', '\}', '\(', '\)', '\[', '\]'] + braces = [r'\{', r'\}', r'\(', r'\)', r'\[', r'\]'] def __init__(self, document=None): super(PythonHighlighter, self).__init__(document) @@ -117,7 +117,8 @@ def highlightBlock(self, text): # Do other syntax formatting for rule in self.rules: expression, nth, formatting = rule - index = expression.indexIn(text, 0) + match = expression.match(text) + index = match.capturedStart() # This is here because you can't do nested logic in regex nested = 0 if rule in self.special_rules: @@ -126,10 +127,10 @@ def highlightBlock(self, text): while index >= 0: # We actually want the index of the nth match - index = expression.pos(nth) - length = len(expression.cap(nth)) + index = match.capturedStart(nth) + length = len(match.captured(nth)) self.setFormat(index, length + nested, formatting) - index = expression.indexIn(text, index + length) + index = match.capturedStart(text) self.setCurrentBlockState(0) @@ -151,17 +152,19 @@ def match_multiline(self, text, delimiter, in_state, style): add = 0 # Otherwise, look for the delimiter on this line else: - start = delimiter.indexIn(text) + match = delimiter.match(text) + start = match.capturedStart() # Move past this match - add = delimiter.matchedLength() + add = match.capturedLength() + 1 # As long as there's a delimiter match on this line... while start >= 0: + match = delimiter.match(text) # Look for the ending delimiter - end = delimiter.indexIn(text, start + add) + end = match.capturedStart() - (start + add) # Ending delimiter on this line? if end >= add: - length = end - start + add + delimiter.matchedLength() + length = end - start + add + match.capturedLength() self.setCurrentBlockState(0) # No; multi-line string else: @@ -170,7 +173,10 @@ def match_multiline(self, text, delimiter, in_state, style): # Apply formatting self.setFormat(start, length, style) # Look for the next match - start = delimiter.indexIn(text, start + length) + match = delimiter.match(text, start + length) + if not match.hasMatch(): + break + start = match.capturedStart() # Return True if still inside a multi-line string, False otherwise if self.currentBlockState() == in_state: diff --git a/nxt_editor/dockwidgets/widget_builder.py b/nxt_editor/dockwidgets/widget_builder.py index b773b2c..1ecb1a2 100644 --- a/nxt_editor/dockwidgets/widget_builder.py +++ b/nxt_editor/dockwidgets/widget_builder.py @@ -15,7 +15,7 @@ QtCore.QStringListModel except AttributeError: del QtCore - from PySide2 import QtCore + from PySide6 import QtCore # Internal import nxt_editor diff --git a/nxt_editor/integration/blender/README.md b/nxt_editor/integration/blender/README.md index ce362ef..b59c230 100644 --- a/nxt_editor/integration/blender/README.md +++ b/nxt_editor/integration/blender/README.md @@ -23,6 +23,7 @@ installing Python (same version as Blender's) on your machine._ 1. Launch Blender with elevated permissions 2. Open the addon manager (Edit > Preferences > Add-ons) 3. Click "Install" and select the `nxt_blender.py` file provided with this addon zip + * In newer versions of Blender you need to click the small arrow on the top right and select "Install from Disk". 4. Enable the `NXT Blender` and twirl down the addon preferences 5. Click `Install NXT dependencies` - It is recommended to open the console window before running the script, so you can see what's happening. Window > Toggle System Console. diff --git a/nxt_editor/integration/blender/nxt_blender.py b/nxt_editor/integration/blender/nxt_blender.py index c9f9a2e..c3729cc 100644 --- a/nxt_editor/integration/blender/nxt_blender.py +++ b/nxt_editor/integration/blender/nxt_blender.py @@ -26,7 +26,7 @@ bl_info = { "name": "NXT Blender", "blender": (3, 4, 0), - "version": (0, 3, 0), + "version": (0, 4, 0), "location": "NXT > Open Editor", "wiki_url": "https://nxt-dev.github.io/", "tracker_url": "https://github.com/nxt-dev/nxt_editor/issues", @@ -41,8 +41,9 @@ b_major, b_minor, b_patch = bpy.app.version if b_major == 2: bl_info["blender"] = (2, 80, 0) -elif b_major != 3: - raise RuntimeError('Unsupported major Blender version: {}'.format(b_major)) +elif b_major > 4: + raise RuntimeError('NXT does not support Blender version: ' + '{}.x'.format(b_major)) class BLENDER_PLUGIN_VERSION(object): diff --git a/nxt_editor/integration/maya/plug-ins/nxt_maya.py b/nxt_editor/integration/maya/plug-ins/nxt_maya.py index c42ee49..57fb831 100644 --- a/nxt_editor/integration/maya/plug-ins/nxt_maya.py +++ b/nxt_editor/integration/maya/plug-ins/nxt_maya.py @@ -107,7 +107,7 @@ def doIt(self, args): if __NXT_INSTANCE__: __NXT_INSTANCE__.close() return - nxt_win = nxt_editor.main_window.MainWindow() + 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) diff --git a/nxt_editor/main_window.py b/nxt_editor/main_window.py index 13971e0..a478e59 100644 --- a/nxt_editor/main_window.py +++ b/nxt_editor/main_window.py @@ -52,7 +52,7 @@ class MainWindow(QtWidgets.QMainWindow): new_log_signal = QtCore.Signal(logging.LogRecord) font_size_changed = QtCore.Signal(int) - def __init__(self, filepath=None, parent=None, start_rpc=True): + def __init__(self, filepath=None, parent=None, start_rpc=False): """Create NXT window. :param parent: parent to attach this UI to. diff --git a/nxt_editor/node_graphics_item.py b/nxt_editor/node_graphics_item.py index b8de726..128f4b7 100644 --- a/nxt_editor/node_graphics_item.py +++ b/nxt_editor/node_graphics_item.py @@ -9,8 +9,6 @@ from Qt import QtWidgets from Qt import QtGui from Qt import QtCore -from PySide2 import __version_info__ as qt_version - # Internal import nxt_editor from nxt import nxt_path, nxt_node @@ -25,16 +23,8 @@ MIN_LOD = user_prefs.get(USER_PREF.LOD, .4) -_pyside_version = qt_version - - -if _pyside_version[1] < 11: - graphic_type = QtWidgets.QGraphicsItem -else: - graphic_type = QtWidgets.QGraphicsObject - -class NodeGraphicsItem(graphic_type): +class NodeGraphicsItem(QtWidgets.QGraphicsObject): """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. @@ -262,13 +252,13 @@ def screen_pos(self): def itemChange(self, change, value): """Override of QtWidgets.QGraphicsItem itemChange.""" # keep connections drawing to node as it moves - if change is self.ItemScenePositionHasChanged: + if change is QtWidgets.QGraphicsItem.ItemPositionChange: graphics = self.view.get_node_connection_graphics(self.node_path) for connection in graphics: connection.rebuild_line() # TODO: Take into account the positions of every selected node and snap them all to a grid as soon as # the user preses shift. This will avoid the weird wavy snapping effect we have right now - if change == self.ItemPositionChange and self.scene(): + if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.scene(): ml = QtWidgets.QApplication.mouseButtons() == QtCore.Qt.LeftButton shift = QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ShiftModifier force_snap = self.view.alignment_actions.snap_action.isChecked() diff --git a/nxt_editor/pixmap_button.py b/nxt_editor/pixmap_button.py index 3239ad4..e4a07f6 100644 --- a/nxt_editor/pixmap_button.py +++ b/nxt_editor/pixmap_button.py @@ -71,7 +71,7 @@ def paintEvent(self, event): pix = self.pixmap_pressed painter = QtGui.QPainter(self) - painter.drawPixmap(event.rect(), pix) + painter.drawPixmap(event.rect(), QtGui.QPixmap(pix)) del painter def enterEvent(self, event): diff --git a/nxt_editor/resources/LICENSE.FEATHERICONS b/nxt_editor/resources/LICENSE.FEATHERICONS index b869713..f26358a 100644 --- a/nxt_editor/resources/LICENSE.FEATHERICONS +++ b/nxt_editor/resources/LICENSE.FEATHERICONS @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2017 Cole Bemis +Copyright (c) 2013-2023 Cole Bemis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/nxt_editor/stage_view.py b/nxt_editor/stage_view.py index 11151ec..d6ad664 100644 --- a/nxt_editor/stage_view.py +++ b/nxt_editor/stage_view.py @@ -8,12 +8,12 @@ from Qt import QtWidgets from Qt import QtGui from Qt import QtCore +from Qt import QtCompat # Interal import nxt_editor from nxt import nxt_node, tokens -from nxt_editor.node_graphics_item import (NodeGraphicsItem, NodeGraphicsPlug, - _pyside_version) +from nxt_editor.node_graphics_item import NodeGraphicsItem, NodeGraphicsPlug from nxt_editor.connection_graphics_item import AttrConnectionGraphic from nxt_editor.dialogs import NxtWarningDialog from nxt_editor.commands import * @@ -42,8 +42,6 @@ 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) @@ -79,8 +77,8 @@ 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) + self.setOptimizationFlag(QtWidgets.QGraphicsView.DontSavePainterState, enabled=True) + self.setOptimizationFlag(QtWidgets.QGraphicsView.DontAdjustForAntialiasing, enabled=True) # scene self._scene = QtWidgets.QGraphicsScene() self.setScene(self._scene) @@ -700,7 +698,7 @@ def mousePressEvent(self, event): self.zoom_start_pos = event.pos() self._previous_mouse_pos = event.pos() event.accept() - if event.buttons() == QtCore.Qt.LeftButton | QtCore.Qt.MidButton: + if event.buttons() == QtCore.Qt.LeftButton | QtCore.Qt.MiddleButton: self.zooming = True self.zoom_start_pos = event.pos() self._previous_mouse_pos = event.pos() @@ -825,11 +823,11 @@ def mouseReleaseEvent(self, event): event.accept() if self.zooming: - if event.buttons() == QtCore.Qt.LeftButton | QtCore.Qt.MidButton: + if event.buttons() == QtCore.Qt.LeftButton | QtCore.Qt.MiddleButton: self.zooming = False elif event.buttons() == QtCore.Qt.LeftButton: self.zooming = False - elif event.buttons() == QtCore.Qt.MidButton: + elif event.buttons() == QtCore.Qt.MiddleButton: self.zooming = False if (self._rubber_band_origin is not None and event.button() is QtCore.Qt.LeftButton): @@ -993,11 +991,11 @@ def mouseDoubleClickEvent(self, event): item.collapse_node() def wheelEvent(self, event): - self._view_pos = event.pos() + self._view_pos = event.position().toPoint() self._scene_pos = self.mapToScene(self._view_pos) try: - new_scale = event.delta() * .001 + 1.0 + new_scale = event.angleDelta().y() * .001 + 1.0 except AttributeError: new_scale = 1.1 diff --git a/nxt_editor/user_dir.py b/nxt_editor/user_dir.py index 6353f68..f0803a8 100644 --- a/nxt_editor/user_dir.py +++ b/nxt_editor/user_dir.py @@ -10,6 +10,7 @@ import json import logging +import shutil import sys if sys.version_info[0] == 2: @@ -19,7 +20,7 @@ # Internal from nxt.constants import USER_DIR -from nxt_editor.constants import PREF_DIR +from nxt_editor.constants import PREF_DIR, PREF_DIR_INT, PREF_DIR_NAME import nxt_editor logger = logging.getLogger(nxt_editor.LOGGER_NAME) @@ -31,7 +32,7 @@ SKIPPOINT_FILE = os.path.join(PREF_DIR, 'skippoints') HOTKEYS_PREF = os.path.join(PREF_DIR, 'hotkeys.json') MAX_RECENT_FILES = 10 - +JSON_PREFS = [USER_PREFS_PATH, BREAKPOINT_FILE, SKIPPOINT_FILE, HOTKEYS_PREF] broken_files = {} @@ -47,7 +48,56 @@ def ensure_pref_dir_exists(): raise Exception('Failed to generate user dir {}' + USER_DIR) +def get_upgradable_prefs(): + """ + Identify preference files that can be safely upgraded + between major editor versions. Only existing preference files + from the nearest older version are copied; missing files are skipped + without warnings. Returns a list of prefs that can upgrade and the + versio number they're coming from. + :returns: (list, int) + """ + upgradable_prefs = [] + upgrade_prefs_from_version = -1 + for pref_file in JSON_PREFS: + if os.path.isfile(pref_file): + break + else: # Didn't find any json prefs in current version prefs + dir_num = PREF_DIR_INT - 1 + while dir_num > -1: + old_pref_dir = os.path.join(USER_DIR, PREF_DIR_NAME, str(dir_num)) + for pref_file in JSON_PREFS: + file_name = os.path.basename(pref_file) + old_pref_file = os.path.join(old_pref_dir, file_name) + if os.path.isfile(old_pref_file): + # In the future if we change the structure of the json + # prefs we'll need a way to convert them or skip + upgradable_prefs.append(old_pref_file) + if upgradable_prefs: + upgrade_prefs_from_version = dir_num + break + dir_num -= 1 + return upgradable_prefs, upgrade_prefs_from_version + + +def upgrade_prefs(prefs_to_upgrade): + """ + Copies old 'upgradeable' prefs to current pref dir, will eat and + exception raised by shutil.copy. In the future this function may do more + than simply copy. + :param prefs_to_upgrade: List of pref filepaths to upgrade + """ + for pref_file in prefs_to_upgrade: + try: + shutil.copy(pref_file, PREF_DIR) + except Exception as e: + logger.error(e) + logger.error(f'Failed to copy old pref file: {pref_file}') + + ensure_pref_dir_exists() +# Must check these before we setup the defaults at the bottom of this file +UPGRADABLE_PREFS, UPGRADE_PREFS_FROM_VERSION = get_upgradable_prefs() class USER_PREF(): @@ -209,10 +259,10 @@ def read(self): return try: with open(self.path, 'r+b') as fp: - if sys.version_info[0] == 2: - contents = pickle.load(fp) - else: - contents = pickle.load(fp, encoding='bytes') + contents = pickle.load(fp, encoding='bytes') + except ModuleNotFoundError as e: + if 'PySide2' in e.msg: + shutil.move(self.path, self.path + '.backup') except pickle.UnpicklingError: broken_files.setdefault(self.path, 0) times_hit = broken_files[self.path] diff --git a/nxt_editor/version.json b/nxt_editor/version.json index 1b7135a..1d30317 100644 --- a/nxt_editor/version.json +++ b/nxt_editor/version.json @@ -1,7 +1,7 @@ { "EDITOR": { - "MAJOR": 3, - "MINOR": 16, + "MAJOR": 4, + "MINOR": 0, "PATCH": 0 } } diff --git a/nxt_env.yml b/nxt_env.yml index 97d32f0..c2bedc6 100644 --- a/nxt_env.yml +++ b/nxt_env.yml @@ -3,7 +3,7 @@ channels: - conda-forge dependencies: - python=3.9 - - qt.py=1.1 - - pyside2=5.13.2 + - qt.py=1.4 + - pyside2=6.7 - twine - requests diff --git a/nxt_env2.yml b/nxt_env2.yml deleted file mode 100644 index ca19199..0000000 --- a/nxt_env2.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: nxt_editor -channels: - - conda-forge -dependencies: - - python=2.7 - - qt.py=1.1 - - pyside2=5.6 - - twine - - requests diff --git a/setup.py b/setup.py index 7b98ff4..6574614 100644 --- a/setup.py +++ b/setup.py @@ -28,10 +28,10 @@ long_description_content_type="text/markdown", url="https://github.com/nxt-dev/nxt_editor", packages=setuptools.find_packages(), - python_requires='>=3.7, <3.11', + python_requires='>=3.9, <3.12', install_requires=['nxt-core<1.0,>=0.14', 'qt.py<3', - 'pyside2>=5.11,<=5.16' + 'PySide6>=6,<6.8' ], package_data={ # covers text nxt files