Merge pull request #747 from pypeclub/feature/launch_hooks

Application launch hooks
This commit is contained in:
Milan Kolar 2020-12-01 23:09:14 +01:00 committed by GitHub
commit 650ffb5e04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 2204 additions and 647 deletions

View file

@ -0,0 +1,45 @@
import os
from pype.lib import PreLaunchHook
class AfterEffectsPrelaunchHook(PreLaunchHook):
"""Launch arguments preparation.
Hook add python executable and execute python script of AfterEffects
implementation before AfterEffects executable.
"""
app_groups = ["aftereffects"]
def execute(self):
# Pop tvpaint executable
aftereffects_executable = self.launch_context.launch_args.pop(0)
# Pop rest of launch arguments - There should not be other arguments!
remainders = []
while self.launch_context.launch_args:
remainders.append(self.launch_context.launch_args.pop(0))
new_launch_args = [
self.python_executable(),
"-c",
(
"import avalon.aftereffects;"
"avalon.aftereffects.launch(\"{}\")"
).format(aftereffects_executable)
]
# Append as whole list as these areguments should not be separated
self.launch_context.launch_args.append(new_launch_args)
if remainders:
self.log.warning((
"There are unexpected launch arguments "
"in AfterEffects launch. {}"
).format(str(remainders)))
self.launch_context.launch_args.extend(remainders)
def python_executable(self):
"""Should lead to python executable."""
# TODO change in Pype 3
return os.environ["PYPE_PYTHON_EXE"]

View file

@ -0,0 +1,127 @@
import os
import shutil
import winreg
from pype.lib import PreLaunchHook
from pype.hosts import celaction
class CelactionPrelaunchHook(PreLaunchHook):
"""
This hook will check if current workfile path has Unreal
project inside. IF not, it initialize it and finally it pass
path to the project by environment variable to Unreal launcher
shell script.
"""
workfile_ext = "scn"
app_groups = ["celaction"]
platforms = ["windows"]
def execute(self):
# Add workfile path to launch arguments
workfile_path = self.workfile_path()
if workfile_path:
self.launch_context.launch_args.append(
"\"{}\"".format(workfile_path)
)
project_name = self.data["project_name"]
asset_name = self.data["asset_name"]
task_name = self.data["task_name"]
# get publish version of celaction
app = "celaction_publish"
# setting output parameters
path = r"Software\CelAction\CelAction2D\User Settings"
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path)
hKey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
"Software\\CelAction\\CelAction2D\\User Settings", 0,
winreg.KEY_ALL_ACCESS)
# TODO: change to root path and pyblish standalone to premiere way
pype_root_path = os.getenv("PYPE_SETUP_PATH")
path = os.path.join(pype_root_path, "pype.bat")
winreg.SetValueEx(hKey, "SubmitAppTitle", 0, winreg.REG_SZ, path)
parameters = [
"launch",
f"--app {app}",
f"--project {project_name}",
f"--asset {asset_name}",
f"--task {task_name}",
"--currentFile \\\"\"*SCENE*\"\\\"",
"--chunk 10",
"--frameStart *START*",
"--frameEnd *END*",
"--resolutionWidth *X*",
"--resolutionHeight *Y*",
# "--programDir \"'*PROGPATH*'\""
]
winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, winreg.REG_SZ,
" ".join(parameters))
# setting resolution parameters
path = r"Software\CelAction\CelAction2D\User Settings\Dialogs"
path += r"\SubmitOutput"
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path)
hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0,
winreg.KEY_ALL_ACCESS)
winreg.SetValueEx(hKey, "SaveScene", 0, winreg.REG_DWORD, 1)
winreg.SetValueEx(hKey, "CustomX", 0, winreg.REG_DWORD, 1920)
winreg.SetValueEx(hKey, "CustomY", 0, winreg.REG_DWORD, 1080)
# making sure message dialogs don't appear when overwriting
path = r"Software\CelAction\CelAction2D\User Settings\Messages"
path += r"\OverwriteScene"
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path)
hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0,
winreg.KEY_ALL_ACCESS)
winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 6)
winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1)
path = r"Software\CelAction\CelAction2D\User Settings\Messages"
path += r"\SceneSaved"
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path)
hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0,
winreg.KEY_ALL_ACCESS)
winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 1)
winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1)
def workfile_path(self):
workfile_path = self.data["last_workfile_path"]
# copy workfile from template if doesnt exist any on path
if not os.path.exists(workfile_path):
# TODO add ability to set different template workfile path via
# settings
pype_celaction_dir = os.path.dirname(
os.path.abspath(celaction.__file__)
)
template_path = os.path.join(
pype_celaction_dir,
"celaction_template_scene.scn"
)
if not os.path.exists(template_path):
self.log.warning(
"Couldn't find workfile template file in {}".format(
template_path
)
)
return
self.log.info(
f"Creating workfile from template: \"{template_path}\""
)
# Copy template workfile to new destinantion
shutil.copy2(
os.path.normpath(template_path),
os.path.normpath(workfile_path)
)
self.log.info(f"Workfile to open: \"{workfile_path}\"")
return workfile_path

View file

@ -0,0 +1,50 @@
import os
import importlib
from pype.lib import PreLaunchHook
from pype.hosts.fusion import utils
class FusionPrelaunch(PreLaunchHook):
"""
This hook will check if current workfile path has Fusion
project inside.
"""
app_groups = ["fusion"]
def execute(self):
# making sure pyton 3.6 is installed at provided path
py36_dir = os.path.normpath(self.env.get("PYTHON36", ""))
assert os.path.isdir(py36_dir), (
"Python 3.6 is not installed at the provided folder path. Either "
"make sure the `environments\resolve.json` is having correctly "
"set `PYTHON36` or make sure Python 3.6 is installed "
f"in given path. \nPYTHON36E: `{py36_dir}`"
)
self.log.info(f"Path to Fusion Python folder: `{py36_dir}`...")
self.env["PYTHON36"] = py36_dir
# setting utility scripts dir for scripts syncing
us_dir = os.path.normpath(
self.env.get("FUSION_UTILITY_SCRIPTS_DIR", "")
)
assert os.path.isdir(us_dir), (
"Fusion utility script dir does not exists. Either make sure "
"the `environments\fusion.json` is having correctly set "
"`FUSION_UTILITY_SCRIPTS_DIR` or reinstall DaVinci Resolve. \n"
f"FUSION_UTILITY_SCRIPTS_DIR: `{us_dir}`"
)
try:
__import__("avalon.fusion")
__import__("pyblish")
except ImportError:
self.log.warning(
"pyblish: Could not load Fusion integration.",
exc_info=True
)
else:
# Resolve Setup integration
importlib.reload(utils)
utils.setup(self.env)

View file

