diff --git a/.pylintrc b/.pylintrc index 3cce629..f4524d7 100644 --- a/.pylintrc +++ b/.pylintrc @@ -355,7 +355,7 @@ ignored-classes=optparse.Values,thread._local,_thread._local # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis). It # supports qualified module names, as well as Unix pattern matching. -ignored-modules=pymxs,debugpy,PySide2,menuhook,mxthread,socketio +ignored-modules=pymxs,debugpy,qtpy,menuhook,mxthread,socketio # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. diff --git a/README.md b/README.md index edd3055..523a50f 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ working as expected ### New content -- Drop maxscript code on a rich text window to get a python translation [mxstranslate](/src/packages/mxstranslate/README.md) +- New with 3dsMax 2025: [Plugin Packages in 2025 and Integration With the New Menu System](/doc/pluginpackage.md) ### Samples @@ -35,7 +35,7 @@ The samples below are translations of [MAXScript How Tos](https://help.autodesk. can be found in the 3ds Max online documentation. The conversion from MaxScript to Python could have been more mechanical but we chose to implement -the Python version in the best Python way known to us. An example of this is that we use PySide2 +the Python version in the best Python way known to us. An example of this is that we use PySide (Qt) for the UI as much as possible instead of using more traditional 3ds Max ui mechanisms. *How To?* @@ -56,6 +56,7 @@ the Python version in the best Python way known to us. An example of this is tha - Run code on thre main thread [mxthread](/src/packages/mxthread/README.md) - Automatically convert maxscript to python [mxs2py](/src/packages/mxs2py/README.md) - Use socketio from 3dsMax [socketioclient](/src/packages/socketioclient/README.md) +- Drop maxscript code on a rich text window to get a python translation [mxstranslate](/src/packages/mxstranslate/README.md) ## Python Samples diff --git a/doc/install.md b/doc/install.md index a4513f1..17670a7 100644 --- a/doc/install.md +++ b/doc/install.md @@ -39,6 +39,9 @@ It is possible to break up the installation in two steps. - The [installstartup.sh](/installstartup.sh) script can be used from bash to install pip and [pystartup.ms](/src/pystartup/pystartup.ms). +In 2025 and above, [adn-devtech-python-howtos](/src/adn-devtech-python-howtos) +is installed instead of pystartup.ms. + It needs to run in the 3ds Max installation directory. You may do only this step if you don't want the HowTos but you diff --git a/doc/pluginpackage.md b/doc/pluginpackage.md new file mode 100644 index 0000000..6191427 --- /dev/null +++ b/doc/pluginpackage.md @@ -0,0 +1,91 @@ +# Plugin Packages in 2025 and Integration With the New Menu System + +In 3ds Max 2025 and above, plugin packages may contain python script components. +When installing the samples from this repo in a 2025 version of 3ds Max, +the [adn-devtech-python-howtos](/src/adn-devtech-python-howtos) plugin package +is copied to "$ProgramData/Autodesk/ApplicationPlugins". The [PackageContents.xml](/src/adn-devtech-python-howtos/PackageContents.xml) +file of this plugin package declares a python pre-start-up script: + +```xml + + + + +``` + +The [./scripts/pyStartup.py](/src/adn-devtech-python-howtos/scripts/pyStartup.py) script +first does what [pystartup.ms](/src/pystartup/pystartup.ms) used to do: + +```python +def _python_startup(): + try: + import pkg_resources + except ImportError: + print('startup Python modules require pip to be installed.') + return + for dist in pkg_resources.working_set: + entrypt = pkg_resources.get_entry_info(dist, '3dsMax', 'startup') + if not (entrypt is None): + try: + fcn = entrypt.load() + fcn() + except Exception as e: + print(f'skipped package startup for {dist} because {e}, startup not working') + +``` + +Then it integrates the samples into the new menu system of 2025: + +```python + # configure 2025 menus + from pymxs import runtime as rt + from menuhook import register_howtos_menu_2025 + def menu_func(): + menumgr = rt.callbacks.notificationparam() + register_howtos_menu_2025(menumgr) + + # menu system + cuiregid = rt.name("cuiRegisterMenus") + howtoid = rt.name("pyScriptHowtoMenu") + rt.callbacks.removescripts(id=cuiregid) + rt.callbacks.addscript(cuiregid, menu_func, id=howtoid) + +``` + + +## Integration With the New Menu System + +The [menuhook](/src/menuhook/) code has been reworked to integrate menu items for the +various howtos into the new menu system: + +```python +def register(action, category, fcn, menu=None, text=None, tooltip=None, in2025_menuid=None, id_2025=None): +``` + +Takes two new parameters: +- `in2025_menuid` : the guid of the containing menu +- `id_2025` : the guid of the item to create + +And stores the needed menu items in the `registered_items` list: + +```python + registered_items.append((in2025_menuid, id_2025, category, action)) +``` + +The `register_howotos_menu_2025` function, called whenever the menu system needs to regenerate its structure, uses the `registered_items` list to add items to the menu manager: + +```python + # hook the registered items + for reg in registered_items: + (in2025_menuid, id_2025, category, action) = reg + scriptmenu = menumgr.getmenubyid(in2025_menuid) + if scriptmenu is not None: + try: + actionitem = scriptmenu.createaction(id_2025, 647394, f"{action}`{category}") + except Exception as e: + print(f"Could not create item {category}, {action} in menu {in2025_menuid} because {e}") + else: + print(f"Could not create item {category}, {action}, in missing menu {in2025_menuid}") + + +``` diff --git a/doc/uninstall.md b/doc/uninstall.md index 93c4628..5b6b9f2 100644 --- a/doc/uninstall.md +++ b/doc/uninstall.md @@ -16,6 +16,7 @@ needed to remove them are explained at the bottom of this page). ## Removing pystartup.ms (manual uninstall) +### For 3dsMax before 2025 After the installation, pystartup.ms will be copied to: "$HOME/AppData/Local/Autodesk/3dsMax/2022 - 64bit/ENU/scripts/startup" @@ -29,6 +30,18 @@ It can simply be removed from there. By removing this file none of the HowTo packages will be started automatically when 3ds Max starts. +### For 3dsMax 2025 and greater + +Starting with 2025, pystartup.ms is no longer needed. Instead the +[adn-devtech-python-howtos](/src/adn-devtech-python-howtos) directory is +copied to "C:\ProgramData\Autodesk\ApplicationPlugins". + +It can be manually removed by doing (from gitbash): + +```bash +rm -fr "$ProgramData/Autodesk/ApplicationPlugins/adn-devtech-python-howtos" +``` + ## Removing pip (manual uninstall) The installation script also install pip in user mode. diff --git a/scripts/inst.sh b/scripts/inst.sh index 8e6feaf..1a82617 100644 --- a/scripts/inst.sh +++ b/scripts/inst.sh @@ -66,11 +66,19 @@ installpip() { ./python.exe "$getpip/get-pip.py" --user fi fi + # update to the latest pip + ./python.exe -m pip install --upgrade pip + ./python.exe -m pip install wheel } -# install pystartup.ms +# install pystartup.ms or adn-devtech-python-howtos (plugin package) for 2025 installpystartup() { - cp "$script/src/pystartup/pystartup.ms" "$startuppath" + if [ "$version" -lt "2025" ] + then + cp "$script/src/pystartup/pystartup.ms" "$startuppath" + else + cp -fr "$script/src/adn-devtech-python-howtos" "$ProgramData/Autodesk/ApplicationPlugins" + fi } diff --git a/src/adn-devtech-python-howtos/PackageContents.xml b/src/adn-devtech-python-howtos/PackageContents.xml new file mode 100644 index 0000000..a68036e --- /dev/null +++ b/src/adn-devtech-python-howtos/PackageContents.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/src/adn-devtech-python-howtos/scripts/pyStartup.py b/src/adn-devtech-python-howtos/scripts/pyStartup.py new file mode 100644 index 0000000..bed9632 --- /dev/null +++ b/src/adn-devtech-python-howtos/scripts/pyStartup.py @@ -0,0 +1,31 @@ +def _python_startup(): + try: + import pkg_resources + except ImportError: + print('startup Python modules require pip to be installed.') + return + for dist in pkg_resources.working_set: + entrypt = pkg_resources.get_entry_info(dist, '3dsMax', 'startup') + if not (entrypt is None): + try: + fcn = entrypt.load() + fcn() + except Exception as e: + print(f'skipped package startup for {dist} because {e}, startup not working') + + # configure 2025 menus + from pymxs import runtime as rt + from menuhook import register_howtos_menu_2025 + def menu_func(): + menumgr = rt.callbacks.notificationparam() + register_howtos_menu_2025(menumgr) + + # menu system + cuiregid = rt.name("cuiRegisterMenus") + howtoid = rt.name("pyScriptHowtoMenu") + rt.callbacks.removescripts(id=cuiregid) + rt.callbacks.addscript(cuiregid, menu_func, id=howtoid) + + +_python_startup() + diff --git a/src/packages/inbrowserhelp/inbrowserhelp/__init__.py b/src/packages/inbrowserhelp/inbrowserhelp/__init__.py index 91b8f79..7255679 100644 --- a/src/packages/inbrowserhelp/inbrowserhelp/__init__.py +++ b/src/packages/inbrowserhelp/inbrowserhelp/__init__.py @@ -1,25 +1,99 @@ """ inbrowserhelp example: inbrowserhelp sample """ +from sys import version_info import webbrowser -import menuhook from pymxs import runtime as rt +import menuhook MAX_VERSION = rt.maxversion()[7] -MAX_HELP = f"help.autodesk.com/view/MAXDEV/{MAX_VERSION}/ENU" - -TOPICS = [ - ("gettingstarted", "Getting Started With Python in 3ds Max", - f"{MAX_HELP}/?guid=Max_Python_API_tutorials_creating_the_dialog_html"), - ("howtos", "Python HowTos Github Repo", - "github.com/ADN-DevTech/3dsMax-Python-HowTos"), - ("samples", "Python samples (Github Repo)", - "github.com/ADN-DevTech/3dsMax-Python-HowTos/tree/master/src/samples"), - ("pymxs", "Pymxs Online Documentation", - f"{MAX_HELP}/?guid=Max_Python_API_using_pymxs_html"), - ("pyside2", "Qt for Python Documentation (PySide2)", - "doc.qt.io/qtforpython/contents.html"), - ("python", "Python 3.7 Documentation", - "docs.python.org/3.7/") +MAX_HELP = "help.autodesk.com/view/MAXDEV" + +PYTHON_VERSION = f"{version_info[0]}.{version_info[1]}" + +MAX_VERSION_TOPICS = { + 2021: [ + ("gettingstarted", + "Getting Started With Python in 3ds Max", + f"{MAX_HELP}/2021/ENU/?guid=Max_Python_API_about_the_3ds_max_python_api_html", + "64651C48-F4F1-42F9-8C8A-FF5D0AA031A2"), + ("pymxs", + "Pymxs Online Documentation", + f"{MAX_HELP}/2021/ENU/?guid=Max_Python_API_using_pymxs_pymxs_module_html", + "44985F87-C175-4F3D-B70F-9FA0B6242AE1") + ], + 2022: [ + ("gettingstarted", + "Getting Started With Python in 3ds Max", + f"{MAX_HELP}/2022/ENU/?guid=MAXDEV_Python_about_the_3ds_max_python_api_html", + "64651C48-F4F1-42F9-8C8A-FF5D0AA031A2"), + ("pymxs", + "Pymxs Online Documentation", + f"{MAX_HELP}/2022/ENU/?guid=MAXDEV_Python_using_pymxs_html", + "44985F87-C175-4F3D-B70F-9FA0B6242AE1") + ], + 2023: [ + ("gettingstarted", + "Getting Started With Python in 3ds Max", + f"{MAX_HELP}/2023/ENU/?guid=MAXDEV_Python_about_the_3ds_max_python_api_html", + "64651C48-F4F1-42F9-8C8A-FF5D0AA031A2"), + ("pymxs", + "Pymxs Online Documentation", + f"{MAX_HELP}/2023/ENU/?guid=MAXDEV_Python_using_pymxs_html", + "44985F87-C175-4F3D-B70F-9FA0B6242AE1") + ], + 2024: [ + ("gettingstarted", + "Getting Started With Python in 3ds Max", + f"{MAX_HELP}/2024/ENU/?guid=MAXDEV_Python_about_the_3ds_max_python_api_html", + "64651C48-F4F1-42F9-8C8A-FF5D0AA031A2"), + ("pymxs", + "Pymxs Online Documentation", + f"{MAX_HELP}/2024/ENU/?guid=MAXDEV_Python_using_pymxs_html", + "44985F87-C175-4F3D-B70F-9FA0B6242AE1") + ], + 2025: [ + ("gettingstarted", + "Getting Started With Python in 3ds Max", + f"{MAX_HELP}/2025/ENU/?guid=MAXDEV_Python_about_the_3ds_max_python_api_html", + "64651C48-F4F1-42F9-8C8A-FF5D0AA031A2"), + ("pymxs", + "Pymxs Online Documentation", + f"{MAX_HELP}/2025/ENU/?guid=MAXDEV_Python_using_pymxs_html", + "44985F87-C175-4F3D-B70F-9FA0B6242AE1") + ] + } + +def get_version_topics(version): + """Get the version-dependent topics, defaulting on the latest + if the requested one does not exist""" + return MAX_VERSION_TOPICS[version if version in MAX_VERSION_TOPICS else 2024] + +V_TOPICS = get_version_topics(MAX_VERSION) + +PYSIDE6_DOC = ("pyside6", + "Qt for Python Documentation (PySide6)", + "doc.qt.io/qtforpython-6/index.html", + "E0E5F945-CD55-404A-840B-81540829E4C4") + +PYSIDE2_DOC = ("pyside2", + "Qt for Python Documentation (PySide2)", + "doc.qt.io/qtforpython-5/contents.html", + "13EEE11E-1BBB-470E-B757-F536D91215A9") + +TOPICS = V_TOPICS + [ + ("howtos", + "Python HowTos Github Repo", + "github.com/ADN-DevTech/3dsMax-Python-HowTos", + "2504EEA5-27D6-4EA0-A7A3-B3C058777ADC"), + ("samples", + "Python samples (Github Repo)", + "github.com/ADN-DevTech/3dsMax-Python-HowTos/tree/master/src/samples", + "8ED9D9CC-3799-435D-8016-0F8F16D84004"), + PYSIDE6_DOC if MAX_VERSION >= 2025 else PYSIDE2_DOC, + ("python", + f"Python {PYTHON_VERSION} Documentation", + f"docs.python.org/{PYTHON_VERSION}/", + "B51BCC07-D9E3-439C-AC88-85BD64B97912") ] MENU_LOCATION = ["&Scripting", "Python3 Development", "Browse Documentation"] @@ -35,4 +109,6 @@ def startup(): lambda topic=topic: webbrowser.open(f"https://{topic[2]}"), MENU_LOCATION, text=topic[1], - tooltip=topic[1]) + tooltip=topic[1], + in2025_menuid=menuhook.BROWSE_DOCUMENTATION, + id_2025=topic[3]) diff --git a/src/packages/menuhook/README.md b/src/packages/menuhook/README.md index ddc0983..31fd1ff 100644 --- a/src/packages/menuhook/README.md +++ b/src/packages/menuhook/README.md @@ -50,12 +50,12 @@ never be overridden. ## Q & A -*Q:* Why not using PySide2 directly? +*Q:* Why not using PySide directly? *A:* 3ds Max uses Qt for its menu and technically they can be inspected -and modified during PySide2. But the Menu Manager inside 3ds Max owns the +and modified during PySide. But the Menu Manager inside 3ds Max owns the menus and can regenerate them (using Qt) at any time during the execution -of 3ds Max. And because of that any change made to the menus using PySide2 +of 3ds Max. And because of that any change made to the menus using PySide instead of the 3ds Max Menu Manager will be lost. In short: things will not behave as expected. diff --git a/src/packages/menuhook/menuhook/__init__.py b/src/packages/menuhook/menuhook/__init__.py index 5f6fbe5..2c1dbc0 100644 --- a/src/packages/menuhook/menuhook/__init__.py +++ b/src/packages/menuhook/menuhook/__init__.py @@ -105,8 +105,14 @@ def add_menu_item(menu, action, category): targetmenu.addItem(newaction, -1) rt.menuman.updateMenuBar() -#pylint: disable=too-many-arguments -def register(action, category, fcn, menu=None, text=None, tooltip=None): +PYTHON_DEVELOPMENT = "82490C17-D86E-40C5-B387-C2E63A64C74D" +BROWSE_DOCUMENTATION = "DAF8D6C5-0C14-4A99-9370-8AA5329EA143" +HOW_TO = "FFBB0A45-5278-4572-8CD9-BB5B4D260153" +OTHER_SAMPLES = "CBB6F619-57B9-4C81-8135-41958BEF5BED" +registered_items = [] + +#pylint: disable=too-many-arguments, line-too-long +def register(action, category, fcn, menu=None, text=None, tooltip=None, in2025_menuid=None, id_2025=None): """ Appends a menu item to one of the menus of the main menubar. If the action already exists, the menu is not added but the @@ -116,9 +122,52 @@ def register(action, category, fcn, menu=None, text=None, tooltip=None): - creating a macro - assign the macro function if the macro is already there - create a menu item for the macro if it is not already there + + For 2025 and above, menu items that need to be created are kept + in the global registered_items list. """ - defined = macro_defined(action, category) - add_macro(action, category, text or action, tooltip or action, fcn) - if not defined and not menu is None: - add_menu_item(menu, action, category) -#pylint: enable=too-many-arguments + if (rt.maxversion())[7] >= 2025: + if in2025_menuid is not None and id_2025 is not None: + add_macro(action, category, text or action, tooltip or action, fcn) + registered_items.append((in2025_menuid, id_2025, category, action)) + else: + defined = macro_defined(action, category) + add_macro(action, category, text or action, tooltip or action, fcn) + if not defined and not menu is None: + add_menu_item(menu, action, category) +#pylint: enable=too-many-arguments, line-too-long + +# for 2025, pre-can a menu for the howtos +def register_howtos_menu_2025(menumgr): + """Register the menu structure in the new menu system""" + menumgr = rt.callbacks.notificationparam() + + scriptingmenu = "658724ec-de09-47dd-b723-918c59a28ad1" + scriptmenu = menumgr.getmenubyid(scriptingmenu) + + python_development = scriptmenu.createsubmenu( + PYTHON_DEVELOPMENT, + "Python 3 Development") + python_development.createsubmenu( + BROWSE_DOCUMENTATION, + "Browse Documentation") + python_development.createsubmenu( + HOW_TO, + "How To") + python_development.createsubmenu( + OTHER_SAMPLES, + "Other Samples") + + # hook the registered items + for reg in registered_items: + (in2025_menuid, id_2025, category, action) = reg + scriptmenu = menumgr.getmenubyid(in2025_menuid) + if scriptmenu is not None: + try: + scriptmenu.createaction(id_2025, 647394, f"{action}`{category}") +#pylint: disable=line-too-long, broad-exception-caught + except Exception as e: + print(f"Could not create item {category}, {action} in menu {in2025_menuid} because {e}") +#pylint: enable=line-too-long, broad-exception-caught + else: + print(f"Could not create item {category}, {action}, in missing menu {in2025_menuid}") diff --git a/src/packages/mxstranslate/mxstranslate/__init__.py b/src/packages/mxstranslate/mxstranslate/__init__.py index 40dd9d9..519ba60 100644 --- a/src/packages/mxstranslate/mxstranslate/__init__.py +++ b/src/packages/mxstranslate/mxstranslate/__init__.py @@ -20,4 +20,6 @@ def startup(): mxstranslate, menu=["&Scripting", "Python3 Development", "How To"], text="Translation window for mxs code", - tooltip="Translation window for mxs code") + tooltip="Translation window for mxs code", + in2025_menuid=menuhook.HOW_TO, + id_2025="004BF4E1-4AB3-42B5-979A-28662B26533C") diff --git a/src/packages/mxstranslate/mxstranslate/translate.py b/src/packages/mxstranslate/mxstranslate/translate.py index e36edc9..873c784 100644 --- a/src/packages/mxstranslate/mxstranslate/translate.py +++ b/src/packages/mxstranslate/mxstranslate/translate.py @@ -2,12 +2,12 @@ Toolbar giving access to experimental MXS -> Python translation """ #pylint: disable= import-error, invalid-name, too-few-public-methods -from PySide2.QtWidgets import (QApplication, QWidget, QDockWidget, +from qtpy.QtWidgets import (QApplication, QWidget, QDockWidget, QVBoxLayout, QLabel, QTextEdit, QStyle, QToolBar, QCommonStyle) -from PySide2.QtGui import (QSyntaxHighlighter, QTextCharFormat, +from qtpy.QtGui import (QSyntaxHighlighter, QTextCharFormat, QFont, Qt, QBrush, QColor, QKeyEvent) -from PySide2 import QtCore -from PySide2.QtCore import QMimeData, QEvent +from qtpy import QtCore +from qtpy.QtCore import QMimeData, QEvent from pygments.lexers.python import PythonLexer from pygments.token import Token from mxs2py import topy diff --git a/src/packages/mxstranslate/setup.py b/src/packages/mxstranslate/setup.py index dc268ec..21aef6f 100644 --- a/src/packages/mxstranslate/setup.py +++ b/src/packages/mxstranslate/setup.py @@ -13,7 +13,8 @@ packages=setuptools.find_packages(), entry_points={'3dsMax': 'startup=mxstranslate:startup'}, install_requires=[ - 'pygments' + 'pygments', + 'qtpy' ], python_requires='>=3.7' ) diff --git a/src/packages/mxthread/README.md b/src/packages/mxthread/README.md index b4c7aff..6189a28 100644 --- a/src/packages/mxthread/README.md +++ b/src/packages/mxthread/README.md @@ -51,7 +51,7 @@ This sample can be saved in a "testmxthread.py" file and then run in 3dsMax. ```python from mxthread import on_main_thread, main_thread_print, run_on_main_thread from pymxs import runtime as rt -from PySide2.QtCore import QThread +from qtpy.QtCore import QThread class Worker(QThread): diff --git a/src/packages/mxthread/mxthread/__init__.py b/src/packages/mxthread/mxthread/__init__.py index 77cd5a8..75f8273 100644 --- a/src/packages/mxthread/mxthread/__init__.py +++ b/src/packages/mxthread/mxthread/__init__.py @@ -6,8 +6,8 @@ import sys import os import functools -from PySide2.QtCore import QObject, Slot, Signal, QThread, QMutex, QWaitCondition, QTimer -from PySide2.QtWidgets import QApplication +from qtpy.QtCore import QObject, Slot, Signal, QThread, QMutex, QWaitCondition, QTimer +from qtpy.QtWidgets import QApplication #pylint: disable=W0703,R0903 class RunnableWaitablePayload(): diff --git a/src/packages/mxthread/setup.py b/src/packages/mxthread/setup.py index 6407fd2..cee451c 100644 --- a/src/packages/mxthread/setup.py +++ b/src/packages/mxthread/setup.py @@ -11,5 +11,8 @@ long_description_content_type="text/markdown", url="https://git.autodesk.com/windish/maxpythontutorials", packages=setuptools.find_packages(), + install_requires=[ + 'qtpy' + ], python_requires='>=3.7' ) diff --git a/src/packages/pyconsole/pyconsole/__init__.py b/src/packages/pyconsole/pyconsole/__init__.py index e994751..73d266a 100644 --- a/src/packages/pyconsole/pyconsole/__init__.py +++ b/src/packages/pyconsole/pyconsole/__init__.py @@ -19,7 +19,9 @@ def startup(): pyconsole, menu=["&Scripting", "Python3 Development", "How To"], text="Python Console", - tooltip="Python Console") + tooltip="Python Console", + in2025_menuid=menuhook.HOW_TO, + id_2025="0F52AF28-D7EE-4A04-AC9D-56C126FE9373") # Create a python console in the command panel # automatically console.new_console(tabto="CommandPanel") diff --git a/src/packages/pyconsole/pyconsole/console.py b/src/packages/pyconsole/pyconsole/console.py index 3a136fc..c733392 100644 --- a/src/packages/pyconsole/pyconsole/console.py +++ b/src/packages/pyconsole/pyconsole/console.py @@ -6,8 +6,8 @@ from qtmax import GetQMaxMainWindow from pyqtconsole.console import PythonConsole import pyqtconsole.highlighter as hl -from PySide2.QtWidgets import QWidget, QDockWidget -from PySide2 import QtCore +from qtpy.QtWidgets import QWidget, QDockWidget +from qtpy import QtCore # My personal choice of colors HUGOS_THEME = { diff --git a/src/packages/pyconsole/setup.py b/src/packages/pyconsole/setup.py index 5501ae7..f86e5e3 100644 --- a/src/packages/pyconsole/setup.py +++ b/src/packages/pyconsole/setup.py @@ -12,8 +12,9 @@ url="https://git.autodesk.com/windish/maxpythontutorials", packages=setuptools.find_packages(), install_requires=[ - 'jedi==0.17.2', - 'pyqtconsole' + 'jedi==0.19.1', + 'pyqtconsole', + 'qtpy' ], entry_points={'3dsMax': 'startup=pyconsole:startup'}, python_requires='>=3.7' diff --git a/src/packages/quickpreview/quickpreview/__init__.py b/src/packages/quickpreview/quickpreview/__init__.py index fe5d1b3..826a73b 100644 --- a/src/packages/quickpreview/quickpreview/__init__.py +++ b/src/packages/quickpreview/quickpreview/__init__.py @@ -29,4 +29,6 @@ def startup(): quickpreview, menu=["&Scripting", "Python3 Development", "How To"], text="Create a quick preview", - tooltip="Create a quick preview") + tooltip="Create a quick preview", + in2025_menuid=menuhook.HOW_TO, + id_2025="E30C825C-E4CD-48FD-A1C7-A75A91B2E9C2") diff --git a/src/packages/reloadmod/reloadmod/__init__.py b/src/packages/reloadmod/reloadmod/__init__.py index 9b513af..5cd7c96 100644 --- a/src/packages/reloadmod/reloadmod/__init__.py +++ b/src/packages/reloadmod/reloadmod/__init__.py @@ -21,4 +21,6 @@ def startup(): python_reload, menu=["&Scripting", "Python3 Development"], text="Reload Python Modules", - tooltip="Reload Python Modules") + tooltip="Reload Python Modules", + in2025_menuid=menuhook.PYTHON_DEVELOPMENT, + id_2025="7A8CFC05-0752-4501-A5BD-F5AF020D7F5F") diff --git a/src/packages/removeallmaterials/removeallmaterials/__init__.py b/src/packages/removeallmaterials/removeallmaterials/__init__.py index de6f891..51960c3 100644 --- a/src/packages/removeallmaterials/removeallmaterials/__init__.py +++ b/src/packages/removeallmaterials/removeallmaterials/__init__.py @@ -19,4 +19,6 @@ def startup(): remove_all_materials, menu=["&Scripting", "Python3 Development", "How To"], text="Remove all materials from the scene", - tooltip="Remove all materials from the scene") + tooltip="Remove all materials from the scene", + in2025_menuid=menuhook.HOW_TO, + id_2025="1A3AE016-3E54-4856-9076-2BE491B2258C") diff --git a/src/packages/renameselected/README.md b/src/packages/renameselected/README.md index a2361dd..0ee55a8 100644 --- a/src/packages/renameselected/README.md +++ b/src/packages/renameselected/README.md @@ -6,13 +6,13 @@ [Source Code](renameselected/__init__.py) *Goals:* -- learn how to create a dialog with PySide2 +- learn how to create a dialog with PySide - learn how to hook a Python function to a 3ds Max ui element ## Explanations This tutorial shows how to rename all selected objects using a base name, -chosen in a PySide2 dialog. +chosen in a PySide dialog. ## Using the tool diff --git a/src/packages/renameselected/renameselected/__init__.py b/src/packages/renameselected/renameselected/__init__.py index a39e879..7ecd705 100644 --- a/src/packages/renameselected/renameselected/__init__.py +++ b/src/packages/renameselected/renameselected/__init__.py @@ -28,4 +28,6 @@ def startup(): showdialog, menu=["&Scripting", "Python3 Development", "How To"], text="Rename all elements in selection", - tooltip="renameselected sample") + tooltip="renameselected sample", + in2025_menuid=menuhook.HOW_TO, + id_2025="163ACF54-313D-4B1B-8615-F6F979AE0FE7") diff --git a/src/packages/renameselected/renameselected/ui.py b/src/packages/renameselected/renameselected/ui.py index 8bb435f..d5710fc 100644 --- a/src/packages/renameselected/renameselected/ui.py +++ b/src/packages/renameselected/renameselected/ui.py @@ -1,9 +1,9 @@ """ - Provide a PySide2 dialog for the tool. + Provide a qtpy dialog for the tool. """ #pylint: disable=no-name-in-module #pylint: disable=too-few-public-methods -from PySide2.QtWidgets import QWidget, QDialog, QLabel, QLineEdit, QVBoxLayout, QPushButton +from qtpy.QtWidgets import QWidget, QDialog, QLabel, QLineEdit, QVBoxLayout, QPushButton from pymxs import runtime as rt class PyMaxDialog(QDialog): diff --git a/src/packages/renameselected/setup.py b/src/packages/renameselected/setup.py index 21c8417..4ab7789 100644 --- a/src/packages/renameselected/setup.py +++ b/src/packages/renameselected/setup.py @@ -11,5 +11,8 @@ long_description_content_type="text/markdown", packages=setuptools.find_packages(), entry_points={'3dsMax': 'startup=renameselected:startup'}, + install_requires=[ + 'qtpy' + ], python_requires='>=3.7' ) diff --git a/src/packages/singleinstancedlg/README.md b/src/packages/singleinstancedlg/README.md index 8b88855..8ca4190 100644 --- a/src/packages/singleinstancedlg/README.md +++ b/src/packages/singleinstancedlg/README.md @@ -3,12 +3,12 @@ This sample shows how to create a single instance modeless dialog. *Goal:* -- learn how how to use findChild in PySide2 to create a single instance +- learn how how to use findChild in PySide to create a single instance dialog ## Explanations -The sample creates a custome PySide2 dialog and calls `setObjectName` +The sample creates a custom PySide dialog and calls `setObjectName` on it with a unique name. The `show_dialog()` function only creates a new dialog if `findChild` cannot find the QDialog with the name specified in `setObjectName`. The dialog (either found or created) is @@ -21,7 +21,7 @@ In [ui.py](singleinstancedlg/ui.py), we first create a new custom dialog class. ```python -from PySide2.QtWidgets import QWidget, QDialog, QVBoxLayout, QPushButton +from qtpy.QtWidgets import QWidget, QDialog, QVBoxLayout, QPushButton from pymxs import runtime as rt MAIN_WINDOW = QWidget.find(rt.windows.getMAXHWND()) diff --git a/src/packages/singleinstancedlg/setup.py b/src/packages/singleinstancedlg/setup.py index 45be9c4..991422c 100644 --- a/src/packages/singleinstancedlg/setup.py +++ b/src/packages/singleinstancedlg/setup.py @@ -11,5 +11,8 @@ long_description_content_type="text/markdown", packages=setuptools.find_packages(), entry_points={'3dsMax': 'startup=singleinstancedlg:startup'}, + install_requires=[ + 'qtpy' + ], python_requires='>=3.7' ) diff --git a/src/packages/singleinstancedlg/singleinstancedlg/__init__.py b/src/packages/singleinstancedlg/singleinstancedlg/__init__.py index b52023c..0a1a544 100644 --- a/src/packages/singleinstancedlg/singleinstancedlg/__init__.py +++ b/src/packages/singleinstancedlg/singleinstancedlg/__init__.py @@ -19,4 +19,6 @@ def startup(): singleinstancedlg, menu=["&Scripting", "Python3 Development", "Other Samples"], text="Single instance modeless dialog", - tooltip="Single instance modeless dialog") + tooltip="Single instance modeless dialog", + in2025_menuid=menuhook.OTHER_SAMPLES, + id_2025="AF515BBA-E826-4DA8-B097-FA9A2C917A91") diff --git a/src/packages/singleinstancedlg/singleinstancedlg/ui.py b/src/packages/singleinstancedlg/singleinstancedlg/ui.py index 71be59f..e853d38 100644 --- a/src/packages/singleinstancedlg/singleinstancedlg/ui.py +++ b/src/packages/singleinstancedlg/singleinstancedlg/ui.py @@ -1,10 +1,10 @@ """ - PySide2 modeless dialog that will not be started more than once + qtpy modeless dialog that will not be started more than once at the same time """ #pylint: disable=no-name-in-module #pylint: disable=too-few-public-methods -from PySide2.QtWidgets import QWidget, QDialog, QVBoxLayout, QPushButton +from qtpy.QtWidgets import QWidget, QDialog, QVBoxLayout, QPushButton from pymxs import runtime as rt MAIN_WINDOW = QWidget.find(rt.windows.getMAXHWND()) diff --git a/src/packages/speedsheet/speedsheet/__init__.py b/src/packages/speedsheet/speedsheet/__init__.py index d7ffcb5..a26dca8 100644 --- a/src/packages/speedsheet/speedsheet/__init__.py +++ b/src/packages/speedsheet/speedsheet/__init__.py @@ -38,5 +38,7 @@ def startup(): "howtos", speedsheet, menu=["&Scripting", "Python3 Development", "How To"], - text="Output Object Data to File", - tooltip="Output Object Data to File") + text="Save object data to file", + tooltip="Save object data to file", + in2025_menuid=menuhook.HOW_TO, + id_2025="FFA6888A-B27A-4FA9-BFED-86FD3683E6B6") diff --git a/src/packages/threadprogressbar/setup.py b/src/packages/threadprogressbar/setup.py index b426e31..cfd0451 100644 --- a/src/packages/threadprogressbar/setup.py +++ b/src/packages/threadprogressbar/setup.py @@ -11,5 +11,8 @@ long_description_content_type="text/markdown", packages=setuptools.find_packages(), entry_points={'3dsMax': 'startup=threadprogressbar:startup'}, + install_requires=[ + 'qtpy' + ], python_requires='>=3.7' ) diff --git a/src/packages/threadprogressbar/threadprogressbar/__init__.py b/src/packages/threadprogressbar/threadprogressbar/__init__.py index 84b4cd4..8596476 100644 --- a/src/packages/threadprogressbar/threadprogressbar/__init__.py +++ b/src/packages/threadprogressbar/threadprogressbar/__init__.py @@ -20,4 +20,6 @@ def startup(): threadprogressbar, menu=["&Scripting", "Python3 Development", "Other Samples"], text="Update a progress bar from a thread", - tooltip="Update a progress bar from a thread") + tooltip="Update a progress bar from a thread", + in2025_menuid=menuhook.OTHER_SAMPLES, + id_2025="AB072ECE-8665-4EC4-8D12-C0E76DA4C919") diff --git a/src/packages/threadprogressbar/threadprogressbar/ui.py b/src/packages/threadprogressbar/threadprogressbar/ui.py index 31d15f8..a9f20bc 100644 --- a/src/packages/threadprogressbar/threadprogressbar/ui.py +++ b/src/packages/threadprogressbar/threadprogressbar/ui.py @@ -1,11 +1,11 @@ """ - PySide2 dialog that launches a worker and monitors its progress. + qtpy dialog that launches a worker and monitors its progress. """ #pylint: disable=no-name-in-module #pylint: disable=too-few-public-methods import time -from PySide2.QtWidgets import QWidget, QDialog, QLabel, QProgressBar, QVBoxLayout, QPushButton -from PySide2.QtCore import QThread, Signal +from qtpy.QtWidgets import QWidget, QDialog, QLabel, QProgressBar, QVBoxLayout, QPushButton +from qtpy.QtCore import QThread, Signal from pymxs import runtime as rt MINRANGE = 1 diff --git a/src/packages/transformlock/transformlock/__init__.py b/src/packages/transformlock/transformlock/__init__.py index 7a0cde2..1adbd5c 100644 --- a/src/packages/transformlock/transformlock/__init__.py +++ b/src/packages/transformlock/transformlock/__init__.py @@ -18,4 +18,6 @@ def startup(): lock_selection, menu=["&Scripting", "Python3 Development", "How To"], text="Lock transformations for the selection", - tooltip="Lock transformations for the selection") + tooltip="Lock transformations for the selection", + in2025_menuid=menuhook.HOW_TO, + id_2025="F9DA574D-185B-4C00-9C37-795B38719E78") diff --git a/src/packages/zdepthchannel/zdepthchannel/__init__.py b/src/packages/zdepthchannel/zdepthchannel/__init__.py index 29e8f83..141e954 100644 --- a/src/packages/zdepthchannel/zdepthchannel/__init__.py +++ b/src/packages/zdepthchannel/zdepthchannel/__init__.py @@ -42,4 +42,6 @@ def startup(): zdepthchannel, menu=["&Scripting", "Python3 Development", "How To"], text="Access the Z-Depth Channel", - tooltip="Access the Z-Depth Channel") + tooltip="Access the Z-Depth Channel", + in2025_menuid=menuhook.HOW_TO, + id_2025="BC1B51C9-2F02-496D-B9ED-9A61022A569D") diff --git a/src/pystartup/pystartup.ms b/src/pystartup/pystartup.ms index 0a37b16..2952eab 100644 --- a/src/pystartup/pystartup.ms +++ b/src/pystartup/pystartup.ms @@ -1,6 +1,6 @@ -if isProperty python "execute" then ( +if isProperty python "execute" and ((maxversion())[8]<2025) then ( python.execute ("def _python_startup():\n" + - " try:\n" + + " try:\n" + " import pkg_resources\n" + " except ImportError:\n" + " print('startup Python modules require pip to be installed.')\n" + @@ -11,8 +11,8 @@ if isProperty python "execute" then ( " try:\n" + " fcn = entrypt.load()\n" + " fcn()\n" + - " except:\n" + - " print('skipped package startup for {}, startup not working'.format(dist))\n" + + " except Exception as e:\n" + + " print(f'skipped package startup for {dist} because {e}, startup not working')\n" + "_python_startup()\n" + "del _python_startup") ) diff --git a/src/samples/PySide2/combine_meshes.py b/src/samples/PySide/combine_meshes.py similarity index 95% rename from src/samples/PySide2/combine_meshes.py rename to src/samples/PySide/combine_meshes.py index 20c3aae..bd40087 100644 --- a/src/samples/PySide2/combine_meshes.py +++ b/src/samples/PySide/combine_meshes.py @@ -1,7 +1,7 @@ ''' Demonstrates combining the mesh of two scene nodes ''' -from PySide2.QtWidgets import QVBoxLayout, QPushButton, QLabel, QDialog, QMessageBox +from qtpy.QtWidgets import QVBoxLayout, QPushButton, QLabel, QDialog, QMessageBox from pymxs import runtime as rt # pylint: disable=import-error from qtmax import GetQMaxMainWindow diff --git a/src/samples/PySide2/cylinder_icon_48.png b/src/samples/PySide/cylinder_icon_48.png similarity index 100% rename from src/samples/PySide2/cylinder_icon_48.png rename to src/samples/PySide/cylinder_icon_48.png diff --git a/src/samples/PySide2/docking_widgets.py b/src/samples/PySide/docking_widgets.py similarity index 90% rename from src/samples/PySide2/docking_widgets.py rename to src/samples/PySide/docking_widgets.py index f4102e1..8092848 100644 --- a/src/samples/PySide2/docking_widgets.py +++ b/src/samples/PySide/docking_widgets.py @@ -1,14 +1,14 @@ ''' - Demonstrates how to create a QWidget with PySide2 and attach it to the 3dsmax main window. + Demonstrates how to create a QWidget with PySide and attach it to the 3dsmax main window. Creates two types of dockable widgets, a QDockWidget and a QToolbar ''' import os import ctypes -from PySide2 import QtCore -from PySide2 import QtGui -from PySide2.QtWidgets import QMainWindow, QDockWidget, QToolButton, QToolBar, QAction +from qtpy import QtCore +from qtpy import QtGui +from qtpy.QtWidgets import QMainWindow, QDockWidget, QToolButton, QToolBar, QAction from pymxs import runtime as rt from qtmax import GetQMaxMainWindow @@ -46,7 +46,7 @@ def create_cylinder(): def demo_docking_widgets(): """ - Demonstrates how to create a QWidget with PySide2 and attach it to the 3dsmax main window. + Demonstrates how to create a QWidget with PySide and attach it to the 3dsmax main window. Creates two types of dockable widgets, a QDockWidget and a QToolbar """ # Retrieve 3ds Max Main Window QWdiget diff --git a/src/samples/PySide2/simple_dialog.py b/src/samples/PySide/simple_dialog.py similarity index 84% rename from src/samples/PySide2/simple_dialog.py rename to src/samples/PySide/simple_dialog.py index 3fc5501..d076cf4 100644 --- a/src/samples/PySide2/simple_dialog.py +++ b/src/samples/PySide/simple_dialog.py @@ -1,8 +1,8 @@ ''' - Demonstrates how to create a QDialog with PySide2 and attach it to the 3ds Max main window. + Demonstrates how to create a QDialog with PySide and attach it to the 3ds Max main window. ''' -from PySide2.QtWidgets import QDialog, QLabel, QVBoxLayout, QPushButton +from qtpy.QtWidgets import QDialog, QLabel, QVBoxLayout, QPushButton from pymxs import runtime as rt from qtmax import GetQMaxMainWindow @@ -39,7 +39,7 @@ def init_ui(self): def demo_simple_dialog(): """ - Entry point for QDialog demo making use of PySide2 and pymxs + Entry point for QDialog demo making use of PySide and pymxs """ # reset 3ds Max rt.resetMaxFile(rt.Name('noPrompt')) diff --git a/src/samples/PySide2/test_ui.ui b/src/samples/PySide/test_ui.ui similarity index 100% rename from src/samples/PySide2/test_ui.ui rename to src/samples/PySide/test_ui.ui diff --git a/src/samples/PySide2/ui_loader.py b/src/samples/PySide/ui_loader.py similarity index 83% rename from src/samples/PySide2/ui_loader.py rename to src/samples/PySide/ui_loader.py index 3369960..57c310d 100644 --- a/src/samples/PySide2/ui_loader.py +++ b/src/samples/PySide/ui_loader.py @@ -1,10 +1,10 @@ ''' - Demonstrates loading .ui files with PySide2 + Demonstrates loading .ui files with PySide ''' import os -from PySide2.QtWidgets import QMainWindow -from PySide2.QtCore import QFile -from PySide2.QtUiTools import QUiLoader +from qtpy.QtWidgets import QMainWindow +from qtpy.QtCore import QFile +from qtpy.QtUiTools import QUiLoader from pymxs import runtime as rt from qtmax import GetQMaxMainWindow diff --git a/uninstall.sh b/uninstall.sh index c05c39d..1ecec0f 100644 --- a/uninstall.sh +++ b/uninstall.sh @@ -18,5 +18,7 @@ echo "Uninstall pip" ./python.exe -m pip uninstall pip ) -echo "Uninstall pystartup" -rm "$startuppath/pystartup.ms" +echo "Uninstall pystartup and adn-devtech-python-howtos" +rm -f "$startuppath/pystartup.ms" +rm -fr "$ProgramData/Autodesk/ApplicationPlugins/adn-devtech-python-howtos" +