diff --git a/.cached_metadata.pickle b/.cached_metadata.pickle new file mode 100644 index 0000000..d274a45 --- /dev/null +++ b/.cached_metadata.pickle @@ -0,0 +1,121 @@ +(dp1 +S'sg_version_data' +p2 +(dp3 +S'code' +p4 +S'v0.9.4' +p5 +sS'description' +p6 +S'Fixes a stability issue on Maya launch.' +p7 +sS'tags' +p8 +(lp9 +sS'sg_payload' +p10 +(dp11 +S'name' +p12 +S'tk-maya_v0.9.4.zip' +p13 +sS'url' +p14 +S'https://tank.shotgunstudio.com/file_serve/attachment/20509' +p15 +sS'content_type' +p16 +S'application/zip' +p17 +sS'type' +p18 +S'Attachment' +p19 +sS'id' +p20 +I20509 +sS'link_type' +p21 +S'upload' +p22 +ssS'sg_status_list' +p23 +S'prod' +p24 +sS'sg_detailed_release_notes' +p25 +(dp26 +S'name' +p27 +S'v0.9.4' +p28 +sS'url' +p29 +S'https://support.shotgunsoftware.com/hc/en-us/articles/219032818#v0.9.4' +p30 +sS'content_type' +p31 +NsS'type' +p32 +S'Attachment' +p33 +sS'id' +p34 +I20506 +sS'link_type' +p35 +S'web' +p36 +ssS'sg_documentation' +p37 +(dp38 +S'name' +p39 +S'Documentation' +p40 +sS'url' +p41 +S'https://support.shotgunsoftware.com/hc/en-us/articles/219032818' +p42 +sS'content_type' +p43 +NsS'type' +p44 +S'Attachment' +p45 +sS'id' +p46 +I20508 +sS'link_type' +p47 +S'web' +p48 +ssS'type' +p49 +S'CustomNonProjectEntity04' +p50 +sS'id' +p51 +I1051 +ssS'sg_bundle_data' +p52 +(dp53 +S'sg_status_list' +p54 +S'depl' +p55 +sS'type' +p56 +S'CustomNonProjectEntity03' +p57 +sS'id' +p58 +I4 +sS'sg_system_name' +p59 +S'tk-maya' +p60 +sS'sg_deprecation_message' +p61 +Nss. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..386e7af --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +*.py[co] + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg + +# PyCharm project settings +.idea + +# Max OS X Desktop Services Store files +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5172753 --- /dev/null +++ b/LICENSE @@ -0,0 +1,146 @@ +SHOTGUN PIPELINE TOOLKIT SOURCE CODE LICENSE + +Version: 7/07/2013 + +Shotgun Software Inc. ("Company") provides the Shotgun Pipeline Toolkit, +software, including source code, in this package or repository folder (the +"Shotgun Toolkit Code") subject to your acceptance of and compliance with +the following terms and conditions (the "License Terms"). By accessing, +downloading, copying, using or modifying any of the Shotgun Toolkit Code, +you agree to these License Terms. + +Eligibility + +The following license to the Shotgun Toolkit Code is valid only if and while +you are a customer of Company in good standing with either: (a) a current, +paid-up (or free-for-evaluation) subscription or fixed-term license for +Company's Shotgun Platform; or (b) a perpetual license and current, paid-up +maintenance and support contract for the Shotgun Platform. + +Shotgun Toolkit Code License + +Subject to the eligibility criteria above and your compliance with these +License Terms, Company grants to you a non-exclusive, limited license to +reproduce, use, and make derivative works of (including by compiling object +code versions of) the Shotgun Toolkit Code solely for your non-commercial or +internal business purposes in connection with your authorized use of the +Shotgun Platform. + +Company reserves all rights in the Shotgun Toolkit Code not expressly granted +above. These License Terms do not grant or require Company to grant, by +implication, estoppel, or otherwise, any other licenses or rights with respect +to the Shotgun Toolkit Code or any of Company's other software or intellectual +property rights. You agree not to take any action with respect to the Shotgun +Toolkit Code that is not expressly authorized above. + +You must keep intact (and, in the case of copies, reproduce) all copyright +and other proprietary notices, including all references to and copies of these +License Terms, as originally included on, in, or with the Shotgun Toolkit +Code. You must ensure that all derivative works you make of the Shotgun +Toolkit Code contain or are accompanied by comparable and conspicuous notices +that the underlying Shotgun Toolkit Code is the confidential information of +Company and is subject to Company's copyrights and these License Terms. + +No Redistribution or Disclosure + +You acknowledge that the Shotgun Toolkit Code is and contains proprietary and +trade-secret information of Company. You may not distribute, disclose to any +third party, operate for the benefit of third parties (for example, on a +hosted basis), or otherwise commercially exploit the Shotgun Toolkit Code or +any portion or derivative work thereof without Company's separate and express +written consent. For purposes of this restriction, third parties do not +include your employees or agents acting on your behalf who are bound to abide +by these License Terms. + +No Warranties or Support + +The Shotgun Toolkit Code is provided "AS IS" and with all faults. Company +makes no warranties whatsoever, whether express, implied, or otherwise, +concerning the Shotgun Toolkit Code. Company has no obligation to provide +maintenance or technical support for the Shotgun Toolkit Code (unless +otherwise expressly agreed in a separate written agreement between you and +Company). + +Liability + +You agree to be solely responsible for your use and modifications of the +Shotgun Toolkit Code, and for any harm or liability arising out of such use +or modifications, including but not limited to any liability for infringement +of third-party intellectual property rights. + +To the fullest extent permitted under applicable law, you agree that: (a) +Company will not be liable under these License Terms or otherwise for any +direct, indirect, incidental, special, consequential, or exemplary damages, +including but not limited to damages for loss of profits, goodwill, use, data +or other intangible losses, in relation to the Shotgun Toolkit Code or your +use or inability to use the Shotgun Toolkit Code, even if Company has been +advised of the possibility of such damages; and (b) in any event, Company's +aggregate liability under these License Terms or in connection with the +Shotgun Toolkit Code, regardless of the form of action and under any theory +(whether in contract, tort, statutory, or otherwise), will not exceed the +greater of $50 or the amount (if any) that you actually paid for access to +the Shotgun Toolkit Code. + +Ownership + +Company retains sole and exclusive ownership of the Shotgun Toolkit Code and +all copyright and other intellectual property rights therein. You will own any +derivative works you make to the Shotgun Toolkit Code, subject to: (a) the +preceding sentence; and (b) the provisions below regarding ownership of any +code you elect to contribute to Company. + +Contributions + +The following terms apply to any derivative works of the Shotgun Toolkit Code +(or any other materials) that you choose to contribute to Company. + +For good and valuable consideration, receipt of which is acknowledged, you +hereby transfer and assign to Company your entire right, title, and interest +(including all rights under copyright) in: (a) any software code, +documentation, and/or other materials that you deliver to Company for +inclusion in, improvement of, use with, or documentation of Company's software +program(s), including but not limited to any code, documentation, and/or other +materials identified in a contribution form you submit to Company in an +applicable form designated by Company; and (b) any future revisions of such +code, documentation, and/or other materials that you make hereafter. The code, +documentation, other materials, and future revisions described above are +collectively referred to below as the "Contribution." + +As used below, the "Company Programs" means and includes the Company software +program(s) identified on any contribution form you submit to Company, and any +other software into which Company incorporates or with which Company uses or +distributes the Contribution or any version or portion thereof. + +Company grants you a non-exclusive right to continue to modify, make +derivative works of, reproduce, and use the Contribution for your +non-commercial or internal business purposes, and to further Company's +development of Company Programs. This grant does not: (a) limit Company's +rights, (b) grant you any rights with respect to the Company Programs; nor +(c) permit you to distribute, operate for the benefit of third parties (for +example, on a hosted basis), or otherwise commercially exploit the +Contribution. + +You acknowledge that if Company elects to distribute the Contribution or any +version or portion thereof, it may do so on any basis that it chooses +(including under any proprietary or open-source licensing terms), without +further compensation to you. + +You agree that if you have or acquire hereafter any patent or interface +copyright or other intellectual property interest dominating the Contribution +or any Company Programs (or use thereof), such dominating interest will not be +used to undermine the effect of the assignment set forth above. Accordingly, +Company and its direct and indirect licensees are licensed to make, use, sell, +distribute, and otherwise exploit, in the Company Programs and their future +versions and derivative works, without royalty or limitation, the subject +matter of the dominating interest. This license provision will be binding on +you and on any assignees of, or other successors to, the dominating interest. + +You hereby represent and warrant that you are the sole copyright holder for +the Contribution and that you have the right and power to enter into this +contract. You shall indemnify and hold harmless Company and its officers, +employees, and agents against any and all claims, actions or damages +(including attorney's reasonable fees) asserted by or paid to any party on +account of a breach or alleged breach of the foregoing warranty. You make no +other express or implied warranty (including without limitation any warranty +of merchantability or fitness for a particular purpose) regarding the +Contribution. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8dfbd74 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +## Documentation +This repository is a part of the Shotgun Pipeline Toolkit. + +- For more information about this app and for release notes, *see the wiki section*. +- For general information and documentation, click here: https://support.shotgunsoftware.com/entries/95441257 +- For information about Shotgun in general, click here: http://www.shotgunsoftware.com/toolkit + +## Using this app in your Setup +All the apps that are part of our standard app suite are pushed to our App Store. +This is where you typically go if you want to install an app into a project you are +working on. For an overview of all the Apps and Engines in the Toolkit App Store, +click here: https://support.shotgunsoftware.com/entries/95441247. + +## Have a Question? +Don't hesitate to contact us! You can find us on support@shotgunsoftware.com diff --git a/engine.py b/engine.py new file mode 100644 index 0000000..8825697 --- /dev/null +++ b/engine.py @@ -0,0 +1,790 @@ +# Copyright (c) 2015 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +""" +A Maya engine for Tank. + +""" + +import tank +import sys +import traceback +import re +import time +import os +import logging +import maya.OpenMaya as OpenMaya +import pymel.core as pm +import maya.cmds as cmds +import maya.utils +from tank.platform import Engine + +############################################################################################### +# methods to support the state when the engine cannot start up +# for example if a non-tank file is loaded in maya + +class SceneEventWatcher(object): + """ + Encapsulates event handling for multiple scene events and routes them + into a single callback. + + This uses OpenMaya.MSceneMessage rather than scriptJobs as the former + can safely be removed from inside of the callback itself + + Specifying run_once=True in the constructor causes all events to be + cleaned up after the first one has triggered + """ + def __init__(self, cb_fn, + scene_events = [OpenMaya.MSceneMessage.kAfterOpen, + OpenMaya.MSceneMessage.kAfterSave, + OpenMaya.MSceneMessage.kAfterNew], + run_once=False): + """ + Constructor. + + :param cb_fn: Callcack to invoke everytime a scene event happens. + :param scene_events: List of scene events to watch for. Defaults to new, open and save. + :param run_once: If True, the watcher will notify only on the first event. Defaults to False. + """ + self.__message_ids = [] + self.__cb_fn = cb_fn + self.__scene_events = scene_events + self.__run_once=run_once + + # register scene event callbacks: + self.start_watching() + + def start_watching(self): + """ + Starts watching for scene events. + """ + # if currently watching then stop: + self.stop_watching() + + # now add callbacks to watch for some scene events: + for ev in self.__scene_events: + try: + msg_id = OpenMaya.MSceneMessage.addCallback(ev, SceneEventWatcher.__scene_event_callback, self); + except Exception: + # report warning... + continue + self.__message_ids.append(msg_id); + + # create a callback that will be run when Maya + # exits so we can do some clean-up: + msg_id = OpenMaya.MSceneMessage.addCallback(OpenMaya.MSceneMessage.kMayaExiting, SceneEventWatcher.__maya_exiting_callback, self) + self.__message_ids.append(msg_id); + + def stop_watching(self): + """ + Stops watching the Maya scene. + """ + for msg_id in self.__message_ids: + OpenMaya.MMessage.removeCallback(msg_id) + self.__message_ids = [] + + @staticmethod + def __scene_event_callback(watcher): + """ + Called on a scene event: + """ + if watcher.__run_once: + watcher.stop_watching() + watcher.__cb_fn() + + @staticmethod + def __maya_exiting_callback(watcher): + """ + Called on Maya exit - should clean up any existing calbacks + """ + watcher.stop_watching() + +def refresh_engine(engine_name, prev_context, menu_name): + """ + refresh the current engine + """ + current_engine = tank.platform.current_engine() + + if not current_engine: + # If we don't have an engine for some reason then we don't have + # anything to do. + return + + if pm.sceneName() == "": + # This is a File->New call, so we just leave the engine in the current + # context and move on. + return + + # determine the tk instance and ctx to use: + tk = current_engine.sgtk + + # loading a scene file + new_path = pm.sceneName().abspath() + + # this file could be in another project altogether, so create a new + # API instance. + try: + tk = tank.tank_from_path(new_path) + except tank.TankError, e: + OpenMaya.MGlobal.displayInfo("Shotgun: Engine cannot be started: %s" % e) + # build disabled menu + create_sgtk_disabled_menu(menu_name) + return + + # shotgun menu may have been removed, so add it back in if its not already there. + current_engine.create_shotgun_menu() + # now remove the shotgun disabled menu if it exists. + remove_sgtk_disabled_menu() + + # and construct the new context for this path: + ctx = tk.context_from_path(new_path, prev_context) + + if ctx != tank.platform.current_engine().context: + current_engine.change_context(ctx) + + +def on_scene_event_callback(engine_name, prev_context, menu_name): + """ + Callback that's run whenever a scene is saved or opened. + """ + try: + refresh_engine(engine_name, prev_context, menu_name) + except Exception: + (exc_type, exc_value, exc_traceback) = sys.exc_info() + message = "" + message += "Message: Shotgun encountered a problem changing the Engine's context.\n" + message += "Please contact support@shotgunsoftware.com\n\n" + message += "Exception: %s - %s\n" % (exc_type, exc_value) + message += "Traceback (most recent call last):\n" + message += "\n".join( traceback.format_tb(exc_traceback)) + OpenMaya.MGlobal.displayError(message) + + +def sgtk_disabled_message(): + """ + Explain why tank is disabled. + """ + msg = ("Shotgun integration is disabled because it cannot recognize " + "the currently opened file. Try opening another file or restarting " + "Maya.") + + cmds.confirmDialog( title="Sgtk is disabled", + message=msg, + button=["Ok"], + defaultButton="Ok", + cancelButton="Ok", + dismissString="Ok" ) + + +def create_sgtk_disabled_menu(menu_name): + """ + Render a special "shotgun is disabled" menu + """ + if cmds.about(batch=True): + # don't create menu in batch mode + return + + # destroy any pre-existing shotgun menu - the one that holds the apps + if pm.menu("ShotgunMenu", exists=True): + pm.deleteUI("ShotgunMenu") + + # create a new shotgun disabled menu if one doesn't exist already. + if not pm.menu("ShotgunMenuDisabled", exists=True): + sg_menu = pm.menu("ShotgunMenuDisabled", label=menu_name, parent=pm.melGlobals["gMainWindow"]) + pm.menuItem(label="Sgtk is disabled.", parent=sg_menu, + command=lambda arg: sgtk_disabled_message()) + + +def remove_sgtk_disabled_menu(): + """ + Remove the special "shotgun is disabled" menu if it exists + + :returns: True if the menu existed and was deleted + """ + if cmds.about(batch=True): + # don't create menu in batch mode + return False + + if pm.menu("ShotgunMenuDisabled", exists=True): + pm.deleteUI("ShotgunMenuDisabled") + return True + + return False + +############################################################################################### +# The Tank Maya engine + +class MayaEngine(Engine): + """ + Toolkit engine for Maya. + """ + + @property + def context_change_allowed(self): + """ + Whether the engine allows a context change without the need for a restart. + """ + return True + + @property + def host_info(self): + """ + :returns: A dictionary with information about the application hosting this engine. + + The returned dictionary is of the following form on success: + + { + "name": "Maya", + "version": "2017 Update 4", + } + + The returned dictionary is of following form on an error preventing + the version identification. + + { + "name": "Maya", + "version: "unknown" + } + """ + + host_info = {"name": "Maya", "version": "unknown"} + try: + # The 'about -installedVersion' Maya MEL command returns: + # - the app name (Maya, Maya LT, Maya IO) + # - the major version (2017, 2018) + # - the update version when applicable (update 4) + maya_installed_version_string = cmds.about(installedVersion=True) + + # group(0) entire match + # group(1) 'Maya' match (name) + # group(2) LT, IO, etc ... match (flavor) + # group(3) 2017 ... match (version) + matches = re.search(r"(maya)\s+([a-zA-Z]+)?\s*(.*)", maya_installed_version_string, re.IGNORECASE) + host_info["name"] = matches.group(1).capitalize().rstrip().lstrip() + host_info["version"] = matches.group(3) + if matches.group(2): + host_info["name"] = host_info["name"] + " " + matches.group(2) + + except: + # Fallback to 'Maya' initialized above + pass + + return host_info + + ########################################################################################## + # init and destroy + + def pre_app_init(self): + """ + Runs after the engine is set up but before any apps have been initialized. + """ + # unicode characters returned by the shotgun api need to be converted + # to display correctly in all of the app windows + from tank.platform.qt import QtCore + # tell QT to interpret C strings as utf-8 + utf8 = QtCore.QTextCodec.codecForName("utf-8") + QtCore.QTextCodec.setCodecForCStrings(utf8) + self.logger.debug("set utf-8 codec for widget text") + + def init_engine(self): + """ + Initializes the Maya engine. + """ + self.logger.debug("%s: Initializing...", self) + + # check that we are running an ok version of maya + current_os = cmds.about(operatingSystem=True) + if current_os not in ["mac", "win64", "linux64"]: + raise tank.TankError("The current platform is not supported! Supported platforms " + "are Mac, Linux 64 and Windows 64.") + + maya_ver = cmds.about(version=True) + if maya_ver.startswith("Maya "): + maya_ver = maya_ver[5:] + if maya_ver.startswith(("2014", "2015", "2016", "2017", "2018")): + self.logger.debug("Running Maya version %s", maya_ver) + + # In the case of Maya 2018 on Windows, we have the possility of locking + # up if we allow the PySide shim to import QtWebEngineWidgets. We can + # stop that happening here by setting the environment variable. + version_num = int(maya_ver[0:4]) + + if version_num >= 2018 and current_os.startswith("win"): + self.logger.debug( + "Maya 2018+ on Windows can deadlock if QtWebEngineWidgets " + "is imported. Setting SHOTGUN_SKIP_QTWEBENGINEWIDGETS_IMPORT=1..." + ) + os.environ["SHOTGUN_SKIP_QTWEBENGINEWIDGETS_IMPORT"] = "1" + elif maya_ver.startswith(("2012", "2013")): + # We won't be able to rely on the warning dialog below, because Maya + # older than 2014 doesn't ship with PySide. Instead, we just have to + # raise an exception so that we bail out here with an error message + # that will hopefully make sense for the user. + msg = "Shotgun integration is not compatible with Maya versions older than 2014." + raise tank.TankError(msg) + else: + # show a warning that this version of Maya isn't yet fully tested with Shotgun: + msg = ("The Shotgun Pipeline Toolkit has not yet been fully tested with Maya %s. " + "You can continue to use Toolkit but you may experience bugs or instability." + "\n\nPlease report any issues to: support@shotgunsoftware.com" + % (maya_ver)) + + # determine if we should show the compatibility warning dialog: + show_warning_dlg = self.has_ui and "SGTK_COMPATIBILITY_DIALOG_SHOWN" not in os.environ + if show_warning_dlg: + # make sure we only show it once per session: + os.environ["SGTK_COMPATIBILITY_DIALOG_SHOWN"] = "1" + + # split off the major version number - accomodate complex version strings and decimals: + major_version_number_str = maya_ver.split(" ")[0].split(".")[0] + if major_version_number_str and major_version_number_str.isdigit(): + # check against the compatibility_dialog_min_version setting: + if int(major_version_number_str) < self.get_setting("compatibility_dialog_min_version"): + show_warning_dlg = False + + if show_warning_dlg: + # Note, title is padded to try to ensure dialog isn't insanely narrow! + title = "Warning - Shotgun Pipeline Toolkit Compatibility! " # padded! + cmds.confirmDialog(title = title, message = msg, button = "Ok") + + # always log the warning to the script editor: + self.logger.warning(msg) + + # In the case of Maya 2018 on Windows, we have the possility of locking + # up if we allow the PySide shim to import QtWebEngineWidgets. We can + # stop that happening here by setting the environment variable. + + if current_os.startswith("win"): + self.logger.debug( + "Maya 2018+ on Windows can deadlock if QtWebEngineWidgets " + "is imported. Setting SHOTGUN_SKIP_QTWEBENGINEWIDGETS_IMPORT=1..." + ) + os.environ["SHOTGUN_SKIP_QTWEBENGINEWIDGETS_IMPORT"] = "1" + + # Set the Maya project based on config + self._set_project() + + # add qt paths and dlls + self._init_pyside() + + # default menu name is Shotgun but this can be overriden + # in the configuration to be Sgtk in case of conflicts + self._menu_name = "Shotgun" + if self.get_setting("use_sgtk_as_menu_name", False): + self._menu_name = "Sgtk" + + if self.get_setting("automatic_context_switch", True): + # need to watch some scene events in case the engine needs rebuilding: + cb_fn = lambda en=self.instance_name, pc=self.context, mn=self._menu_name:on_scene_event_callback(en, pc, mn) + self.__watcher = SceneEventWatcher(cb_fn) + self.logger.debug("Registered open and save callbacks.") + + # Initialize a dictionary of Maya panels that have been created by the engine. + # Each panel entry has a Maya panel name key and an app widget instance value. + self._maya_panel_dict = {} + + def create_shotgun_menu(self): + """ + Creates the main shotgun menu in maya. + Note that this only creates the menu, not the child actions + :return: bool + """ + + # only create the shotgun menu if not in batch mode and menu doesn't already exist + if self.has_ui and not pm.menu("ShotgunMenu", exists=True): + + self._menu_handle = pm.menu("ShotgunMenu", label=self._menu_name, parent=pm.melGlobals["gMainWindow"]) + # create our menu handler + tk_maya = self.import_module("tk_maya") + self._menu_generator = tk_maya.MenuGenerator(self, self._menu_handle) + # hook things up so that the menu is created every time it is clicked + self._menu_handle.postMenuCommand(self._menu_generator.create_menu) + + # Restore the persisted Shotgun app panels. + tk_maya.panel_generation.restore_panels(self) + return True + + return False + + def post_app_init(self): + """ + Called when all apps have initialized + """ + self.create_shotgun_menu() + + # Run a series of app instance commands at startup. + self._run_app_instance_commands() + + def post_context_change(self, old_context, new_context): + """ + Runs after a context change. The Maya event watching will be stopped + and new callbacks registered containing the new context information. + + :param old_context: The context being changed away from. + :param new_context: The new context being changed to. + """ + if self.get_setting("automatic_context_switch", True): + # We need to stop watching, and then replace with a new watcher + # that has a callback registered with the new context baked in. + # This will ensure that the context_from_path call that occurs + # after a File->Open receives an up-to-date "previous" context. + self.__watcher.stop_watching() + cb_fn = lambda en=self.instance_name, pc=new_context, mn=self._menu_name:on_scene_event_callback( + engine_name=en, + prev_context=pc, + menu_name=mn, + ) + self.__watcher = SceneEventWatcher(cb_fn) + self.logger.debug( + "Registered new open and save callbacks before changing context." + ) + + # Set the Maya project to match the new context. + self._set_project() + + def _run_app_instance_commands(self): + """ + Runs the series of app instance commands listed in the 'run_at_startup' setting + of the environment configuration yaml file. + """ + + # Build a dictionary mapping app instance names to dictionaries of commands they registered with the engine. + app_instance_commands = {} + for (command_name, value) in self.commands.iteritems(): + app_instance = value["properties"].get("app") + if app_instance: + # Add entry 'command name: command function' to the command dictionary of this app instance. + command_dict = app_instance_commands.setdefault(app_instance.instance_name, {}) + command_dict[command_name] = value["callback"] + + # Run the series of app instance commands listed in the 'run_at_startup' setting. + for app_setting_dict in self.get_setting("run_at_startup", []): + + app_instance_name = app_setting_dict["app_instance"] + # Menu name of the command to run or '' to run all commands of the given app instance. + setting_command_name = app_setting_dict["name"] + + # Retrieve the command dictionary of the given app instance. + command_dict = app_instance_commands.get(app_instance_name) + + if command_dict is None: + self.logger.warning( + "%s configuration setting 'run_at_startup' requests app '%s' that is not installed.", + self.name, app_instance_name) + else: + if not setting_command_name: + # Run all commands of the given app instance. + # Run these commands once Maya will have completed its UI update and be idle + # in order to run them after the ones that restore the persisted Shotgun app panels. + for (command_name, command_function) in command_dict.iteritems(): + self.logger.debug("%s startup running app '%s' command '%s'.", + self.name, app_instance_name, command_name) + maya.utils.executeDeferred(command_function) + else: + # Run the command whose name is listed in the 'run_at_startup' setting. + # Run this command once Maya will have completed its UI update and be idle + # in order to run it after the ones that restore the persisted Shotgun app panels. + command_function = command_dict.get(setting_command_name) + if command_function: + self.logger.debug("%s startup running app '%s' command '%s'.", + self.name, app_instance_name, setting_command_name) + maya.utils.executeDeferred(command_function) + else: + known_commands = ', '.join("'%s'" % name for name in command_dict) + self.logger.warning( + "%s configuration setting 'run_at_startup' requests app '%s' unknown command '%s'. " + "Known commands: %s", + self.name, app_instance_name, setting_command_name, known_commands) + + + def destroy_engine(self): + """ + Stops watching scene events and tears down menu. + """ + self.logger.debug("%s: Destroying...", self) + + # Clear the dictionary of Maya panels to keep the garbage collector happy. + self._maya_panel_dict = {} + + if self.get_setting("automatic_context_switch", True): + # stop watching scene events + self.__watcher.stop_watching() + + # clean up UI: + if self.has_ui and pm.menu(self._menu_handle, exists=True): + pm.deleteUI(self._menu_handle) + + def _init_pyside(self): + """ + Handles the pyside init + """ + + # first see if pyside2 is present + try: + from PySide2 import QtGui + except: + # fine, we don't expect PySide2 to be present just yet + self.logger.debug("PySide2 not detected - trying for PySide now...") + else: + # looks like pyside2 is already working! No need to do anything + self.logger.debug("PySide2 detected - the existing version will be used.") + return + + # then see if pyside is present + try: + from PySide import QtGui + except: + # must be a very old version of Maya. + self.logger.debug("PySide not detected - it will be added to the setup now...") + else: + # looks like pyside is already working! No need to do anything + self.logger.debug("PySide detected - the existing version will be used.") + return + + if sys.platform == "darwin": + pyside_path = os.path.join(self.disk_location, "resources","pyside112_py26_qt471_mac", "python") + self.logger.debug("Adding pyside to sys.path: %s", pyside_path) + sys.path.append(pyside_path) + + elif sys.platform == "win32": + # default windows version of pyside for 2011 and 2012 + pyside_path = os.path.join(self.disk_location, "resources","pyside111_py26_qt471_win64", "python") + self.logger.debug("Adding pyside to sys.path: %s", pyside_path) + sys.path.append(pyside_path) + dll_path = os.path.join(self.disk_location, "resources","pyside111_py26_qt471_win64", "lib") + path = os.environ.get("PATH", "") + path += ";%s" % dll_path + os.environ["PATH"] = path + + elif sys.platform == "linux2": + pyside_path = os.path.join(self.disk_location, "resources","pyside112_py26_qt471_linux", "python") + self.logger.debug("Adding pyside to sys.path: %s", pyside_path) + sys.path.append(pyside_path) + + else: + self.logger.error("Unknown platform - cannot initialize PySide!") + + # now try to import it + try: + from PySide import QtGui + except Exception, e: + self.logger.error("PySide could not be imported! Apps using pyside will not " + "operate correctly! Error reported: %s", e) + + def _get_dialog_parent(self): + """ + Get the QWidget parent for all dialogs created through + show_dialog & show_modal. + """ + # Find a parent for the dialog - this is the Maya mainWindow() + from tank.platform.qt import QtGui + import maya.OpenMayaUI as OpenMayaUI + + try: + import shiboken2 as shiboken + except ImportError: + import shiboken + + ptr = OpenMayaUI.MQtUtil.mainWindow() + parent = shiboken.wrapInstance(long(ptr), QtGui.QMainWindow) + + return parent + + @property + def has_ui(self): + """ + Detect and return if maya is running in batch mode + """ + if cmds.about(batch=True): + # batch mode or prompt mode + return False + else: + return True + + ########################################################################################## + # logging + + def _emit_log_message(self, handler, record): + """ + Called by the engine to log messages in Maya script editor. + All log messages from the toolkit logging namespace will be passed to this method. + + :param handler: Log handler that this message was dispatched from. + Its default format is "[levelname basename] message". + :type handler: :class:`~python.logging.LogHandler` + :param record: Standard python logging record. + :type record: :class:`~python.logging.LogRecord` + """ + # Give a standard format to the message: + # Shotgun : + # where "basename" is the leaf part of the logging record name, + # for example "tk-multi-shotgunpanel" or "qt_importer". + if record.levelno < logging.INFO: + formatter = logging.Formatter("Debug: Shotgun %(basename)s: %(message)s") + else: + formatter = logging.Formatter("Shotgun %(basename)s: %(message)s") + + msg = formatter.format(record) + + # Select Maya display function to use according to the logging record level. + if record.levelno < logging.WARNING: + fct = OpenMaya.MGlobal.displayInfo + elif record.levelno < logging.ERROR: + fct = OpenMaya.MGlobal.displayWarning + else: + fct = OpenMaya.MGlobal.displayError + + # Display the message in Maya script editor in a thread safe manner. + self.async_execute_in_main_thread(fct, msg) + + ########################################################################################## + # scene and project management + + def _set_project(self): + """ + Set the maya project + """ + setting = self.get_setting("template_project") + if setting is None: + return + + tmpl = self.tank.templates.get(setting) + fields = self.context.as_template_fields(tmpl) + proj_path = tmpl.apply_fields(fields) + self.logger.info("Setting Maya project to '%s'", proj_path) + pm.mel.setProject(proj_path) + + ########################################################################################## + # panel support + + def show_panel(self, panel_id, title, bundle, widget_class, *args, **kwargs): + """ + Docks an app widget in a maya panel. + + :param panel_id: Unique identifier for the panel, as obtained by register_panel(). + :param title: The title of the panel + :param bundle: The app, engine or framework object that is associated with this window + :param widget_class: The class of the UI to be constructed. This must derive from QWidget. + + Additional parameters specified will be passed through to the widget_class constructor. + + :returns: the created widget_class instance + """ + from tank.platform.qt import QtCore, QtGui + + tk_maya = self.import_module("tk_maya") + + self.logger.debug("Begin showing panel %s", panel_id) + + # The general approach below is as follows: + # + # 1. First create our qt tk app widget using QT. + # parent it to the Maya main window to give it + # a well established parent. If the widget already + # exists, don't create it again, just retrieve its + # handle. + # + # 2. Now dock our QT control in a new panel tab of + # Maya Channel Box dock area. We use the + # Qt object name property to do the bind. + # + # 3. Lastly, since our widgets won't get notified about + # when the parent dock is closed (and sometimes when it + # needs redrawing), attach some QT event watchers to it + # + # Note: It is possible that the close event and some of the + # refresh doesn't propagate down to the widget because + # of a misaligned parenting: The tk widget exists inside + # the pane layout but is still parented to the main + # Maya window. It's possible that by setting up the parenting + # explicitly, the missing signals we have to compensate for + # may start to work. I tried a bunch of stuff but couldn't get + # it to work and instead resorted to the event watcher setup. + + # make a unique id for the app widget based off of the panel id + widget_id = tk_maya.panel_generation.SHOTGUN_APP_PANEL_PREFIX + panel_id + + if pm.control(widget_id, query=1, exists=1): + self.logger.debug("Reparent existing toolkit widget %s.", widget_id) + # Find the Shotgun app panel widget for later use. + for widget in QtGui.QApplication.allWidgets(): + if widget.objectName() == widget_id: + widget_instance = widget + # Reparent the Shotgun app panel widget under Maya main window + # to prevent it from being deleted with the existing Maya panel. + self.logger.debug("Reparenting widget %s under Maya main window.", widget_id) + parent = self._get_dialog_parent() + widget_instance.setParent(parent) + # The Shotgun app panel was retrieved from under an existing Maya panel. + break + else: + self.logger.debug("Create toolkit widget %s", widget_id) + # parent the UI to the main maya window + parent = self._get_dialog_parent() + widget_instance = widget_class(*args, **kwargs) + widget_instance.setParent(parent) + # set its name - this means that it can also be found via the maya API + widget_instance.setObjectName(widget_id) + self.logger.debug("Created widget %s: %s", widget_id, widget_instance) + # apply external stylesheet + self._apply_external_styleshet(bundle, widget_instance) + # The Shotgun app panel was just created. + + # Dock the Shotgun app panel into a new Maya panel in the active Maya window. + maya_panel_name = tk_maya.panel_generation.dock_panel(self, widget_instance, title) + + # Add the new panel to the dictionary of Maya panels that have been created by the engine. + # The panel entry has a Maya panel name key and an app widget instance value. + # Note that the panel entry will not be removed from the dictionary when the panel is + # later deleted since the added complexity of updating the dictionary from our panel + # close callback is outweighed by the limited length of the dictionary that will never + # be longer than the number of apps configured to be runnable by the engine. + self._maya_panel_dict[maya_panel_name] = widget_instance + + return widget_instance + + def close_windows(self): + """ + Closes the various windows (dialogs, panels, etc.) opened by the engine. + """ + + # Make a copy of the list of Tank dialogs that have been created by the engine and + # are still opened since the original list will be updated when each dialog is closed. + opened_dialog_list = self.created_qt_dialogs[:] + + # Loop through the list of opened Tank dialogs. + for dialog in opened_dialog_list: + dialog_window_title = dialog.windowTitle() + try: + # Close the dialog and let its close callback remove it from the original dialog list. + self.logger.debug("Closing dialog %s.", dialog_window_title) + dialog.close() + except Exception, exception: + self.logger.error("Cannot close dialog %s: %s", dialog_window_title, exception) + + # Loop through the dictionary of Maya panels that have been created by the engine. + for (maya_panel_name, widget_instance) in self._maya_panel_dict.iteritems(): + # Make sure the Maya panel is still opened. + if pm.control(maya_panel_name, query=True, exists=True): + try: + # Reparent the Shotgun app panel widget under Maya main window + # to prevent it from being deleted with the existing Maya panel. + self.logger.debug("Reparenting widget %s under Maya main window.", + widget_instance.objectName()) + parent = self._get_dialog_parent() + widget_instance.setParent(parent) + # The Maya panel can now be deleted safely. + self.logger.debug("Deleting Maya panel %s.", maya_panel_name) + pm.deleteUI(maya_panel_name) + except Exception, exception: + self.logger.error("Cannot delete Maya panel %s: %s", maya_panel_name, exception) + + # Clear the dictionary of Maya panels now that they were deleted. + self._maya_panel_dict = {} diff --git a/hooks/tk-multi-publish2/basic/collector.py b/hooks/tk-multi-publish2/basic/collector.py new file mode 100644 index 0000000..4084bc2 --- /dev/null +++ b/hooks/tk-multi-publish2/basic/collector.py @@ -0,0 +1,342 @@ +# Copyright (c) 2017 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import glob +import os +import maya.cmds as cmds +import maya.mel as mel +import sgtk + +HookBaseClass = sgtk.get_hook_baseclass() + + +class MayaSessionCollector(HookBaseClass): + """ + Collector that operates on the maya session. Should inherit from the basic + collector hook. + """ + + @property + def settings(self): + """ + Dictionary defining the settings that this collector expects to receive + through the settings parameter in the process_current_session and + process_file methods. + + A dictionary on the following form:: + + { + "Settings Name": { + "type": "settings_type", + "default": "default_value", + "description": "One line description of the setting" + } + + The type string should be one of the data types that toolkit accepts as + part of its environment configuration. + """ + + # grab any base class settings + collector_settings = super(MayaSessionCollector, self).settings or {} + + # settings specific to this collector + maya_session_settings = { + "Work Template": { + "type": "template", + "default": None, + "description": "Template path for artist work files. Should " + "correspond to a template defined in " + "templates.yml. If configured, is made available" + "to publish plugins via the collected item's " + "properties. ", + }, + } + + # update the base settings with these settings + collector_settings.update(maya_session_settings) + + return collector_settings + + def process_current_session(self, settings, parent_item): + """ + Analyzes the current session open in Maya and parents a subtree of + items under the parent_item passed in. + + :param dict settings: Configured settings for this collector + :param parent_item: Root item instance + + """ + + # create an item representing the current maya session + item = self.collect_current_maya_session(settings, parent_item) + project_root = item.properties["project_root"] + + # look at the render layers to find rendered images on disk + self.collect_rendered_images(item) + + # if we can determine a project root, collect other files to publish + if project_root: + + self.logger.info( + "Current Maya project is: %s." % (project_root,), + extra={ + "action_button": { + "label": "Change Project", + "tooltip": "Change to a different Maya project", + "callback": lambda: mel.eval('setProject ""') + } + } + ) + + self.collect_playblasts(item, project_root) + self.collect_alembic_caches(item, project_root) + else: + + self.logger.info( + "Could not determine the current Maya project.", + extra={ + "action_button": { + "label": "Set Project", + "tooltip": "Set the Maya project", + "callback": lambda: mel.eval('setProject ""') + } + } + ) + + if cmds.ls(geometry=True, noIntermediate=True): + self._collect_session_geometry(item) + + def collect_current_maya_session(self, settings, parent_item): + """ + Creates an item that represents the current maya session. + + :param parent_item: Parent Item instance + + :returns: Item of type maya.session + """ + + publisher = self.parent + + # get the path to the current file + path = cmds.file(query=True, sn=True) + + # determine the display name for the item + if path: + file_info = publisher.util.get_file_path_components(path) + display_name = file_info["filename"] + else: + display_name = "Current Maya Session" + + # create the session item for the publish hierarchy + session_item = parent_item.create_item( + "maya.session", + "Maya Session", + display_name + ) + + # get the icon path to display for this item + icon_path = os.path.join( + self.disk_location, + os.pardir, + "icons", + "maya.png" + ) + session_item.set_icon_from_path(icon_path) + + # discover the project root which helps in discovery of other + # publishable items + project_root = cmds.workspace(q=True, rootDirectory=True) + session_item.properties["project_root"] = project_root + + # if a work template is defined, add it to the item properties so + # that it can be used by attached publish plugins + work_template_setting = settings.get("Work Template") + if work_template_setting: + + work_template = publisher.engine.get_template_by_name( + work_template_setting.value) + + # store the template on the item for use by publish plugins. we + # can't evaluate the fields here because there's no guarantee the + # current session path won't change once the item has been created. + # the attached publish plugins will need to resolve the fields at + # execution time. + session_item.properties["work_template"] = work_template + self.logger.debug("Work template defined for Maya collection.") + + self.logger.info("Collected current Maya scene") + + return session_item + + def collect_alembic_caches(self, parent_item, project_root): + """ + Creates items for alembic caches + + Looks for a 'project_root' property on the parent item, and if such + exists, look for alembic caches in a 'cache/alembic' subfolder. + + :param parent_item: Parent Item instance + :param str project_root: The maya project root to search for alembics + """ + + # ensure the alembic cache dir exists + cache_dir = os.path.join(project_root, "cache", "alembic") + if not os.path.exists(cache_dir): + return + + self.logger.info( + "Processing alembic cache folder: %s" % (cache_dir,), + extra={ + "action_show_folder": { + "path": cache_dir + } + } + ) + + # look for alembic files in the cache folder + for filename in os.listdir(cache_dir): + cache_path = os.path.join(cache_dir, filename) + + # do some early pre-processing to ensure the file is of the right + # type. use the base class item info method to see what the item + # type would be. + item_info = self._get_item_info(filename) + if item_info["item_type"] != "file.alembic": + continue + + # allow the base class to collect and create the item. it knows how + # to handle alembic files + super(MayaSessionCollector, self)._collect_file( + parent_item, + cache_path + ) + + def _collect_session_geometry(self, parent_item): + """ + Creates items for session geometry to be exported. + + :param parent_item: Parent Item instance + """ + + geo_item = parent_item.create_item( + "maya.session.geometry", + "Geometry", + "All Session Geometry" + ) + + # get the icon path to display for this item + icon_path = os.path.join( + self.disk_location, + os.pardir, + "icons", + "geometry.png" + ) + + geo_item.set_icon_from_path(icon_path) + + def collect_playblasts(self, parent_item, project_root): + """ + Creates items for quicktime playblasts. + + Looks for a 'project_root' property on the parent item, and if such + exists, look for movie files in a 'movies' subfolder. + + :param parent_item: Parent Item instance + :param str project_root: The maya project root to search for playblasts + """ + + movie_dir_name = None + + # try to query the file rule folder name for movies. This will give + # us the directory name set for the project where movies will be + # written + if "movie" in cmds.workspace(fileRuleList=True): + # this could return an empty string + movie_dir_name = cmds.workspace(fileRuleEntry='movie') + + if not movie_dir_name: + # fall back to the default + movie_dir_name = "movies" + + # ensure the movies dir exists + movies_dir = os.path.join(project_root, movie_dir_name) + if not os.path.exists(movies_dir): + return + + self.logger.info( + "Processing movies folder: %s" % (movies_dir,), + extra={ + "action_show_folder": { + "path": movies_dir + } + } + ) + + # look for movie files in the movies folder + for filename in os.listdir(movies_dir): + + # do some early pre-processing to ensure the file is of the right + # type. use the base class item info method to see what the item + # type would be. + item_info = self._get_item_info(filename) + if item_info["item_type"] != "file.video": + continue + + movie_path = os.path.join(movies_dir, filename) + + # allow the base class to collect and create the item. it knows how + # to handle movie files + item = super(MayaSessionCollector, self)._collect_file( + parent_item, + movie_path + ) + + # the item has been created. update the display name to include + # the an indication of what it is and why it was collected + item.name = "%s (%s)" % (item.name, "playblast") + + def collect_rendered_images(self, parent_item): + """ + Creates items for any rendered images that can be identified by + render layers in the file. + + :param parent_item: Parent Item instance + :return: + """ + + # iterate over defined render layers and query the render settings for + # information about a potential render + for layer in cmds.ls(type="renderLayer"): + + self.logger.info("Processing render layer: %s" % (layer,)) + + # use the render settings api to get a path where the frame number + # spec is replaced with a '*' which we can use to glob + (frame_glob,) = cmds.renderSettings( + genericFrameImageName="*", + fullPath=True, + layer=layer + ) + + # see if there are any files on disk that match this pattern + rendered_paths = glob.glob(frame_glob) + + if rendered_paths: + # we only need one path to publish, so take the first one and + # let the base class collector handle it + item = super(MayaSessionCollector, self)._collect_file( + parent_item, + rendered_paths[0], + frame_sequence=True + ) + + # the item has been created. update the display name to include + # the an indication of what it is and why it was collected + item.name = "%s (Render Layer: %s)" % (item.name, layer) diff --git a/hooks/tk-multi-publish2/basic/publish_session.py b/hooks/tk-multi-publish2/basic/publish_session.py new file mode 100644 index 0000000..2e433ad --- /dev/null +++ b/hooks/tk-multi-publish2/basic/publish_session.py @@ -0,0 +1,458 @@ +# Copyright (c) 2017 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import os +import maya.cmds as cmds +import maya.mel as mel +import sgtk +from sgtk.util.filesystem import ensure_folder_exists + +HookBaseClass = sgtk.get_hook_baseclass() + + +class MayaSessionPublishPlugin(HookBaseClass): + """ + Plugin for publishing an open maya session. + + This hook relies on functionality found in the base file publisher hook in + the publish2 app and should inherit from it in the configuration. The hook + setting for this plugin should look something like this:: + + hook: "{self}/publish_file.py:{engine}/tk-multi-publish2/basic/publish_session.py" + + """ + + # NOTE: The plugin icon and name are defined by the base file plugin. + + @property + def description(self): + """ + Verbose, multi-line description of what the plugin does. This can + contain simple html for formatting. + """ + + loader_url = "https://support.shotgunsoftware.com/hc/en-us/articles/219033078" + + return """ + Publishes the file to Shotgun. A Publish entry will be + created in Shotgun which will include a reference to the file's current + path on disk. If a publish template is configured, a copy of the + current session will be copied to the publish template path which + will be the file that is published. Other users will be able to access + the published file via the Loader so long as + they have access to the file's location on disk. + + If the session has not been saved, validation will fail and a button + will be provided in the logging output to save the file. + +

File versioning

+ If the filename contains a version number, the process will bump the + file to the next version after publishing. + + The version field of the resulting Publish in + Shotgun will also reflect the version number identified in the filename. + The basic worklfow recognizes the following version formats by default: + + + + After publishing, if a version number is detected in the work file, the + work file will automatically be saved to the next incremental version + number. For example, filename.v001.ext will be published + and copied to filename.v002.ext + + If the next incremental version of the file already exists on disk, the + validation step will produce a warning, and a button will be provided in + the logging output which will allow saving the session to the next + available version number prior to publishing. + +

NOTE: any amount of version number padding is supported. for + non-template based workflows. + +

Overwriting an existing publish

+ In non-template workflows, a file can be published multiple times, + however only the most recent publish will be available to other users. + Warnings will be provided during validation if there are previous + publishes. + """ % (loader_url,) + + @property + def settings(self): + """ + Dictionary defining the settings that this plugin expects to receive + through the settings parameter in the accept, validate, publish and + finalize methods. + + A dictionary on the following form:: + + { + "Settings Name": { + "type": "settings_type", + "default": "default_value", + "description": "One line description of the setting" + } + + The type string should be one of the data types that toolkit accepts as + part of its environment configuration. + """ + + # inherit the settings from the base publish plugin + base_settings = super(MayaSessionPublishPlugin, self).settings or {} + + # settings specific to this class + maya_publish_settings = { + "Publish Template": { + "type": "template", + "default": None, + "description": "Template path for published work files. Should" + "correspond to a template defined in " + "templates.yml.", + } + } + + # update the base settings + base_settings.update(maya_publish_settings) + + return base_settings + + @property + def item_filters(self): + """ + List of item types that this plugin is interested in. + + Only items matching entries in this list will be presented to the + accept() method. Strings can contain glob patters such as *, for example + ["maya.*", "file.maya"] + """ + return ["maya.session"] + + def accept(self, settings, item): + """ + Method called by the publisher to determine if an item is of any + interest to this plugin. Only items matching the filters defined via the + item_filters property will be presented to this method. + + A publish task will be generated for each item accepted here. Returns a + dictionary with the following booleans: + + - accepted: Indicates if the plugin is interested in this value at + all. Required. + - enabled: If True, the plugin will be enabled in the UI, otherwise + it will be disabled. Optional, True by default. + - visible: If True, the plugin will be visible in the UI, otherwise + it will be hidden. Optional, True by default. + - checked: If True, the plugin will be checked in the UI, otherwise + it will be unchecked. Optional, True by default. + + :param settings: Dictionary of Settings. The keys are strings, matching + the keys returned in the settings property. The values are `Setting` + instances. + :param item: Item to process + + :returns: dictionary with boolean keys accepted, required and enabled + """ + + # if a publish template is configured, disable context change. This + # is a temporary measure until the publisher handles context switching + # natively. + if settings.get("Publish Template").value: + item.context_change_allowed = False + + path = _session_path() + + if not path: + # the session has not been saved before (no path determined). + # provide a save button. the session will need to be saved before + # validation will succeed. + self.logger.warn( + "The Maya session has not been saved.", + extra=_get_save_as_action() + ) + + self.logger.info( + "Maya '%s' plugin accepted the current Maya session." % + (self.name,) + ) + return { + "accepted": True, + "checked": True + } + + def validate(self, settings, item): + """ + Validates the given item to check that it is ok to publish. Returns a + boolean to indicate validity. + + :param settings: Dictionary of Settings. The keys are strings, matching + the keys returned in the settings property. The values are `Setting` + instances. + :param item: Item to process + :returns: True if item is valid, False otherwise. + """ + + publisher = self.parent + path = _session_path() + + # ---- ensure the session has been saved + + if not path: + # the session still requires saving. provide a save button. + # validation fails. + error_msg = "The Maya session has not been saved." + self.logger.error( + error_msg, + extra=_get_save_as_action() + ) + raise Exception(error_msg) + + # ensure we have an updated project root + project_root = cmds.workspace(q=True, rootDirectory=True) + item.properties["project_root"] = project_root + + # log if no project root could be determined. + if not project_root: + self.logger.info( + "Your session is not part of a maya project.", + extra={ + "action_button": { + "label": "Set Project", + "tooltip": "Set the maya project", + "callback": lambda: mel.eval('setProject ""') + } + } + ) + + # ---- check the session against any attached work template + + # get the path in a normalized state. no trailing separator, + # separators are appropriate for current os, no double separators, + # etc. + path = sgtk.util.ShotgunPath.normalize(path) + + # if the session item has a known work template, see if the path + # matches. if not, warn the user and provide a way to save the file to + # a different path + work_template = item.properties.get("work_template") + if work_template: + if not work_template.validate(path): + self.logger.warning( + "The current session does not match the configured work " + "file template.", + extra={ + "action_button": { + "label": "Save File", + "tooltip": "Save the current Maya session to a " + "different file name", + # will launch wf2 if configured + "callback": _get_save_as_action() + } + } + ) + else: + self.logger.debug( + "Work template configured and matches session file.") + else: + self.logger.debug("No work template configured.") + + # ---- see if the version can be bumped post-publish + + # check to see if the next version of the work file already exists on + # disk. if so, warn the user and provide the ability to jump to save + # to that version now + (next_version_path, version) = self._get_next_version_info(path, item) + if next_version_path and os.path.exists(next_version_path): + + # determine the next available version_number. just keep asking for + # the next one until we get one that doesn't exist. + while os.path.exists(next_version_path): + (next_version_path, version) = self._get_next_version_info( + next_version_path, item) + + error_msg = "The next version of this file already exists on disk." + self.logger.error( + error_msg, + extra={ + "action_button": { + "label": "Save to v%s" % (version,), + "tooltip": "Save to the next available version number, " + "v%s" % (version,), + "callback": lambda: _save_session(next_version_path) + } + } + ) + raise Exception(error_msg) + + # ---- populate the necessary properties and call base class validation + + # populate the publish template on the item if found + publish_template_setting = settings.get("Publish Template") + publish_template = publisher.engine.get_template_by_name( + publish_template_setting.value) + if publish_template: + item.properties["publish_template"] = publish_template + + # set the session path on the item for use by the base plugin validation + # step. NOTE: this path could change prior to the publish phase. + item.properties["path"] = path + + # run the base class validation + return super(MayaSessionPublishPlugin, self).validate(settings, item) + + def publish(self, settings, item): + """ + Executes the publish logic for the given item and settings. + + :param settings: Dictionary of Settings. The keys are strings, matching + the keys returned in the settings property. The values are `Setting` + instances. + :param item: Item to process + """ + + # get the path in a normalized state. no trailing separator, separators + # are appropriate for current os, no double separators, etc. + # print "pub_session_item:",item + path = sgtk.util.ShotgunPath.normalize(_session_path()) + + # ensure the session is saved + _save_session(path) + + # update the item with the saved session path + item.properties["path"] = path + + # add dependencies for the base class to register when publishing + item.properties["publish_dependencies"] = \ + _maya_find_additional_session_dependencies() + + # let the base class register the publish + super(MayaSessionPublishPlugin, self).publish(settings, item) + + def finalize(self, settings, item): + """ + Execute the finalization pass. This pass executes once all the publish + tasks have completed, and can for example be used to version up files. + + :param settings: Dictionary of Settings. The keys are strings, matching + the keys returned in the settings property. The values are `Setting` + instances. + :param item: Item to process + """ + + # do the base class finalization + super(MayaSessionPublishPlugin, self).finalize(settings, item) + + # bump the session file to the next version + self._save_to_next_version(item.properties["path"], item, _save_session) + + +def _maya_find_additional_session_dependencies(): + """ + Find additional dependencies from the session + """ + + # default implementation looks for references and + # textures (file nodes) and returns any paths that + # match a template defined in the configuration + ref_paths = set() + + # first let's look at maya references + ref_nodes = cmds.ls(references=True) + for ref_node in ref_nodes: + # get the path: + ref_path = cmds.referenceQuery(ref_node, filename=True) + # make it platform dependent + # (maya uses C:/style/paths) + ref_path = ref_path.replace("/", os.path.sep) + if ref_path: + ref_paths.add(ref_path) + + # now look at file texture nodes + for file_node in cmds.ls(l=True, type="file"): + # ensure this is actually part of this session and not referenced + if cmds.referenceQuery(file_node, isNodeReferenced=True): + # this is embedded in another reference, so don't include it in + # the breakdown + continue + + # get path and make it platform dependent + # (maya uses C:/style/paths) + texture_path = cmds.getAttr( + "%s.fileTextureName" % file_node).replace("/", os.path.sep) + if texture_path: + ref_paths.add(texture_path) + + return list(ref_paths) + + +def _session_path(): + """ + Return the path to the current session + :return: + """ + path = cmds.file(query=True, sn=True) + + if isinstance(path, unicode): + path = path.encode("utf-8") + + return path + + +def _save_session(path): + """ + Save the current session to the supplied path. + """ + + # Maya can choose the wrong file type so we should set it here + # explicitly based on the extension + maya_file_type = None + if path.lower().endswith(".ma"): + maya_file_type = "mayaAscii" + elif path.lower().endswith(".mb"): + maya_file_type = "mayaBinary" + + # Maya won't ensure that the folder is created when saving, so we must make sure it exists + folder = os.path.dirname(path) + ensure_folder_exists(folder) + + cmds.file(rename=path) + + # save the scene: + if maya_file_type: + cmds.file(save=True, force=True, type=maya_file_type) + else: + cmds.file(save=True, force=True) + + +# TODO: method duplicated in all the maya hooks +def _get_save_as_action(): + """ + Simple helper for returning a log action dict for saving the session + """ + + engine = sgtk.platform.current_engine() + + # default save callback + callback = cmds.SaveScene + + # if workfiles2 is configured, use that for file save + if "tk-multi-workfiles2" in engine.apps: + app = engine.apps["tk-multi-workfiles2"] + if hasattr(app, "show_file_save_dlg"): + callback = app.show_file_save_dlg + + return { + "action_button": { + "label": "Save As...", + "tooltip": "Save the current session", + "callback": callback + } + } diff --git a/hooks/tk-multi-publish2/basic/publish_session_geometry.py b/hooks/tk-multi-publish2/basic/publish_session_geometry.py new file mode 100644 index 0000000..7a8497a --- /dev/null +++ b/hooks/tk-multi-publish2/basic/publish_session_geometry.py @@ -0,0 +1,353 @@ +# Copyright (c) 2017 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import os +import pprint +import maya.cmds as cmds +import maya.mel as mel +import sgtk + +HookBaseClass = sgtk.get_hook_baseclass() + + +class MayaSessionGeometryPublishPlugin(HookBaseClass): + """ + Plugin for publishing an open maya session. + + This hook relies on functionality found in the base file publisher hook in + the publish2 app and should inherit from it in the configuration. The hook + setting for this plugin should look something like this:: + + hook: "{self}/publish_file.py:{engine}/tk-multi-publish2/basic/publish_session.py" + + """ + + # NOTE: The plugin icon and name are defined by the base file plugin. + + @property + def description(self): + """ + Verbose, multi-line description of what the plugin does. This can + contain simple html for formatting. + """ + + return """ +

This plugin publishes session geometry for the current session. Any + session geometry will be exported to the path defined by this plugin's + configured "Publish Template" setting. The plugin will fail to validate + if the "AbcExport" plugin is not enabled or cannot be found.

+ """ + + @property + def settings(self): + """ + Dictionary defining the settings that this plugin expects to receive + through the settings parameter in the accept, validate, publish and + finalize methods. + + A dictionary on the following form:: + + { + "Settings Name": { + "type": "settings_type", + "default": "default_value", + "description": "One line description of the setting" + } + + The type string should be one of the data types that toolkit accepts as + part of its environment configuration. + """ + # inherit the settings from the base publish plugin + base_settings = super(MayaSessionGeometryPublishPlugin, self).settings or {} + + # settings specific to this class + maya_publish_settings = { + "Publish Template": { + "type": "template", + "default": None, + "description": "Template path for published work files. Should" + "correspond to a template defined in " + "templates.yml.", + } + } + + # update the base settings + base_settings.update(maya_publish_settings) + + return base_settings + + @property + def item_filters(self): + """ + List of item types that this plugin is interested in. + + Only items matching entries in this list will be presented to the + accept() method. Strings can contain glob patters such as *, for example + ["maya.*", "file.maya"] + """ + return ["maya.session.geometry"] + + def accept(self, settings, item): + """ + Method called by the publisher to determine if an item is of any + interest to this plugin. Only items matching the filters defined via the + item_filters property will be presented to this method. + + A publish task will be generated for each item accepted here. Returns a + dictionary with the following booleans: + + - accepted: Indicates if the plugin is interested in this value at + all. Required. + - enabled: If True, the plugin will be enabled in the UI, otherwise + it will be disabled. Optional, True by default. + - visible: If True, the plugin will be visible in the UI, otherwise + it will be hidden. Optional, True by default. + - checked: If True, the plugin will be checked in the UI, otherwise + it will be unchecked. Optional, True by default. + + :param settings: Dictionary of Settings. The keys are strings, matching + the keys returned in the settings property. The values are `Setting` + instances. + :param item: Item to process + + :returns: dictionary with boolean keys accepted, required and enabled + """ + + accepted = True + publisher = self.parent + template_name = settings["Publish Template"].value + + # ensure a work file template is available on the parent item + work_template = item.parent.properties.get("work_template") + if not work_template: + self.logger.debug( + "A work template is required for the session item in order to " + "publish session geometry. Not accepting session geom item." + ) + accepted = False + + # ensure the publish template is defined and valid and that we also have + publish_template = publisher.get_template_by_name(template_name) + if not publish_template: + self.logger.debug( + "The valid publish template could not be determined for the " + "session geometry item. Not accepting the item." + ) + accepted = False + + # we've validated the publish template. add it to the item properties + # for use in subsequent methods + item.properties["publish_template"] = publish_template + + # check that the AbcExport command is available! + if not mel.eval("exists \"AbcExport\""): + self.logger.debug( + "Item not accepted because alembic export command 'AbcExport' " + "is not available. Perhaps the plugin is not enabled?" + ) + accepted = False + + # because a publish template is configured, disable context change. This + # is a temporary measure until the publisher handles context switching + # natively. + item.context_change_allowed = False + + return { + "accepted": accepted, + "checked": True + } + + def validate(self, settings, item): + """ + Validates the given item to check that it is ok to publish. Returns a + boolean to indicate validity. + + :param settings: Dictionary of Settings. The keys are strings, matching + the keys returned in the settings property. The values are `Setting` + instances. + :param item: Item to process + :returns: True if item is valid, False otherwise. + """ + + path = _session_path() + + # ---- ensure the session has been saved + + if not path: + # the session still requires saving. provide a save button. + # validation fails. + error_msg = "The Maya session has not been saved." + self.logger.error( + error_msg, + extra=_get_save_as_action() + ) + raise Exception(error_msg) + + # get the normalized path + path = sgtk.util.ShotgunPath.normalize(path) + + # check that there is still geometry in the scene: + if not cmds.ls(geometry=True, noIntermediate=True): + error_msg = ( + "Validation failed because there is no geometry in the scene " + "to be exported. You can uncheck this plugin or create " + "geometry to export to avoid this error." + ) + self.logger.error(error_msg) + raise Exception(error_msg) + + # get the configured work file template + work_template = item.parent.properties.get("work_template") + publish_template = item.properties.get("publish_template") + + # get the current scene path and extract fields from it using the work + # template: + work_fields = work_template.get_fields(path) + + # ensure the fields work for the publish template + missing_keys = publish_template.missing_keys(work_fields) + if missing_keys: + error_msg = "Work file '%s' missing keys required for the " \ + "publish template: %s" % (path, missing_keys) + self.logger.error(error_msg) + raise Exception(error_msg) + + # create the publish path by applying the fields. store it in the item's + # properties. This is the path we'll create and then publish in the base + # publish plugin. Also set the publish_path to be explicit. + item.properties["path"] = publish_template.apply_fields(work_fields) + item.properties["publish_path"] = item.properties["path"] + + + # use the work file's version number when publishing + if "version" in work_fields: + item.properties["publish_version"] = work_fields["version"] + + # run the base class validation + return super(MayaSessionGeometryPublishPlugin, self).validate( + settings, item) + + def publish(self, settings, item): + """ + Executes the publish logic for the given item and settings. + + :param settings: Dictionary of Settings. The keys are strings, matching + the keys returned in the settings property. The values are `Setting` + instances. + :param item: Item to process + """ + + publisher = self.parent + + print "geometry publish..." + self.logger.debug("geometry publish...") + + # get the path to create and publish + publish_path = item.properties["path"] + + # ensure the publish folder exists: + publish_folder = os.path.dirname(publish_path) + self.parent.ensure_folder_exists(publish_folder) + + # set the alembic args that make the most sense when working with Mari. + # These flags will ensure the export of an Alembic file that contains + # all visible geometry from the current scene together with UV's and + # face sets for use in Mari. + alembic_args = [ + # only renderable objects (visible and not templated) + "-renderableOnly", + # write shading group set assignments (Maya 2015+) + "-writeFaceSets", + # write uv's (only the current uv set gets written) + "-uvWrite" + ] + + # find the animated frame range to use: + start_frame, end_frame = _find_scene_animation_range() + if start_frame and end_frame: + alembic_args.append("-fr %d %d" % (start_frame, end_frame)) + + # Set the output path: + # Note: The AbcExport command expects forward slashes! + alembic_args.append("-file %s" % publish_path.replace("\\", "/")) + + # build the export command. Note, use AbcExport -help in Maya for + # more detailed Alembic export help + abc_export_cmd = ("AbcExport -j \"%s\"" % " ".join(alembic_args)) + + # ...and execute it: + try: + self.parent.log_debug("Executing command: %s" % abc_export_cmd) + mel.eval(abc_export_cmd) + except Exception, e: + self.logger.error("Failed to export Geometry: %s" % e) + return + + # Now that the path has been generated, hand it off to the + super(MayaSessionGeometryPublishPlugin, self).publish(settings, item) + + +def _find_scene_animation_range(): + """ + Find the animation range from the current scene. + """ + # look for any animation in the scene: + animation_curves = cmds.ls(typ="animCurve") + + # if there aren't any animation curves then just return + # a single frame: + if not animation_curves: + return 1, 1 + + # something in the scene is animated so return the + # current timeline. This could be extended if needed + # to calculate the frame range of the animated curves. + start = int(cmds.playbackOptions(q=True, min=True)) + end = int(cmds.playbackOptions(q=True, max=True)) + + return start, end + + +def _session_path(): + """ + Return the path to the current session + :return: + """ + path = cmds.file(query=True, sn=True) + + if isinstance(path, unicode): + path = path.encode("utf-8") + + return path + + +def _get_save_as_action(): + """ + Simple helper for returning a log action dict for saving the session + """ + + engine = sgtk.platform.current_engine() + + # default save callback + callback = cmds.SaveScene + + # if workfiles2 is configured, use that for file save + if "tk-multi-workfiles2" in engine.apps: + app = engine.apps["tk-multi-workfiles2"] + if hasattr(app, "show_file_save_dlg"): + callback = app.show_file_save_dlg + + return { + "action_button": { + "label": "Save As...", + "tooltip": "Save the current session", + "callback": callback + } + } diff --git a/hooks/tk-multi-publish2/basic/start_version_control.py b/hooks/tk-multi-publish2/basic/start_version_control.py new file mode 100644 index 0000000..52fa849 --- /dev/null +++ b/hooks/tk-multi-publish2/basic/start_version_control.py @@ -0,0 +1,358 @@ +# Copyright (c) 2017 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import os +import maya.cmds as cmds +import sgtk + +HookBaseClass = sgtk.get_hook_baseclass() + + +class MayaStartVersionControlPlugin(HookBaseClass): + """ + Simple plugin to insert a version number into the maya file path if one + does not exist. + """ + + @property + def icon(self): + """ + Path to an png icon on disk + """ + + # look for icon one level up from this hook's folder in "icons" folder + return os.path.join( + self.disk_location, + os.pardir, + "icons", + "version_up.png" + ) + + @property + def name(self): + """ + One line display name describing the plugin + """ + return "Begin file versioning" + + @property + def description(self): + """ + Verbose, multi-line description of what the plugin does. This can + contain simple html for formatting. + """ + return """ + Adds a version number to the filename.

+ + Once a version number exists in the file, the publishing will + automatically bump the version number. For example, + filename.ext will be saved to + filename.v001.ext.

+ + If the session has not been saved, validation will fail and a button + will be provided in the logging output to save the file.

+ + If a file already exists on disk with a version number, validation will + fail and the logging output will include button to save the file to a + different name.

+ """ + + @property + def item_filters(self): + """ + List of item types that this plugin is interested in. + + Only items matching entries in this list will be presented to the + accept() method. Strings can contain glob patters such as *, for example + ["maya.*", "file.maya"] + """ + return ["maya.session"] + + @property + def settings(self): + """ + Dictionary defining the settings that this plugin expects to receive + through the settings parameter in the accept, validate, publish and + finalize methods. + + A dictionary on the following form:: + + { + "Settings Name": { + "type": "settings_type", + "default": "default_value", + "description": "One line description of the setting" + } + + The type string should be one of the data types that toolkit accepts as + part of its environment configuration. + """ + return {} + + def accept(self, settings, item): + """ + Method called by the publisher to determine if an item is of any + interest to this plugin. Only items matching the filters defined via the + item_filters property will be presented to this method. + + A publish task will be generated for each item accepted here. Returns a + dictionary with the following booleans: + + - accepted: Indicates if the plugin is interested in this value at + all. Required. + - enabled: If True, the plugin will be enabled in the UI, otherwise + it will be disabled. Optional, True by default. + - visible: If True, the plugin will be visible in the UI, otherwise + it will be hidden. Optional, True by default. + - checked: If True, the plugin will be checked in the UI, otherwise + it will be unchecked. Optional, True by default. + + :param settings: Dictionary of Settings. The keys are strings, matching + the keys returned in the settings property. The values are `Setting` + instances. + :param item: Item to process + + :returns: dictionary with boolean keys accepted, required and enabled + """ + + path = _session_path() + + if path: + version_number = self._get_version_number(path, item) + if version_number is not None: + self.logger.info( + "Maya '%s' plugin rejected the current session..." % + (self.name,) + ) + self.logger.info( + " There is already a version number in the file...") + self.logger.info(" Maya file path: %s" % (path,)) + return {"accepted": False} + else: + # the session has not been saved before (no path determined). + # provide a save button. the session will need to be saved before + # validation will succeed. + self.logger.warn( + "The Maya session has not been saved.", + extra=_get_save_as_action() + ) + + self.logger.info( + "Maya '%s' plugin accepted the current session." % + (self.name,), + extra=_get_version_docs_action() + ) + + # accept the plugin, but don't force the user to add a version number + # (leave it unchecked) + return { + "accepted": True, + "checked": False + } + + def validate(self, settings, item): + """ + Validates the given item to check that it is ok to publish. + + Returns a boolean to indicate validity. + + :param settings: Dictionary of Settings. The keys are strings, matching + the keys returned in the settings property. The values are `Setting` + instances. + :param item: Item to process + + :returns: True if item is valid, False otherwise. + """ + + publisher = self.parent + path = _session_path() + + if not path: + # the session still requires saving. provide a save button. + # validation fails + error_msg = "The Maya session has not been saved." + self.logger.error( + error_msg, + extra=_get_save_as_action() + ) + raise Exception(error_msg) + + # NOTE: If the plugin is attached to an item, that means no version + # number could be found in the path. If that's the case, the work file + # template won't be much use here as it likely has a version number + # field defined within it. Simply use the path info hook to inject a + # version number into the current file path + + # get the path to a versioned copy of the file. + version_path = publisher.util.get_version_path(path, "v001") + if os.path.exists(version_path): + error_msg = "A file already exists with a version number. Please " \ + "choose another name." + self.logger.error( + error_msg, + extra=_get_save_as_action() + ) + raise Exception(error_msg) + + return True + + def publish(self, settings, item): + """ + Executes the publish logic for the given item and settings. + + :param settings: Dictionary of Settings. The keys are strings, matching + the keys returned in the settings property. The values are `Setting` + instances. + :param item: Item to process + """ + + publisher = self.parent + + # get the path in a normalized state. no trailing separator, separators + # are appropriate for current os, no double separators, etc. + path = sgtk.util.ShotgunPath.normalize(_session_path()) + + # ensure the session is saved in its current state + _save_session(path) + + # get the path to a versioned copy of the file. + version_path = publisher.util.get_version_path(path, "v001") + + # save to the new version path + _save_session(version_path) + self.logger.info("A version number has been added to the Maya file...") + self.logger.info(" Maya file path: %s" % (version_path,)) + + def finalize(self, settings, item): + """ + Execute the finalization pass. This pass executes once + all the publish tasks have completed, and can for example + be used to version up files. + + :param settings: Dictionary of Settings. The keys are strings, matching + the keys returned in the settings property. The values are `Setting` + instances. + :param item: Item to process + """ + pass + + def _get_version_number(self, path, item): + """ + Try to extract and return a version number for the supplied path. + + :param path: The path to the current session + + :return: The version number as an `int` if it can be determined, else + None. + + NOTE: This method will use the work template provided by the + session collector, if configured, to determine the version number. If + not configured, the version number will be extracted using the zero + config path_info hook. + """ + + publisher = self.parent + version_number = None + + work_template = item.properties.get("work_template") + if work_template: + if work_template.validate(path): + self.logger.debug( + "Using work template to determine version number.") + work_fields = work_template.get_fields(path) + if "version" in work_fields: + version_number = work_fields.get("version") + else: + self.logger.debug( + "Work template did not match path") + else: + self.logger.debug( + "Work template unavailable for version extraction.") + + if version_number is None: + self.logger.debug( + "Using path info hook to determine version number.") + version_number = publisher.util.get_version_number(path) + + return version_number + +def _session_path(): + """ + Return the path to the current session + :return: + """ + path = cmds.file(query=True, sn=True) + + if isinstance(path, unicode): + path = path.encode("utf-8") + + return path + + +def _save_session(path): + """ + Save the current session to the supplied path. + """ + + # Maya can choose the wrong file type so we should set it here + # explicitly based on the extension + maya_file_type = None + if path.lower().endswith(".ma"): + maya_file_type = "mayaAscii" + elif path.lower().endswith(".mb"): + maya_file_type = "mayaBinary" + + cmds.file(rename=path) + + # save the scene: + if maya_file_type: + cmds.file(save=True, force=True, type=maya_file_type) + else: + cmds.file(save=True, force=True) + + +# TODO: method duplicated in all the maya hooks +def _get_save_as_action(): + """ + + Simple helper for returning a log action dict for saving the session + """ + + engine = sgtk.platform.current_engine() + + # default save callback + callback = cmds.SaveScene + + # if workfiles2 is configured, use that for file save + if "tk-multi-workfiles2" in engine.apps: + app = engine.apps["tk-multi-workfiles2"] + if hasattr(app, "show_file_save_dlg"): + callback = app.show_file_save_dlg + + return { + "action_button": { + "label": "Save As...", + "tooltip": "Save the current session", + "callback": callback + } + } + + +def _get_version_docs_action(): + """ + Simple helper for returning a log action to show version docs + """ + return { + "action_open_url": { + "label": "Version Docs", + "tooltip": "Show docs for version formats", + "url": "https://support.shotgunsoftware.com/hc/en-us/articles/115000068574-User-Guide-WIP-#What%20happens%20when%20you%20publish" + } + } diff --git a/hooks/tk-multi-publish2/icons/geometry.png b/hooks/tk-multi-publish2/icons/geometry.png new file mode 100644 index 0000000..eb33b0f Binary files /dev/null and b/hooks/tk-multi-publish2/icons/geometry.png differ diff --git a/hooks/tk-multi-publish2/icons/maya.png b/hooks/tk-multi-publish2/icons/maya.png new file mode 100644 index 0000000..86be366 Binary files /dev/null and b/hooks/tk-multi-publish2/icons/maya.png differ diff --git a/hooks/tk-multi-publish2/icons/publish.png b/hooks/tk-multi-publish2/icons/publish.png new file mode 100644 index 0000000..b486daf Binary files /dev/null and b/hooks/tk-multi-publish2/icons/publish.png differ diff --git a/hooks/tk-multi-publish2/icons/version_up.png b/hooks/tk-multi-publish2/icons/version_up.png new file mode 100644 index 0000000..ac9ff95 Binary files /dev/null and b/hooks/tk-multi-publish2/icons/version_up.png differ diff --git a/icon_256.png b/icon_256.png new file mode 100644 index 0000000..86be366 Binary files /dev/null and b/icon_256.png differ diff --git a/info.yml b/info.yml new file mode 100644 index 0000000..dcab364 --- /dev/null +++ b/info.yml @@ -0,0 +1,97 @@ +# Copyright (c) 2015 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +# Metadata defining the behaviour and requirements for this engine + +# expected fields in the configuration file for this engine +configuration: + + automatic_context_switch: + type: bool + description: "Controls whether toolkit should attempt to automatically adjust its + context every time the currently loaded file changes. Defaults to True." + default_value: True + + compatibility_dialog_min_version: + type: int + description: "Specify the minimum Application major version that will prompt a warning if + it isn't yet fully supported and tested with Toolkit. To disable the warning + dialog for the version you are testing, it is recomended that you set this + value to the current major version + 1." + default_value: 2015 + + debug_logging: + type: bool + description: Controls whether debug messages should be emitted to the logger + default_value: false + + menu_favourites: + type: list + description: "Controls the favourites section on the main menu. This is a list + and each menu item is a dictionary with keys app_instance and name. + The app_instance parameter connects this entry to a particular + app instance defined in the environment configuration file. The name + is a menu name to make a favourite." + allows_empty: True + values: + type: dict + items: + name: { type: str } + app_instance: { type: str } + + run_at_startup: + type: list + description: "Controls what apps will run on startup. This is a list where each element + is a dictionary with two keys: 'app_instance' and 'name'. The app_instance + value connects this entry to a particular app instance defined in the + environment configuration file. The name is the menu name of the command + to run when the Maya engine starts up. If name is '' then all commands from the + given app instance are started." + allows_empty: True + default_value: [] + values: + type: dict + items: + name: { type: str } + app_instance: { type: str } + + template_project: + type: template + description: "Template to use to determine where to set the maya project location. + This should be a string specifying the template to use but can also be + empty if you do not wish the Maya project to be automatically set." + allows_empty: True + + use_sgtk_as_menu_name: + type: bool + description: Optionally choose to use 'Sgtk' as the primary menu name instead of 'Shotgun' + default_value: false + + launch_builtin_plugins: + type: list + description: Comma-separated list of tk-maya plugins to load when launching Maya. Use + of this feature disables the classic mechanism for bootstrapping Toolkit + when Maya is launched. + allows_empty: True + default_value: [] + values: + type: str + +# the Shotgun fields that this engine needs in order to operate correctly +requires_shotgun_fields: + +# More verbose description of this item +display_name: "Shotgun Engine for Maya" +description: "Shotgun Integration in Maya" + +# Required minimum versions for this item to run +requires_shotgun_version: +requires_core_version: "v0.18.8" + diff --git a/plugins/basic/README.md b/plugins/basic/README.md new file mode 100644 index 0000000..6a3dcd9 --- /dev/null +++ b/plugins/basic/README.md @@ -0,0 +1,128 @@ +# Maya Basic Toolkit workflow plugin + +This is a Shotgun Pipeline Toolkit plugin, +embedding the Shotgun Pipeline Toolkit and allowing +you to easily run and deploy Toolkit Apps and Engines. + +The plugin will appear as `shotgun.py` inside of Maya +and will load the `tk-config-basic` configuration. + +It is auto-updating and will attempt to check for new +versions of this configuration during startup. + +The plugin can either run directly from the engine, +via the toolkit launch application, or as a standalone plugin. + +### Technical Details + +This is a Maya Module that enables basic Shotgun integration +inside Maya. The plugin source is located in the [toolkit maya engine repository](https://github.com/shotgunsoftware/tk-maya/tree/develop/plugin/plugins/basic). +Maya version 2014 and above are supported. +You can read more about maya modules [here](http://help.autodesk.com/view/MAYAUL/2017/ENU/?guid=__files_GUID_CB76E356_753B_4837_8C5B_3296C14872CA_htm). + + +# Engine-based plugin + +If you are using toolkit's application launcher `tk-multi-launchapp`, you can +configure this to start up the plugin as part of launching maya. + +This is all done as part of the maya engine configuration. Simply include +the parameter `launch_builtin_plugins: [basic]` as part of your engine configuration +and the launch app will load the plugin whenever maya is launched for that context: + +``` + tk-maya: + apps: + ... + + launch_builtin_plugins: [basic] + location: + type: app_store + name: tk-maya + version: v1.2.3 +``` + + +# Standalone Plugin + +## Building the plugin + +If you want to run the plugin as a standalone module, you +first need to build it. The build process will prepare the +plugin for a standalone run and will cache all necessary +toolkit components in a special `bundle_cache` directory +that comes with the plugin. + +In order to build it into a plugin which can be loaded into maya, it needs to be +built using a [build script](https://github.com/shotgunsoftware/tk-core/blob/master/developer/build_plugin.py) +that comes as part of the Toolkit Core API. + +In order to build the plugin, follow these steps: + +- Clone the Toolkit API: `git clone git@github.com:shotgunsoftware/tk-core.git` + +- Clone the Toolkit Maya Engine: `git@github.com:shotgunsoftware/tk-maya.git` + +- Checkout the plugin branch: `git checkout develop/plugin` + +- The build script is located in the `developer` subfolder and called `build_plugin.py` + +- In order to build the plugin in `/tmp/maya_plugin, run `python build_plugin.py MAYA_ENGINE/plugins/basic /tmp/maya_plugin` + + +A more complete example would be: + +``` +mkdir /tmp/build_example +cd /tmp/build_example + +# checkout code +git clone git@github.com:shotgunsoftware/tk-core.git +git clone git@github.com:shotgunsoftware/tk-maya.git + +# switch to develop/plugin branch +cd /tmp/build_example/tk-maya +git checkout develop/plugin + +# build the plugin +cd /tmp/build_example/tk-core/developer +python build_plugin.py /tmp/build_example/tk-maya/plugins/basic /tmp/build_example/built_plugin +``` + + +## Using the plugin + +The easiest way to get the plugin loaded is to add an entry to the +`MAYA_MODULE_PATH`. + +For example, if you have put the plugin in +`/Users/john.smith/Documents/shotgun_basic`, just add this path to your existing +Maya module path and restart maya. + +### Using a maya.env file + +If you are using a `Maya.env` file, you can define the `MAYA_MODULE_PATH` +environment variable there. + +For example, on Linux and Mac OS X: `MAYA_MODULE_PATH=$HOME/Documents/shotgun_basic` + +For example, on Windows: `MAYA_MODULE_PATH=%HOME%\Documents\shotgun_basic` + +For more information about Maya plugins and `maya.env`, please see the Maya Documentation. + + +# Additional documentation resources + +If you are a developer making changes to this plugin or it's components, +you may find the following resources useful: + +- Read more about Plugin development + in our [Toolkit Developer Documentation](http://developer.shotgunsoftware.com/tk-core/bootstrap.html#developing-plugins). + +- The plugin needs to be built before it can be executed. You do this by + executing the build tools found [here](https://github.com/shotgunsoftware/tk-core/blob/master/developer). + +- For more information about Toolkit, see http://support.shotgunsoftware.com/ + + + diff --git a/plugins/basic/info.yml b/plugins/basic/info.yml new file mode 100644 index 0000000..247d2f6 --- /dev/null +++ b/plugins/basic/info.yml @@ -0,0 +1,37 @@ +# Copyright (c) 2016 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + + +base_configuration: + # The default configuration that the plugin should use. + # For documentation and details, see + # http://developer.shotgunsoftware.com/tk-core/bootstrap.html#sgtk.bootstrap.ToolkitManager.base_configuration + # + # This is expressed in the form of a Toolkit Descriptor. For more + # information about Toolkit descriptors, see + # http://developer.shotgunsoftware.com/tk-core/descriptor.html + # + # If your descriptor supports a version token and you want it to + # always use the latest version available, simply omit the version token. + type: app_store + name: tk-config-basic + +# The Plugin Id helps uniquely identify this plugin and can be +# used to override and customize it. For more information, see +# http://developer.shotgunsoftware.com/tk-core/bootstrap.html#sgtk.bootstrap.ToolkitManager.plugin_id +# +# When the plugin is built, this file will be converted into a manifest.py file +# and located in a python module named based on the plugin id in order to ensure +# uniqueness. If your plugin_id is set to 'review.maya', the python module +# will be called sgtk_plugin_review_maya and will be located in a python subfolder +# once the plugin has been built. +plugin_id: "basic.maya" + + diff --git a/plugins/basic/plug-ins/shotgun.py b/plugins/basic/plug-ins/shotgun.py new file mode 100644 index 0000000..ee2c978 --- /dev/null +++ b/plugins/basic/plug-ins/shotgun.py @@ -0,0 +1,142 @@ +# Copyright (c) 2016 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import os +import sys + +import maya.api.OpenMaya as OpenMaya2 # Python API 2.0 +import maya.cmds as cmds +import maya.mel as mel +import maya.utils + +PLUGIN_FILENAME = "shotgun.py" + +def maya_useNewAPI(): + """ + The presence of this function lets Maya know that this plug-in uses Python API 2.0 objects. + """ + pass + + +def initializePlugin(mobject): + """ + Registers the plug-in services with Maya when this plug-in is loaded. + + :param mobject: Maya plug-in MObject. + :raises: Exception raised by maya.api.OpenMaya.MFnPlugin registerCommand method. + """ + # Make sure the plug-in is running in Maya 2014 or later. + maya_version = mel.eval("getApplicationVersionAsFloat()") + if maya_version < 2014: + msg = "The Shotgun plug-in is not compatible with version %s of Maya; it requires Maya 2014 or later." + OpenMaya2.MGlobal.displayError(msg % maya_version) + # Ask Maya to unload the plug-in after returning from here. + maya.utils.executeDeferred(cmds.unloadPlugin, PLUGIN_FILENAME) + # Use the plug-in version to indicate that uninitialization should not be done when unloading it, + # while keeping in mind that this version can be displayed in Maya Plug-in Information window. + OpenMaya2.MFnPlugin(mobject, version="Unknown") + # Return to Maya without further initializing the plug-in. + return + + # We currently don't support running multiple engines + # if an engine is already running, exit with an error. + try: + import sgtk + + if sgtk.platform.current_engine(): + msg = "The Shotgun plug-in cannot be loaded because Shotgun Toolkit is already running." + OpenMaya2.MGlobal.displayError(msg) + # Ask Maya to unload the plug-in after returning from here. + maya.utils.executeDeferred(cmds.unloadPlugin, PLUGIN_FILENAME) + # Use the plug-in version to indicate that uninitialization should not be done when unloading it, + # while keeping in mind that this version can be displayed in Maya Plug-in Information window. + OpenMaya2.MFnPlugin(mobject, version="Unknown") + # Return to Maya without further initializing the plug-in. + return + except ImportError: + # no sgtk available + pass + + # Retrieve the plug-in root directory path, set by the module + plugin_root_path = os.environ.get("TK_MAYA_BASIC_ROOT") + + # Prepend the plug-in python package path to the python module search path. + plugin_python_path = os.path.join(plugin_root_path, "python") + if plugin_python_path not in sys.path: + sys.path.insert(0, plugin_python_path) + + # --- Import Core --- + # + # - If we are running the plugin built as a stand-alone unit, + # try to retrieve the path to sgtk core and add that to the pythonpath. + # When the plugin has been built, there is a sgtk_plugin_basic_maya + # module which we can use to retrieve the location of core and add it + # to the pythonpath. + # - If we are running toolkit as part of a larger zero config workflow + # and not from a standalone workflow, we are running the plugin code + # directly from the engine folder without a bundle cache and with this + # configuration, core already exists in the pythonpath. + + try: + from sgtk_plugin_basic_maya import manifest + running_as_standalone_plugin = True + except ImportError: + running_as_standalone_plugin = False + + if running_as_standalone_plugin: + # Retrieve the Shotgun toolkit core included with the plug-in and + # prepend its python package path to the python module search path. + tkcore_python_path = manifest.get_sgtk_pythonpath(plugin_root_path) + sys.path.insert(0, tkcore_python_path) + import sgtk + + else: + # Running as part of the the launch process and as part of zero + # config. The launch logic that started maya has already + # added sgtk to the pythonpath. + import sgtk + + # as early as possible, start up logging to the backend file + sgtk.LogManager().initialize_base_file_handler("tk-maya") + + # Set the plug-in root directory path constant of the plug-in python package. + from tk_maya_basic import constants + from tk_maya_basic import plugin_logic + + # Set the plug-in vendor name and version number to display in Maya Plug-in Information window + # alongside the plug-in name set by Maya from the name of this file minus its '.py' extension. + OpenMaya2.MFnPlugin( + mobject, + vendor=constants.PLUGIN_AUTHOR, + version=constants.PLUGIN_VERSION + ) + + # Bootstrap the plug-in logic once Maya has settled. + maya.utils.executeDeferred(plugin_logic.bootstrap) + + +def uninitializePlugin(mobject): + """ + Deregisters the plug-in services with Maya when this plug-in is unloaded. + + :param mobject: Maya plug-in MObject. + :raises: Exception raised by maya.api.OpenMaya.MFnPlugin deregisterCommand method. + """ + plugin = OpenMaya2.MFnPlugin(mobject) + + if plugin.version == "Unknown": + # As requested earlier when initializing the plug-in, + # return to Maya without further uninitializing it. + return + + # Shutdown the plug-in logic. + from tk_maya_basic import plugin_logic + plugin_logic.shutdown() + diff --git a/plugins/basic/python/tk_maya_basic/__init__.py b/plugins/basic/python/tk_maya_basic/__init__.py new file mode 100644 index 0000000..d6ee633 --- /dev/null +++ b/plugins/basic/python/tk_maya_basic/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2016 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. \ No newline at end of file diff --git a/plugins/basic/python/tk_maya_basic/constants.py b/plugins/basic/python/tk_maya_basic/constants.py new file mode 100644 index 0000000..3e0c2a1 --- /dev/null +++ b/plugins/basic/python/tk_maya_basic/constants.py @@ -0,0 +1,17 @@ +# Copyright (c) 2017 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +# the constants associated with this plugin + +# the version number for this maya plugin - displayed in the maya plugin dialog +PLUGIN_VERSION = "1.0.0" +# the author of this maya plugin - displayed in the maya plugin dialog +PLUGIN_AUTHOR = "Shotgun Software" + diff --git a/plugins/basic/python/tk_maya_basic/plugin_engine.py b/plugins/basic/python/tk_maya_basic/plugin_engine.py new file mode 100644 index 0000000..b2ad4d7 --- /dev/null +++ b/plugins/basic/python/tk_maya_basic/plugin_engine.py @@ -0,0 +1,123 @@ +# Copyright (c) 2016 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import os + +def bootstrap(sg_user, progress_callback, completed_callback, failed_callback): + """ + Bootstraps the engine using the plug-in manifest data to drive some bootstrap options. + + :param sg_user: A :class:`sgtk.authentication.ShotgunUser` instance providing the logged in user credentials. + :param progress_callback: Callback function that reports back on the toolkit and engine bootstrap progress. + :param completed_callback: Callback function that handles cleanup after successful completion of the bootstrap. + :param failed_callback: Callback function that handles cleanup after failed completion of the bootstrap. + """ + + # The first time around, import the toolkit core included with the plug-in, + # but also re-import it later to ensure usage of a swapped in version. + import sgtk + + logger = sgtk.LogManager.get_logger(__name__) + + # get information about this plugin (plugin id & base config) + plugin_info = _get_plugin_info() + + # Create a boostrap manager for the logged in user with the plug-in configuration data. + toolkit_mgr = sgtk.bootstrap.ToolkitManager(sg_user) + toolkit_mgr.base_configuration = plugin_info["base_configuration"] + toolkit_mgr.plugin_id = plugin_info["plugin_id"] + plugin_root_path = os.environ.get("TK_MAYA_BASIC_ROOT") + toolkit_mgr.bundle_cache_fallback_paths = [os.path.join(plugin_root_path, "bundle_cache")] + + # Retrieve the Shotgun entity type and id when they exist in the environment. + entity = toolkit_mgr.get_entity_from_environment() + logger.debug("Will launch the engine with entity: %s" % entity) + + # Install the bootstrap progress reporting callback. + toolkit_mgr.progress_callback = progress_callback + + # Bootstrap a toolkit instance asynchronously in a background thread, + # followed by launching the engine synchronously in the main application thread. + # Before bootstrapping the engine for the first time around, + # the toolkit manager may swap the toolkit core to its latest version. + toolkit_mgr.bootstrap_engine_async( + "tk-maya", + entity, + completed_callback=completed_callback, + failed_callback=failed_callback + ) + + +def _get_plugin_info(): + """ + Returns a dictionary of information about the plugin of the form: + + { + plugin_id: , + base_configuration: + } + """ + + try: + # first, see if we can get the info from the manifest. if we can, no + # need to parse info.yml + from sgtk_plugin_basic_maya import manifest + plugin_id = manifest.plugin_id + base_configuration = manifest.base_configuration + except ImportError: + # no manifest, running in situ from the engine. just parse the info.yml + # file to get at the info we need. + + # import the yaml parser + from tank_vendor import yaml + + # build the path to the info.yml file + plugin_info_yml = os.path.abspath( + os.path.join( + __file__, + "..", + "..", + "..", + "info.yml" + ) + ) + + # open the yaml file and read the data + with open(plugin_info_yml, "r") as plugin_info_fh: + info_yml = yaml.load(plugin_info_fh) + plugin_id = info_yml["plugin_id"] + base_configuration = info_yml["base_configuration"] + + # return a dictionary with the required info + return dict( + plugin_id=plugin_id, + base_configuration=base_configuration, + ) + + +def shutdown(): + """ + Shuts down the running engine. + """ + + # Re-import the toolkit core to ensure usage of a swapped in version. + import sgtk + logger = sgtk.LogManager.get_logger(__name__) + engine = sgtk.platform.current_engine() + + if engine: + logger.info("Stopping the Shotgun engine.") + # Close the various windows (dialogs, panels, etc.) opened by the engine. + engine.close_windows() + # Turn off your engine! Step away from the car! + engine.destroy() + + else: + logger.debug("The Shotgun engine was already stopped!") diff --git a/plugins/basic/python/tk_maya_basic/plugin_logic.py b/plugins/basic/python/tk_maya_basic/plugin_logic.py new file mode 100644 index 0000000..9638d17 --- /dev/null +++ b/plugins/basic/python/tk_maya_basic/plugin_logic.py @@ -0,0 +1,329 @@ +# Copyright (c) 2016 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import logging + +import maya.utils +import pymel.core as pm +import maya.OpenMaya as OpenMaya +import maya.OpenMayaUI as OpenMayaUI + +try: + import shiboken2 as shiboken +except ImportError: + import shiboken + +# For now, import the Shotgun toolkit core included with the plug-in, +# but also re-import it later to ensure usage of a swapped in version. +import sgtk + +# Knowing that the plug-in is only loaded for Maya 2014 and later, +# import PySide packages without having to worry about the version to use +# (PySide in Maya 2014-2015-2016 and PySide2 in Maya 2017 and later). +from sgtk.util.qt_importer import QtImporter +qt_importer = QtImporter() +QtCore = qt_importer.QtCore +QtGui = qt_importer.QtGui + +from . import plugin_engine + +MENU_LOGIN = "ShotgunMenuLogin" +MENU_LABEL = "Shotgun" + +logger = sgtk.LogManager.get_logger(__name__) + +class ProgressHandler(QtCore.QObject): + """ + An object that wraps a QTimer that is used to periodically check + for updates that need to be made to the progress bar in Maya. This + will always execute progress updates on the main thread. + """ + PROGRESS_INTERVAL = 150 # milliseconds + + def __init__(self): + ptr = OpenMayaUI.MQtUtil.mainWindow() + parent = shiboken.wrapInstance(long(ptr), QtGui.QMainWindow) + + super(ProgressHandler, self).__init__(parent=parent) + + self._progress_value = None + self._message = None + self._timer = QtCore.QTimer(parent=self) + + self._timer.timeout.connect(self._update_progress) + self._timer.start(self.PROGRESS_INTERVAL) + + @property + def timer(self): + """ + The QTimer instance that's updating progress. + """ + return self._timer + + def _update_progress(self): + """ + Sets progress. Must be run from the main thread! + """ + if self._message is not None and self._progress_value is not None: + _show_progress_bar(self._progress_value, self._message) + self._message = None + self._progress_value = None + + def _handle_bootstrap_progress(self, progress_value, message): + """ + Callback function that reports back on the toolkit and engine bootstrap progress. + + .. note:: This method is, and must remain, thread safe. It will be called from + a non-main thread. + + :param progress_value: Current progress value, ranging from 0.0 to 1.0. + :param message: Progress message to report. + """ + + logger.debug("Bootstrapping Shotgun: %s" % message) + + # Set some state that will trigger our timer to update the progress bar. + self._progress_value = progress_value + self._message = message + + +progress_handler = ProgressHandler() + + +def bootstrap(): + """ + Bootstraps the plug-in logic handling user login and logout. + """ + if sgtk.authentication.ShotgunAuthenticator().get_default_user(): + # When the user is already authenticated, automatically log him/her in. + _login_user() + else: + # When the user is not yet authenticated, display a login menu. + _create_login_menu() + + +def shutdown(): + """ + Shutdowns the plug-in logic handling user login and logout. + """ + + if sgtk.platform.current_engine(): + # When the user is logged in with a running engine, shut down the engine. + plugin_engine.shutdown() + else: + # When the user is logged out, delete the displayed login menu. + _delete_login_menu() + + +def _login_user(): + """ + Logs in the user to Shotgun and starts the engine. + """ + + try: + # When the user is not yet authenticated, + # pop up the Shotgun login dialog to get the user's credentials, + # otherwise, get the cached user's credentials. + user = sgtk.authentication.ShotgunAuthenticator().get_user() + + except sgtk.authentication.AuthenticationCancelled: + # When the user cancelled the Shotgun login dialog, + # keep around the displayed login menu. + OpenMaya.MGlobal.displayInfo("Shotgun login was cancelled by the user.") + return + + # Get rid of the displayed login menu since the engine menu will take over. + # We need to make sure the Shotgun login dialog closing events have been + # processed before deleting the menu to avoid a crash in Maya 2017. + maya.utils.executeDeferred(_delete_login_menu) + + OpenMaya.MGlobal.displayInfo("Loading Shotgun integration...") + + # Show a progress bar, and set its initial value and message. + _show_progress_bar(0.0, "Loading...") + + # Before bootstrapping the engine for the first time around, + # the toolkit manager may swap the toolkit core to its latest version. + try: + plugin_engine.bootstrap( + user, + progress_callback=progress_handler._handle_bootstrap_progress, + completed_callback=_handle_bootstrap_completed, + failed_callback=_handle_bootstrap_failed + ) + except Exception, e: + # return to normal state + _handle_bootstrap_failed(phase=None, exception=e) + # also print the full call stack + logger.exception("Shotgun reported the following exception during startup:") + + +def _handle_bootstrap_completed(engine): + """ + Callback function that handles cleanup after successful completion of the bootstrap. + + This function is executed in the main thread by the main event loop. + + :param engine: Launched :class:`sgtk.platform.Engine` instance. + """ + progress_handler.timer.stop() + + # Needed global to re-import the toolkit core. + global sgtk + + # Re-import the toolkit core to ensure usage of a swapped in version. + import sgtk + + # Hide the progress bar. + _hide_progress_bar() + + # Report completion of the bootstrap. + logger.debug("Maya Plugin bootstrapped.") + + # Add a logout menu item to the engine context menu, but only if + # running as a standalone plugin + if sgtk.platform.current_engine().context.project is None: + sgtk.platform.current_engine().register_command( + "Log Out of Shotgun", + _logout_user, + {"type": "context_menu"} + ) + + +def _handle_bootstrap_failed(phase, exception): + """ + Callback function that handles cleanup after failed completion of the bootstrap. + + This function is executed in the main thread by the main event loop. + + :param phase: Bootstrap phase that raised the exception, + ``ToolkitManager.TOOLKIT_BOOTSTRAP_PHASE`` or ``ToolkitManager.ENGINE_STARTUP_PHASE``. + :param exception: Python exception raised while bootstrapping. + """ + progress_handler.timer.stop() + + # Needed global to re-import the toolkit core. + global sgtk + + if phase is None or phase == sgtk.bootstrap.ToolkitManager.ENGINE_STARTUP_PHASE: + # Re-import the toolkit core to ensure usage of a swapped in version. + import sgtk + + # Hide the progress bar. + _hide_progress_bar() + + # Report the encountered exception. + # the message displayed last will be the one visible in the script editor, + # so make sure this is the error message summary. + OpenMaya.MGlobal.displayError("An exception was raised during Shotgun startup: %s" % exception) + OpenMaya.MGlobal.displayError("For details, see log files in %s" % sgtk.LogManager().log_folder) + OpenMaya.MGlobal.displayError("Error loading Shotgun integration.") + + # Clear the user's credentials to log him/her out. + sgtk.authentication.ShotgunAuthenticator().clear_default_user() + + # Re-display the login menu. + _create_login_menu() + + +def _logout_user(): + """ + Shuts down the engine and logs out the user of Shotgun. + """ + + # Shutting down the engine also get rid of the engine menu. + plugin_engine.shutdown() + + # Clear the user's credentials to log him/her out. + sgtk.authentication.ShotgunAuthenticator().clear_default_user() + + # Re-display the login menu. + _create_login_menu() + + +def _show_progress_bar(progress_value, message): + """ + Shows a non-interruptable progress bar, and sets its value and message. + + :param progress_value: Current progress value, ranging from 0.0 to 1.0. + :param message: Progress message to report. + """ + + # Show the main progress bar (normally in the Help Line) making sure it uses + # the bootstrap progress configuration (since it might have been taken over by another process). + main_progress_bar = pm.ui.MainProgressBar(minValue=0, maxValue=100, interruptable=False) + main_progress_bar.beginProgress() + + # Set the main progress bar value and message. + main_progress_bar.setProgress(int(progress_value * 100.0)) + main_progress_bar.setStatus("Shotgun: %s" % message) + + +def _hide_progress_bar(): + """ + Hides the progress bar. + """ + + # Hide the main progress bar (normally in the Help Line). + main_progress_bar = pm.getMainProgressBar() + main_progress_bar.endProgress() + + +def _create_login_menu(): + """ + Creates and displays a Shotgun user login menu. + """ + + # Creates the menu entry in the application menu bar. + menu = pm.menu(MENU_LOGIN, label=MENU_LABEL, parent=pm.melGlobals["gMainWindow"]) + + # Add the login menu item. + pm.menuItem( + parent=menu, + label="Log In to Shotgun...", + command=pm.Callback(_login_user) + ) + + pm.menuItem(parent=menu, divider=True) + + # Add the website menu items. + pm.menuItem( + parent=menu, + label="Learn about Shotgun...", + command=pm.Callback(_jump_to_website) + ) + pm.menuItem( + parent=menu, + label="Try Shotgun for Free...", + command=pm.Callback(_jump_to_signup) + ) + + +def _delete_login_menu(): + """ + Deletes the displayed Shotgun user login menu. + """ + + if pm.menu(MENU_LOGIN, exists=True): + pm.deleteUI(MENU_LOGIN) + + +def _jump_to_website(): + """ + Jumps to the Shotgun website in the default web browser. + """ + QtGui.QDesktopServices.openUrl(QtCore.QUrl("https://www.shotgunsoftware.com")) + + +def _jump_to_signup(): + """ + Jumps to the Shotgun signup page in the default web browser. + """ + QtGui.QDesktopServices.openUrl(QtCore.QUrl("https://www.shotgunsoftware.com/signup")) diff --git a/plugins/basic/shotgun.mod b/plugins/basic/shotgun.mod new file mode 100644 index 0000000..0f84826 --- /dev/null +++ b/plugins/basic/shotgun.mod @@ -0,0 +1,12 @@ +# Copyright (c) 2016 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + ++ tk-maya-basic 1.0.0 . +TK_MAYA_BASIC_ROOT:=. diff --git a/python/__init__.py b/python/__init__.py new file mode 100644 index 0000000..7d57406 --- /dev/null +++ b/python/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) 2015 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +from . import tk_maya diff --git a/python/tk_maya/__init__.py b/python/tk_maya/__init__.py new file mode 100644 index 0000000..689e84b --- /dev/null +++ b/python/tk_maya/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2015 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +from .menu_generation import MenuGenerator +from . import panel_generation diff --git a/python/tk_maya/menu_generation.py b/python/tk_maya/menu_generation.py new file mode 100644 index 0000000..3e3ebb4 --- /dev/null +++ b/python/tk_maya/menu_generation.py @@ -0,0 +1,355 @@ +# Copyright (c) 2015 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +""" +Menu handling for Maya + +""" + +import tank +import sys +import os +import unicodedata +import maya.OpenMaya as OpenMaya +import pymel.core as pm +import maya.cmds as cmds +import maya +from tank.platform.qt import QtGui, QtCore +from pymel.core import Callback + + +class MenuGenerator(object): + """ + Menu generation functionality for Maya + """ + + def __init__(self, engine, menu_handle): + self._engine = engine + self._menu_handle = menu_handle + self._dialogs = [] + + ########################################################################################## + # public methods + + def create_menu(self, *args): + """ + Render the entire Shotgun menu. + In order to have commands enable/disable themselves based on the enable_callback, + re-create the menu items every time. + """ + self._menu_handle.deleteAllItems() + + # now add the context item on top of the main menu + self._context_menu = self._add_context_menu() + pm.menuItem(divider=True, parent=self._menu_handle) + + + # now enumerate all items and create menu objects for them + menu_items = [] + for (cmd_name, cmd_details) in self._engine.commands.items(): + menu_items.append( AppCommand(cmd_name, cmd_details) ) + + # sort list of commands in name order + menu_items.sort(key=lambda x: x.name) + + # now add favourites + for fav in self._engine.get_setting("menu_favourites"): + app_instance_name = fav["app_instance"] + menu_name = fav["name"] + # scan through all menu items + for cmd in menu_items: + if cmd.get_app_instance_name() == app_instance_name and cmd.name == menu_name: + # found our match! + cmd.add_command_to_menu(self._menu_handle) + # mark as a favourite item + cmd.favourite = True + + pm.menuItem(divider=True, parent=self._menu_handle) + + # now go through all of the menu items. + # separate them out into various sections + commands_by_app = {} + + for cmd in menu_items: + + if cmd.get_type() == "context_menu": + # context menu! + cmd.add_command_to_menu(self._context_menu) + + else: + # normal menu + app_name = cmd.get_app_name() + if app_name is None: + # un-parented app + app_name = "Other Items" + if not app_name in commands_by_app: + commands_by_app[app_name] = [] + commands_by_app[app_name].append(cmd) + + # now add all apps to main menu + self._add_app_menu(commands_by_app) + + ########################################################################################## + # context menu and UI + + def _add_context_menu(self): + """ + Adds a context menu which displays the current context + """ + + ctx = self._engine.context + ctx_name = str(ctx) + + # create the menu object + # the label expects a unicode object so we cast it to support when the context may + # contain info with non-ascii characters + ctx_menu = pm.subMenuItem(label=ctx_name.decode("utf-8"), parent=self._menu_handle) + + # link to UI + pm.menuItem(label="Jump to Shotgun", + parent=ctx_menu, + command=Callback(self._jump_to_sg)) + + # Add the menu item only when there are some file system locations. + if ctx.filesystem_locations: + pm.menuItem(label="Jump to File System", + parent=ctx_menu, + command=Callback(self._jump_to_fs)) + + # divider (apps may register entries below this divider) + pm.menuItem(divider=True, parent=ctx_menu) + + return ctx_menu + + + def _jump_to_sg(self): + """ + Jump to shotgun, launch web browser + """ + url = self._engine.context.shotgun_url + QtGui.QDesktopServices.openUrl(QtCore.QUrl(url)) + + + def _jump_to_fs(self): + """ + Jump from context to FS + """ + # launch one window for each location on disk + paths = self._engine.context.filesystem_locations + for disk_location in paths: + + # get the setting + system = sys.platform + + # run the app + if system == "linux2": + cmd = 'xdg-open "%s"' % disk_location + elif system == "darwin": + cmd = 'open "%s"' % disk_location + elif system == "win32": + cmd = 'cmd.exe /C start "Folder" "%s"' % disk_location + else: + raise Exception("Platform '%s' is not supported." % system) + + exit_code = os.system(cmd) + if exit_code != 0: + self._engine.logger.error("Failed to launch '%s'!", cmd) + + + ########################################################################################## + # app menus + + + def _add_app_menu(self, commands_by_app): + """ + Add all apps to the main menu, process them one by one. + """ + for app_name in sorted(commands_by_app.keys()): + + if len(commands_by_app[app_name]) > 1: + # more than one menu entry fort his app + # make a sub menu and put all items in the sub menu + app_menu = pm.subMenuItem(label=app_name, parent=self._menu_handle) + + # get the list of menu cmds for this app + cmds = commands_by_app[app_name] + # make sure it is in alphabetical order + cmds.sort(key=lambda x: x.name) + + for cmd in cmds: + cmd.add_command_to_menu(app_menu) + + else: + + # this app only has a single entry. + # display that on the menu + # todo: Should this be labelled with the name of the app + # or the name of the menu item? Not sure. + cmd_obj = commands_by_app[app_name][0] + if not cmd_obj.favourite: + # skip favourites since they are already on the menu + cmd_obj.add_command_to_menu(self._menu_handle) + + + + + +class AppCommand(object): + """ + Wraps around a single command that you get from engine.commands + """ + + def __init__(self, name, command_dict): + self.name = name + self.properties = command_dict["properties"] + self.callback = command_dict["callback"] + self.favourite = False + + def get_app_name(self): + """ + Returns the name of the app that this command belongs to + """ + if "app" in self.properties: + return self.properties["app"].display_name + return None + + def get_app_instance_name(self): + """ + Returns the name of the app instance, as defined in the environment. + Returns None if not found. + """ + if "app" not in self.properties: + return None + + app_instance = self.properties["app"] + engine = app_instance.engine + + for (app_instance_name, app_instance_obj) in engine.apps.items(): + if app_instance_obj == app_instance: + # found our app! + return app_instance_name + + return None + + def get_documentation_url_str(self): + """ + Returns the documentation as a str + """ + if "app" in self.properties: + app = self.properties["app"] + doc_url = app.documentation_url + # deal with nuke's inability to handle unicode. #fail + if doc_url.__class__ == unicode: + doc_url = unicodedata.normalize('NFKD', doc_url).encode('ascii', 'ignore') + return doc_url + + return None + + def get_type(self): + """ + returns the command type. Returns node, custom_pane or default + """ + return self.properties.get("type", "default") + + def add_command_to_menu(self, menu): + """ + Adds an app command to the menu + """ + + # create menu sub-tree if need to: + # Support menu items seperated by '/' + parent_menu = menu + parts = self.name.split("/") + for item_label in parts[:-1]: + + # see if there is already a sub-menu item + sub_menu = self._find_sub_menu_item(parent_menu, item_label) + if sub_menu: + # already have sub menu + parent_menu = sub_menu + else: + # create new sub menu + params = { + "label" : item_label, + "parent" : parent_menu, + "subMenu" : True + } + parent_menu = pm.menuItem(**params) + + # finally create the command menu item: + params = { + "label": parts[-1],#self.name, + "command": Callback(self._execute_deferred), + "parent": parent_menu, + } + if "tooltip" in self.properties: + params["annotation"] = self.properties["tooltip"] + if "enable_callback" in self.properties: + params["enable"] = self.properties["enable_callback"]() + + pm.menuItem(**params) + + def _execute_deferred(self): + """ + Execute the callback deferred to avoid potential problems with the command resulting in the menu + being deleted, e.g. if the context changes resulting in an engine restart! - this was causing a + segmentation fault crash on Linux + """ + # note that we use a single shot timer instead of cmds.evalDeferred as we were experiencing + # odd behaviour when the deferred command presented a modal dialog that then performed a file + # operation that resulted in a QMessageBox being shown - the deferred command would then run + # a second time, presumably from the event loop of the modal dialog from the first command! + # + # As the primary purpose of this method is to detach the executing code from the menu invocation, + # using a singleShot timer achieves this without the odd behaviour exhibited by evalDeferred. + QtCore.QTimer.singleShot(0, self._execute_within_exception_trap) + + def _execute_within_exception_trap(self): + """ + Execute the callback and log any exception that gets raised which may otherwise have been + swallowed by the deferred execution of the callback. + """ + try: + self.callback() + except Exception, e: + current_engine = tank.platform.current_engine() + current_engine.logger.exception("An exception was raised from Toolkit") + + def _find_sub_menu_item(self, menu, label): + """ + Find the 'sub-menu' menu item with the given label + """ + items = pm.menu(menu, query=True, itemArray=True) + for item in items: + item_path = "%s|%s" % (menu, item) + + # only care about menuItems that have sub-menus: + if not pm.menuItem(item_path, query=True, subMenu=True): + continue + + item_label = pm.menuItem(item_path, query=True, label=True) + if item_label == label: + return item_path + + return None + + + + + + + + + + + + + + diff --git a/python/tk_maya/panel_generation.py b/python/tk_maya/panel_generation.py new file mode 100644 index 0000000..be5b975 --- /dev/null +++ b/python/tk_maya/panel_generation.py @@ -0,0 +1,319 @@ +# Copyright (c) 2016 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import maya.cmds as cmds +import maya.mel as mel +import maya.utils + +from . import panel_util + + +# Prefix prepended to the Shotgun app panel unique identifier to create +# the name given to the Qt widget at the root of the Shotgun app panel. +SHOTGUN_APP_PANEL_PREFIX = "panel_" + +# Prefix prepended to the Shotgun app panel name to create the name +# given to the Maya panel embedding the Shotgun app panel widget. +MAYA_PANEL_PREFIX = "maya_" + + +def restore_panels(engine): + """ + Restores the persisted Shotgun app panels into their visible + Maya workspace controls in the active Maya window. + + .. note:: This function is only meaningful for Maya 2017 and later, + and does nothing for previous versions of Maya. + + :param engine: :class:`MayaEngine` instance running in Maya. + """ + + # Only restore Shotgun app panels in Maya 2017 and later. + if mel.eval("getApplicationVersionAsFloat()") < 2017: + return + + # Search for the Shotgun app panels that need to be restored + # among the panels registered with the engine. + for panel_id in engine.panels: + + # Recreate a Maya panel name with the Shotgun app panel unique identifier. + maya_panel_name = MAYA_PANEL_PREFIX + SHOTGUN_APP_PANEL_PREFIX + panel_id + + # When the current Maya workspace contains the Maya panel workspace control, + # the Shotgun app panel needs to be recreated and docked. + if cmds.workspaceControl(maya_panel_name, exists=True): + + # Once Maya will have completed its UI update and be idle, + # recreate and dock the Shotgun app panel. + maya.utils.executeDeferred(engine.panels[panel_id]["callback"]) + + +def dock_panel(engine, shotgun_panel, title): + """ + Docks a Shotgun app panel into a new Maya panel in the active Maya window. + + In Maya 2016 and before, the panel is docked into a new tab of Maya Channel Box dock area. + In Maya 2017 and after, the panel is docked into a new workspace area in the active Maya workspace. + + :param engine: :class:`MayaEngine` instance running in Maya. + :param shotgun_panel: Qt widget at the root of the Shotgun app panel. + This Qt widget is assumed to be child of Maya main window. + Its name can be used in standard Maya commands to reparent it under a Maya panel. + :param title: Title to give to the new dock tab. + :returns: Name of the newly created Maya panel. + """ + + # Retrieve the Shotgun app panel name. + shotgun_panel_name = shotgun_panel.objectName() + + # Create a Maya panel name. + maya_panel_name = MAYA_PANEL_PREFIX + shotgun_panel_name + + # Use the proper Maya panel docking method according to the Maya version. + if mel.eval("getApplicationVersionAsFloat()") < 2017: + + import pymel.core as pm + + # When the Maya panel already exists, it can be deleted safely since its embedded + # Shotgun app panel has already been reparented under Maya main window. + if pm.control(maya_panel_name, query=True, exists=True): + engine.logger.debug("Deleting existing Maya panel %s.", maya_panel_name) + pm.deleteUI(maya_panel_name) + + # Create a new Maya window. + maya_window = pm.window() + engine.logger.debug("Created Maya window %s.", maya_window) + + # Add a layout to the Maya window. + maya_layout = pm.formLayout(parent=maya_window) + engine.logger.debug("Created Maya layout %s.", maya_layout) + + # Reparent the Shotgun app panel under the Maya window layout. + engine.logger.debug("Reparenting Shotgun app panel %s under Maya layout %s.", shotgun_panel_name, maya_layout) + pm.control(shotgun_panel_name, edit=True, parent=maya_layout) + + # Keep the Shotgun app panel sides aligned with the Maya window layout sides. + pm.formLayout(maya_layout, + edit=True, + attachForm=[(shotgun_panel_name, 'top', 1), + (shotgun_panel_name, 'left', 1), + (shotgun_panel_name, 'bottom', 1), + (shotgun_panel_name, 'right', 1)] + ) + + # Dock the Maya window into a new tab of Maya Channel Box dock area. + engine.logger.debug("Creating Maya panel %s.", maya_panel_name) + pm.dockControl(maya_panel_name, area="right", content=maya_window, label=title) + + # Since Maya does not give us any hints when a panel is being closed, + # install an event filter on Maya dock control to monitor its close event + # in order to gracefully close and delete the Shotgun app panel widget. + # Some obscure issues relating to UI refresh are also resolved by the event filter. + panel_util.install_event_filter_by_name(maya_panel_name, shotgun_panel_name) + + # Once Maya will have completed its UI update and be idle, + # raise (with "r=True") the new dock tab to the top. + maya.utils.executeDeferred("import maya.cmds as cmds\n" \ + "cmds.dockControl('%s', edit=True, r=True)" % maya_panel_name) + + else: # Maya 2017 and later + + import uuid + + # When the current Maya workspace contains our Maya panel workspace control, + # embed the Shotgun app panel into this workspace control. + # This can happen when the engine has just been started and the Shotgun app panel is + # displayed for the first time around, or when the user reinvokes a displayed panel. + if cmds.workspaceControl(maya_panel_name, exists=True): + + engine.logger.debug("Restoring Maya workspace panel %s.", maya_panel_name) + + # Set the Maya default parent to be our Maya panel workspace control. + cmds.setParent(maya_panel_name) + + # Embed the Shotgun app panel into the Maya panel workspace control. + build_workspace_control_ui(shotgun_panel_name) + + if cmds.control(maya_panel_name, query=True, isObscured=True): + # When the panel is not visible, raise it to the top of its workspace area. + engine.logger.debug("Raising workspace panel %s.", maya_panel_name) + cmds.workspaceControl(maya_panel_name, edit=True, r=True) + else: + # When the panel is visible, use a workaround to force Maya 2017 to refresh the panel size. + # We encased this workaround in a try/except since we cannot be sure + # that it will still work without errors in future versions of Maya. + try: + engine.logger.debug("Forcing Maya to refresh workspace panel %s size.", maya_panel_name) + + # Create a new empty workspace control tab. + name = cmds.workspaceControl(uuid.uuid4().hex, + tabToControl=(maya_panel_name, -1), # -1 to append a new tab + uiScript="", + r=True) # raise at the top of its workspace area + # Delete the empty workspace control. + cmds.deleteUI(name) + # Delete the empty workspace control state that was created + # when deleting the empty workspace control. + cmds.workspaceControlState(name, remove=True) + except: + engine.logger.debug("Cannot force Maya to refresh workspace panel %s size.", maya_panel_name) + + return maya_panel_name + + # Retrieve the Channel Box dock area, with error reporting turned off. + # This MEL function is declared in Maya startup script file UIComponents.mel. + # It returns an empty string when a dock area cannot be found, but Maya will + # retrieve the Channel Box dock area even when it is not shown in the current workspace. + dock_area = mel.eval('getUIComponentDockControl("Channel Box / Layer Editor", false)') + engine.logger.debug("Retrieved Maya dock area %s.", dock_area) + + # This UI script will be called to build the UI of the new dock tab. + # It will embed the Shotgun app panel into a Maya workspace control. + # Since Maya 2017 expects this script to be passed in as a string, + # not as a function pointer, it must retrieve the current module in order + # to call function build_workspace_control_ui() that actually builds the UI. + # Note that this script will be saved automatically with the workspace control state + # in the Maya layout preference file when the user quits Maya, and will be executed + # automatically when Maya is restarted later by the user. + ui_script = "import sys\n" \ + "import maya.api.OpenMaya\n" \ + "import maya.utils\n" \ + "for m in sys.modules:\n" \ + " if 'tk_maya.panel_generation' in m:\n" \ + " try:\n" \ + " sys.modules[m].build_workspace_control_ui('%(panel_name)s')\n" \ + " except Exception, e:\n" \ + " msg = 'Shotgun: Cannot restore %(panel_name)s: %%s' %% e\n" \ + " fct = maya.api.OpenMaya.MGlobal.displayError\n" \ + " maya.utils.executeInMainThreadWithResult(fct, msg)\n" \ + " break\n" \ + "else:\n" \ + " msg = 'Shotgun: Cannot restore %(panel_name)s: Shotgun is not currently running'\n" \ + " fct = maya.api.OpenMaya.MGlobal.displayError\n" \ + " maya.utils.executeInMainThreadWithResult(fct, msg)\n" \ + % {"panel_name": shotgun_panel_name} + + # Dock the Shotgun app panel into a new workspace control in the active Maya workspace. + engine.logger.debug("Creating Maya workspace panel %s.", maya_panel_name) + + kwargs = {"uiScript": ui_script, + "retain": False, # delete the dock tab when it is closed + "label": title, + "r": True} # raise at the top of its workspace area + + # When we are in a Maya workspace where the Channel Box dock area can be found, + # dock the Shotgun app panel into a new tab of this Channel Box dock area + # since the user was used to this behaviour in previous versions of Maya. + # When we are in a Maya workspace where the Channel Box dock area can not be found, + # let Maya embed the Shotgun app panel into a floating workspace control window. + kwargs["tabToControl"] = (dock_area, -1) # -1 to append a new tab + + cmds.workspaceControl(maya_panel_name, **kwargs) + + return maya_panel_name + + +def build_workspace_control_ui(shotgun_panel_name): + """ + Embeds a Shotgun app panel into the calling Maya workspace control. + + This function will be called in two cases: + - When the workspace control is being created by Maya command workspaceControl; + - When the workspace control is being restored from a workspace control state + created by Maya when this workspace control was previously closed and deleted. + + .. note:: This function is only for Maya 2017 and later. + + :param shotgun_panel_name: Name of the Qt widget at the root of a Shotgun app panel. + """ + + from maya.OpenMayaUI import MQtUtil + + # In the context of this function, we know that we are running in Maya 2017 and later + # with the newer versions of PySide and shiboken. + from PySide2 import QtWidgets + from shiboken2 import wrapInstance + + import sgtk.platform + + # Retrieve the Maya engine. + engine = sgtk.platform.current_engine() + + # Retrieve the calling Maya workspace control. + ptr = MQtUtil.getCurrentParent() + workspace_control = wrapInstance(long(ptr), QtWidgets.QWidget) + + # Search for the Shotgun app panel widget. + for widget in QtWidgets.QApplication.allWidgets(): + if widget.objectName() == shotgun_panel_name: + + maya_panel_name = workspace_control.objectName() + + engine.logger.debug("Reparenting Shotgun app panel %s under Maya workspace panel %s.", + shotgun_panel_name, maya_panel_name) + + # When possible, give a minimum width to the workspace control; + # otherwise, it will use the width of the currently displayed tab. + # Note that we did not use the workspace control "initialWidth" and "minimumWidth" + # to set the minimum width to the initial width since these values are not + # properly saved by Maya 2017 in its layout preference files. + # This minimum width behaviour is consistent with Maya standard panels. + size_hint = widget.sizeHint() + if size_hint.isValid(): + # Use the widget recommended width as the workspace control minimum width. + minimum_width = size_hint.width() + engine.logger.debug("Setting Maya workspace panel %s minimum width to %s.", + maya_panel_name, minimum_width) + workspace_control.setMinimumWidth(minimum_width) + else: + # The widget has no recommended size. + engine.logger.debug("Cannot set Maya workspace panel %s minimum width.", maya_panel_name) + + # Reparent the Shotgun app panel widget under Maya workspace control. + widget.setParent(workspace_control) + + # Add the Shotgun app panel widget to the Maya workspace control layout. + workspace_control.layout().addWidget(widget) + + # Install an event filter on Maya workspace control to monitor + # its close event in order to reparent the Shotgun app panel widget + # under Maya main window for later use. + engine.logger.debug("Installing a close event filter on Maya workspace panel %s.", maya_panel_name) + panel_util.install_event_filter_by_widget(workspace_control, shotgun_panel_name) + + # Delete any leftover workspace control state to avoid a spurious deletion + # of our workspace control when the user switches to another workspace and back. + if cmds.workspaceControlState(maya_panel_name, exists=True): + # Once Maya will have completed its UI update and be idle, + # delete the leftover workspace control state. + engine.logger.debug("Deleting leftover Maya workspace control state %s.", maya_panel_name) + maya.utils.executeDeferred(cmds.workspaceControlState, maya_panel_name, remove=True) + + break + else: + # The Shotgun app panel widget was not found and needs to be recreated. + + # Search for the Shotgun app panel that needs to be restored + # among the panels registered with the engine. + for panel_id in engine.panels: + + # The name of the Qt widget at the root of the Shotgun app panel + # was constructed by prepending to the panel unique identifier. + if shotgun_panel_name.endswith(panel_id): + + # Once Maya will have completed its UI update and be idle, + # recreate and dock the Shotgun app panel. + maya.utils.executeDeferred(engine.panels[panel_id]["callback"]) + + break + else: + # The Shotgun app panel that needs to be restored is not in the context configuration. + engine.logger.error("Cannot restore %s: Shotgun app panel not found. " \ + "Make sure the app is in the context configuration. ", shotgun_panel_name) diff --git a/python/tk_maya/panel_util.py b/python/tk_maya/panel_util.py new file mode 100644 index 0000000..d8e9cda --- /dev/null +++ b/python/tk_maya/panel_util.py @@ -0,0 +1,151 @@ +# Copyright (c) 2015 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +""" +Panel support utilities for Maya +""" + +import maya.mel as mel +import maya.OpenMayaUI as OpenMayaUI + +from sgtk.platform.qt import QtCore, QtGui + +try: + import shiboken2 as shiboken +except ImportError: + import shiboken + + +def install_event_filter_by_name(maya_panel_name, shotgun_panel_name): + """ + Retreives a Maya panel widget using its name and installs an event filter on it + to monitor some of its events in order to gracefully handle refresh, close and + deallocation of the embedded Shotgun app panel widget. + + :param maya_panel_name: Name of the Qt widget of a Maya panel. + :param shotgun_panel_name: Name of the Qt widget at the root of a Shotgun app panel. + """ + + maya_panel = _find_widget(maya_panel_name) + + if maya_panel: + install_event_filter_by_widget(maya_panel, shotgun_panel_name) + +def install_event_filter_by_widget(maya_panel, shotgun_panel_name): + """ + Installs an event filter on a Maya panel widget to monitor some of its events in order + to gracefully handle refresh, close and deallocation of the embedded Shotgun app panel widget. + + :param maya_panel: Qt widget of a Maya panel. + :param shotgun_panel_name: Name of the Qt widget at the root of a Shotgun app panel. + """ + + filter = CloseEventFilter(maya_panel) + filter.set_associated_widget(shotgun_panel_name) + filter.parent_dirty.connect(_on_parent_refresh_callback) + filter.parent_closed.connect(_on_parent_closed_callback) + + maya_panel.installEventFilter(filter) + +def _find_widget(widget_name): + """ + Given a name, return the first corresponding + QT widget that is found. + + :param widget_name: QT object name to look for + :returns: QWidget object or None if nothing was found + """ + for widget in QtGui.QApplication.allWidgets(): + if widget.objectName() == widget_name: + return widget + return None + +def _on_parent_closed_callback(widget_id): + """ + Callback which fires when a panel is closed. + This will locate the widget with the given id + and close and delete this. + + :param widget_id: Object name of widget to close + """ + widget = _find_widget(widget_id) + if widget: + # Use the proper close logic according to the Maya version. + if mel.eval("getApplicationVersionAsFloat()") < 2017: + # Close and delete the Shotgun app panel widget. + # It needs to be deleted later since we are inside a slot. + widget.close() + widget.deleteLater() + else: # Maya 2017 and later + # Reparent the Shotgun app panel widget under Maya main window for later use. + ptr = OpenMayaUI.MQtUtil.mainWindow() + main_window = shiboken.wrapInstance(long(ptr), QtGui.QMainWindow) + widget.setParent(main_window) + +def _on_parent_refresh_callback(widget_id): + """ + Callback which fires when a UI refresh is needed. + + :param widget_id: Object name of widget to refresh + """ + widget = _find_widget(widget_id) + if widget: + # this is a pretty blunt tool, but right now I cannot + # come up with a better solution - it seems the internal + # window parenting in maya is a little off - and/or I am + # not parenting up the QT widgets correctly, and I think + # this is the reason the UI refresh isn't working correctly. + # the only way to ensure a fully refreshed UI is to repaint + # the entire window. + widget.window().update() + +class CloseEventFilter(QtCore.QObject): + """ + Event filter which emits a parent_closed signal whenever + the monitored widget closes. + """ + parent_closed = QtCore.Signal(str) + parent_dirty = QtCore.Signal(str) + + def set_associated_widget(self, widget_id): + """ + Set the widget that should be closed + + :param widget_id: Object name of widget to close + """ + self._widget_id = widget_id + + def eventFilter(self, obj, event): + """ + QT Event filter callback + + :param obj: The object where the event originated from + :param event: The actual event object + :returns: True if event was consumed, False if not + """ + # peek at the message + if event.type() == QtCore.QEvent.Close: + # make sure the associated widget is still a descendant of the object + parent = _find_widget(self._widget_id) + while parent: + if parent == obj: + # re-broadcast the close event + self.parent_closed.emit(self._widget_id) + break + parent = parent.parent() + + if event.type() == QtCore.QEvent.LayoutRequest: + # this event seems to be fairly representatative + # (without too many false positives) of when a tab + # needs to trigger a UI redraw of content + self.parent_dirty.emit(self._widget_id) + + # pass it on! + return False diff --git a/software_credits b/software_credits new file mode 100644 index 0000000..1279b6f --- /dev/null +++ b/software_credits @@ -0,0 +1,65 @@ +The following licenses and copyright notices apply to various components +of the Shotgun Pipeline Toolkit Maya Engine as outlined below. + +=== PySide (http://qt-project.org/wiki/About-PySide) ======================= + +PySide is licensed under the LGPL version 2.1 license. + + +=== Shiboken (https://github.com/PySide/Shiboken) ========================== + +PySide is licensed under the LGPL version 2.1 license. + + +=== OpenSSL (https://www.openssl.org) ========================== + +Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +3. All advertising materials mentioning features or use of this + software must display the following acknowledgment: + "This product includes software developed by the OpenSSL Project + for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + +4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + endorse or promote products derived from this software without + prior written permission. For written permission, please contact + openssl-core@openssl.org. + +5. Products derived from this software may not be called "OpenSSL" + nor may "OpenSSL" appear in their names without prior written + permission of the OpenSSL Project. + +6. Redistributions of any form whatsoever must retain the following + acknowledgment: + "This product includes software developed by the OpenSSL Project + for use in the OpenSSL Toolkit (http://www.openssl.org/)" + +THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY +EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR +ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +OF THE POSSIBILITY OF SUCH DAMAGE. +==================================================================== + + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com) diff --git a/startup.py b/startup.py new file mode 100644 index 0000000..b3c65ae --- /dev/null +++ b/startup.py @@ -0,0 +1,258 @@ +# Copyright (c) 2016 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import os +import sys + +import sgtk +from sgtk.platform import SoftwareLauncher, SoftwareVersion, LaunchInformation + + +class MayaLauncher(SoftwareLauncher): + """ + Handles launching Maya executables. Automatically starts up + a tk-maya engine with the current context in the new session + of Maya. + """ + + # Named regex strings to insert into the executable template paths when + # matching against supplied versions and products. Similar to the glob + # strings, these allow us to alter the regex matching for any of the + # variable components of the path in one place + COMPONENT_REGEX_LOOKUP = { + "version": "[\d.]+", + "mach": "x\d+" + } + + # This dictionary defines a list of executable template strings for each + # of the supported operating systems. The templates are used for both + # globbing and regex matches by replacing the named format placeholders + # with an appropriate glob or regex string. As Side FX adds modifies the + # install path on a given OS for a new release, a new template will need + # to be added here. + EXECUTABLE_TEMPLATES = { + "darwin": [ + # /Applications/Autodesk/maya2015/Maya.app + "/Applications/Autodesk/maya{version}/Maya.app", + ], + "win32": [ + # C:/Program Files/Autodesk/Maya2015/bin/maya.exe + "C:/Program Files/Autodesk/Maya{version}/bin/maya.exe", + ], + "linux2": [ + # /usr/autodesk/maya2016/bin/maya + "/usr/autodesk/maya{version}/bin/maya", + "/usr/autodesk/maya{version}-{mach}/bin/maya", + ] + } + + @property + def minimum_supported_version(self): + """ + The minimum software version that is supported by the launcher. + """ + return "2014" + + def prepare_launch(self, exec_path, args, file_to_open=None): + """ + Prepares an environment to launch Maya in that will automatically + load Toolkit and the tk-maya engine when Maya starts. + + :param str exec_path: Path to Maya executable to launch. + :param str args: Command line arguments as strings. + :param str file_to_open: (optional) Full path name of a file to open on launch. + :returns: :class:`LaunchInformation` instance + """ + required_env = {} + + # Run the engine's userSetup.py file when Maya starts up + # by appending it to the env PYTHONPATH. + startup_path = os.path.join(self.disk_location, "startup") + sgtk.util.append_path_to_env_var("PYTHONPATH", startup_path) + required_env["PYTHONPATH"] = os.environ["PYTHONPATH"] + + # Check the engine settings to see whether any plugins have been + # specified to load. + find_plugins = self.get_setting("launch_builtin_plugins") + if find_plugins: + # Parse the specified comma-separated list of plugins + self.logger.debug( + "Plugins found from 'launch_builtin_plugins': %s" % find_plugins + ) + + # Keep track of the specific list of Toolkit plugins to load when + # launching Maya. This list is passed through the environment and + # used by the startup/userSetup.py file. + load_maya_plugins = [] + + # Add Toolkit plugins to load to the MAYA_MODULE_PATH environment + # variable so the Maya loadPlugin command can find them. + maya_module_paths = os.environ.get("MAYA_MODULE_PATH") or [] + if maya_module_paths: + maya_module_paths = maya_module_paths.split(os.pathsep) + + for find_plugin in find_plugins: + load_plugin = os.path.join( + self.disk_location, "plugins", find_plugin + ) + if os.path.exists(load_plugin): + # If the plugin path exists, add it to the list of MAYA_MODULE_PATHS + # so Maya can find it and to the list of SGTK_LOAD_MAYA_PLUGINS so + # the startup's userSetup.py file knows what plugins to load. + self.logger.debug("Preparing to launch builtin plugin '%s'" % load_plugin) + load_maya_plugins.append(load_plugin) + if load_plugin not in maya_module_paths: + # Insert at beginning of list to give priority to toolkit plugins + # launched from the desktop app over standalone ones whose + # path is already part of the MAYA_MODULE_PATH env var + maya_module_paths.insert(0, load_plugin) + else: + # Report the missing plugin directory + self.logger.warning( + "Resolved plugin path '%s' does not exist!" % + load_plugin + ) + + # Add MAYA_MODULE_PATH and SGTK_LOAD_MAYA_PLUGINS to the launch + # environment. + required_env["MAYA_MODULE_PATH"] = os.pathsep.join(maya_module_paths) + required_env["SGTK_LOAD_MAYA_PLUGINS"] = os.pathsep.join(load_maya_plugins) + + # Add context and site info + std_env = self.get_standard_plugin_environment() + required_env.update(std_env) + + else: + # Prepare the launch environment with variables required by the + # classic bootstrap approach. + self.logger.debug("Preparing Maya Launch via Toolkit Classic methodology ...") + required_env["SGTK_ENGINE"] = self.engine_name + required_env["SGTK_CONTEXT"] = sgtk.context.serialize(self.context) + + if file_to_open: + # Add the file name to open to the launch environment + required_env["SGTK_FILE_TO_OPEN"] = file_to_open + + return LaunchInformation(exec_path, args, required_env) + + ########################################################################################## + # private methods + + def _icon_from_executable(self, exec_path): + """ + Find the application icon based on the executable path and + current platform. + + :param exec_path: Full path to the executable. + + :returns: Full path to application icon as a string or None. + """ + + # the engine icon in case we need to use it as a fallback + engine_icon = os.path.join(self.disk_location, "icon_256.png") + + self.logger.debug( + "Looking for Application icon for executable '%s' ..." % + exec_path + ) + icon_base_path = "" + if sys.platform == "darwin" and "Maya.app" in exec_path: + # e.g. /Applications/Autodesk/maya2016.5/Maya.app/Contents + icon_base_path = os.path.join( + "".join(exec_path.partition("Maya.app")[0:2]), + "Contents" + ) + + elif sys.platform in ["win32", "linux2"] and "bin" in exec_path: + # e.g. C:\Program Files\Autodesk\Maya2017\ or + # /usr/autodesk/maya2017/ + icon_base_path = "".join(exec_path.partition("bin")[0:1]) + + if not icon_base_path: + # use the bundled engine icon + self.logger.debug("Couldn't find bundled icon. Using engine icon.") + return engine_icon + + # Append the standard icon to the base path and + # return that path if it exists, else None. + icon_path = os.path.join(icon_base_path, "icons", "mayaico.png") + if not os.path.exists(icon_path): + self.logger.debug( + "Icon path '%s' resolved from executable '%s' does not exist!" + "Falling back on engine icon." % (icon_path, exec_path) + ) + return engine_icon + + # Record what the resolved icon path was. + self.logger.debug( + "Resolved icon path '%s' from input executable '%s'." % + (icon_path, exec_path) + ) + return icon_path + + def scan_software(self): + """ + Scan the filesystem for maya executables. + + :return: A list of :class:`SoftwareVersion` objects. + """ + + self.logger.debug("Scanning for Maya executables...") + + supported_sw_versions = [] + for sw_version in self._find_software(): + (supported, reason) = self._is_supported(sw_version) + if supported: + supported_sw_versions.append(sw_version) + else: + self.logger.debug( + "SoftwareVersion %s is not supported: %s" % + (sw_version, reason) + ) + + return supported_sw_versions + + def _find_software(self): + """ + Find executables in the default install locations. + """ + + # all the executable templates for the current OS + executable_templates = self.EXECUTABLE_TEMPLATES.get(sys.platform, []) + + # all the discovered executables + sw_versions = [] + + for executable_template in executable_templates: + + self.logger.debug("Processing template %s.", executable_template) + + executable_matches = self._glob_and_match( + executable_template, + self.COMPONENT_REGEX_LOOKUP + ) + + # Extract all products from that executable. + for (executable_path, key_dict) in executable_matches: + + # extract the matched keys form the key_dict (default to None if + # not included) + executable_version = key_dict.get("version") + + sw_versions.append( + SoftwareVersion( + executable_version, + "Maya", + executable_path, + self._icon_from_executable(executable_path) + ) + ) + + return sw_versions diff --git a/startup/userSetup.py b/startup/userSetup.py new file mode 100644 index 0000000..6213385 --- /dev/null +++ b/startup/userSetup.py @@ -0,0 +1,177 @@ +# Copyright (c) 2016 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +""" +This file is loaded automatically by Maya at startup +It sets up the Toolkit context and prepares the tk-maya engine. +""" + +import os +import maya.OpenMaya as OpenMaya +import maya.cmds as cmds +import sys + +print "-----------CFA SHOTGUN PIPELINE------------." +# add custom env +env_path = r'P:\cfa_tools\cfa_plug-ins\cfa_tools' +sys.path.append(env_path) +import env_parse +env_parse.addCfaPath() +def start_toolkit_classic(): + """ + Parse enviornment variables for an engine name and + serialized Context to use to startup Toolkit and + the tk-maya engine and environment. + """ + import sgtk + logger = sgtk.LogManager.get_logger(__name__) + + logger.debug("Launching toolkit in classic mode.") + + # Get the name of the engine to start from the environement + env_engine = os.environ.get("SGTK_ENGINE") + if not env_engine: + OpenMaya.MGlobal.displayError( + "Shotgun: Missing required environment variable SGTK_ENGINE." + ) + return + + # Get the context load from the environment. + env_context = os.environ.get("SGTK_CONTEXT") + if not env_context: + OpenMaya.MGlobal.displayError( + "Shotgun: Missing required environment variable SGTK_CONTEXT." + ) + return + try: + # Deserialize the environment context + context = sgtk.context.deserialize(env_context) + except Exception, e: + OpenMaya.MGlobal.displayError( + "Shotgun: Could not create context! Shotgun Pipeline Toolkit will " + "be disabled. Details: %s" % e + ) + return + + try: + # Start up the toolkit engine from the environment data + logger.debug("Launching engine instance '%s' for context %s" % (env_engine, env_context)) + engine = sgtk.platform.start_engine(env_engine, context.sgtk, context) + except Exception, e: + OpenMaya.MGlobal.displayError( + "Shotgun: Could not start engine: %s" % e + ) + return + + +def start_toolkit_with_plugins(): + """ + Parse environment variables for a list of plugins to load that will + ultimately startup Toolkit and the tk-maya engine and environment. + """ + import sgtk + logger = sgtk.LogManager.get_logger(__name__) + + logger.debug("Launching maya in plugin mode") + + for plugin_path in os.environ["SGTK_LOAD_MAYA_PLUGINS"].split(os.pathsep): + # Find the appropriate "plugin" sub directory. Maya will not be + # able to find any plugins under the base directory without this. + if os.path.isdir(os.path.join(plugin_path, "plug-ins")): + load_path = os.path.join(plugin_path, "plug-ins") + elif os.path.isdir(os.path.join(plugin_path, "plugins")): + load_path = os.path.join(plugin_path, "plugins") + else: + load_path = plugin_path + + # Load the plugins from the resolved path individually, as the + # loadPlugin Maya command has difficulties loading all (*) plugins + # from a path that contains a string in the form of 'v#.#.#': + # loadPlugin "/shotgun/site/project/install/app_store/tk-maya/v0.7.10/plugins/basic/plug-ins/*"; + # // Error: line 1: Plug-in, "/shotgun/site/project/install/app_store/tk-maya/v0.7.10/plugins/basic/plug-ins/*", was not found on MAYA_PLUG_IN_PATH. // + # loadPlugin "/shotgun/site/project/install/app_store/tk-maya-no_version/plugins/basic/plug-ins/*"; + # // Result: shotgun // + for plugin_filename in os.listdir(load_path): + if not plugin_filename.endswith(".py"): + # Skip files/directories that are not plugins + continue + + # Construct the OS agnostic full path to the plugin + # and attempt to load the plugin. Note that the loadPlugin + # command always returns a list, even when loading a single plugin. + full_plugin_path = os.path.join(load_path, plugin_filename) + logger.debug("Loading plugin %s" % full_plugin_path) + + loaded_plugins = cmds.loadPlugin(full_plugin_path) + # note: loadPlugin returns a list of the loaded plugins + if not loaded_plugins: + OpenMaya.MGlobal.displayWarning( + "Shotgun: Could not load plugin: %s" % full_plugin_path + ) + continue + + +def start_toolkit(): + """ + Import Toolkit and start up a tk-maya engine based on + environment variables. + """ + + # Verify sgtk can be loaded. + try: + import sgtk + except Exception, e: + OpenMaya.MGlobal.displayError( + "Shotgun: Could not import sgtk! Disabling for now: %s" % e + ) + return + + # start up toolkit logging to file + sgtk.LogManager().initialize_base_file_handler("tk-maya") + + if os.environ.get("SGTK_LOAD_MAYA_PLUGINS"): + # Plugins will take care of initalizing everything + start_toolkit_with_plugins() + else: + # Rely on the classic boostrapping method + start_toolkit_classic() + + # Check if a file was specified to open and open it. + file_to_open = os.environ.get("SGTK_FILE_TO_OPEN") + if file_to_open: + OpenMaya.MGlobal.displayInfo( + "Shotgun: Opening '%s'..." % file_to_open + ) + cmds.file(file_to_open, force=True, open=True) + + # Clean up temp env variables. + del_vars = [ + "SGTK_ENGINE", "SGTK_CONTEXT", "SGTK_FILE_TO_OPEN", + "SGTK_LOAD_MAYA_PLUGINS", + ] + for var in del_vars: + if var in os.environ: + del os.environ[var] + + # add custom maya menu + import menuParse + menuParse.main() + autoLoadPlugin() +def autoLoadPlugin(): + # AbcExport.mll + # AbcImport.mll + print "auto load plugin..." + load_plugin = ["AbcExport.mll","AbcImport.mll"] + for plugin in load_plugin: + if not cmds.pluginInfo(plugin,q = True,l = True): + cmds.loadPlugin(plugin) + +# Fire up Toolkit and the environment engine when there's time. +cmds.evalDeferred("start_toolkit()") diff --git a/tk-metadata/install_complete b/tk-metadata/install_complete new file mode 100644 index 0000000..e69de29