@ -0,0 +1,184 @@
import os
import ftrack_api
from pype.api import config
from pype.lib import PostLaunchHook
class PostFtrackHook(PostLaunchHook):
order = None
def execute(self):
project_name = self.data.get("project_name")
asset_name = self.data.get("asset_name")
task_name = self.data.get("task_name")
missing_context_keys = set()
if not project_name:
missing_context_keys.add("project_name")
if not asset_name:
missing_context_keys.add("asset_name")
if not task_name:
missing_context_keys.add("task_name")
if missing_context_keys:
missing_keys_str = ", ".join([
"\"{}\"".format(key) for key in missing_context_keys
])
self.log.debug("Hook {} skipped. Missing data keys: {}".format(
self.__class__.__name__, missing_keys_str
))
return
required_keys = ("FTRACK_SERVER", "FTRACK_API_USER", "FTRACK_API_KEY")
for key in required_keys:
if not os.environ.get(key):
self.log.debug((
"Missing required environment \"{}\""
" for Ftrack after launch procedure."
).format(key))
return
try:
session = ftrack_api.Session(auto_connect_event_hub=True)
self.log.debug("Ftrack session created")
except Exception:
self.log.warning("Couldn't create Ftrack session")
return
try:
entity = self.find_ftrack_task_entity(
session, project_name, asset_name, task_name
)
if entity:
self.ftrack_status_change(session, entity, project_name)
self.start_timer(session, entity, ftrack_api)
except Exception:
self.log.warning(
"Couldn't finish Ftrack procedure.", exc_info=True
)
return
finally:
session.close()
def find_ftrack_task_entity(
self, session, project_name, asset_name, task_name
):
project_entity = session.query(
"Project where full_name is \"{}\"".format(project_name)
).first()
if not project_entity:
self.log.warning(
"Couldn't find project \"{}\" in Ftrack.".format(project_name)
)
return
potential_task_entities = session.query((
"TypedContext where parent.name is \"{}\" and project_id is \"{}\""
).format(asset_name, project_entity["id"])).all()
filtered_entities = []
for _entity in potential_task_entities:
if (
_entity.entity_type.lower() == "task"
and _entity["name"] == task_name
):
filtered_entities.append(_entity)
if not filtered_entities:
self.log.warning((
"Couldn't find task \"{}\" under parent \"{}\" in Ftrack."
).format(task_name, asset_name))
return
if len(filtered_entities) > 1:
self.log.warning((
"Found more than one task \"{}\""
" under parent \"{}\" in Ftrack."
).format(task_name, asset_name))
return
return filtered_entities[0]
def ftrack_status_change(self, session, entity, project_name):
# TODO use settings
presets = config.get_presets(project_name)["ftrack"]["ftrack_config"]
statuses = presets.get("status_update")
if not statuses:
return
actual_status = entity["status"]["name"].lower()
already_tested = set()
ent_path = "/".join(
[ent["name"] for ent in entity["link"]]
)
while True:
next_status_name = None
for key, value in statuses.items():
if key in already_tested:
continue
if actual_status in value or "_any_" in value:
if key != "_ignore_":
next_status_name = key
already_tested.add(key)
break
already_tested.add(key)
if next_status_name is None:
break
try:
query = "Status where name is \"{}\"".format(
next_status_name
)
status = session.query(query).one()
entity["status"] = status
session.commit()
self.log.debug("Changing status to \"{}\" <{}>".format(
next_status_name, ent_path
))
break
except Exception:
session.rollback()
msg = (
"Status \"{}\" in presets wasn't found"
" on Ftrack entity type \"{}\""
).format(next_status_name, entity.entity_type)
self.log.warning(msg)
def start_timer(self, session, entity, _ftrack_api):
"""Start Ftrack timer on task from context."""
self.log.debug("Triggering timer start.")
user_entity = session.query("User where username is \"{}\"".format(
os.environ["FTRACK_API_USER"]
)).first()
if not user_entity:
self.log.warning(
"Couldn't find user with username \"{}\" in Ftrack".format(
os.environ["FTRACK_API_USER"]
)
)
return
source = {
"user": {
"id": user_entity["id"],
"username": user_entity["username"]
}
}
event_data = {
"actionIdentifier": "start.timer",
"selection": [{"entityId": entity["id"], "entityType": "task"}]
}
session.event_hub.publish(
_ftrack_api.event.base.Event(
topic="ftrack.action.launch",
data=event_data,
source=source
),
on_error="ignore"
)
self.log.debug("Timer start triggered successfully.")

View file

