diff --git a/openpype/hosts/3dsmax/api/__init__.py b/openpype/hosts/3dsmax/api/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openpype/hosts/3dsmax/plugins/__init__.py b/openpype/hosts/3dsmax/plugins/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openpype/hosts/3dsmax/startup/startup.py b/openpype/hosts/3dsmax/startup/startup.py deleted file mode 100644 index dd8c08a6b9..0000000000 --- a/openpype/hosts/3dsmax/startup/startup.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -print("inside python startup") \ No newline at end of file diff --git a/openpype/hosts/max/__init__.py b/openpype/hosts/max/__init__.py new file mode 100644 index 0000000000..8da0e0ee42 --- /dev/null +++ b/openpype/hosts/max/__init__.py @@ -0,0 +1,10 @@ +from .addon import ( + MaxAddon, + MAX_HOST_DIR, +) + + +__all__ = ( + "MaxAddon", + "MAX_HOST_DIR", +) \ No newline at end of file diff --git a/openpype/hosts/max/addon.py b/openpype/hosts/max/addon.py new file mode 100644 index 0000000000..734b87dd21 --- /dev/null +++ b/openpype/hosts/max/addon.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +import os +from openpype.modules import OpenPypeModule, IHostAddon + +MAX_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class MaxAddon(OpenPypeModule, IHostAddon): + name = "max" + host_name = "max" + + def initialize(self, module_settings): + self.enabled = True + + def get_workfile_extensions(self): + return [".max"] diff --git a/openpype/hosts/max/api/__init__.py b/openpype/hosts/max/api/__init__.py new file mode 100644 index 0000000000..b6998df862 --- /dev/null +++ b/openpype/hosts/max/api/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +"""Public API for 3dsmax""" + +from .pipeline import ( + MaxHost +) +from .menu import OpenPypeMenu + + +__all__ = [ + "MaxHost", + "OpenPypeMenu" +] diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py new file mode 100644 index 0000000000..e50de85f68 --- /dev/null +++ b/openpype/hosts/max/api/lib.py @@ -0,0 +1,2 @@ +def imprint(attr, data): + ... diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py new file mode 100644 index 0000000000..13ca503b4d --- /dev/null +++ b/openpype/hosts/max/api/menu.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""3dsmax menu definition of OpenPype.""" +from abc import ABCMeta, abstractmethod +import six +from Qt import QtWidgets, QtCore +from pymxs import runtime as rt + +from openpype.tools.utils import host_tools + + +@six.add_metaclass(ABCMeta) +class OpenPypeMenu(object): + + def __init__(self): + self.main_widget = self.get_main_widget() + + @staticmethod + def get_main_widget(): + """Get 3dsmax main window.""" + return QtWidgets.QWidget.find(rt.windows.getMAXHWND()) + + def get_main_menubar(self): + """Get main Menubar by 3dsmax main window.""" + return list(self.main_widget.findChildren(QtWidgets.QMenuBar))[0] + + def get_or_create_openpype_menu(self, name="&OpenPype", before="&Help"): + menu_bar = self.get_main_menubar() + menu_items = menu_bar.findChildren( + QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly) + help_action = None + for item in menu_items: + if name in item.title(): + # we already have OpenPype menu + return item + + if before in item.title(): + help_action = item.menuAction() + + op_menu = QtWidgets.QMenu("&OpenPype") + menu_bar.insertMenu(before, op_menu) + return op_menu + + def build_openpype_menu(self): + openpype_menu = self.get_or_create_openpype_menu() + load_action = QtWidgets.QAction("Load...", openpype_menu) + load_action.triggered.connect(self.load_callback) + openpype_menu.addAction(load_action) + + publish_action = QtWidgets.QAction("Publish...", openpype_menu) + publish_action.triggered.connect(self.publish_callback) + openpype_menu.addAction(publish_action) + + manage_action = QtWidgets.QAction("Manage...", openpype_menu) + manage_action.triggered.connect(self.manage_callback) + openpype_menu.addAction(manage_action) + + library_action = QtWidgets.QAction("Library...", openpype_menu) + library_action.triggered.connect(self.library_callback) + openpype_menu.addAction(library_action) + + openpype_menu.addSeparator() + + workfiles_action = QtWidgets.QAction("Work Files...", openpype_menu) + workfiles_action.triggered.connect(self.workfiles_callback) + openpype_menu.addAction(workfiles_action) + + def load_callback(self): + host_tools.show_loader(parent=self.main_widget) + + def publish_callback(self): + host_tools.show_publisher(parent=self.main_widget) + + def manage_callback(self): + host_tools.show_subset_manager(parent=self.main_widget) + + def library_callback(self): + host_tools.show_library_loader(parent=self.main_widget) + + def workfiles_callback(self): + host_tools.show_workfiles(parent=self.main_widget) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py new file mode 100644 index 0000000000..2ee5989871 --- /dev/null +++ b/openpype/hosts/max/api/pipeline.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +"""Pipeline tools for OpenPype Houdini integration.""" +import os +import sys +import logging +import contextlib + +from openpype.host import HostBase, IWorkfileHost, ILoadHost, INewPublisher +import pyblish.api +from openpype.pipeline import ( + register_creator_plugin_path, + register_loader_plugin_path, + AVALON_CONTAINER_ID, +) +from openpype.hosts.max.api import OpenPypeMenu +from openpype.hosts.max.api import lib +from openpype.hosts.max import MAX_HOST_DIR +from openpype.pipeline.load import any_outdated_containers +from openpype.lib import ( + register_event_callback, + emit_event, +) +from pymxs import runtime as rt # noqa + +log = logging.getLogger("openpype.hosts.max") + +PLUGINS_DIR = os.path.join(MAX_HOST_DIR, "plugins") +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") + + +class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): + name = "max" + menu = None + + def __init__(self): + super(MaxHost, self).__init__() + self._op_events = {} + self._has_been_setup = False + + def install(self): + pyblish.api.register_host("max") + + pyblish.api.register_plugin_path(PUBLISH_PATH) + register_loader_plugin_path(LOAD_PATH) + register_creator_plugin_path(CREATE_PATH) + log.info("Building menu ...") + + self.menu = OpenPypeMenu() + + log.info("Installing callbacks ... ") + # register_event_callback("init", on_init) + self._register_callbacks() + + # register_event_callback("before.save", before_save) + # register_event_callback("save", on_save) + # register_event_callback("open", on_open) + # register_event_callback("new", on_new) + + # pyblish.api.register_callback( + # "instanceToggled", on_pyblish_instance_toggled + # ) + + self._has_been_setup = True + + def has_unsaved_changes(self): + # TODO: how to get it from 3dsmax? + return True + + def get_workfile_extensions(self): + return [".hip", ".hiplc", ".hipnc"] + + def save_workfile(self, dst_path=None): + rt.saveMaxFile(dst_path) + return dst_path + + def open_workfile(self, filepath): + rt.checkForSave() + rt.loadMaxFile(filepath) + return filepath + + def get_current_workfile(self): + return os.path.join(rt.maxFilePath, rt.maxFileName) + + def get_containers(self): + return ls() + + def _register_callbacks(self): + for event in self._op_events.copy().values(): + if event is None: + continue + + try: + rt.callbacks.removeScript(id=rt.name(event.name)) + except RuntimeError as e: + log.info(e) + + rt.callbacks.addScript( + event.name, event.callback, id=rt.Name('OpenPype')) + + @staticmethod + def create_context_node(): + """Helper for creating context holding node.""" + + root_scene = rt.rootScene + + create_attr_script = (""" +attributes "OpenPypeContext" +( + parameters main rollout:params + ( + context type: #string + ) + + rollout params "OpenPype Parameters" + ( + editText editTextContext "Context" type: #string + ) +) + """) + + attr = rt.execute(create_attr_script) + rt.custAttributes.add(root_scene, attr) + + return root_scene.OpenPypeContext.context + + def update_context_data(self, data, changes): + try: + context = rt.rootScene.OpenPypeContext.context + except AttributeError: + # context node doesn't exists + context = self.create_context_node() + + lib.imprint(context, data) + + def get_context_data(self): + try: + context = rt.rootScene.OpenPypeContext.context + except AttributeError: + # context node doesn't exists + context = self.create_context_node() + return lib.read(context) + + def save_file(self, dst_path=None): + # Force forwards slashes to avoid segfault + dst_path = dst_path.replace("\\", "/") + rt.saveMaxFile(dst_path) + + +def ls(): + ... \ No newline at end of file diff --git a/openpype/hosts/max/hooks/set_paths.py b/openpype/hosts/max/hooks/set_paths.py new file mode 100644 index 0000000000..3db5306344 --- /dev/null +++ b/openpype/hosts/max/hooks/set_paths.py @@ -0,0 +1,17 @@ +from openpype.lib import PreLaunchHook + + +class SetPath(PreLaunchHook): + """Set current dir to workdir. + + Hook `GlobalHostDataHook` must be executed before this hook. + """ + app_groups = ["max"] + + def execute(self): + workdir = self.launch_context.env.get("AVALON_WORKDIR", "") + if not workdir: + self.log.warning("BUG: Workdir is not filled.") + return + + self.launch_context.kwargs["cwd"] = workdir diff --git a/openpype/hosts/3dsmax/__init__.py b/openpype/hosts/max/plugins/__init__.py similarity index 100% rename from openpype/hosts/3dsmax/__init__.py rename to openpype/hosts/max/plugins/__init__.py diff --git a/openpype/hosts/3dsmax/startup/startup.ms b/openpype/hosts/max/startup/startup.ms similarity index 100% rename from openpype/hosts/3dsmax/startup/startup.ms rename to openpype/hosts/max/startup/startup.ms diff --git a/openpype/hosts/max/startup/startup.py b/openpype/hosts/max/startup/startup.py new file mode 100644 index 0000000000..afcbd2d132 --- /dev/null +++ b/openpype/hosts/max/startup/startup.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from openpype.hosts.max.api import MaxHost +from openpype.pipeline import install_host + +host = MaxHost() +install_host(host) + diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index c07350ba07..c0c103ea10 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -152,7 +152,7 @@ class HostsEnumEntity(BaseEnumEntity): schema_types = ["hosts-enum"] all_host_names = [ - "3dsmax", + "max", "aftereffects", "blender", "celaction",