[Automated] Merged develop into main

This commit is contained in:
ynbot 2023-03-11 04:25:43 +01:00 committed by GitHub
commit 2777fe7cf8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 868 additions and 50 deletions

View file

@ -39,3 +39,5 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin):
context.data["frameEnd"] = int(end)
context.data["frameStartHandle"] = int(global_start)
context.data["frameEndHandle"] = int(global_end)
context.data["handleStart"] = int(start) - int(global_start)
context.data["handleEnd"] = int(global_end) - int(end)

View file

@ -39,6 +39,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin):
"frameEnd": context.data["frameEnd"],
"frameStartHandle": context.data["frameStartHandle"],
"frameEndHandle": context.data["frameStartHandle"],
"handleStart": context.data["handleStart"],
"handleEnd": context.data["handleEnd"],
"fps": context.data["fps"],
})

View file

@ -26,6 +26,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
"rig",
"camerarig",
"staticMesh",
"skeletalMesh",
"mvLook"]
representations = ["ma", "abc", "fbx", "mb"]

View file

@ -222,18 +222,21 @@ class LoadClip(plugin.NukeLoader):
"""
representation = deepcopy(representation)
context = representation["context"]
template = representation["data"]["template"]
# Get the frame from the context and hash it
frame = context["frame"]
hashed_frame = "#" * len(str(frame))
# Replace the frame with the hash in the originalBasename
if (
"{originalBasename}" in template
and "frame" in context
"{originalBasename}" in representation["data"]["template"]
):
frame = context["frame"]
hashed_frame = "#" * len(str(frame))
origin_basename = context["originalBasename"]
context["originalBasename"] = origin_basename.replace(
frame, hashed_frame
)
# Replace the frame with the hash in the frame
representation["context"]["frame"] = hashed_frame
return representation

View file

@ -3,7 +3,14 @@
import os
import copy
from pathlib import Path
from openpype.widgets.splash_screen import SplashScreen
from qtpy import QtCore
from openpype.hosts.unreal.ue_workers import (
UEProjectGenerationWorker,
UEPluginInstallWorker
)
from openpype import resources
from openpype.lib import (
PreLaunchHook,
ApplicationLaunchFailed,
@ -22,6 +29,7 @@ class UnrealPrelaunchHook(PreLaunchHook):
shell script.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -58,6 +66,78 @@ class UnrealPrelaunchHook(PreLaunchHook):
# Return filename
return filled_anatomy[workfile_template_key]["file"]
def exec_plugin_install(self, engine_path: Path, env: dict = None):
# set up the QThread and worker with necessary signals
env = env or os.environ
q_thread = QtCore.QThread()
ue_plugin_worker = UEPluginInstallWorker()
q_thread.started.connect(ue_plugin_worker.run)
ue_plugin_worker.setup(engine_path, env)
ue_plugin_worker.moveToThread(q_thread)
splash_screen = SplashScreen(
"Installing plugin",
resources.get_resource("app_icons", "ue4.png")
)
# set up the splash screen with necessary triggers
ue_plugin_worker.installing.connect(
splash_screen.update_top_label_text
)
ue_plugin_worker.progress.connect(splash_screen.update_progress)
ue_plugin_worker.log.connect(splash_screen.append_log)
ue_plugin_worker.finished.connect(splash_screen.quit_and_close)
ue_plugin_worker.failed.connect(splash_screen.fail)
splash_screen.start_thread(q_thread)
splash_screen.show_ui()
if not splash_screen.was_proc_successful():
raise ApplicationLaunchFailed("Couldn't run the application! "
"Plugin failed to install!")
def exec_ue_project_gen(self,
engine_version: str,
unreal_project_name: str,
engine_path: Path,
project_dir: Path):
self.log.info((
f"{self.signature} Creating unreal "
f"project [ {unreal_project_name} ]"
))
q_thread = QtCore.QThread()
ue_project_worker = UEProjectGenerationWorker()
ue_project_worker.setup(
engine_version,
unreal_project_name,
engine_path,
project_dir
)
ue_project_worker.moveToThread(q_thread)
q_thread.started.connect(ue_project_worker.run)
splash_screen = SplashScreen(
"Initializing UE project",
resources.get_resource("app_icons", "ue4.png")
)
ue_project_worker.stage_begin.connect(
splash_screen.update_top_label_text
)
ue_project_worker.progress.connect(splash_screen.update_progress)
ue_project_worker.log.connect(splash_screen.append_log)
ue_project_worker.finished.connect(splash_screen.quit_and_close)
ue_project_worker.failed.connect(splash_screen.fail)
splash_screen.start_thread(q_thread)
splash_screen.show_ui()
if not splash_screen.was_proc_successful():
raise ApplicationLaunchFailed("Couldn't run the application! "
"Failed to generate the project!")
def execute(self):
"""Hook entry method."""
workdir = self.launch_context.env["AVALON_WORKDIR"]
@ -137,23 +217,18 @@ class UnrealPrelaunchHook(PreLaunchHook):
if self.launch_context.env.get(env_key):
os.environ[env_key] = self.launch_context.env[env_key]
engine_path = detected[engine_version]
engine_path: Path = Path(detected[engine_version])
unreal_lib.try_installing_plugin(Path(engine_path), os.environ)
if not unreal_lib.check_plugin_existence(engine_path):
self.exec_plugin_install(engine_path)
project_file = project_path / unreal_project_filename
if not project_file.is_file():
self.log.info((
f"{self.signature} creating unreal "
f"project [ {unreal_project_name} ]"
))
unreal_lib.create_unreal_project(
unreal_project_name,
engine_version,
project_path,
engine_path=Path(engine_path)
)
if not project_file.is_file():
self.exec_ue_project_gen(engine_version,
unreal_project_name,
engine_path,
project_path)
self.launch_context.env["OPENPYPE_UNREAL_VERSION"] = engine_version
# Append project file to launch arguments

View file

@ -30,7 +30,7 @@ void UAssetContainer::OnAssetAdded(const FAssetData& AssetData)
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClassPath.ToString();
FString assetFName = AssetData.ObjectPath.ToString();
UE_LOG(LogTemp, Log, TEXT("asset name %s"), *assetFName);
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
@ -60,7 +60,7 @@ void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData)
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClassPath.ToString();
FString assetFName = AssetData.ObjectPath.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
@ -93,7 +93,7 @@ void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString&
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClassPath.ToString();
FString assetFName = AssetData.ObjectPath.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);

View file

@ -252,7 +252,7 @@ def create_unreal_project(project_name: str,
with open(project_file.as_posix(), mode="r+") as pf:
pf_json = json.load(pf)
pf_json["EngineAssociation"] = _get_build_id(engine_path, ue_version)
pf_json["EngineAssociation"] = get_build_id(engine_path, ue_version)
pf.seek(0)
json.dump(pf_json, pf, indent=4)
pf.truncate()
@ -338,7 +338,7 @@ def get_path_to_ubt(engine_path: Path, ue_version: str) -> Path:
return Path(u_build_tool_path)
def _get_build_id(engine_path: Path, ue_version: str) -> str:
def get_build_id(engine_path: Path, ue_version: str) -> str:
ue_modules = Path()
if platform.system().lower() == "windows":
ue_modules_path = engine_path / "Engine/Binaries/Win64"
@ -365,6 +365,26 @@ def _get_build_id(engine_path: Path, ue_version: str) -> str:
return "{" + loaded_modules.get("BuildId") + "}"
def check_plugin_existence(engine_path: Path, env: dict = None) -> bool:
env = env or os.environ
integration_plugin_path: Path = Path(env.get("OPENPYPE_UNREAL_PLUGIN", ""))
if not os.path.isdir(integration_plugin_path):
raise RuntimeError("Path to the integration plugin is null!")
# Create a path to the plugin in the engine
op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype"
if not op_plugin_path.is_dir():
return False
if not (op_plugin_path / "Binaries").is_dir() \
or not (op_plugin_path / "Intermediate").is_dir():
return False
return True
def try_installing_plugin(engine_path: Path, env: dict = None) -> None:
env = env or os.environ
@ -377,7 +397,6 @@ def try_installing_plugin(engine_path: Path, env: dict = None) -> None:
op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype"
if not op_plugin_path.is_dir():
print("--- OpenPype Plugin is not present. Installing ...")
op_plugin_path.mkdir(parents=True, exist_ok=True)
engine_plugin_config_path: Path = op_plugin_path / "Config"
@ -387,7 +406,6 @@ def try_installing_plugin(engine_path: Path, env: dict = None) -> None:
if not (op_plugin_path / "Binaries").is_dir() \
or not (op_plugin_path / "Intermediate").is_dir():
print("--- Binaries are not present. Building the plugin ...")
_build_and_move_plugin(engine_path, op_plugin_path, env)

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
"""Loader for layouts."""
import json
import collections
from pathlib import Path
import unreal
@ -12,9 +13,7 @@ from unreal import FBXImportType
from unreal import MovieSceneLevelVisibilityTrack
from unreal import MovieSceneSubTrack
from bson.objectid import ObjectId
from openpype.client import get_asset_by_name, get_assets
from openpype.client import get_asset_by_name, get_assets, get_representations
from openpype.pipeline import (
discover_loader_plugins,
loaders_from_representation,
@ -410,6 +409,30 @@ class LayoutLoader(plugin.Loader):
return sequence, (min_frame, max_frame)
def _get_repre_docs_by_version_id(self, data):
version_ids = {
element.get("version")
for element in data
if element.get("representation")
}
version_ids.discard(None)
output = collections.defaultdict(list)
if not version_ids:
return output
project_name = legacy_io.active_project()
repre_docs = get_representations(
project_name,
representation_names=["fbx", "abc"],
version_ids=version_ids,
fields=["_id", "parent", "name"]
)
for repre_doc in repre_docs:
version_id = str(repre_doc["parent"])
output[version_id].append(repre_doc)
return output
def _process(self, lib_path, asset_dir, sequence, repr_loaded=None):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
@ -429,31 +452,21 @@ class LayoutLoader(plugin.Loader):
loaded_assets = []
repre_docs_by_version_id = self._get_repre_docs_by_version_id(data)
for element in data:
representation = None
repr_format = None
if element.get('representation'):
# representation = element.get('representation')
self.log.info(element.get("version"))
valid_formats = ['fbx', 'abc']
repr_data = legacy_io.find_one({
"type": "representation",
"parent": ObjectId(element.get("version")),
"name": {"$in": valid_formats}
})
repr_format = repr_data.get('name')
if not repr_data:
repre_docs = repre_docs_by_version_id[element.get("version")]
if not repre_docs:
self.log.error(
f"No valid representation found for version "
f"{element.get('version')}")
continue
repre_doc = repre_docs[0]
representation = str(repre_doc["_id"])
repr_format = repre_doc["name"]
representation = str(repr_data.get('_id'))
print(representation)
# This is to keep compatibility with old versions of the
# json format.
elif element.get('reference_fbx'):

View file

@ -0,0 +1,335 @@
import json
import os
import platform
import re
import subprocess
from distutils import dir_util
from pathlib import Path
from typing import List
import openpype.hosts.unreal.lib as ue_lib
from qtpy import QtCore
def parse_comp_progress(line: str, progress_signal: QtCore.Signal(int)) -> int:
match = re.search('\[[1-9]+/[0-9]+\]', line)
if match is not None:
split: list[str] = match.group().split('/')
curr: float = float(split[0][1:])
total: float = float(split[1][:-1])
progress_signal.emit(int((curr / total) * 100.0))
def parse_prj_progress(line: str, progress_signal: QtCore.Signal(int)) -> int:
match = re.search('@progress', line)
if match is not None:
percent_match = re.search('\d{1,3}', line)
progress_signal.emit(int(percent_match.group()))
class UEProjectGenerationWorker(QtCore.QObject):
finished = QtCore.Signal(str)
failed = QtCore.Signal(str)
progress = QtCore.Signal(int)
log = QtCore.Signal(str)
stage_begin = QtCore.Signal(str)
ue_version: str = None
project_name: str = None
env = None
engine_path: Path = None
project_dir: Path = None
dev_mode = False
def setup(self, ue_version: str,
project_name,
engine_path: Path,
project_dir: Path,
dev_mode: bool = False,
env: dict = None):
self.ue_version = ue_version
self.project_dir = project_dir
self.env = env or os.environ
preset = ue_lib.get_project_settings(
project_name
)["unreal"]["project_setup"]
if dev_mode or preset["dev_mode"]:
self.dev_mode = True
self.project_name = project_name
self.engine_path = engine_path
def run(self):
# engine_path should be the location of UE_X.X folder
ue_editor_exe = ue_lib.get_editor_exe_path(self.engine_path,
self.ue_version)
cmdlet_project = ue_lib.get_path_to_cmdlet_project(self.ue_version)
project_file = self.project_dir / f"{self.project_name}.uproject"
print("--- Generating a new project ...")
# 1st stage
stage_count = 2
if self.dev_mode:
stage_count = 4
self.stage_begin.emit(f'Generating a new UE project ... 1 out of '
f'{stage_count}')
commandlet_cmd = [f'{ue_editor_exe.as_posix()}',
f'{cmdlet_project.as_posix()}',
f'-run=OPGenerateProject',
f'{project_file.resolve().as_posix()}']
if self.dev_mode:
commandlet_cmd.append('-GenerateCode')
gen_process = subprocess.Popen(commandlet_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
for line in gen_process.stdout:
decoded_line = line.decode(errors="replace")
print(decoded_line, end='')
self.log.emit(decoded_line)
gen_process.stdout.close()
return_code = gen_process.wait()
if return_code and return_code != 0:
msg = 'Failed to generate ' + self.project_name \
+ f' project! Exited with return code {return_code}'
self.failed.emit(msg, return_code)
raise RuntimeError(msg)
print("--- Project has been generated successfully.")
self.stage_begin.emit(f'Writing the Engine ID of the build UE ... 1'
f' out of {stage_count}')
if not project_file.is_file():
msg = "Failed to write the Engine ID into .uproject file! Can " \
"not read!"
self.failed.emit(msg)
raise RuntimeError(msg)
with open(project_file.as_posix(), mode="r+") as pf:
pf_json = json.load(pf)
pf_json["EngineAssociation"] = ue_lib.get_build_id(
self.engine_path,
self.ue_version
)
print(pf_json["EngineAssociation"])
pf.seek(0)
json.dump(pf_json, pf, indent=4)
pf.truncate()
print(f'--- Engine ID has been written into the project file')
self.progress.emit(90)
if self.dev_mode:
# 2nd stage
self.stage_begin.emit(f'Generating project files ... 2 out of '
f'{stage_count}')
self.progress.emit(0)
ubt_path = ue_lib.get_path_to_ubt(self.engine_path,
self.ue_version)
arch = "Win64"
if platform.system().lower() == "windows":
arch = "Win64"
elif platform.system().lower() == "linux":
arch = "Linux"
elif platform.system().lower() == "darwin":
# we need to test this out
arch = "Mac"
gen_prj_files_cmd = [ubt_path.as_posix(),
"-projectfiles",
f"-project={project_file}",
"-progress"]
gen_proc = subprocess.Popen(gen_prj_files_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
for line in gen_proc.stdout:
decoded_line: str = line.decode(errors='replace')
print(decoded_line, end='')
self.log.emit(decoded_line)
parse_prj_progress(decoded_line, self.progress)
gen_proc.stdout.close()
return_code = gen_proc.wait()
if return_code and return_code != 0:
msg = 'Failed to generate project files! ' \
f'Exited with return code {return_code}'
self.failed.emit(msg, return_code)
raise RuntimeError(msg)
self.stage_begin.emit(f'Building the project ... 3 out of '
f'{stage_count}')
self.progress.emit(0)
# 3rd stage
build_prj_cmd = [ubt_path.as_posix(),
f"-ModuleWithSuffix={self.project_name},3555",
arch,
"Development",
"-TargetType=Editor",
f'-Project={project_file}',
f'{project_file}',
"-IgnoreJunk"]
build_prj_proc = subprocess.Popen(build_prj_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
for line in build_prj_proc.stdout:
decoded_line: str = line.decode(errors='replace')
print(decoded_line, end='')
self.log.emit(decoded_line)
parse_comp_progress(decoded_line, self.progress)
build_prj_proc.stdout.close()
return_code = build_prj_proc.wait()
if return_code and return_code != 0:
msg = 'Failed to build project! ' \
f'Exited with return code {return_code}'
self.failed.emit(msg, return_code)
raise RuntimeError(msg)
# ensure we have PySide2 installed in engine
self.progress.emit(0)
self.stage_begin.emit(f'Checking PySide2 installation... {stage_count}'
f' out of {stage_count}')
python_path = None
if platform.system().lower() == "windows":
python_path = self.engine_path / ("Engine/Binaries/ThirdParty/"
"Python3/Win64/python.exe")
if platform.system().lower() == "linux":
python_path = self.engine_path / ("Engine/Binaries/ThirdParty/"
"Python3/Linux/bin/python3")
if platform.system().lower() == "darwin":
python_path = self.engine_path / ("Engine/Binaries/ThirdParty/"
"Python3/Mac/bin/python3")
if not python_path:
msg = "Unsupported platform"
self.failed.emit(msg, 1)
raise NotImplementedError(msg)
if not python_path.exists():
msg = f"Unreal Python not found at {python_path}"
self.failed.emit(msg, 1)
raise RuntimeError(msg)
subprocess.check_call(
[python_path.as_posix(), "-m", "pip", "install", "pyside2"]
)
self.progress.emit(100)
self.finished.emit("Project successfully built!")
class UEPluginInstallWorker(QtCore.QObject):
finished = QtCore.Signal(str)
installing = QtCore.Signal(str)
failed = QtCore.Signal(str, int)
progress = QtCore.Signal(int)
log = QtCore.Signal(str)
engine_path: Path = None
env = None
def setup(self, engine_path: Path, env: dict = None, ):
self.engine_path = engine_path
self.env = env or os.environ
def _build_and_move_plugin(self, plugin_build_path: Path):
uat_path: Path = ue_lib.get_path_to_uat(self.engine_path)
src_plugin_dir = Path(self.env.get("OPENPYPE_UNREAL_PLUGIN", ""))
if not os.path.isdir(src_plugin_dir):
msg = "Path to the integration plugin is null!"
self.failed.emit(msg, 1)
raise RuntimeError(msg)
if not uat_path.is_file():
msg = "Building failed! Path to UAT is invalid!"
self.failed.emit(msg, 1)
raise RuntimeError(msg)
temp_dir: Path = src_plugin_dir.parent / "Temp"
temp_dir.mkdir(exist_ok=True)
uplugin_path: Path = src_plugin_dir / "OpenPype.uplugin"
# in order to successfully build the plugin,
# It must be built outside the Engine directory and then moved
build_plugin_cmd: List[str] = [f'{uat_path.as_posix()}',
'BuildPlugin',
f'-Plugin={uplugin_path.as_posix()}',
f'-Package={temp_dir.as_posix()}']
build_proc = subprocess.Popen(build_plugin_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
for line in build_proc.stdout:
decoded_line: str = line.decode(errors='replace')
print(decoded_line, end='')
self.log.emit(decoded_line)
parse_comp_progress(decoded_line, self.progress)
build_proc.stdout.close()
return_code = build_proc.wait()
if return_code and return_code != 0:
msg = 'Failed to build plugin' \
f' project! Exited with return code {return_code}'
self.failed.emit(msg, return_code)
raise RuntimeError(msg)
# Copy the contents of the 'Temp' dir into the
# 'OpenPype' directory in the engine
dir_util.copy_tree(temp_dir.as_posix(),
plugin_build_path.as_posix())
# We need to also copy the config folder.
# The UAT doesn't include the Config folder in the build
plugin_install_config_path: Path = plugin_build_path / "Config"
src_plugin_config_path = src_plugin_dir / "Config"
dir_util.copy_tree(src_plugin_config_path.as_posix(),
plugin_install_config_path.as_posix())
dir_util.remove_tree(temp_dir.as_posix())
def run(self):
src_plugin_dir = Path(self.env.get("OPENPYPE_UNREAL_PLUGIN", ""))
if not os.path.isdir(src_plugin_dir):
msg = "Path to the integration plugin is null!"
self.failed.emit(msg, 1)
raise RuntimeError(msg)
# Create a path to the plugin in the engine
op_plugin_path = self.engine_path / "Engine/Plugins/Marketplace" \
"/OpenPype"
if not op_plugin_path.is_dir():
self.installing.emit("Installing and building the plugin ...")
op_plugin_path.mkdir(parents=True, exist_ok=True)
engine_plugin_config_path = op_plugin_path / "Config"
engine_plugin_config_path.mkdir(exist_ok=True)
dir_util._path_created = {}
if not (op_plugin_path / "Binaries").is_dir() \
or not (op_plugin_path / "Intermediate").is_dir():
self.installing.emit("Building the plugin ...")
print("--- Building the plugin...")
self._build_and_move_plugin(op_plugin_path)
self.finished.emit("Plugin successfully installed")

View file

@ -10,7 +10,7 @@
"type": "number",
"key": "fps",
"label": "Frame Rate",
"decimal": 2,
"decimal": 3,
"minimum": 0
},
{

View file

@ -83,15 +83,18 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate):
decimals(int): How many decimal points can be used. Float will be used
as value if is higher than 0.
"""
def __init__(self, minimum, maximum, decimals, *args, **kwargs):
def __init__(self, minimum, maximum, decimals, step, *args, **kwargs):
super(NumberDelegate, self).__init__(*args, **kwargs)
self.minimum = minimum
self.maximum = maximum
self.decimals = decimals
self.step = step
def createEditor(self, parent, option, index):
if self.decimals > 0:
editor = DoubleSpinBoxScrollFixed(parent)
editor.setSingleStep(self.step)
editor.setDecimals(self.decimals)
else:
editor = SpinBoxScrollFixed(parent)

View file

@ -26,10 +26,11 @@ class NameDef:
class NumberDef:
def __init__(self, minimum=None, maximum=None, decimals=None):
def __init__(self, minimum=None, maximum=None, decimals=None, step=None):
self.minimum = 0 if minimum is None else minimum
self.maximum = 999999999 if maximum is None else maximum
self.decimals = 0 if decimals is None else decimals
self.step = 1 if decimals is None else step
class TypeDef:
@ -73,14 +74,14 @@ class HierarchyView(QtWidgets.QTreeView):
"type": TypeDef(),
"frameStart": NumberDef(1),
"frameEnd": NumberDef(1),
"fps": NumberDef(1, decimals=2),
"fps": NumberDef(1, decimals=3, step=1),
"resolutionWidth": NumberDef(0),
"resolutionHeight": NumberDef(0),
"handleStart": NumberDef(0),
"handleEnd": NumberDef(0),
"clipIn": NumberDef(1),
"clipOut": NumberDef(1),
"pixelAspect": NumberDef(0, decimals=2),
"pixelAspect": NumberDef(0, decimals=2, step=0.01),
"tools_env": ToolsDef()
}
@ -96,6 +97,10 @@ class HierarchyView(QtWidgets.QTreeView):
"stretch": QtWidgets.QHeaderView.Interactive,
"width": 140
},
"fps": {
"stretch": QtWidgets.QHeaderView.Interactive,
"width": 65
},
"tools_env": {
"stretch": QtWidgets.QHeaderView.Interactive,
"width": 200
@ -148,7 +153,8 @@ class HierarchyView(QtWidgets.QTreeView):
delegate = NumberDelegate(
item_type.minimum,
item_type.maximum,
item_type.decimals
item_type.decimals,
item_type.step
)
elif isinstance(item_type, TypeDef):

102
openpype/widgets/README.md Normal file
View file

@ -0,0 +1,102 @@
# Widgets
## Splash Screen
This widget is used for executing a monitoring progress of a process which has been executed on a different thread.
To properly use this widget certain preparation has to be done in order to correctly execute the process and show the
splash screen.
### Prerequisites
In order to run a function or an operation on another thread, a `QtCore.QObject` class needs to be created with the
desired code. The class has to have a method as an entry point for the thread to execute the code.
For utilizing the functionalities of the splash screen, certain signals need to be declared to let it know what is
happening in the thread and how is it progressing. It is also recommended to have a function to set up certain variables
which are needed in the worker's code
For example:
```python
from qtpy import QtCore
class ExampleWorker(QtCore.QObject):
finished = QtCore.Signal()
failed = QtCore.Signal(str)
progress = QtCore.Signal(int)
log = QtCore.Signal(str)
stage_begin = QtCore.Signal(str)
foo = None
bar = None
def run(self):
# The code goes here
print("Hello world!")
self.finished.emit()
def setup(self,
foo: str,
bar: str,):
self.foo = foo
self.bar = bar
```
### Creating the splash screen
```python
import os
from qtpy import QtCore
from pathlib import Path
from openpype.widgets.splash_screen import SplashScreen
from openpype import resources
def exec_plugin_install( engine_path: Path, env: dict = None):
env = env or os.environ
q_thread = QtCore.QThread()
example_worker = ExampleWorker()
q_thread.started.connect(example_worker.run)
example_worker.setup(engine_path, env)
example_worker.moveToThread(q_thread)
splash_screen = SplashScreen("Executing process ...",
resources.get_openpype_icon_filepath())
# set up the splash screen with necessary events
example_worker.installing.connect(splash_screen.update_top_label_text)
example_worker.progress.connect(splash_screen.update_progress)
example_worker.log.connect(splash_screen.append_log)
example_worker.finished.connect(splash_screen.quit_and_close)
example_worker.failed.connect(splash_screen.fail)
splash_screen.start_thread(q_thread)
splash_screen.show_ui()
```
In this example code, before executing the process the worker needs to be instantiated and moved onto a newly created
`QtCore.QThread` object. After this, needed signals have to be connected to the desired slots to make full use of
the splash screen. Finally, the `start_thread` and `show_ui` is called.
**Note that when the `show_ui` function is called the thread is blocked until the splash screen quits automatically, or
it is closed by the user in case the process fails! The `start_thread` method in that case must be called before
showing the UI!**
The most important signals are
```python
q_thread.started.connect(example_worker.run)
```
and
```python
example_worker.finished.connect(splash_screen.quit_and_close)
```
These ensure that when the `start_thread` method is called (which takes as a parameter the `QtCore.QThread` object and
saves it as a reference), the `QThread` object starts and signals the worker to
start executing its own code. Once the worker is done and emits a signal that it has finished with the `quit_and_close`
slot, the splash screen quits the `QtCore.QThread` and closes itself.
It is highly recommended to also use the `fail` slot in case an exception or other error occurs during the execution of
the worker's code (You would use in this case the `failed` signal in the `ExampleWorker`).

View file

@ -0,0 +1,258 @@
from qtpy import QtWidgets, QtCore, QtGui
from openpype import style, resources
from igniter.nice_progress_bar import NiceProgressBar
class SplashScreen(QtWidgets.QDialog):
"""Splash screen for executing a process on another thread. It is able
to inform about the progress of the process and log given information.
"""
splash_icon = None
top_label = None
show_log_btn: QtWidgets.QLabel = None
progress_bar = None
log_text: QtWidgets.QLabel = None
scroll_area: QtWidgets.QScrollArea = None
close_btn: QtWidgets.QPushButton = None
scroll_bar: QtWidgets.QScrollBar = None
is_log_visible = False
is_scroll_auto = True
thread_return_code = None
q_thread: QtCore.QThread = None
def __init__(self,
window_title: str,
splash_icon=None,
window_icon=None):
"""
Args:
window_title (str): String which sets the window title
splash_icon (str | bytes | None): A resource (pic) which is used
for the splash icon
window_icon (str | bytes | None: A resource (pic) which is used for
the window's icon
"""
super(SplashScreen, self).__init__()
if splash_icon is None:
splash_icon = resources.get_openpype_icon_filepath()
if window_icon is None:
window_icon = resources.get_openpype_icon_filepath()
self.splash_icon = splash_icon
self.setWindowIcon(QtGui.QIcon(window_icon))
self.setWindowTitle(window_title)
self.init_ui()
def was_proc_successful(self) -> bool:
if self.thread_return_code == 0:
return True
return False
def start_thread(self, q_thread: QtCore.QThread):
"""Saves the reference to this thread and starts it.
Args:
q_thread (QtCore.QThread): A QThread containing a given worker
(QtCore.QObject)
Returns:
None
"""
if not q_thread:
raise RuntimeError("Failed to run a worker thread! "
"The thread is null!")
self.q_thread = q_thread
self.q_thread.start()
@QtCore.Slot()
def quit_and_close(self):
"""Quits the thread and closes the splash screen. Note that this means
the thread has exited with the return code 0!
Returns:
None
"""
self.thread_return_code = 0
self.q_thread.quit()
self.close()
@QtCore.Slot()
def toggle_log(self):
if self.is_log_visible:
self.scroll_area.hide()
width = self.width()
self.adjustSize()
self.resize(width, self.height())
else:
self.scroll_area.show()
self.scroll_bar.setValue(self.scroll_bar.maximum())
self.resize(self.width(), 300)
self.is_log_visible = not self.is_log_visible
def show_ui(self):
"""Shows the splash screen. BEWARE THAT THIS FUNCTION IS BLOCKING
(The execution of code can not proceed further beyond this function
until the splash screen is closed!)
Returns:
None
"""
self.show()
self.exec_()
def init_ui(self):
self.resize(450, 100)
self.setMinimumWidth(250)
self.setStyleSheet(style.load_stylesheet())
# Top Section
self.top_label = QtWidgets.QLabel(self)
self.top_label.setText("Starting process ...")
self.top_label.setWordWrap(True)
icon = QtWidgets.QLabel(self)
icon.setPixmap(QtGui.QPixmap(self.splash_icon))
icon.setFixedHeight(45)
icon.setFixedWidth(45)
icon.setScaledContents(True)
self.close_btn = QtWidgets.QPushButton(self)
self.close_btn.setText("Quit")
self.close_btn.clicked.connect(self.close)
self.close_btn.setFixedWidth(80)
self.close_btn.hide()
self.show_log_btn = QtWidgets.QPushButton(self)
self.show_log_btn.setText("Show log")
self.show_log_btn.setFixedWidth(80)
self.show_log_btn.clicked.connect(self.toggle_log)
button_layout = QtWidgets.QVBoxLayout()
button_layout.addWidget(self.show_log_btn)
button_layout.addWidget(self.close_btn)
# Progress Bar
self.progress_bar = NiceProgressBar()
self.progress_bar.setValue(0)
self.progress_bar.setAlignment(QtCore.Qt.AlignTop)
# Log Content
self.scroll_area = QtWidgets.QScrollArea(self)
self.scroll_area.hide()
log_widget = QtWidgets.QWidget(self.scroll_area)
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setHorizontalScrollBarPolicy(
QtCore.Qt.ScrollBarAlwaysOn
)
self.scroll_area.setVerticalScrollBarPolicy(
QtCore.Qt.ScrollBarAlwaysOn
)
self.scroll_area.setWidget(log_widget)
self.scroll_bar = self.scroll_area.verticalScrollBar()
self.scroll_bar.sliderMoved.connect(self.on_scroll)
self.log_text = QtWidgets.QLabel(self)
self.log_text.setText('')
self.log_text.setAlignment(QtCore.Qt.AlignTop)
log_layout = QtWidgets.QVBoxLayout(log_widget)
log_layout.addWidget(self.log_text)
top_layout = QtWidgets.QHBoxLayout()
top_layout.setAlignment(QtCore.Qt.AlignTop)
top_layout.addWidget(icon)
top_layout.addSpacing(10)
top_layout.addWidget(self.top_label)
top_layout.addSpacing(10)
top_layout.addLayout(button_layout)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addLayout(top_layout)
main_layout.addSpacing(10)
main_layout.addWidget(self.progress_bar)
main_layout.addSpacing(10)
main_layout.addWidget(self.scroll_area)
self.setWindowFlags(
QtCore.Qt.Window
| QtCore.Qt.CustomizeWindowHint
| QtCore.Qt.WindowTitleHint
| QtCore.Qt.WindowMinimizeButtonHint
)
desktop_rect = QtWidgets.QApplication.desktop().availableGeometry(self)
center = desktop_rect.center()
self.move(
center.x() - (self.width() * 0.5),
center.y() - (self.height() * 0.5)
)
@QtCore.Slot(int)
def update_progress(self, value: int):
self.progress_bar.setValue(value)
@QtCore.Slot(str)
def update_top_label_text(self, text: str):
self.top_label.setText(text)
@QtCore.Slot(str, str)
def append_log(self, text: str, end: str = ''):
"""A slot used for receiving log info and appending it to scroll area's
content.
Args:
text (str): A log text that will append to the current one in the
scroll area.
end (str): end string which can be appended to the end of the given
line (for ex. a line break).
Returns:
None
"""
self.log_text.setText(self.log_text.text() + text + end)
if self.is_scroll_auto:
self.scroll_bar.setValue(self.scroll_bar.maximum())
@QtCore.Slot(int)
def on_scroll(self, position: int):
"""
A slot for the vertical scroll bar's movement. This ensures the
auto-scrolling feature of the scroll area when the scroll bar is at its
maximum value.
Args:
position (int): Position value of the scroll bar.
Returns:
None
"""
if self.scroll_bar.maximum() == position:
self.is_scroll_auto = True
return
self.is_scroll_auto = False
@QtCore.Slot(str, int)
def fail(self, text: str, return_code: int = 1):
"""
A slot used for signals which can emit when a worker (process) has
failed. at this moment the splash screen doesn't close by itself.
it has to be closed by the user.
Args:
text (str): A text which can be set to the top label.
Returns:
return_code (int): Return code of the thread's code
"""
self.top_label.setText(text)
self.close_btn.show()
self.thread_return_code = return_code
self.q_thread.exit(return_code)