@ -0,0 +1,363 @@
import os
import re
import json
import getpass
import copy
from pype.api import (
Anatomy,
config
)
from pype.lib import (
env_value_to_bool,
PreLaunchHook,
ApplicationLaunchFailed
)
import acre
import avalon.api
class GlobalHostDataHook(PreLaunchHook):
order = -100
def execute(self):
"""Prepare global objects to `data` that will be used for sure."""
if not self.application.is_host:
self.log.info(
"Skipped hook {}. Application is not marked as host.".format(
self.__class__.__name__
)
)
return
self.prepare_global_data()
self.prepare_host_environments()
self.prepare_context_environments()
def prepare_global_data(self):
"""Prepare global objects to `data` that will be used for sure."""
# Mongo documents
project_name = self.data.get("project_name")
if not project_name:
self.log.info(
"Skipping global data preparation."
" Key `project_name` was not found in launch context."
)
return
self.log.debug("Project name is set to \"{}\"".format(project_name))
# Anatomy
self.data["anatomy"] = Anatomy(project_name)
# Mongo connection
dbcon = avalon.api.AvalonMongoDB()
dbcon.Session["AVALON_PROJECT"] = project_name
dbcon.install()
self.data["dbcon"] = dbcon
# Project document
project_doc = dbcon.find_one({"type": "project"})
self.data["project_doc"] = project_doc
asset_name = self.data.get("asset_name")
if not asset_name:
self.log.warning(
"Asset name was not set. Skipping asset document query."
)
return
asset_doc = dbcon.find_one({
"type": "asset",
"name": asset_name
})
self.data["asset_doc"] = asset_doc
def _merge_env(self, env, current_env):
"""Modified function(merge) from acre module."""
result = current_env.copy()
for key, value in env.items():
# Keep missing keys by not filling `missing` kwarg
value = acre.lib.partial_format(value, data=current_env)
result[key] = value
return result
def prepare_host_environments(self):
"""Modify launch environments based on launched app and context."""
# Keys for getting environments
env_keys = [self.app_group, self.app_name]
asset_doc = self.data.get("asset_doc")
if asset_doc:
# Add tools environments
for key in asset_doc["data"].get("tools_env") or []:
tool = self.manager.tools.get(key)
if tool:
if tool.group_name not in env_keys:
env_keys.append(tool.group_name)
if tool.name not in env_keys:
env_keys.append(tool.name)
self.log.debug(
"Finding environment groups for keys: {}".format(env_keys)
)
settings_env = self.data["settings_env"]
env_values = {}
for env_key in env_keys:
_env_values = settings_env.get(env_key)
if not _env_values:
continue
# Choose right platform
tool_env = acre.parse(_env_values)
# Merge dictionaries
env_values = self._merge_env(tool_env, env_values)
final_env = self._merge_env(
acre.compute(env_values), self.launch_context.env
)
# Update env
self.launch_context.env.update(final_env)
def prepare_context_environments(self):
"""Modify launch environemnts with context data for launched host."""
# Context environments
project_doc = self.data.get("project_doc")
asset_doc = self.data.get("asset_doc")
task_name = self.data.get("task_name")
if (
not project_doc
or not asset_doc
or not task_name
):
self.log.info(
"Skipping context environments preparation."
" Launch context does not contain required data."
)
return
workdir_data = self._prepare_workdir_data(
project_doc, asset_doc, task_name
)
self.data["workdir_data"] = workdir_data
hierarchy = workdir_data["hierarchy"]
anatomy = self.data["anatomy"]
try:
anatomy_filled = anatomy.format(workdir_data)
workdir = os.path.normpath(anatomy_filled["work"]["folder"])
if not os.path.exists(workdir):
self.log.debug(
"Creating workdir folder: \"{}\"".format(workdir)
)
os.makedirs(workdir)
except Exception as exc:
raise ApplicationLaunchFailed(
"Error in anatomy.format: {}".format(str(exc))
)
context_env = {
"AVALON_PROJECT": project_doc["name"],
"AVALON_ASSET": asset_doc["name"],
"AVALON_TASK": task_name,
"AVALON_APP": self.host_name,
"AVALON_APP_NAME": self.app_name,
"AVALON_HIERARCHY": hierarchy,
"AVALON_WORKDIR": workdir
}
self.log.debug(
"Context environemnts set:\n{}".format(
json.dumps(context_env, indent=4)
)
)
self.launch_context.env.update(context_env)
self.prepare_last_workfile(workdir)
def _prepare_workdir_data(self, project_doc, asset_doc, task_name):
hierarchy = "/".join(asset_doc["data"]["parents"])
data = {
"project": {
"name": project_doc["name"],
"code": project_doc["data"].get("code")
},
"task": task_name,
"asset": asset_doc["name"],
"app": self.host_name,
"hierarchy": hierarchy
}
return data
def prepare_last_workfile(self, workdir):
"""last workfile workflow preparation.
Function check if should care about last workfile workflow and tries
to find the last workfile. Both information are stored to `data` and
environments.
Last workfile is filled always (with version 1) even if any workfile
exists yet.
Args:
workdir (str): Path to folder where workfiles should be stored.
"""
_workdir_data = self.data.get("workdir_data")
if not _workdir_data:
self.log.info(
"Skipping last workfile preparation."
" Key `workdir_data` not filled."
)
return
workdir_data = copy.deepcopy(_workdir_data)
project_name = self.data["project_name"]
task_name = self.data["task_name"]
start_last_workfile = self.should_start_last_workfile(
project_name, self.host_name, task_name
)
self.data["start_last_workfile"] = start_last_workfile
# Store boolean as "0"(False) or "1"(True)
self.launch_context.env["AVALON_OPEN_LAST_WORKFILE"] = (
str(int(bool(start_last_workfile)))
)
_sub_msg = "" if start_last_workfile else " not"
self.log.debug(
"Last workfile should{} be opened on start.".format(_sub_msg)
)
# Last workfile path
last_workfile_path = ""
extensions = avalon.api.HOST_WORKFILE_EXTENSIONS.get(
self.host_name
)
if extensions:
anatomy = self.data["anatomy"]
# Find last workfile
file_template = anatomy.templates["work"]["file"]
workdir_data.update({
"version": 1,
"user": os.environ.get("PYPE_USERNAME") or getpass.getuser(),
"ext": extensions[0]
})
last_workfile_path = avalon.api.last_workfile(
workdir, file_template, workdir_data, extensions, True
)
if os.path.exists(last_workfile_path):
self.log.debug((
"Workfiles for launch context does not exists"
" yet but path will be set."
))
self.log.debug(
"Setting last workfile path: {}".format(last_workfile_path)
)
self.launch_context.env["AVALON_LAST_WORKFILE"] = last_workfile_path
self.data["last_workfile_path"] = last_workfile_path
def should_start_last_workfile(self, project_name, host_name, task_name):
"""Define if host should start last version workfile if possible.
Default output is `False`. Can be overriden with environment variable
`AVALON_OPEN_LAST_WORKFILE`, valid values without case sensitivity are
`"0", "1", "true", "false", "yes", "no"`.
Args:
project_name (str): Name of project.
host_name (str): Name of host which is launched. In avalon's
application context it's value stored in app definition under
key `"application_dir"`. Is not case sensitive.
task_name (str): Name of task which is used for launching the host.
Task name is not case sensitive.
Returns:
bool: True if host should start workfile.
"""
default_output = env_value_to_bool(
"AVALON_OPEN_LAST_WORKFILE", default=False
)
# TODO convert to settings
try:
startup_presets = (
config.get_presets(project_name)
.get("tools", {})
.get("workfiles", {})
.get("last_workfile_on_startup")
)
except Exception:
startup_presets = None
self.log.warning("Couldn't load pype's presets", exc_info=True)
if not startup_presets:
return default_output
host_name_lowered = host_name.lower()
task_name_lowered = task_name.lower()
max_points = 2
matching_points = -1
matching_item = None
for item in startup_presets:
hosts = item.get("hosts") or tuple()
tasks = item.get("tasks") or tuple()
hosts_lowered = tuple(_host_name.lower() for _host_name in hosts)
# Skip item if has set hosts and current host is not in
if hosts_lowered and host_name_lowered not in hosts_lowered:
continue
tasks_lowered = tuple(_task_name.lower() for _task_name in tasks)
# Skip item if has set tasks and current task is not in
if tasks_lowered:
task_match = False
for task_regex in self.compile_list_of_regexes(tasks_lowered):
if re.match(task_regex, task_name_lowered):
task_match = True
break
if not task_match:
continue
points = int(bool(hosts_lowered)) + int(bool(tasks_lowered))
if points > matching_points:
matching_item = item
matching_points = points
if matching_points == max_points:
break
if matching_item is not None:
output = matching_item.get("enabled")
if output is None:
output = default_output
return output
return default_output
@staticmethod
def compile_list_of_regexes(in_list):
"""Convert strings in entered list to compiled regex objects."""
regexes = list()
if not in_list:
return regexes
for item in in_list:
if item:
try:
regexes.append(re.compile(item))
except TypeError:
print((
"Invalid type \"{}\" value \"{}\"."
" Expected string based object. Skipping."
).format(str(type(item)), str(item)))
return regexes

View file

@ -0,0 +1,44 @@
import os
from pype.lib import PreLaunchHook
class HarmonyPrelaunchHook(PreLaunchHook):
"""Launch arguments preparation.
Hook add python executable and execute python script of harmony
implementation before harmony executable.
"""
app_groups = ["harmony"]
def execute(self):
# Pop tvpaint executable
harmony_executable = self.launch_context.launch_args.pop(0)
# Pop rest of launch arguments - There should not be other arguments!
remainders = []
while self.launch_context.launch_args:
remainders.append(self.launch_context.launch_args.pop(0))
new_launch_args = [
self.python_executable(),
"-c",
(
"import avalon.harmony;"
"avalon.harmony.launch(\"{}\")"
).format(harmony_executable)
]
# Append as whole list as these areguments should not be separated
self.launch_context.launch_args.append(new_launch_args)
if remainders:
self.log.warning((
"There are unexpected launch arguments in Harmony launch. {}"
).format(str(remainders)))
self.launch_context.launch_args.extend(remainders)
def python_executable(self):
"""Should lead to python executable."""
# TODO change in Pype 3
return os.environ["PYPE_PYTHON_EXE"]

View file

@ -0,0 +1,17 @@
import os
from pype.lib import PreLaunchHook
class HieroLaunchArguments(PreLaunchHook):
order = 0
app_groups = ["hiero"]
def execute(self):
"""Prepare suprocess launch arguments for Hiero."""
# Add path to workfile to arguments
if self.data.get("start_last_workfile"):
last_workfile = self.data.get("last_workfile_path")
if os.path.exists(last_workfile):
self.launch_context.launch_args.append(
"\"{}\"".format(last_workfile)
)

View file

@ -0,0 +1,18 @@
import os
from pype.lib import PreLaunchHook
class MayaLaunchArguments(PreLaunchHook):
"""Add path to last workfile to launch arguments."""
order = 0
app_groups = ["maya"]
def execute(self):
"""Prepare suprocess launch arguments for Maya."""
# Add path to workfile to arguments
if self.data.get("start_last_workfile"):
last_workfile = self.data.get("last_workfile_path")
if os.path.exists(last_workfile):
self.launch_context.launch_args.append(
"\"{}\"".format(last_workfile)
)

