mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Implementation of a new splash screen
This commit is contained in:
parent
3a62ce57b0
commit
c58778194f
6 changed files with 793 additions and 18 deletions
|
|
@ -3,7 +3,11 @@
|
|||
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 +26,7 @@ class UnrealPrelaunchHook(PreLaunchHook):
|
|||
shell script.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
|
@ -58,6 +63,70 @@ 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 +206,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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
338
openpype/hosts/unreal/ue_workers.py
Normal file
338
openpype/hosts/unreal/ue_workers.py
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
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):
|
||||
|
||||
|
||||
ue_id = ".".join(self.ue_version.split(".")[:2])
|
||||
# get unreal engine identifier
|
||||
# -------------------------------------------------------------------------
|
||||
# FIXME (antirotor): As of 4.26 this is problem with UE4 built from
|
||||
# sources. In that case Engine ID is calculated per machine/user and not
|
||||
# from Engine files as this code then reads. This then prevents UE4
|
||||
# to directly open project as it will complain about project being
|
||||
# created in different UE4 version. When user convert such project
|
||||
# to his UE4 version, Engine ID is replaced in uproject file. If some
|
||||
# other user tries to open it, it will present him with similar error.
|
||||
|
||||
# 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 out'
|
||||
f' of {stage_count}')
|
||||
|
||||
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)
|
||||
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")
|
||||
102
openpype/widgets/README.md
Normal file
102
openpype/widgets/README.md
Normal 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`).
|
||||
253
openpype/widgets/splash_screen.py
Normal file
253
openpype/widgets/splash_screen.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue