diff --git a/openpype/hosts/unreal/__init__.py b/openpype/hosts/unreal/__init__.py index 1280442916..e6ca1e833d 100644 --- a/openpype/hosts/unreal/__init__.py +++ b/openpype/hosts/unreal/__init__.py @@ -3,11 +3,12 @@ import os def add_implementation_envs(env, _app): """Modify environments to contain all required for implementation.""" - # Set AVALON_UNREAL_PLUGIN required for Unreal implementation + # Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation unreal_plugin_path = os.path.join( - os.environ["OPENPYPE_REPOS_ROOT"], "repos", "avalon-unreal-integration" + os.environ["OPENPYPE_ROOT"], "openpype", "hosts", + "unreal", "integration" ) - env["AVALON_UNREAL_PLUGIN"] = unreal_plugin_path + env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path # Set default environments if are not set via settings defaults = { diff --git a/openpype/hosts/unreal/api/helpers.py b/openpype/hosts/unreal/api/helpers.py new file mode 100644 index 0000000000..6fc89cf176 --- /dev/null +++ b/openpype/hosts/unreal/api/helpers.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +import unreal # noqa + + +class OpenPypeUnrealException(Exception): + pass + + +@unreal.uclass() +class OpenPypeHelpers(unreal.OpenPypeLib): + """Class wrapping some useful functions for OpenPype. + + This class is extending native BP class in OpenPype Integration Plugin. + + """ + + @unreal.ufunction(params=[str, unreal.LinearColor, bool]) + def set_folder_color(self, path: str, color: unreal.LinearColor) -> Bool: + """Set color on folder in Content Browser. + + This method sets color on folder in Content Browser. Unfortunately + there is no way to refresh Content Browser so new color isn't applied + immediately. They are saved to config file and appears correctly + only after Editor is restarted. + + Args: + path (str): Path to folder + color (:class:`unreal.LinearColor`): Color of the folder + + Example: + + AvalonHelpers().set_folder_color( + "/Game/Path", unreal.LinearColor(a=1.0, r=1.0, g=0.5, b=0) + ) + + Note: + This will take effect only after Editor is restarted. I couldn't + find a way to refresh it. Also this saves the color definition + into the project config, binding this path with color. So if you + delete this path and later re-create, it will set this color + again. + + """ + self.c_set_folder_color(path, color, False) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 61dac46fac..e04606a333 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -169,11 +169,11 @@ def create_unreal_project(project_name: str, env: dict = None) -> None: """This will create `.uproject` file at specified location. - As there is no way I know to create project via command line, this is - easiest option. Unreal project file is basically JSON file. If we find - `AVALON_UNREAL_PLUGIN` environment variable we assume this is location - of Avalon Integration Plugin and we copy its content to project folder - and enable this plugin. + As there is no way I know to create a project via command line, this is + easiest option. Unreal project file is basically a JSON file. If we find + the `OPENPYPE_UNREAL_PLUGIN` environment variable we assume this is the + location of the Integration Plugin and we copy its content to the project + folder and enable this plugin. Args: project_name (str): Name of the project. @@ -254,14 +254,14 @@ def create_unreal_project(project_name: str, {"Name": "PythonScriptPlugin", "Enabled": True}, {"Name": "EditorScriptingUtilities", "Enabled": True}, {"Name": "SequencerScripting", "Enabled": True}, - {"Name": "Avalon", "Enabled": True} + {"Name": "OpenPype", "Enabled": True} ] } if dev_mode or preset["dev_mode"]: - # this will add project module and necessary source file to make it - # C++ project and to (hopefully) make Unreal Editor to compile all - # sources at start + # this will add the project module and necessary source file to + # make it a C++ project and to (hopefully) make Unreal Editor to + # compile all # sources at start data["Modules"] = [{ "Name": project_name, diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py new file mode 100644 index 0000000000..c255005f31 --- /dev/null +++ b/openpype/hosts/unreal/api/pipeline.py @@ -0,0 +1,388 @@ +# -*- coding: utf-8 -*- +import sys +import pyblish.api +from avalon.pipeline import AVALON_CONTAINER_ID + +import unreal # noqa +from typing import List + +from openpype.tools.utils import host_tools + +from avalon import api + + +AVALON_CONTAINERS = "OpenPypeContainers" + + +def install(): + + pyblish.api.register_host("unreal") + _register_callbacks() + _register_events() + + +def _register_callbacks(): + """ + TODO: Implement callbacks if supported by UE4 + """ + pass + + +def _register_events(): + """ + TODO: Implement callbacks if supported by UE4 + """ + pass + + +def uninstall(): + pyblish.api.deregister_host("unreal") + + +class Creator(api.Creator): + hosts = ["unreal"] + asset_types = [] + + def process(self): + nodes = list() + + with unreal.ScopedEditorTransaction("Avalon Creating Instance"): + if (self.options or {}).get("useSelection"): + self.log.info("setting ...") + print("settings ...") + nodes = unreal.EditorUtilityLibrary.get_selected_assets() + + asset_paths = [a.get_path_name() for a in nodes] + self.name = move_assets_to_path( + "/Game", self.name, asset_paths + ) + + instance = create_publish_instance("/Game", self.name) + imprint(instance, self.data) + + return instance + + +class Loader(api.Loader): + hosts = ["unreal"] + + +def ls(): + """ + List all containers found in *Content Manager* of Unreal and return + metadata from them. Adding `objectName` to set. + """ + ar = unreal.AssetRegistryHelpers.get_asset_registry() + avalon_containers = ar.get_assets_by_class("AssetContainer", True) + + # get_asset_by_class returns AssetData. To get all metadata we need to + # load asset. get_tag_values() work only on metadata registered in + # Asset Registy Project settings (and there is no way to set it with + # python short of editing ini configuration file). + for asset_data in avalon_containers: + asset = asset_data.get_asset() + data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) + data["objectName"] = asset_data.asset_name + data = cast_map_to_str_dict(data) + + yield data + + +def parse_container(container): + """ + To get data from container, AssetContainer must be loaded. + + Args: + container(str): path to container + + Returns: + dict: metadata stored on container + """ + asset = unreal.EditorAssetLibrary.load_asset(container) + data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) + data["objectName"] = asset.get_name() + data = cast_map_to_str_dict(data) + + return data + + +def publish(): + """Shorthand to publish from within host""" + import pyblish.util + + return pyblish.util.publish() + + +def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"): + + """Bundles *nodes* (assets) into a *container* and add metadata to it. + + Unreal doesn't support *groups* of assets that you can add metadata to. + But it does support folders that helps to organize asset. Unfortunately + those folders are just that - you cannot add any additional information + to them. `Avalon Integration Plugin`_ is providing way out - Implementing + `AssetContainer` Blueprint class. This class when added to folder can + handle metadata on it using standard + :func:`unreal.EditorAssetLibrary.set_metadata_tag()` and + :func:`unreal.EditorAssetLibrary.get_metadata_tag_values()`. It also + stores and monitor all changes in assets in path where it resides. List of + those assets is available as `assets` property. + + This is list of strings starting with asset type and ending with its path: + `Material /Game/Avalon/Test/TestMaterial.TestMaterial` + + .. _Avalon Integration Plugin: + https://github.com/pypeclub/avalon-unreal-integration + + """ + # 1 - create directory for container + root = "/Game" + container_name = "{}{}".format(name, suffix) + new_name = move_assets_to_path(root, container_name, nodes) + + # 2 - create Asset Container there + path = "{}/{}".format(root, new_name) + create_container(container=container_name, path=path) + + namespace = path + + data = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": new_name, + "namespace": namespace, + "loader": str(loader), + "representation": context["representation"]["_id"], + } + # 3 - imprint data + imprint("{}/{}".format(path, container_name), data) + return path + + +def instantiate(root, name, data, assets=None, suffix="_INS"): + """ + Bundles *nodes* into *container* marking it with metadata as publishable + instance. If assets are provided, they are moved to new path where + `AvalonPublishInstance` class asset is created and imprinted with metadata. + + This can then be collected for publishing by Pyblish for example. + + Args: + root (str): root path where to create instance container + name (str): name of the container + data (dict): data to imprint on container + assets (list of str): list of asset paths to include in publish + instance + suffix (str): suffix string to append to instance name + """ + container_name = "{}{}".format(name, suffix) + + # if we specify assets, create new folder and move them there. If not, + # just create empty folder + if assets: + new_name = move_assets_to_path(root, container_name, assets) + else: + new_name = create_folder(root, name) + + path = "{}/{}".format(root, new_name) + create_publish_instance(instance=container_name, path=path) + + imprint("{}/{}".format(path, container_name), data) + + +def imprint(node, data): + loaded_asset = unreal.EditorAssetLibrary.load_asset(node) + for key, value in data.items(): + # Support values evaluated at imprint + if callable(value): + value = value() + # Unreal doesn't support NoneType in metadata values + if value is None: + value = "" + unreal.EditorAssetLibrary.set_metadata_tag( + loaded_asset, key, str(value) + ) + + with unreal.ScopedEditorTransaction("Avalon containerising"): + unreal.EditorAssetLibrary.save_asset(node) + + +def show_tools_popup(): + """Show popup with tools. + + Popup will disappear on click or loosing focus. + """ + from openpype.hosts.unreal.api import tools_ui + + tools_ui.show_tools_popup() + + +def show_tools_dialog(): + """Show dialog with tools. + + Dialog will stay visible. + """ + from openpype.hosts.unreal.api import tools_ui + + tools_ui.show_tools_dialog() + + +def show_creator(): + host_tools.show_creator() + + +def show_loader(): + host_tools.show_loader(use_context=True) + + +def show_publisher(): + host_tools.show_publish() + + +def show_manager(): + host_tools.show_scene_inventory() + + +def show_experimental_tools(): + host_tools.show_experimental_tools_dialog() + + +def create_folder(root: str, name: str) -> str: + """Create new folder + + If folder exists, append number at the end and try again, incrementing + if needed. + + Args: + root (str): path root + name (str): folder name + + Returns: + str: folder name + + Example: + >>> create_folder("/Game/Foo") + /Game/Foo + >>> create_folder("/Game/Foo") + /Game/Foo1 + + """ + eal = unreal.EditorAssetLibrary + index = 1 + while True: + if eal.does_directory_exist("{}/{}".format(root, name)): + name = "{}{}".format(name, index) + index += 1 + else: + eal.make_directory("{}/{}".format(root, name)) + break + + return name + + +def move_assets_to_path(root: str, name: str, assets: List[str]) -> str: + """ + Moving (renaming) list of asset paths to new destination. + + Args: + root (str): root of the path (eg. `/Game`) + name (str): name of destination directory (eg. `Foo` ) + assets (list of str): list of asset paths + + Returns: + str: folder name + + Example: + This will get paths of all assets under `/Game/Test` and move them + to `/Game/NewTest`. If `/Game/NewTest` already exists, then resulting + path will be `/Game/NewTest1` + + >>> assets = unreal.EditorAssetLibrary.list_assets("/Game/Test") + >>> move_assets_to_path("/Game", "NewTest", assets) + NewTest + + """ + eal = unreal.EditorAssetLibrary + name = create_folder(root, name) + + unreal.log(assets) + for asset in assets: + loaded = eal.load_asset(asset) + eal.rename_asset( + asset, "{}/{}/{}".format(root, name, loaded.get_name()) + ) + + return name + + +def create_container(container: str, path: str) -> unreal.Object: + """ + Helper function to create Asset Container class on given path. + This Asset Class helps to mark given path as Container + and enable asset version control on it. + + Args: + container (str): Asset Container name + path (str): Path where to create Asset Container. This path should + point into container folder + + Returns: + :class:`unreal.Object`: instance of created asset + + Example: + + create_avalon_container( + "/Game/modelingFooCharacter_CON", + "modelingFooCharacter_CON" + ) + + """ + factory = unreal.AssetContainerFactory() + tools = unreal.AssetToolsHelpers().get_asset_tools() + + asset = tools.create_asset(container, path, None, factory) + return asset + + +def create_publish_instance(instance: str, path: str) -> unreal.Object: + """ + Helper function to create Avalon Publish Instance on given path. + This behaves similary as :func:`create_avalon_container`. + + Args: + path (str): Path where to create Publish Instance. + This path should point into container folder + instance (str): Publish Instance name + + Returns: + :class:`unreal.Object`: instance of created asset + + Example: + + create_publish_instance( + "/Game/modelingFooCharacter_INST", + "modelingFooCharacter_INST" + ) + + """ + factory = unreal.AvalonPublishInstanceFactory() + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset = tools.create_asset(instance, path, None, factory) + return asset + + +def cast_map_to_str_dict(map) -> dict: + """Cast Unreal Map to dict. + + Helper function to cast Unreal Map object to plain old python + dict. This will also cast values and keys to str. Useful for + metadata dicts. + + Args: + map: Unreal Map object + + Returns: + dict + + """ + return {str(key): str(value) for (key, value) in map.items()} diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 5a6b236730..2327fc09c8 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -1,5 +1,8 @@ -from avalon import api +# -*- coding: utf-8 -*- +from abc import ABC + import openpype.api +import avalon.api class Creator(openpype.api.Creator): @@ -7,6 +10,6 @@ class Creator(openpype.api.Creator): defaults = ['Main'] -class Loader(api.Loader): +class Loader(avalon.api.Loader, ABC): """This serves as skeleton for future OpenPype specific functionality""" pass diff --git a/openpype/hosts/unreal/integration/.gitignore b/openpype/hosts/unreal/integration/.gitignore new file mode 100644 index 0000000000..b32a6f55e5 --- /dev/null +++ b/openpype/hosts/unreal/integration/.gitignore @@ -0,0 +1,35 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +/Binaries +/Intermediate diff --git a/openpype/hosts/unreal/integration/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/Content/Python/init_unreal.py new file mode 100644 index 0000000000..48e931bb04 --- /dev/null +++ b/openpype/hosts/unreal/integration/Content/Python/init_unreal.py @@ -0,0 +1,27 @@ +import unreal + +avalon_detected = True +try: + from avalon import api + from avalon import unreal as avalon_unreal +except ImportError as exc: + avalon_detected = False + unreal.log_error("Avalon: cannot load avalon [ {} ]".format(exc)) + +if avalon_detected: + api.install(avalon_unreal) + + +@unreal.uclass() +class AvalonIntegration(unreal.AvalonPythonBridge): + @unreal.ufunction(override=True) + def RunInPython_Popup(self): + unreal.log_warning("Avalon: showing tools popup") + if avalon_detected: + avalon_unreal.show_tools_popup() + + @unreal.ufunction(override=True) + def RunInPython_Dialog(self): + unreal.log_warning("Avalon: showing tools dialog") + if avalon_detected: + avalon_unreal.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/OpenPype.uplugin b/openpype/hosts/unreal/integration/OpenPype.uplugin new file mode 100644 index 0000000000..4c7a74403c --- /dev/null +++ b/openpype/hosts/unreal/integration/OpenPype.uplugin @@ -0,0 +1,24 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "OpenPype", + "Description": "OpenPype Integration", + "Category": "OpenPype.Integration", + "CreatedBy": "Ondrej Samohel", + "CreatedByURL": "https://openpype.io", + "DocsURL": "https://openpype.io/docs/artist_hosts_unreal", + "MarketplaceURL": "", + "SupportURL": "https://pype.club/", + "CanContainContent": true, + "IsBetaVersion": true, + "IsExperimentalVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "OpenPype", + "Type": "Editor", + "LoadingPhase": "Default" + } + ] +} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/README.md b/openpype/hosts/unreal/integration/README.md new file mode 100644 index 0000000000..a32d89aab8 --- /dev/null +++ b/openpype/hosts/unreal/integration/README.md @@ -0,0 +1,11 @@ +# OpenPype Unreal Integration plugin + +This is plugin for Unreal Editor, creating menu for [OpenPype](https://github.com/getavalon) tools to run. + +## How does this work + +Plugin is creating basic menu items in **Window/OpenPype** section of Unreal Editor main menu and a button +on the main toolbar with associated menu. Clicking on those menu items is calling callbacks that are +declared in c++ but needs to be implemented during Unreal Editor +startup in `Plugins/OpenPype/Content/Python/init_unreal.py` - this should be executed by Unreal Editor +automatically. diff --git a/openpype/hosts/unreal/integration/Resources/openpype128.png b/openpype/hosts/unreal/integration/Resources/openpype128.png new file mode 100644 index 0000000000..abe8a807ef Binary files /dev/null and b/openpype/hosts/unreal/integration/Resources/openpype128.png differ diff --git a/openpype/hosts/unreal/integration/Resources/openpype40.png b/openpype/hosts/unreal/integration/Resources/openpype40.png new file mode 100644 index 0000000000..f983e7a1f2 Binary files /dev/null and b/openpype/hosts/unreal/integration/Resources/openpype40.png differ diff --git a/openpype/hosts/unreal/integration/Resources/openpype512.png b/openpype/hosts/unreal/integration/Resources/openpype512.png new file mode 100644 index 0000000000..97c4d4326b Binary files /dev/null and b/openpype/hosts/unreal/integration/Resources/openpype512.png differ diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Avalon.Build.cs b/openpype/hosts/unreal/integration/Source/Avalon/Avalon.Build.cs new file mode 100644 index 0000000000..5068e37d80 --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Avalon.Build.cs @@ -0,0 +1,57 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class Avalon : ModuleRules +{ + public Avalon(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "Projects", + "InputCore", + "UnrealEd", + "LevelEditor", + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/Source/Avalon/Private/AssetContainer.cpp new file mode 100644 index 0000000000..c766f87a8e --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Private/AssetContainer.cpp @@ -0,0 +1,115 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "AssetContainer.h" +#include "AssetRegistryModule.h" +#include "Misc/PackageName.h" +#include "Engine.h" +#include "Containers/UnrealString.h" + +UAssetContainer::UAssetContainer(const FObjectInitializer& ObjectInitializer) +: UAssetUserData(ObjectInitializer) +{ + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + FString path = UAssetContainer::GetPathName(); + UE_LOG(LogTemp, Warning, TEXT("UAssetContainer %s"), *path); + FARFilter Filter; + Filter.PackagePaths.Add(FName(*path)); + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAssetContainer::OnAssetAdded); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAssetContainer::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAssetContainer::OnAssetRenamed); +} + +void UAssetContainer::OnAssetAdded(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + assets.Add(assetPath); + assetsData.Add(AssetData); + UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); + } + } +} + +void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + FString path = UAssetContainer::GetPathName(); + FString lpp = FPackageName::GetLongPackagePath(*path); + + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); + assets.Remove(assetPath); + assetsData.Remove(AssetData); + } + } +} + +void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + + assets.Remove(str); + assets.Add(assetPath); + assetsData.Remove(AssetData); + // UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str); + } + } +} + diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Private/AssetContainerFactory.cpp b/openpype/hosts/unreal/integration/Source/Avalon/Private/AssetContainerFactory.cpp new file mode 100644 index 0000000000..b943150bdd --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Private/AssetContainerFactory.cpp @@ -0,0 +1,20 @@ +#include "AssetContainerFactory.h" +#include "AssetContainer.h" + +UAssetContainerFactory::UAssetContainerFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAssetContainer::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + UAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); + return AssetContainer; +} + +bool UAssetContainerFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Private/Avalon.cpp b/openpype/hosts/unreal/integration/Source/Avalon/Private/Avalon.cpp new file mode 100644 index 0000000000..ed782f4870 --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Private/Avalon.cpp @@ -0,0 +1,103 @@ +#include "Avalon.h" +#include "LevelEditor.h" +#include "AvalonPythonBridge.h" +#include "AvalonStyle.h" + + +static const FName AvalonTabName("Avalon"); + +#define LOCTEXT_NAMESPACE "FAvalonModule" + +// This function is triggered when the plugin is staring up +void FAvalonModule::StartupModule() +{ + + FAvalonStyle::Initialize(); + FAvalonStyle::SetIcon("Logo", "openpype40"); + + // Create the Extender that will add content to the menu + FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); + + TSharedPtr MenuExtender = MakeShareable(new FExtender()); + TSharedPtr ToolbarExtender = MakeShareable(new FExtender()); + + MenuExtender->AddMenuExtension( + "LevelEditor", + EExtensionHook::After, + NULL, + FMenuExtensionDelegate::CreateRaw(this, &FAvalonModule::AddMenuEntry) + ); + ToolbarExtender->AddToolBarExtension( + "Settings", + EExtensionHook::After, + NULL, + FToolBarExtensionDelegate::CreateRaw(this, &FAvalonModule::AddToobarEntry)); + + + LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender); + LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender); + +} + +void FAvalonModule::ShutdownModule() +{ + FAvalonStyle::Shutdown(); +} + + +void FAvalonModule::AddMenuEntry(FMenuBuilder& MenuBuilder) +{ + // Create Section + MenuBuilder.BeginSection("OpenPype", TAttribute(FText::FromString("OpenPype"))); + { + // Create a Submenu inside of the Section + MenuBuilder.AddMenuEntry( + FText::FromString("Tools..."), + FText::FromString("Pipeline tools"), + FSlateIcon(FAvalonStyle::GetStyleSetName(), "OpenPype.Logo"), + FUIAction(FExecuteAction::CreateRaw(this, &FAvalonModule::MenuPopup)) + ); + + MenuBuilder.AddMenuEntry( + FText::FromString("Tools dialog..."), + FText::FromString("Pipeline tools dialog"), + FSlateIcon(FAvalonStyle::GetStyleSetName(), "OpenPype.Logo"), + FUIAction(FExecuteAction::CreateRaw(this, &FAvalonModule::MenuDialog)) + ); + + } + MenuBuilder.EndSection(); +} + +void FAvalonModule::AddToobarEntry(FToolBarBuilder& ToolbarBuilder) +{ + ToolbarBuilder.BeginSection(TEXT("OpenPype")); + { + ToolbarBuilder.AddToolBarButton( + FUIAction( + FExecuteAction::CreateRaw(this, &FAvalonModule::MenuPopup), + NULL, + FIsActionChecked() + + ), + NAME_None, + LOCTEXT("OpenPype_label", "OpenPype"), + LOCTEXT("OpenPype_tooltip", "OpenPype Tools"), + FSlateIcon(FAvalonStyle::GetStyleSetName(), "OpenPype.Logo") + ); + } + ToolbarBuilder.EndSection(); +} + + +void FAvalonModule::MenuPopup() { + UAvalonPythonBridge* bridge = UAvalonPythonBridge::Get(); + bridge->RunInPython_Popup(); +} + +void FAvalonModule::MenuDialog() { + UAvalonPythonBridge* bridge = UAvalonPythonBridge::Get(); + bridge->RunInPython_Dialog(); +} + +IMPLEMENT_MODULE(FAvalonModule, Avalon) diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonLib.cpp b/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonLib.cpp new file mode 100644 index 0000000000..312656424c --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonLib.cpp @@ -0,0 +1,48 @@ +#include "AvalonLib.h" +#include "Misc/Paths.h" +#include "Misc/ConfigCacheIni.h" +#include "UObject/UnrealType.h" + +/** + * Sets color on folder icon on given path + * @param InPath - path to folder + * @param InFolderColor - color of the folder + * @warning This color will appear only after Editor restart. Is there a better way? + */ + +void UAvalonLib::CSetFolderColor(FString FolderPath, FLinearColor FolderColor, bool bForceAdd) +{ + auto SaveColorInternal = [](FString InPath, FLinearColor InFolderColor) + { + // Saves the color of the folder to the config + if (FPaths::FileExists(GEditorPerProjectIni)) + { + GConfig->SetString(TEXT("PathColor"), *InPath, *InFolderColor.ToString(), GEditorPerProjectIni); + } + + }; + + SaveColorInternal(FolderPath, FolderColor); + +} +/** + * Returns all poperties on given object + * @param cls - class + * @return TArray of properties + */ +TArray UAvalonLib::GetAllProperties(UClass* cls) +{ + TArray Ret; + if (cls != nullptr) + { + for (TFieldIterator It(cls); It; ++It) + { + FProperty* Property = *It; + if (Property->HasAnyPropertyFlags(EPropertyFlags::CPF_Edit)) + { + Ret.Add(Property->GetName()); + } + } + } + return Ret; +} diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonPublishInstance.cpp b/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonPublishInstance.cpp new file mode 100644 index 0000000000..2bb31a4853 --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonPublishInstance.cpp @@ -0,0 +1,108 @@ +#pragma once + +#include "AvalonPublishInstance.h" +#include "AssetRegistryModule.h" + + +UAvalonPublishInstance::UAvalonPublishInstance(const FObjectInitializer& ObjectInitializer) + : UObject(ObjectInitializer) +{ + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + FString path = UAvalonPublishInstance::GetPathName(); + FARFilter Filter; + Filter.PackagePaths.Add(FName(*path)); + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAvalonPublishInstance::OnAssetAdded); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAvalonPublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAvalonPublishInstance::OnAssetRenamed); +} + +void UAvalonPublishInstance::OnAssetAdded(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAvalonPublishInstance::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AvalonPublishInstance") + { + assets.Add(assetPath); + UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); + } + } +} + +void UAvalonPublishInstance::OnAssetRemoved(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAvalonPublishInstance::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + FString path = UAvalonPublishInstance::GetPathName(); + FString lpp = FPackageName::GetLongPackagePath(*path); + + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AvalonPublishInstance") + { + // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); + assets.Remove(assetPath); + } + } +} + +void UAvalonPublishInstance::OnAssetRenamed(const FAssetData& AssetData, const FString& str) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAvalonPublishInstance::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + + assets.Remove(str); + assets.Add(assetPath); + // UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str); + } + } +} diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonPublishInstanceFactory.cpp new file mode 100644 index 0000000000..e14a14f1e5 --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonPublishInstanceFactory.cpp @@ -0,0 +1,20 @@ +#include "AvalonPublishInstanceFactory.h" +#include "AvalonPublishInstance.h" + +UAvalonPublishInstanceFactory::UAvalonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAvalonPublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAvalonPublishInstanceFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + UAvalonPublishInstance* AvalonPublishInstance = NewObject(InParent, Class, Name, Flags); + return AvalonPublishInstance; +} + +bool UAvalonPublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonPythonBridge.cpp b/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonPythonBridge.cpp new file mode 100644 index 0000000000..8642ab6b63 --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonPythonBridge.cpp @@ -0,0 +1,13 @@ +#include "AvalonPythonBridge.h" + +UAvalonPythonBridge* UAvalonPythonBridge::Get() +{ + TArray AvalonPythonBridgeClasses; + GetDerivedClasses(UAvalonPythonBridge::StaticClass(), AvalonPythonBridgeClasses); + int32 NumClasses = AvalonPythonBridgeClasses.Num(); + if (NumClasses > 0) + { + return Cast(AvalonPythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); + } + return nullptr; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonStyle.cpp b/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonStyle.cpp new file mode 100644 index 0000000000..5b3d1269b0 --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Private/AvalonStyle.cpp @@ -0,0 +1,69 @@ +#include "AvalonStyle.h" +#include "Framework/Application/SlateApplication.h" +#include "Styling/SlateStyle.h" +#include "Styling/SlateStyleRegistry.h" + + +TUniquePtr< FSlateStyleSet > FAvalonStyle::AvalonStyleInstance = nullptr; + +void FAvalonStyle::Initialize() +{ + if (!AvalonStyleInstance.IsValid()) + { + AvalonStyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle(*AvalonStyleInstance); + } +} + +void FAvalonStyle::Shutdown() +{ + if (AvalonStyleInstance.IsValid()) + { + FSlateStyleRegistry::UnRegisterSlateStyle(*AvalonStyleInstance); + AvalonStyleInstance.Reset(); + } +} + +FName FAvalonStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT("AvalonStyle")); + return StyleSetName; +} + +FName FAvalonStyle::GetContextName() +{ + static FName ContextName(TEXT("OpenPype")); + return ContextName; +} + +#define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) + +const FVector2D Icon40x40(40.0f, 40.0f); + +TUniquePtr< FSlateStyleSet > FAvalonStyle::Create() +{ + TUniquePtr< FSlateStyleSet > Style = MakeUnique(GetStyleSetName()); + Style->SetContentRoot(FPaths::ProjectPluginsDir() / TEXT("Avalon/Resources")); + + return Style; +} + +void FAvalonStyle::SetIcon(const FString& StyleName, const FString& ResourcePath) +{ + FSlateStyleSet* Style = AvalonStyleInstance.Get(); + + FString Name(GetContextName().ToString()); + Name = Name + "." + StyleName; + Style->Set(*Name, new FSlateImageBrush(Style->RootToContentDir(ResourcePath, TEXT(".png")), Icon40x40)); + + + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); +} + +#undef IMAGE_BRUSH + +const ISlateStyle& FAvalonStyle::Get() +{ + check(AvalonStyleInstance); + return *AvalonStyleInstance; +} diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Public/AssetContainer.h b/openpype/hosts/unreal/integration/Source/Avalon/Public/AssetContainer.h new file mode 100644 index 0000000000..1195f95cba --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Public/AssetContainer.h @@ -0,0 +1,39 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "Engine/AssetUserData.h" +#include "AssetData.h" +#include "AssetContainer.generated.h" + +/** + * + */ +UCLASS(Blueprintable) +class AVALON_API UAssetContainer : public UAssetUserData +{ + GENERATED_BODY() + +public: + + UAssetContainer(const FObjectInitializer& ObjectInitalizer); + // ~UAssetContainer(); + + UPROPERTY(EditAnywhere, BlueprintReadOnly) + TArray assets; + + // There seems to be no reflection option to expose array of FAssetData + /* + UPROPERTY(Transient, BlueprintReadOnly, Category = "Python", meta=(DisplayName="Assets Data")) + TArray assetsData; + */ +private: + TArray assetsData; + void OnAssetAdded(const FAssetData& AssetData); + void OnAssetRemoved(const FAssetData& AssetData); + void OnAssetRenamed(const FAssetData& AssetData, const FString& str); +}; + + diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Public/AssetContainerFactory.h b/openpype/hosts/unreal/integration/Source/Avalon/Public/AssetContainerFactory.h new file mode 100644 index 0000000000..62b6e73640 --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Public/AssetContainerFactory.h @@ -0,0 +1,21 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "AssetContainerFactory.generated.h" + +/** + * + */ +UCLASS() +class AVALON_API UAssetContainerFactory : public UFactory +{ + GENERATED_BODY() + +public: + UAssetContainerFactory(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + virtual bool ShouldShowInNewMenu() const override; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Public/Avalon.h b/openpype/hosts/unreal/integration/Source/Avalon/Public/Avalon.h new file mode 100644 index 0000000000..2dd6a825ab --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Public/Avalon.h @@ -0,0 +1,21 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Engine.h" + + +class FAvalonModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +private: + + void AddMenuEntry(FMenuBuilder& MenuBuilder); + void AddToobarEntry(FToolBarBuilder& ToolbarBuilder); + void MenuPopup(); + void MenuDialog(); + +}; diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonLib.h b/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonLib.h new file mode 100644 index 0000000000..da3369970c --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonLib.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Engine.h" +#include "AvalonLib.generated.h" + + +UCLASS(Blueprintable) +class AVALON_API UAvalonLib : public UObject +{ + + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = Python) + static void CSetFolderColor(FString FolderPath, FLinearColor FolderColor, bool bForceAdd); + + UFUNCTION(BlueprintCallable, Category = Python) + static TArray GetAllProperties(UClass* cls); +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonPublishInstance.h b/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonPublishInstance.h new file mode 100644 index 0000000000..7678f78924 --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonPublishInstance.h @@ -0,0 +1,21 @@ +#pragma once + +#include "Engine.h" +#include "AvalonPublishInstance.generated.h" + + +UCLASS(Blueprintable) +class AVALON_API UAvalonPublishInstance : public UObject +{ + GENERATED_BODY() + +public: + UAvalonPublishInstance(const FObjectInitializer& ObjectInitalizer); + + UPROPERTY(EditAnywhere, BlueprintReadOnly) + TArray assets; +private: + void OnAssetAdded(const FAssetData& AssetData); + void OnAssetRemoved(const FAssetData& AssetData); + void OnAssetRenamed(const FAssetData& AssetData, const FString& str); +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonPublishInstanceFactory.h new file mode 100644 index 0000000000..79e781c60c --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonPublishInstanceFactory.h @@ -0,0 +1,19 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "AvalonPublishInstanceFactory.generated.h" + +/** + * + */ +UCLASS() +class AVALON_API UAvalonPublishInstanceFactory : public UFactory +{ + GENERATED_BODY() + +public: + UAvalonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + virtual bool ShouldShowInNewMenu() const override; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonPythonBridge.h b/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonPythonBridge.h new file mode 100644 index 0000000000..db4b16d53f --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonPythonBridge.h @@ -0,0 +1,20 @@ +#pragma once +#include "Engine.h" +#include "AvalonPythonBridge.generated.h" + +UCLASS(Blueprintable) +class UAvalonPythonBridge : public UObject +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = Python) + static UAvalonPythonBridge* Get(); + + UFUNCTION(BlueprintImplementableEvent, Category = Python) + void RunInPython_Popup() const; + + UFUNCTION(BlueprintImplementableEvent, Category = Python) + void RunInPython_Dialog() const; + +}; diff --git a/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonStyle.h b/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonStyle.h new file mode 100644 index 0000000000..ffb2bc7aa4 --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/Avalon/Public/AvalonStyle.h @@ -0,0 +1,22 @@ +#pragma once +#include "CoreMinimal.h" + +class FSlateStyleSet; +class ISlateStyle; + + +class FAvalonStyle +{ +public: + static void Initialize(); + static void Shutdown(); + static const ISlateStyle& Get(); + static FName GetStyleSetName(); + static FName GetContextName(); + + static void SetIcon(const FString& StyleName, const FString& ResourcePath); + +private: + static TUniquePtr< FSlateStyleSet > Create(); + static TUniquePtr< FSlateStyleSet > AvalonStyleInstance; +}; \ No newline at end of file