View file

@ -0,0 +1,17 @@
import os
from pype.lib import PreLaunchHook
class NukeStudioLaunchArguments(PreLaunchHook):
order = 0
app_groups = ["nukestudio"]
def execute(self):
"""Prepare suprocess launch arguments for NukeStudio."""
# Add path to workfile to arguments
if self.data.get("start_last_workfile"):
last_workfile = self.data.get("last_workfile_path")
if os.path.exists(last_workfile):
self.launch_context.launch_args.append(
"\"{}\"".format(last_workfile)
)

View file

@ -0,0 +1,17 @@
import os
from pype.lib import PreLaunchHook
class NukeXLaunchArguments(PreLaunchHook):
order = 0
app_groups = ["nukex"]
def execute(self):
"""Prepare suprocess launch arguments for NukeX."""
# Add path to workfile to arguments
if self.data.get("start_last_workfile"):
last_workfile = self.data.get("last_workfile_path")
if os.path.exists(last_workfile):
self.launch_context.launch_args.append(
"\"{}\"".format(last_workfile)
)

View file

@ -0,0 +1,44 @@
import os
from pype.lib import PreLaunchHook
class PhotoshopPrelaunchHook(PreLaunchHook):
"""Launch arguments preparation.
Hook add python executable and execute python script of photoshop
implementation before photoshop executable.
"""
app_groups = ["photoshop"]
def execute(self):
# Pop tvpaint executable
photoshop_executable = self.launch_context.launch_args.pop(0)
# Pop rest of launch arguments - There should not be other arguments!
remainders = []
while self.launch_context.launch_args:
remainders.append(self.launch_context.launch_args.pop(0))
new_launch_args = [
self.python_executable(),
"-c",
(
"import avalon.photoshop;"
"avalon.photoshop.launch(\"{}\")"
).format(photoshop_executable)
]
# Append as whole list as these areguments should not be separated
self.launch_context.launch_args.append(new_launch_args)
if remainders:
self.log.warning((
"There are unexpected launch arguments in Photoshop launch. {}"
).format(str(remainders)))
self.launch_context.launch_args.extend(remainders)
def python_executable(self):
"""Should lead to python executable."""
# TODO change in Pype 3
return os.environ["PYPE_PYTHON_EXE"]

View file

@ -0,0 +1,58 @@
import os
import importlib
from pype.lib import PreLaunchHook
from pype.hosts.resolve import utils
class ResolvePrelaunch(PreLaunchHook):
"""
This hook will check if current workfile path has Resolve
project inside. IF not, it initialize it and finally it pass
path to the project by environment variable to Premiere launcher
shell script.
"""
app_groups = ["resolve"]
def execute(self):
# making sure pyton 3.6 is installed at provided path
py36_dir = os.path.normpath(self.env.get("PYTHON36_RESOLVE", ""))
assert os.path.isdir(py36_dir), (
"Python 3.6 is not installed at the provided folder path. Either "
"make sure the `environments\resolve.json` is having correctly "
"set `PYTHON36_RESOLVE` or make sure Python 3.6 is installed "
f"in given path. \nPYTHON36_RESOLVE: `{py36_dir}`"
)
self.log.info(f"Path to Resolve Python folder: `{py36_dir}`...")
self.env["PYTHON36_RESOLVE"] = py36_dir
# setting utility scripts dir for scripts syncing
us_dir = os.path.normpath(
self.env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "")
)
assert os.path.isdir(us_dir), (
"Resolve utility script dir does not exists. Either make sure "
"the `environments\resolve.json` is having correctly set "
"`RESOLVE_UTILITY_SCRIPTS_DIR` or reinstall DaVinci Resolve. \n"
f"RESOLVE_UTILITY_SCRIPTS_DIR: `{us_dir}`"
)
self.log.debug(f"-- us_dir: `{us_dir}`")
# correctly format path for pre python script
pre_py_sc = os.path.normpath(self.env.get("PRE_PYTHON_SCRIPT", ""))
self.env["PRE_PYTHON_SCRIPT"] = pre_py_sc
self.log.debug(f"-- pre_py_sc: `{pre_py_sc}`...")
try:
__import__("pype.hosts.resolve")
__import__("pyblish")
except ImportError:
self.log.warning(
"pyblish: Could not load Resolve integration.",
exc_info=True
)
else:
# Resolve Setup integration
importlib.reload(utils)
self.log.debug(f"-- utils.__file__: `{utils.__file__}`")
utils.setup(self.env)

View file

@ -0,0 +1,35 @@
from pype.lib import (
PreLaunchHook,
ApplicationLaunchFailed,
_subprocess
)
class PreInstallPyWin(PreLaunchHook):
"""Hook makes sure there is installed python module pywin32 on windows."""
# WARNING This hook will probably be deprecated in Pype 3 - kept for test
order = 10
app_groups = ["tvpaint"]
platforms = ["windows"]
def execute(self):
installed = False
try:
from win32com.shell import shell
self.log.debug("Python module `pywin32` already installed.")
installed = True
except Exception:
pass
if installed:
return
try:
output = _subprocess(
["pip", "install", "pywin32==227"]
)
self.log.debug("Pip install pywin32 output:\n{}'".format(output))
except RuntimeError:
msg = "Installation of python module `pywin32` crashed."
self.log.warning(msg, exc_info=True)
raise ApplicationLaunchFailed(msg)

View file

@ -0,0 +1,105 @@
import os
import shutil
from pype.hosts import tvpaint
from pype.lib import PreLaunchHook
import avalon
class TvpaintPrelaunchHook(PreLaunchHook):
"""Launch arguments preparation.
Hook add python executable and script path to tvpaint implementation before
tvpaint executable and add last workfile path to launch arguments.
Existence of last workfile is checked. If workfile does not exists tries
to copy templated workfile from predefined path.
"""
app_groups = ["tvpaint"]
def execute(self):
# Pop tvpaint executable
tvpaint_executable = self.launch_context.launch_args.pop(0)
# Pop rest of launch arguments - There should not be other arguments!
remainders = []
while self.launch_context.launch_args:
remainders.append(self.launch_context.launch_args.pop(0))
new_launch_args = [
self.main_executable(),
self.launch_script_path(),
tvpaint_executable
]
# Add workfile to launch arguments
workfile_path = self.workfile_path()
if workfile_path:
new_launch_args.append(
"\"{}\"".format(workfile_path)
)
# How to create new command line
# if platform.system().lower() == "windows":
# new_launch_args = [
# "cmd.exe",
# "/c",
# "Call cmd.exe /k",
# *new_launch_args
# ]
# Append as whole list as these areguments should not be separated
self.launch_context.launch_args.append(new_launch_args)
if remainders:
self.log.warning((
"There are unexpected launch arguments in TVPaint launch. {}"
).format(str(remainders)))
self.launch_context.launch_args.extend(remainders)
def main_executable(self):
"""Should lead to python executable."""
# TODO change in Pype 3
return os.path.normpath(os.environ["PYPE_PYTHON_EXE"])
def launch_script_path(self):
avalon_dir = os.path.dirname(os.path.abspath(avalon.__file__))
script_path = os.path.join(
avalon_dir,
"tvpaint",
"launch_script.py"
)
return script_path
def workfile_path(self):
workfile_path = self.data["last_workfile_path"]
# copy workfile from template if doesnt exist any on path
if not os.path.exists(workfile_path):
# TODO add ability to set different template workfile path via
# settings
pype_dir = os.path.dirname(os.path.abspath(tvpaint.__file__))
template_path = os.path.join(pype_dir, "template.tvpp")
if not os.path.exists(template_path):
self.log.warning(
"Couldn't find workfile template file in {}".format(
template_path
)
)
return
self.log.info(
f"Creating workfile from template: \"{template_path}\""
)
# Copy template workfile to new destinantion
shutil.copy2(
os.path.normpath(template_path),
os.path.normpath(workfile_path)
)
self.log.info(f"Workfile to open: \"{workfile_path}\"")
return workfile_path

View file

@ -0,0 +1,95 @@
import os
from pype.lib import (
PreLaunchHook,
ApplicationLaunchFailed
)
from pype.hosts.unreal import lib as unreal_lib
class UnrealPrelaunchHook(PreLaunchHook):
"""
This hook will check if current workfile path has Unreal
project inside. IF not, it initialize it and finally it pass
path to the project by environment variable to Unreal launcher
shell script.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.signature = "( {} )".format(self.__class__.__name__)
def execute(self):
asset_name = self.data["asset_name"]
task_name = self.data["task_name"]
workdir = self.env["AVALON_WORKDIR"]
engine_version = self.app_name.split("_")[-1]
unreal_project_name = f"{asset_name}_{task_name}"
# Unreal is sensitive about project names longer then 20 chars
if len(unreal_project_name) > 20:
self.log.warning((
f"Project name exceed 20 characters ({unreal_project_name})!"
))
# Unreal doesn't accept non alphabet characters at the start
# of the project name. This is because project name is then used
# in various places inside c++ code and there variable names cannot
# start with non-alpha. We append 'P' before project name to solve it.
# 😱
if not unreal_project_name[:1].isalpha():
self.log.warning((
"Project name doesn't start with alphabet "
f"character ({unreal_project_name}). Appending 'P'"
))
unreal_project_name = f"P{unreal_project_name}"
project_path = os.path.join(workdir, unreal_project_name)
self.log.info((
f"{self.signature} requested UE4 version: "
f"[ {engine_version} ]"
))
detected = unreal_lib.get_engine_versions()
detected_str = ', '.join(detected.keys()) or 'none'
self.log.info((
f"{self.signature} detected UE4 versions: "
f"[ {detected_str} ]"
))
engine_version = ".".join(engine_version.split(".")[:2])
if engine_version not in detected.keys():
raise ApplicationLaunchFailed((
f"{self.signature} requested version not "
f"detected [ {engine_version} ]"
))
os.makedirs(project_path, exist_ok=True)
project_file = os.path.join(
project_path,
f"{unreal_project_name}.uproject"
)
if not os.path.isfile(project_file):
engine_path = detected[engine_version]
self.log.info((
f"{self.signature} creating unreal "
f"project [ {unreal_project_name} ]"
))
# Set "AVALON_UNREAL_PLUGIN" to current process environment for
# execution of `create_unreal_project`
env_key = "AVALON_UNREAL_PLUGIN"
if self.env.get(env_key):
os.environ[env_key] = self.env[env_key]
unreal_lib.create_unreal_project(
unreal_project_name,
engine_version,
project_path,
engine_path=engine_path
)
# Append project file to launch arguments
self.launch_context.launch_args.append(f"\"{project_file}\"")

View file

@ -11,6 +11,12 @@ from .env_tools import (
get_paths_from_environ
)
from .python_module_tools import (
modules_from_path,
recursive_bases_from_class,
classes_from_module
)
from .avalon_context import (
is_latest,
any_outdated,
@ -28,6 +34,8 @@ from .applications import (
ApplictionExecutableNotFound,
ApplicationNotFound,
ApplicationManager,
PreLaunchHook,
PostLaunchHook,
launch_application,
ApplicationAction,
_subprocess
@ -53,6 +61,10 @@ __all__ = [
"env_value_to_bool",
"get_paths_from_environ",
"modules_from_path",
"recursive_bases_from_class",
"classes_from_module",
"is_latest",
"any_outdated",
"get_asset",
@ -68,6 +80,8 @@ __all__ = [
"ApplictionExecutableNotFound",
"ApplicationNotFound",
"ApplicationManager",
"PreLaunchHook",
"PostLaunchHook",
"launch_application",
"ApplicationAction",

File diff suppressed because it is too large Load diff

View file

@ -20,9 +20,9 @@ def env_value_to_bool(env_key=None, value=None, default=False):
if value is not None:
value = str(value).lower()
if value in ("true", "yes", "1"):
if value in ("true", "yes", "1", "on"):
return True
elif value in ("false", "no", "0"):
elif value in ("false", "no", "0", "off"):
return False
return default

View file

@ -0,0 +1,113 @@
import os
import sys
import types
import importlib
import inspect
import logging
log = logging.getLogger(__name__)
PY3 = sys.version_info[0] == 3
def modules_from_path(folder_path):
"""Get python scripts as modules from a path.
Arguments:
path (str): Path to folder containing python scripts.
Returns:
List of modules.
"""
folder_path = os.path.normpath(folder_path)
modules = []
if not os.path.isdir(folder_path):
log.warning("Not a directory path: {}".format(folder_path))
return modules
for filename in os.listdir(folder_path):
# Ignore files which start with underscore
if filename.startswith("_"):
continue
mod_name, mod_ext = os.path.splitext(filename)
if not mod_ext == ".py":
continue
full_path = os.path.join(folder_path, filename)
if not os.path.isfile(full_path):
continue
try:
# Prepare module object where content of file will be parsed
module = types.ModuleType(mod_name)
if PY3:
# Use loader so module has full specs
module_loader = importlib.machinery.SourceFileLoader(
mod_name, full_path
)
module_loader.exec_module(module)
else:
# Execute module code and store content to module
with open(full_path) as _stream:
# Execute content and store it to module object
exec(_stream.read(), module.__dict__)
module.__file__ = full_path
modules.append(module)
except Exception:
log.warning(
"Failed to load path: \"{0}\"".format(full_path),
exc_info=True
)
continue
return modules
def recursive_bases_from_class(klass):
"""Extract all bases from entered class."""
result = []
bases = klass.__bases__
result.extend(bases)
for base in bases:
result.extend(recursive_bases_from_class(base))
return result
def classes_from_module(superclass, module):
"""Return plug-ins from module
Arguments:
superclass (superclass): Superclass of subclasses to look for
module (types.ModuleType): Imported module from which to
parse valid Avalon plug-ins.
Returns:
List of plug-ins, or empty list if none is found.
"""
classes = list()
for name in dir(module):
# It could be anything at this point
obj = getattr(module, name)
if not inspect.isclass(obj):
continue
# These are subclassed from nothing, not even `object`
if not len(obj.__bases__) > 0:
continue
# Use string comparison rather than `issubclass`
# in order to support reloading of this module.
bases = recursive_bases_from_class(obj)
if not any(base.__name__ == superclass.__name__ for base in bases):
continue
classes.append(obj)
return classes

View file

@ -17,7 +17,10 @@ from bson.errors import InvalidId
from pymongo import UpdateOne
import ftrack_api
from pype.api import config
from pype.lib import (
ApplicationManager,
env_value_to_bool
)
log = Logger().get_logger(__name__)
@ -186,12 +189,28 @@ def get_project_apps(in_app_list):
dictionary of warnings
"""
apps = []
warnings = collections.defaultdict(list)
if env_value_to_bool("PYPE_USE_APP_MANAGER", default=False):
missing_app_msg = "Missing definition of application"
application_manager = ApplicationManager()
for app_name in in_app_list:
app = application_manager.applications.get(app_name)
if app:
apps.append({
"name": app_name,
"label": app.full_label
})
else:
warnings[missing_app_msg].append(app_name)
return apps, warnings
# TODO report
missing_toml_msg = "Missing config file for application"
error_msg = (
"Unexpected error happend during preparation of application"
)
warnings = collections.defaultdict(list)
for app in in_app_list:
try:
toml_path = avalon.lib.which_app(app)

View file

@ -3,7 +3,7 @@
"enabled": true,
"label": "Autodesk Maya",
"icon": "{}/app_icons/maya.png",
"is_host": true,
"host_name": "maya",
"environment": {
"__environment_keys__": {
"maya": [
@ -39,11 +39,17 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Autodesk\\Maya2020\\bin\\maya.exe"
[
"C:\\Program Files\\Autodesk\\Maya2020\\bin\\maya.exe",
""
]
],
"darwin": [],
"linux": [
"/usr/autodesk/maya2020/bin/maya"
[
"/usr/autodesk/maya2020/bin/maya",
""
]
]
},
"environment": {
@ -62,11 +68,17 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Autodesk\\Maya2019\\bin\\maya.exe"
[
"C:\\Program Files\\Autodesk\\Maya2019\\bin\\maya.exe",
""
]
],
"darwin": [],
"linux": [
"/usr/autodesk/maya2019/bin/maya"
[
"/usr/autodesk/maya2019/bin/maya",
""
]
]
},
"environment": {
@ -85,11 +97,17 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Autodesk\\Maya2017\\bin\\maya.exe"
[
"C:\\Program Files\\Autodesk\\Maya2017\\bin\\maya.exe",
""
]
],
"darwin": [],
"linux": [
"/usr/autodesk/maya2018/bin/maya"
[
"/usr/autodesk/maya2018/bin/maya",
""
]
]
},
"environment": {
@ -107,7 +125,7 @@
"enabled": true,
"label": "Autodesk MayaBatch",
"icon": "{}/app_icons/maya.png",
"is_host": false,
"host_name": "maya",
"environment": {
"__environment_keys__": {
"mayabatch": [
@ -143,7 +161,10 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Autodesk\\Maya2020\\bin\\mayabatch.exe"
[
"C:\\Program Files\\Autodesk\\Maya2020\\bin\\mayabatch.exe",
""
]
],
"darwin": [],
"linux": []
@ -164,7 +185,10 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Autodesk\\Maya2019\\bin\\mayabatch.exe"
[
"C:\\Program Files\\Autodesk\\Maya2019\\bin\\mayabatch.exe",
""
]
],
"darwin": [],
"linux": []
@ -185,7 +209,10 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Autodesk\\Maya2018\\bin\\mayabatch.exe"
[
"C:\\Program Files\\Autodesk\\Maya2018\\bin\\mayabatch.exe",
""
]
],
"darwin": [],
"linux": []
@ -205,7 +232,7 @@
"enabled": true,
"label": "Nuke",
"icon": "{}/app_icons/nuke.png",
"is_host": true,
"host_name": "nuke",
"environment": {
"__environment_keys__": {
"nuke": [
@ -230,11 +257,17 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe"
[
"C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe",
""
]
],
"darwin": [],
"linux": [
"/usr/local/Nuke12.0v1/Nuke12.0"
[
"/usr/local/Nuke12.0v1/Nuke12.0",
""
]
]
},
"environment": {
@ -250,11 +283,17 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe"
[
"C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe",
""
]
],
"darwin": [],
"linux": [
"/usr/local/Nuke11.3v5/Nuke11.3"
[
"/usr/local/Nuke11.3v5/Nuke11.3",
""
]
]
},
"environment": {
@ -270,7 +309,10 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe"
[
"C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe",
""
]
],
"darwin": [],
"linux": []
@ -287,7 +329,7 @@
"enabled": true,
"label": "Nuke X",
"icon": "{}/app_icons/nuke.png",
"is_host": true,
"host_name": "nuke",
"environment": {
"__environment_keys__": {
"nukex": [
@ -312,11 +354,17 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe"
[
"C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe",
"--nukex"
]
],
"darwin": [],
"linux": [
"/usr/local/Nuke12.0v1/Nuke12.0"
[
"/usr/local/Nuke12.0v1/Nuke12.0",
"--nukex"
]
]
},
"environment": {
@ -332,11 +380,17 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe"
[
"C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe",
"--nukex"
]
],
"darwin": [],
"linux": [
"/usr/local/Nuke11.3v5/Nuke11.3"
[
"/usr/local/Nuke11.3v5/Nuke11.3",
"--nukex"
]
]
},
"environment": {
@ -352,7 +406,10 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe"
[
"C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe",
"--nukex"
]
],
"darwin": [],
"linux": []
@ -369,7 +426,7 @@
"enabled": true,
"label": "Nuke Studio",
"icon": "{}/app_icons/nuke.png",
"is_host": true,
"host_name": "hiero",
"environment": {
"__environment_keys__": {
"nukestudio": [
@ -398,11 +455,17 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe"
[
"C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe",
"--studio"
]
],
"darwin": [],
"linux": [
"/usr/local/Nuke12.0v1/Nuke12.0"
[
"/usr/local/Nuke12.0v1/Nuke12.0",
"--studio"
]
]
},
"environment": {
@ -418,11 +481,17 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe"
[
"C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe",
"--studio"
]
],
"darwin": [],
"linux": [
"/usr/local/Nuke11.3v5/Nuke11.3"
[
"/usr/local/Nuke11.3v5/Nuke11.3",
"--studio"
]
]
},
"environment": {
@ -453,7 +522,7 @@
"enabled": true,
"label": "Hiero",
"icon": "{}/app_icons/hiero.png",
"is_host": true,
"host_name": "hiero",
"environment": {
"__environment_keys__": {
"hiero": [
@ -482,11 +551,17 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe"
[
"C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe",
"--hiero"
]
],
"darwin": [],
"linux": [
"/usr/local/Nuke12.0v1/Nuke12.0"
[
"/usr/local/Nuke12.0v1/Nuke12.0",
"--hiero"
]
]
},
"environment": {
@ -502,11 +577,17 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe"
[
"C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe",
"--hiero"
]
],
"darwin": [],
"linux": [
"/usr/local/Nuke11.3v5/Nuke11.3"
[
"/usr/local/Nuke11.3v5/Nuke11.3",
"--hiero"
]
]
},
"environment": {
@ -522,7 +603,10 @@
"icon": "",
"executables": {
"windows": [
"C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe"
[
"C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe",
"--hiero"
]
],
"darwin": [],
"linux": []
@ -539,7 +623,7 @@
"enabled": true,
"label": "BlackMagic Fusion",
"icon": "{}/app_icons/fusion.png",
"is_host": true,
"host_name": "fusion",
"environment": {
"__environment_keys__": {
"fusion": []
@ -584,7 +668,7 @@
"enabled": true,
"label": "Blackmagic DaVinci Resolve",
"icon": "{}/app_icons/resolve.png",
"is_host": true,
"host_name": "resolve",
"environment": {
"__environment_keys__": {
"resolve": [
@ -662,7 +746,7 @@
"enabled": true,
"label": "SideFX Houdini",
"icon": "{}/app_icons/houdini.png",
"is_host": true,
"host_name": "houdini",
"environment": {
"__environment_keys__": {
"houdini": [
@ -720,7 +804,7 @@
"enabled": true,
"label": "Blender",
"icon": "{}/app_icons/blender.png",
"is_host": true,
"host_name": "blender",
"environment": {
"__environment_keys__": {
"blender": [
@ -743,7 +827,12 @@
"variant_label": "2.90",
"icon": "",
"executables": {
"windows": [],
"windows": [
[
"C:\\Program Files\\Blender Foundation\\Blender 2.90\\blender.exe",
""
]
],
"darwin": [],
"linux": []
},
@ -759,7 +848,12 @@
"variant_label": "2.83",
"icon": "",
"executables": {
"windows": [],
"windows": [
[
"C:\\Program Files\\Blender Foundation\\Blender 2.83\\blender.exe",
""
]
],
"darwin": [],
"linux": []
},
@ -775,7 +869,7 @@
"enabled": true,
"label": "Toon Boom Harmony",
"icon": "{}/app_icons/harmony.png",
"is_host": true,
"host_name": "harmony",
"environment": {
"__environment_keys__": {
"harmony": [
@ -843,7 +937,10 @@
"executables": {
"windows": [],
"darwin": [
"/Applications/Toon Boom Harmony 17 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium"
[
"/Applications/Toon Boom Harmony 17 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium",
""
]
],
"linux": []
},
@ -855,11 +952,69 @@
}
}
},
"tvpaint": {
"enabled": true,
"label": "TVPaint",
"icon": "{}/app_icons/tvpaint.png",
"host_name": "tvpaint",
"environment": {
"__environment_keys__": {
"tvpaint": [
"PYPE_LOG_NO_COLORS"
]
},
"PYPE_LOG_NO_COLORS": "True"
},
"variants": {
"tvpaint_Animation 11 (64bits)": {
"enabled": true,
"label": "",
"variant_label": "Animation 11 (64bits)",
"icon": "",
"executables": {
"windows": [
[
"C:\\Program Files\\TVPaint Developpement\\TVPaint Animation 11 (64bits)\\TVPaint Animation 11 (64bits).exe",
""
]
],
"darwin": [],
"linux": []
},
"environment": {
"__environment_keys__": {
"tvpaint_Animation 11 (64bits)": []
}
}
},
"tvpaint_Animation 11 (32bits)": {
"enabled": true,
"label": "",
"variant_label": "Animation 11 (32bits)",
"icon": "",
"executables": {
"windows": [
[
"C:\\Program Files (x86)\\TVPaint Developpement\\TVPaint Animation 11 (32bits)\\TVPaint Animation 11 (32bits).exe",
""
]
],
"darwin": [],
"linux": []
},
"environment": {
"__environment_keys__": {
"tvpaint_Animation 11 (32bits)": []
}
}
}
}
},
"photoshop": {
"enabled": true,
"label": "Adobe Photoshop",
"icon": "{}/app_icons/photoshop.png",
"is_host": true,
"host_name": "photoshop",
"environment": {
"__environment_keys__": {
"photoshop": [
@ -883,7 +1038,12 @@
"variant_label": "2020",
"icon": "",
"executables": {
"windows": [],
"windows": [
[
"C:\\Program Files\\Adobe\\Adobe Photoshop 2020\\Photoshop.exe",
""
]
],
"darwin": [],
"linux": []
},
@ -892,6 +1052,93 @@
"photoshop_2020": []
}
}
},
"photoshop_2021": {
"enabled": true,
"label": "",
"variant_label": "2021",
"icon": "",
"executables": {
"windows": [
[
"C:\\Program Files\\Adobe\\Adobe Photoshop 2021\\Photoshop.exe",
""
]
],
"darwin": [],
"linux": []
},
"environment": {
"__environment_keys__": {
"photoshop_2021": []
}
}
}
}
},
"aftereffects": {
"enabled": true,
"label": "Adobe AfterEffects",
"icon": "{}/app_icons/aftereffects.png",
"host_name": "aftereffects",
"environment": {
"__environment_keys__": {
"aftereffects": [
"AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH",
"PYTHONPATH",
"PYPE_LOG_NO_COLORS",
"WEBSOCKET_URL",
"WORKFILES_SAVE_AS"
]
},
"AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH": "1",
"PYTHONPATH": "{PYTHONPATH}",
"PYPE_LOG_NO_COLORS": "Yes",
"WEBSOCKET_URL": "ws://localhost:8097/ws/",
"WORKFILES_SAVE_AS": "Yes"
},
"variants": {
"aftereffects_2020": {
"enabled": true,
"label": "",
"variant_label": "2020",
"icon": "",
"executables": {
"windows": [
[
"C:\\Program Files\\Adobe\\Adobe After Effects 2020\\Support Files\\AfterFX.exe",
""
]
],
"darwin": [],
"linux": []
},
"environment": {
"__environment_keys__": {
"aftereffects_2020": []
}
}
},
"aftereffects_2021": {
"enabled": true,
"label": "",
"variant_label": "2021",
"icon": "",
"executables": {
"windows": [
[
"C:\\Program Files\\Adobe\\Adobe After Effects 2021\\Support Files\\AfterFX.exe",
""
]
],
"darwin": [],
"linux": []
},
"environment": {
"__environment_keys__": {
"aftereffects_2021": []
}
}
}
}
},
@ -899,7 +1146,7 @@
"enabled": true,
"label": "CelAction 2D",
"icon": "app_icons/celaction.png",
"is_host": true,
"host_name": "celaction",
"environment": {
"__environment_keys__": {
"celaction": [
@ -914,7 +1161,10 @@
"label": "",
"variant_label": "Local",
"icon": "{}/app_icons/celaction_local.png",
"executables": "",
"executables": [
"",
""
],
"environment": {
"__environment_keys__": {
"celation_Local": []
@ -926,7 +1176,10 @@
"label": "",
"variant_label": "Pulblish",
"icon": "",
"executables": "",
"executables": [
"",
""
],
"environment": {
"__environment_keys__": {
"celation_Publish": []
@ -939,7 +1192,7 @@
"enabled": true,
"label": "Unreal Editor",
"icon": "{}/app_icons/ue4.png'",
"is_host": true,
"host_name": "unreal",
"environment": {
"__environment_keys__": {
"unreal": [
@ -1033,7 +1286,7 @@
"enabled": true,
"label": "DJV View",
"icon": "{}/app_icons/djvView.png",
"is_host": false,
"host_name": "",
"environment": {
"__environment_keys__": {
"djvview": []

View file

@ -0,0 +1,41 @@
{
"type": "dict",
"key": "aftereffects",
"label": "Adobe AfterEffects",
"collapsable": true,
"checkbox_key": "enabled",
"children": [{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "schema_template",
"name": "template_host_unchangables"
},
{
"key": "environment",
"label": "Environment",
"type": "raw-json",
"env_group_key": "aftereffects"
},
{
"type": "dict-invisible",
"key": "variants",
"children": [{
"type": "schema_template",
"name": "template_host_variant",
"template_data": [
{
"host_version": "2020",
"host_name": "aftereffects"
},
{
"host_version": "2021",
"host_name": "aftereffects"
}
]
}]
}
]
}

View file

@ -30,14 +30,6 @@
"host_version": "20",
"host_name": "harmony"
},
{
"host_version": "19",
"host_name": "harmony"
},
{
"host_version": "18",
"host_name": "harmony"
},
{
"host_version": "17",
"host_name": "harmony"

View file

@ -29,6 +29,10 @@
{
"host_version": "2020",
"host_name": "photoshop"
},
{
"host_version": "2021",
"host_name": "photoshop"
}
]
}]

View file

@ -0,0 +1,41 @@
{
"type": "dict",
"key": "tvpaint",
"label": "TVPaint",
"collapsable": true,
"checkbox_key": "enabled",
"children": [{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "schema_template",
"name": "template_host_unchangables"
},
{
"key": "environment",
"label": "Environment",
"type": "raw-json",
"env_group_key": "tvpaint"
},
{
"type": "dict-invisible",
"key": "variants",
"children": [{
"type": "schema_template",
"name": "template_host_variant",
"template_data": [
{
"host_version": "Animation 11 (64bits)",
"host_name": "tvpaint"
},
{
"host_version": "Animation 11 (32bits)",
"host_name": "tvpaint"
}
]
}]
}
]
}

View file

@ -13,8 +13,24 @@
"roles": ["developer"]
},
{
"type": "boolean",
"key": "is_host",
"label": "Has host implementation",
"type": "enum",
"key": "host_name",
"label": "Host implementation",
"enum_items": [
{"": "< without host >"},
{"aftereffects": "aftereffects"},
{"blender": "blender"},
{"celaction": "celaction"},
{"fusion": "fusion"},
{"harmony": "harmony"},
{"hiero": "hiero"},
{"houdini": "houdini"},
{"maya": "maya"},
{"nuke": "nuke"},
{"photoshop": "photoshop"},
{"resolve": "resolve"},
{"tvpaint": "tvpaint"},
{"unreal": "unreal"}
],
"roles": ["developer"]
}]

View file

@ -43,7 +43,8 @@
"key": "executables",
"label": "Executables",
"multiplatform": "{multiplatform}",
"multipath": "{multipath_executables}"
"multipath": "{multipath_executables}",
"with_arguments": true
},
{
"key": "environment",

View file

@ -65,10 +65,18 @@
"type": "schema",
"name": "schema_harmony"
},
{
"type": "schema",
"name": "schema_tvpaint"
},
{
"type": "schema",
"name": "schema_photoshop"
},
{
"type": "schema",
"name": "schema_aftereffects"
},
{
"type": "schema",
"name": "schema_celaction"

View file

@ -5,7 +5,6 @@ from .widgets import (
IconButton,
ExpandingWidget,
NumberSpinBox,
PathInput,
GridLabelWidget,
ComboBox,
NiceCheckbox
@ -1084,7 +1083,7 @@ class TextWidget(QtWidgets.QWidget, InputObject):
class PathInputWidget(QtWidgets.QWidget, InputObject):
default_input_value = ""
value_changed = QtCore.Signal(object)
valid_value_types = (str, )
valid_value_types = (str, list)
def __init__(
self, schema_data, parent, as_widget=False, parent_widget=None
@ -1095,6 +1094,8 @@ class PathInputWidget(QtWidgets.QWidget, InputObject):
self.initial_attributes(schema_data, parent, as_widget)
self.with_arguments = schema_data.get("with_arguments", False)
def create_ui(self, label_widget=None):
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
@ -1106,22 +1107,37 @@ class PathInputWidget(QtWidgets.QWidget, InputObject):
layout.addWidget(label_widget, 0)
self.label_widget = label_widget
self.input_field = PathInput(self)
self.setFocusProxy(self.input_field)
layout.addWidget(self.input_field, 1)
self.input_field = QtWidgets.QLineEdit(self)
self.args_input_field = None
if self.with_arguments:
self.input_field.setPlaceholderText("Executable path")
self.args_input_field = QtWidgets.QLineEdit(self)
self.args_input_field.setPlaceholderText("Arguments")
self.setFocusProxy(self.input_field)
layout.addWidget(self.input_field, 8)
self.input_field.textChanged.connect(self._on_value_change)
if self.args_input_field:
layout.addWidget(self.args_input_field, 2)
self.args_input_field.textChanged.connect(self._on_value_change)
def set_value(self, value):
self.validate_value(value)
self.input_field.setText(value)
def focusOutEvent(self, event):
self.input_field.clear_end_path()
super(PathInput, self).focusOutEvent(event)
if not isinstance(value, list):
self.input_field.setText(value)
elif self.with_arguments:
self.input_field.setText(value[0])
self.args_input_field.setText(value[1])
else:
self.input_field.setText(value[0])
def item_value(self):
return self.input_field.text()
path_value = self.input_field.text()
if self.with_arguments:
return [path_value, self.args_input_field.text()]
return path_value
class EnumeratorWidget(QtWidgets.QWidget, InputObject):
@ -3191,6 +3207,7 @@ class PathWidget(QtWidgets.QWidget, SettingObject):
self.multiplatform = schema_data.get("multiplatform", False)
self.multipath = schema_data.get("multipath", False)
self.with_arguments = schema_data.get("with_arguments", False)
self.input_field = None
@ -3230,8 +3247,11 @@ class PathWidget(QtWidgets.QWidget, SettingObject):
def create_ui_inputs(self):
if not self.multiplatform and not self.multipath:
input_data = {"key": self.key}
path_input = PathInputWidget(input_data, self, as_widget=True)
item_schema = {
"key": self.key,
"with_arguments": self.with_arguments
}
path_input = PathInputWidget(item_schema, self, as_widget=True)
path_input.create_ui(label_widget=self.label_widget)
self.setFocusProxy(path_input)
@ -3243,7 +3263,10 @@ class PathWidget(QtWidgets.QWidget, SettingObject):
if not self.multiplatform:
item_schema = {
"key": self.key,
"object_type": "path-input"
"object_type": {
"type": "path-input",
"with_arguments": self.with_arguments
}
}
input_widget = ListWidget(item_schema, self, as_widget=True)
input_widget.create_ui(label_widget=self.label_widget)
@ -3266,9 +3289,13 @@ class PathWidget(QtWidgets.QWidget, SettingObject):
}
if self.multipath:
child_item["type"] = "list"
child_item["object_type"] = "path-input"
child_item["object_type"] = {
"type": "path-input",
"with_arguments": self.with_arguments
}
else:
child_item["type"] = "path-input"
child_item["with_arguments"] = self.with_arguments
item_schema["children"].append(child_item)

View file

@ -51,6 +51,11 @@ class ComboBox(QtWidgets.QComboBox):
super(ComboBox, self).__init__(*args, **kwargs)
self.currentIndexChanged.connect(self._on_change)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
def wheelEvent(self, event):
if self.hasFocus():
return super(ComboBox, self).wheelEvent(event)
def _on_change(self, *args, **kwargs):
self.value_changed.emit()
@ -66,35 +71,6 @@ class ComboBox(QtWidgets.QComboBox):
return self.itemData(self.currentIndex(), role=QtCore.Qt.UserRole)
class PathInput(QtWidgets.QLineEdit):
def clear_end_path(self):
value = self.text().strip()
if value.endswith("/"):
while value and value[-1] == "/":
value = value[:-1]
self.setText(value)
def keyPressEvent(self, event):
# Always change backslash `\` for forwardslash `/`
if event.key() == QtCore.Qt.Key_Backslash:
event.accept()
new_event = QtGui.QKeyEvent(
event.type(),
QtCore.Qt.Key_Slash,
event.modifiers(),
"/",
event.isAutoRepeat(),
event.count()
)
QtWidgets.QApplication.sendEvent(self, new_event)
return
super(PathInput, self).keyPressEvent(event)
def focusOutEvent(self, event):
super(PathInput, self).focusOutEvent(event)
self.clear_end_path()
class ClickableWidget(QtWidgets.QWidget):
clicked = QtCore.Signal()