Merge branch 'develop' into feature/OP-4146_nuke-workfile-template-builder-add-creator-plugins

This commit is contained in:
Jakub Jezek 2023-01-04 14:56:35 +01:00
commit efbbc91c73
No known key found for this signature in database
GPG key ID: 730D7C02726179A7
53 changed files with 520 additions and 142 deletions

View file

@ -276,8 +276,8 @@ class CreatorWidget(QtWidgets.QDialog):
elif v["type"] == "QSpinBox":
data[k]["value"] = self.create_row(
content_layout, "QSpinBox", v["label"],
setRange=(1, 9999999), setValue=v["value"],
setToolTip=tool_tip)
setValue=v["value"], setMinimum=0,
setMaximum=100000, setToolTip=tool_tip)
return data

View file

@ -41,7 +41,7 @@ class ExtractThumnail(publish.Extractor):
track_item_name, thumb_frame, ".png")
thumb_path = os.path.join(staging_dir, thumb_file)
thumbnail = track_item.thumbnail(thumb_frame).save(
thumbnail = track_item.thumbnail(thumb_frame, "colour").save(
thumb_path,
format='png'
)

View file

@ -34,7 +34,7 @@ class CreateHDA(plugin.HoudiniCreator):
}
return subset_name.lower() in existing_subset_names_low
def _create_instance_node(
def create_instance_node(
self, node_name, parent, node_type="geometry"):
import hou

View file

@ -25,7 +25,7 @@ class CollectInstanceActiveState(pyblish.api.InstancePlugin):
# Check bypass state and reverse
active = True
node = hou.node(instance.get("instance_node"))
node = hou.node(instance.data.get("instance_node"))
if hasattr(node, "isBypassed"):
active = not node.isBypassed()

View file

@ -69,7 +69,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
def process(self, instance):
rop = hou.node(instance.get("instance_node"))
rop = hou.node(instance.data.get("instance_node"))
# Collect chunkSize
chunk_size_parm = rop.parm("chunkSize")

View file

@ -21,7 +21,7 @@ class CollectUsdLayers(pyblish.api.InstancePlugin):
self.log.debug("No output node found..")
return
rop_node = hou.node(instance.get("instance_node"))
rop_node = hou.node(instance.data["instance_node"])
save_layers = []
for layer in usdlib.get_configured_save_layers(rop_node):
@ -56,6 +56,7 @@ class CollectUsdLayers(pyblish.api.InstancePlugin):
layer_inst.data["subset"] = "__stub__"
layer_inst.data["label"] = label
layer_inst.data["asset"] = instance.data["asset"]
layer_inst.data["instance_node"] = instance.data["instance_node"]
# include same USD ROP
layer_inst.append(rop_node)
# include layer data

View file

@ -17,7 +17,7 @@ class ExtractRedshiftProxy(publish.Extractor):
def process(self, instance):
ropnode = hou.node(instance.get("instance_node"))
ropnode = hou.node(instance.data.get("instance_node"))
# Get the filename from the filename parameter
# `.evalParm(parameter)` will make sure all tokens are resolved

View file

@ -18,7 +18,7 @@ class ExtractUSD(publish.Extractor):
def process(self, instance):
ropnode = hou.node(instance.get("instance_node"))
ropnode = hou.node(instance.data.get("instance_node"))
# Get the filename from the filename parameter
output = ropnode.evalParm("lopoutput")

View file

@ -187,7 +187,7 @@ class ExtractUSDLayered(publish.Extractor):
# Main ROP node, either a USD Rop or ROP network with
# multiple USD ROPs
node = hou.node(instance.get("instance_node"))
node = hou.node(instance.data["instance_node"])
# Collect any output dependencies that have not been processed yet
# during extraction of other instances

View file

@ -17,7 +17,7 @@ class ExtractVDBCache(publish.Extractor):
def process(self, instance):
ropnode = hou.node(instance.get("instance_node"))
ropnode = hou.node(instance.data["instance_node"])
# Get the filename from the filename parameter
# `.evalParm(parameter)` will make sure all tokens are resolved

View file

@ -37,8 +37,7 @@ class ValidateAnimationSettings(pyblish.api.InstancePlugin):
@classmethod
def get_invalid(cls, instance):
node = hou.node(instance.get("instance_node"))
node = hou.node(instance.data["instance_node"])
# Check trange parm, 0 means Render Current Frame
frame_range = node.evalParm("trange")
if frame_range == 0:

View file

@ -37,6 +37,6 @@ class ValidateBypassed(pyblish.api.InstancePlugin):
@classmethod
def get_invalid(cls, instance):
rop = hou.node(instance.get("instance_node"))
rop = hou.node(instance.data["instance_node"])
if hasattr(rop, "isBypassed") and rop.isBypassed():
return [rop]

View file

@ -48,7 +48,7 @@ class ValidateCopOutputNode(pyblish.api.InstancePlugin):
)
if output_node is None:
node = hou.node(instance.get("instance_node"))
node = hou.node(instance.data.get("instance_node"))
cls.log.error(
"COP Output node in '%s' does not exist. "
"Ensure a valid COP output path is set." % node.path()

View file

@ -37,8 +37,7 @@ class ValidateFrameToken(pyblish.api.InstancePlugin):
@classmethod
def get_invalid(cls, instance):
node = hou.node(instance.get("instance_node"))
node = hou.node(instance.data["instance_node"])
# Check trange parm, 0 means Render Current Frame
frame_range = node.evalParm("trange")
if frame_range == 0:

View file

@ -38,7 +38,7 @@ class ValidateNoErrors(pyblish.api.InstancePlugin):
validate_nodes = []
if len(instance) > 0:
validate_nodes.append(hou.node(instance.get("instance_node")))
validate_nodes.append(hou.node(instance.data.get("instance_node")))
output_node = instance.data.get("output_node")
if output_node:
validate_nodes.append(output_node)

View file

@ -28,7 +28,7 @@ class ValidateUSDLayerPathBackslashes(pyblish.api.InstancePlugin):
def process(self, instance):
rop = hou.node(instance.get("instance_node"))
rop = hou.node(instance.data.get("instance_node"))
lop_path = hou_usdlib.get_usd_rop_loppath(rop)
stage = lop_path.stage(apply_viewport_overrides=False)

View file

@ -40,7 +40,7 @@ class ValidateUsdModel(pyblish.api.InstancePlugin):
def process(self, instance):
rop = hou.node(instance.get("instance_node"))
rop = hou.node(instance.data.get("instance_node"))
lop_path = hou_usdlib.get_usd_rop_loppath(rop)
stage = lop_path.stage(apply_viewport_overrides=False)

View file

@ -36,7 +36,7 @@ class ValidateUSDOutputNode(pyblish.api.InstancePlugin):
output_node = instance.data["output_node"]
if output_node is None:
node = hou.node(instance.get("instance_node"))
node = hou.node(instance.data.get("instance_node"))
cls.log.error(
"USD node '%s' LOP path does not exist. "
"Ensure a valid LOP path is set." % node.path()

View file

@ -24,7 +24,7 @@ class ValidateUsdSetDress(pyblish.api.InstancePlugin):
from pxr import UsdGeom
import hou
rop = hou.node(instance.get("instance_node"))
rop = hou.node(instance.data.get("instance_node"))
lop_path = hou_usdlib.get_usd_rop_loppath(rop)
stage = lop_path.stage(apply_viewport_overrides=False)

View file

@ -20,7 +20,7 @@ class ValidateUsdShadeWorkspace(pyblish.api.InstancePlugin):
def process(self, instance):
rop = hou.node(instance.get("instance_node"))
rop = hou.node(instance.data.get("instance_node"))
workspace = rop.parent()
definition = workspace.type().definition()

View file

@ -38,7 +38,7 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin):
if node is None:
cls.log.error(
"SOP path is not correctly set on "
"ROP node '%s'." % instance.get("instance_node")
"ROP node '%s'." % instance.data.get("instance_node")
)
return [instance]

View file

@ -23,8 +23,6 @@ class CameraWindow(QtWidgets.QDialog):
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
self.camera = None
self.static_image_plane = False
self.show_in_all_views = False
self.widgets = {
"label": QtWidgets.QLabel("Select camera for image plane."),
@ -45,8 +43,6 @@ class CameraWindow(QtWidgets.QDialog):
for camera in cameras:
self.widgets["list"].addItem(camera)
self.widgets["staticImagePlane"].setText("Make Image Plane Static")
self.widgets["showInAllViews"].setText("Show Image Plane in All Views")
# Build buttons.
layout = QtWidgets.QHBoxLayout(self.widgets["buttons"])
@ -57,8 +53,6 @@ class CameraWindow(QtWidgets.QDialog):
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.widgets["label"])
layout.addWidget(self.widgets["list"])
layout.addWidget(self.widgets["staticImagePlane"])
layout.addWidget(self.widgets["showInAllViews"])
layout.addWidget(self.widgets["buttons"])
layout.addWidget(self.widgets["warning"])
@ -73,8 +67,6 @@ class CameraWindow(QtWidgets.QDialog):
if self.camera is None:
self.widgets["warning"].setVisible(True)
return
self.show_in_all_views = self.widgets["showInAllViews"].isChecked()
self.static_image_plane = self.widgets["staticImagePlane"].isChecked()
self.close()
@ -82,7 +74,6 @@ class CameraWindow(QtWidgets.QDialog):
self.camera = None
self.close()
class ImagePlaneLoader(load.LoaderPlugin):
"""Specific loader of plate for image planes on selected camera."""
@ -106,12 +97,10 @@ class ImagePlaneLoader(load.LoaderPlugin):
# Get camera from user selection.
camera = None
is_static_image_plane = None
is_in_all_views = None
# is_static_image_plane = None
# is_in_all_views = None
if data:
camera = pm.PyNode(data.get("camera"))
is_static_image_plane = data.get("static_image_plane")
is_in_all_views = data.get("in_all_views")
if not camera:
cameras = pm.ls(type="camera")
@ -119,11 +108,11 @@ class ImagePlaneLoader(load.LoaderPlugin):
camera_names["Create new camera."] = "create_camera"
window = CameraWindow(camera_names.keys())
window.exec_()
# Skip if no camera was selected (Dialog was closed)
if window.camera not in camera_names:
return
camera = camera_names[window.camera]
is_static_image_plane = window.static_image_plane
is_in_all_views = window.show_in_all_views
if camera == "create_camera":
camera = pm.createNode("camera")
@ -139,18 +128,14 @@ class ImagePlaneLoader(load.LoaderPlugin):
# Create image plane
image_plane_transform, image_plane_shape = pm.imagePlane(
fileName=context["representation"]["data"]["path"],
camera=camera, showInAllViews=is_in_all_views
)
camera=camera)
image_plane_shape.depth.set(image_plane_depth)
if is_static_image_plane:
image_plane_shape.detach()
image_plane_transform.setRotation(camera.getRotation())
start_frame = pm.playbackOptions(q=True, min=True)
end_frame = pm.playbackOptions(q=True, max=True)
image_plane_shape.frameOffset.set(1 - start_frame)
image_plane_shape.frameOffset.set(0)
image_plane_shape.frameIn.set(start_frame)
image_plane_shape.frameOut.set(end_frame)
image_plane_shape.frameCache.set(end_frame)
@ -180,9 +165,17 @@ class ImagePlaneLoader(load.LoaderPlugin):
QtWidgets.QMessageBox.Cancel
)
if reply == QtWidgets.QMessageBox.Ok:
pm.delete(
image_plane_shape.listConnections(type="expression")[0]
)
# find the input and output of frame extension
expressions = image_plane_shape.frameExtension.inputs()
frame_ext_output = image_plane_shape.frameExtension.outputs()
if expressions:
# the "time1" node is non-deletable attr
# in Maya, use disconnectAttr instead
pm.disconnectAttr(expressions, frame_ext_output)
if not image_plane_shape.frameExtension.isFreeToChange():
raise RuntimeError("Can't set frame extension for {}".format(image_plane_shape)) # noqa
# get the node of time instead and set the time for it.
image_plane_shape.frameExtension.set(start_frame)
new_nodes.extend(
@ -233,7 +226,8 @@ class ImagePlaneLoader(load.LoaderPlugin):
)
start_frame = asset["data"]["frameStart"]
end_frame = asset["data"]["frameEnd"]
image_plane_shape.frameOffset.set(1 - start_frame)
image_plane_shape.frameOffset.set(0)
image_plane_shape.frameIn.set(start_frame)
image_plane_shape.frameOut.set(end_frame)
image_plane_shape.frameCache.set(end_frame)

View file

@ -0,0 +1,2 @@
[/Script/OpenPype.OpenPypeSettings]
FolderColor=(R=91,G=197,B=220,A=255)

View file

@ -42,6 +42,7 @@ public class OpenPype : ModuleRules
"Engine",
"Slate",
"SlateCore",
"AssetTools"
// ... add private dependencies that you statically link with here ...
}
);

View file

@ -1,6 +1,11 @@
#include "OpenPype.h"
#include "ISettingsContainer.h"
#include "ISettingsModule.h"
#include "ISettingsSection.h"
#include "LevelEditor.h"
#include "OpenPypePythonBridge.h"
#include "OpenPypeSettings.h"
#include "OpenPypeStyle.h"
@ -11,13 +16,12 @@ static const FName OpenPypeTabName("OpenPype");
// This function is triggered when the plugin is staring up
void FOpenPypeModule::StartupModule()
{
FOpenPypeStyle::Initialize();
FOpenPypeStyle::SetIcon("Logo", "openpype40");
// Create the Extender that will add content to the menu
FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
TSharedPtr<FExtender> MenuExtender = MakeShareable(new FExtender());
TSharedPtr<FExtender> ToolbarExtender = MakeShareable(new FExtender());
@ -37,6 +41,7 @@ void FOpenPypeModule::StartupModule()
LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender);
LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender);
RegisterSettings();
}
void FOpenPypeModule::ShutdownModule()
@ -64,7 +69,6 @@ void FOpenPypeModule::AddMenuEntry(FMenuBuilder& MenuBuilder)
FSlateIcon(FOpenPypeStyle::GetStyleSetName(), "OpenPype.Logo"),
FUIAction(FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuDialog))
);
}
MenuBuilder.EndSection();
}
@ -89,13 +93,58 @@ void FOpenPypeModule::AddToobarEntry(FToolBarBuilder& ToolbarBuilder)
ToolbarBuilder.EndSection();
}
void FOpenPypeModule::RegisterSettings()
{
ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked<ISettingsModule>("Settings");
void FOpenPypeModule::MenuPopup() {
// Create the new category
// TODO: After the movement of the plugin from the game to editor, it might be necessary to move this!
ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project");
UOpenPypeSettings* Settings = GetMutableDefault<UOpenPypeSettings>();
// Register the settings
ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "OpenPype", "General",
LOCTEXT("RuntimeGeneralSettingsName",
"General"),
LOCTEXT("RuntimeGeneralSettingsDescription",
"Base configuration for Open Pype Module"),
Settings
);
// Register the save handler to your settings, you might want to use it to
// validate those or just act to settings changes.
if (SettingsSection.IsValid())
{
SettingsSection->OnModified().BindRaw(this, &FOpenPypeModule::HandleSettingsSaved);
}
}
bool FOpenPypeModule::HandleSettingsSaved()
{
UOpenPypeSettings* Settings = GetMutableDefault<UOpenPypeSettings>();
bool ResaveSettings = false;
// You can put any validation code in here and resave the settings in case an invalid
// value has been entered
if (ResaveSettings)
{
Settings->SaveConfig();
}
return true;
}
void FOpenPypeModule::MenuPopup()
{
UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get();
bridge->RunInPython_Popup();
}
void FOpenPypeModule::MenuDialog() {
void FOpenPypeModule::MenuDialog()
{
UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get();
bridge->RunInPython_Dialog();
}

View file

@ -1,4 +1,6 @@
#include "OpenPypeLib.h"
#include "AssetViewUtils.h"
#include "Misc/Paths.h"
#include "Misc/ConfigCacheIni.h"
#include "UObject/UnrealType.h"
@ -10,21 +12,23 @@
* @warning This color will appear only after Editor restart. Is there a better way?
*/
void UOpenPypeLib::CSetFolderColor(FString FolderPath, FLinearColor FolderColor, bool bForceAdd)
bool UOpenPypeLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd)
{
auto SaveColorInternal = [](FString InPath, FLinearColor InFolderColor)
if (AssetViewUtils::DoesFolderExist(FolderPath))
{
// Saves the color of the folder to the config
if (FPaths::FileExists(GEditorPerProjectIni))
{
GConfig->SetString(TEXT("PathColor"), *InPath, *InFolderColor.ToString(), GEditorPerProjectIni);
}
const TSharedPtr<FLinearColor> LinearColor = MakeShared<FLinearColor>(FolderColor);
};
SaveColorInternal(FolderPath, FolderColor);
AssetViewUtils::SaveColor(FolderPath, LinearColor, true);
UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(),
*FolderPath)
return true;
}
UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"),
*FolderColor.ToString(), *FolderPath)
return false;
}
/**
* Returns all poperties on given object
* @param cls - class

View file

@ -3,6 +3,8 @@
#include "OpenPypePublishInstance.h"
#include "AssetRegistryModule.h"
#include "NotificationManager.h"
#include "OpenPypeLib.h"
#include "OpenPypeSettings.h"
#include "SNotificationList.h"
//Moves all the invalid pointers to the end to prepare them for the shrinking
@ -36,6 +38,11 @@ UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& Obj
AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UOpenPypePublishInstance::OnAssetCreated);
AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UOpenPypePublishInstance::OnAssetRemoved);
AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UOpenPypePublishInstance::OnAssetUpdated);
#ifdef WITH_EDITOR
ColorOpenPypeDirs();
#endif
}
void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData)
@ -58,7 +65,7 @@ void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData)
if (AssetDataInternal.Emplace(Asset).IsValidId())
{
UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"),
*this->GetName(), *Asset->GetName());
*this->GetName(), *Asset->GetName());
}
}
}
@ -96,6 +103,48 @@ bool UOpenPypePublishInstance::IsUnderSameDir(const UObject* InAsset) const
#ifdef WITH_EDITOR
void UOpenPypePublishInstance::ColorOpenPypeDirs()
{
FString PathName = this->GetPathName();
//Check whether the path contains the defined OpenPype folder
if (!PathName.Contains(TEXT("OpenPype"))) return;
//Get the base path for open pype
FString PathLeft, PathRight;
PathName.Split(FString("OpenPype"), &PathLeft, &PathRight);
if (PathLeft.IsEmpty() || PathRight.IsEmpty())
{
UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base OpenPype directory!"))
return;
}
PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive);
//Get the current settings
const UOpenPypeSettings* Settings = GetMutableDefault<UOpenPypeSettings>();
//Color the base folder
UOpenPypeLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false);
//Get Sub paths, iterate through them and color them according to the folder color in UOpenPypeSettings
const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(
"AssetRegistry");
TArray<FString> PathList;
AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true);
if (PathList.Num() > 0)
{
for (const FString& Path : PathList)
{
UOpenPypeLib::SetFolderColor(Path, Settings->GetFolderFColor(), false);
}
}
}
void UOpenPypePublishInstance::SendNotification(const FString& Text) const
{
FNotificationInfo Info{FText::FromString(Text)};

View file

@ -0,0 +1,21 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "OpenPypeSettings.h"
#include "IPluginManager.h"
#include "UObjectGlobals.h"
/**
* Mainly is used for initializing default values if the DefaultOpenPypeSettings.ini file does not exist in the saved config
*/
UOpenPypeSettings::UOpenPypeSettings(const FObjectInitializer& ObjectInitializer)
{
const FString ConfigFilePath = OPENPYPE_SETTINGS_FILEPATH;
// This has to be probably in the future set using the UE Reflection system
FColor Color;
GConfig->GetColor(TEXT("/Script/OpenPype.OpenPypeSettings"), TEXT("FolderColor"), Color, ConfigFilePath);
FolderColor = Color;
}

View file

@ -12,10 +12,11 @@ public:
virtual void ShutdownModule() override;
private:
void RegisterSettings();
bool HandleSettingsSaved();
void AddMenuEntry(FMenuBuilder& MenuBuilder);
void AddToobarEntry(FToolBarBuilder& ToolbarBuilder);
void MenuPopup();
void MenuDialog();
};

View file

@ -5,14 +5,14 @@
UCLASS(Blueprintable)
class OPENPYPE_API UOpenPypeLib : public UObject
class OPENPYPE_API UOpenPypeLib : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = Python)
static void CSetFolderColor(FString FolderPath, FLinearColor FolderColor, bool bForceAdd);
static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const bool& bForceAdd);
UFUNCTION(BlueprintCallable, Category = Python)
static TArray<FString> GetAllProperties(UClass* cls);

View file

@ -8,10 +8,8 @@ UCLASS(Blueprintable)
class OPENPYPE_API UOpenPypePublishInstance : public UPrimaryDataAsset
{
GENERATED_UCLASS_BODY()
public:
/**
/**
* Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is
* placed in)
@ -58,8 +56,10 @@ public:
UFUNCTION(BlueprintCallable, BlueprintPure)
TSet<UObject*> GetAllAssets() const
{
const TSet<TSoftObjectPtr<UObject>>& IteratedSet = bAddExternalAssets ? AssetDataInternal.Union(AssetDataExternal) : AssetDataInternal;
const TSet<TSoftObjectPtr<UObject>>& IteratedSet = bAddExternalAssets
? AssetDataInternal.Union(AssetDataExternal)
: AssetDataInternal;
//Create a new TSet only with raw pointers.
TSet<UObject*> ResultSet;
@ -69,12 +69,10 @@ public:
return ResultSet;
}
private:
UPROPERTY(VisibleAnywhere, Category="Assets")
TSet<TSoftObjectPtr<UObject>> AssetDataInternal;
/**
* This property allows exposing the array to include other assets from any other directory than what it's currently
* monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added!
@ -93,11 +91,11 @@ private:
bool IsUnderSameDir(const UObject* InAsset) const;
#ifdef WITH_EDITOR
void ColorOpenPypeDirs();
void SendNotification(const FString& Text) const;
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
};

View file

@ -0,0 +1,32 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Object.h"
#include "OpenPypeSettings.generated.h"
#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultOpenPypeSettings.ini")
UCLASS(Config=OpenPypeSettings, DefaultConfig)
class OPENPYPE_API UOpenPypeSettings : public UObject
{
GENERATED_UCLASS_BODY()
UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings)
FColor GetFolderFColor() const
{
return FolderColor;
}
UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings)
FLinearColor GetFolderFLinearColor() const
{
return FLinearColor(FolderColor);
}
protected:
UPROPERTY(config, EditAnywhere, Category = Folders)
FColor FolderColor = FColor(25,45,223);
};

View file

@ -0,0 +1,2 @@
[/Script/OpenPype.OpenPypeSettings]
FolderColor=(R=91,G=197,B=220,A=255)

View file

@ -48,6 +48,7 @@ public class OpenPype : ModuleRules
"Engine",
"Slate",
"SlateCore",
"AssetTools"
// ... add private dependencies that you statically link with here ...
}
);

View file

@ -1,8 +1,12 @@
#include "OpenPype.h"
#include "ISettingsContainer.h"
#include "ISettingsModule.h"
#include "ISettingsSection.h"
#include "OpenPypeStyle.h"
#include "OpenPypeCommands.h"
#include "OpenPypePythonBridge.h"
#include "LevelEditor.h"
#include "OpenPypeSettings.h"
#include "Misc/MessageDialog.h"
#include "ToolMenus.h"
@ -29,7 +33,10 @@ void FOpenPypeModule::StartupModule()
FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuDialog),
FCanExecuteAction());
UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FOpenPypeModule::RegisterMenus));
UToolMenus::RegisterStartupCallback(
FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FOpenPypeModule::RegisterMenus));
RegisterSettings();
}
void FOpenPypeModule::ShutdownModule()
@ -43,6 +50,50 @@ void FOpenPypeModule::ShutdownModule()
FOpenPypeCommands::Unregister();
}
void FOpenPypeModule::RegisterSettings()
{
ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked<ISettingsModule>("Settings");
// Create the new category
// TODO: After the movement of the plugin from the game to editor, it might be necessary to move this!
ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project");
UOpenPypeSettings* Settings = GetMutableDefault<UOpenPypeSettings>();
// Register the settings
ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "OpenPype", "General",
LOCTEXT("RuntimeGeneralSettingsName",
"General"),
LOCTEXT("RuntimeGeneralSettingsDescription",
"Base configuration for Open Pype Module"),
Settings
);
// Register the save handler to your settings, you might want to use it to
// validate those or just act to settings changes.
if (SettingsSection.IsValid())
{
SettingsSection->OnModified().BindRaw(this, &FOpenPypeModule::HandleSettingsSaved);
}
}
bool FOpenPypeModule::HandleSettingsSaved()
{
UOpenPypeSettings* Settings = GetMutableDefault<UOpenPypeSettings>();
bool ResaveSettings = false;
// You can put any validation code in here and resave the settings in case an invalid
// value has been entered
if (ResaveSettings)
{
Settings->SaveConfig();
}
return true;
}
void FOpenPypeModule::RegisterMenus()
{
// Owner will be used for cleanup in call to UToolMenus::UnregisterOwner
@ -64,7 +115,8 @@ void FOpenPypeModule::RegisterMenus()
{
FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools");
{
FToolMenuEntry& Entry = Section.AddEntry(FToolMenuEntry::InitToolBarButton(FOpenPypeCommands::Get().OpenPypeTools));
FToolMenuEntry& Entry = Section.AddEntry(
FToolMenuEntry::InitToolBarButton(FOpenPypeCommands::Get().OpenPypeTools));
Entry.SetCommandList(PluginCommands);
}
}
@ -72,12 +124,14 @@ void FOpenPypeModule::RegisterMenus()
}
void FOpenPypeModule::MenuPopup() {
void FOpenPypeModule::MenuPopup()
{
UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get();
bridge->RunInPython_Popup();
}
void FOpenPypeModule::MenuDialog() {
void FOpenPypeModule::MenuDialog()
{
UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get();
bridge->RunInPython_Dialog();
}

View file

@ -1,4 +1,6 @@
#include "OpenPypeLib.h"
#include "AssetViewUtils.h"
#include "Misc/Paths.h"
#include "Misc/ConfigCacheIni.h"
#include "UObject/UnrealType.h"
@ -10,21 +12,23 @@
* @warning This color will appear only after Editor restart. Is there a better way?
*/
void UOpenPypeLib::CSetFolderColor(FString FolderPath, FLinearColor FolderColor, bool bForceAdd)
bool UOpenPypeLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd)
{
auto SaveColorInternal = [](FString InPath, FLinearColor InFolderColor)
if (AssetViewUtils::DoesFolderExist(FolderPath))
{
// Saves the color of the folder to the config
if (FPaths::FileExists(GEditorPerProjectIni))
{
GConfig->SetString(TEXT("PathColor"), *InPath, *InFolderColor.ToString(), GEditorPerProjectIni);
}
const TSharedPtr<FLinearColor> LinearColor = MakeShared<FLinearColor>(FolderColor);
};
SaveColorInternal(FolderPath, FolderColor);
AssetViewUtils::SaveColor(FolderPath, LinearColor, true);
UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(),
*FolderPath)
return true;
}
UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"),
*FolderColor.ToString(), *FolderPath)
return false;
}
/**
* Returns all poperties on given object
* @param cls - class

View file

@ -4,8 +4,11 @@
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetToolsModule.h"
#include "Framework/Notifications/NotificationManager.h"
#include "OpenPypeLib.h"
#include "OpenPypeSettings.h"
#include "Widgets/Notifications/SNotificationList.h"
//Moves all the invalid pointers to the end to prepare them for the shrinking
#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \
VAR.Shrink();
@ -16,8 +19,11 @@ UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& Obj
const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<
FAssetRegistryModule>("AssetRegistry");
const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>(
"PropertyEditor");
FString Left, Right;
GetPathName().Split(GetName(), &Left, &Right);
GetPathName().Split("/" + GetName(), &Left, &Right);
FARFilter Filter;
Filter.PackagePaths.Emplace(FName(Left));
@ -34,15 +40,17 @@ UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& Obj
AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UOpenPypePublishInstance::OnAssetCreated);
AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UOpenPypePublishInstance::OnAssetRemoved);
AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UOpenPypePublishInstance::OnAssetUpdated);
#ifdef WITH_EDITOR
ColorOpenPypeDirs();
#endif
}
void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData)
{
TArray<FString> split;
const TObjectPtr<UObject> Asset = InAssetData.GetAsset();
UObject* Asset = InAssetData.GetAsset();
if (!IsValid(Asset))
{
@ -58,7 +66,7 @@ void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData)
if (AssetDataInternal.Emplace(Asset).IsValidId())
{
UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"),
*this->GetName(), *Asset->GetName());
*this->GetName(), *Asset->GetName());
}
}
}
@ -86,7 +94,7 @@ void UOpenPypePublishInstance::OnAssetUpdated(const FAssetData& InAssetData)
REMOVE_INVALID_ENTRIES(AssetDataExternal);
}
bool UOpenPypePublishInstance::IsUnderSameDir(const TObjectPtr<UObject>& InAsset) const
bool UOpenPypePublishInstance::IsUnderSameDir(const UObject* InAsset) const
{
FString ThisLeft, ThisRight;
this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight);
@ -96,6 +104,48 @@ bool UOpenPypePublishInstance::IsUnderSameDir(const TObjectPtr<UObject>& InAsset
#ifdef WITH_EDITOR
void UOpenPypePublishInstance::ColorOpenPypeDirs()
{
FString PathName = this->GetPathName();
//Check whether the path contains the defined OpenPype folder
if (!PathName.Contains(TEXT("OpenPype"))) return;
//Get the base path for open pype
FString PathLeft, PathRight;
PathName.Split(FString("OpenPype"), &PathLeft, &PathRight);
if (PathLeft.IsEmpty() || PathRight.IsEmpty())
{
UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base OpenPype directory!"))
return;
}
PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive);
//Get the current settings
const UOpenPypeSettings* Settings = GetMutableDefault<UOpenPypeSettings>();
//Color the base folder
UOpenPypeLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false);
//Get Sub paths, iterate through them and color them according to the folder color in UOpenPypeSettings
const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(
"AssetRegistry");
TArray<FString> PathList;
AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true);
if (PathList.Num() > 0)
{
for (const FString& Path : PathList)
{
UOpenPypeLib::SetFolderColor(Path, Settings->GetFolderFColor(), false);
}
}
}
void UOpenPypePublishInstance::SendNotification(const FString& Text) const
{
FNotificationInfo Info{FText::FromString(Text)};
@ -125,16 +175,15 @@ void UOpenPypePublishInstance::PostEditChangeProperty(FPropertyChangedEvent& Pro
PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(
UOpenPypePublishInstance, AssetDataExternal))
{
// Check for duplicated assets
for (const auto& Asset : AssetDataInternal)
{
if (AssetDataExternal.Contains(Asset))
{
AssetDataExternal.Remove(Asset);
return SendNotification("You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!");
return SendNotification(
"You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!");
}
}
// Check if no UOpenPypePublishInstance type assets are included

View file

@ -0,0 +1,21 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "OpenPypeSettings.h"
#include "Interfaces/IPluginManager.h"
#include "UObject/UObjectGlobals.h"
/**
* Mainly is used for initializing default values if the DefaultOpenPypeSettings.ini file does not exist in the saved config
*/
UOpenPypeSettings::UOpenPypeSettings(const FObjectInitializer& ObjectInitializer)
{
const FString ConfigFilePath = OPENPYPE_SETTINGS_FILEPATH;
// This has to be probably in the future set using the UE Reflection system
FColor Color;
GConfig->GetColor(TEXT("/Script/OpenPype.OpenPypeSettings"), TEXT("FolderColor"), Color, ConfigFilePath);
FolderColor = Color;
}

View file

@ -14,6 +14,8 @@ public:
private:
void RegisterMenus();
void RegisterSettings();
bool HandleSettingsSaved();
void MenuPopup();
void MenuDialog();

View file

@ -5,14 +5,14 @@
UCLASS(Blueprintable)
class OPENPYPE_API UOpenPypeLib : public UObject
class OPENPYPE_API UOpenPypeLib : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = Python)
static void CSetFolderColor(FString FolderPath, FLinearColor FolderColor, bool bForceAdd);
static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const bool& bForceAdd);
UFUNCTION(BlueprintCallable, Category = Python)
static TArray<FString> GetAllProperties(UClass* cls);

View file

@ -8,7 +8,9 @@ UCLASS(Blueprintable)
class OPENPYPE_API UOpenPypePublishInstance : public UPrimaryDataAsset
{
GENERATED_UCLASS_BODY()
public:
/**
/**
* Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is
* placed in)
@ -55,8 +57,10 @@ public:
UFUNCTION(BlueprintCallable, BlueprintPure)
TSet<UObject*> GetAllAssets() const
{
const TSet<TSoftObjectPtr<UObject>>& IteratedSet = bAddExternalAssets ? AssetDataInternal.Union(AssetDataExternal) : AssetDataInternal;
const TSet<TSoftObjectPtr<UObject>>& IteratedSet = bAddExternalAssets
? AssetDataInternal.Union(AssetDataExternal)
: AssetDataInternal;
//Create a new TSet only with raw pointers.
TSet<UObject*> ResultSet;
@ -71,24 +75,26 @@ private:
TSet<TSoftObjectPtr<UObject>> AssetDataInternal;
/**
* This property allows the instance to include other assets from any other directory than what it's currently
* monitoring.
* @attention assets have to be added manually! They are not automatically registered or added!
* This property allows exposing the array to include other assets from any other directory than what it's currently
* monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added!
*/
UPROPERTY(EditAnywhere, Category="Assets")
UPROPERTY(EditAnywhere, Category = "Assets")
bool bAddExternalAssets = false;
UPROPERTY(EditAnywhere, Category="Assets", meta=(EditCondition="bAddExternalAssets"))
UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets")
TSet<TSoftObjectPtr<UObject>> AssetDataExternal;
void OnAssetCreated(const FAssetData& InAssetData);
void OnAssetRemoved(const FAssetData& InAssetData);
void OnAssetUpdated(const FAssetData& InAssetData);
bool IsUnderSameDir(const TObjectPtr<UObject>& InAsset) const;
bool IsUnderSameDir(const UObject* InAsset) const;
#ifdef WITH_EDITOR
void ColorOpenPypeDirs();
void SendNotification(const FString& Text) const;
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;

View file

@ -0,0 +1,32 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "OpenPypeSettings.generated.h"
#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultOpenPypeSettings.ini")
UCLASS(Config=OpenPypeSettings, DefaultConfig)
class OPENPYPE_API UOpenPypeSettings : public UObject
{
GENERATED_UCLASS_BODY()
UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings)
FColor GetFolderFColor() const
{
return FolderColor;
}
UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings)
FLinearColor GetFolderFLinearColor() const
{
return FLinearColor(FolderColor);
}
protected:
UPROPERTY(config, EditAnywhere, Category = Folders)
FColor FolderColor = FColor(25,45,223);
};

View file

@ -1,4 +1,5 @@
import os
import re
import six
import pyblish.api
import copy
@ -132,14 +133,14 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin):
fill_key = "task[{}]".format(key)
fill_pairs.append((fill_key, value))
self.log.debug("fill_pairs ::{}".format(fill_pairs))
multiple_case_variants = prepare_template_data(fill_pairs)
fill_data.update(multiple_case_variants)
message = None
message = ''
try:
message = message_templ.format(**fill_data)
message = self._escape_missing_keys(message_templ, fill_data).\
format(**fill_data)
except Exception:
# shouldn't happen
self.log.warning(
"Some keys are missing in {}".format(message_templ),
exc_info=True)
@ -162,17 +163,21 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin):
def _get_review_path(self, instance):
"""Returns abs url for review if present in instance repres"""
published_path = None
review_path = None
for repre in instance.data.get("representations", []):
tags = repre.get('tags', [])
if (repre.get("review")
or "review" in tags
or "burnin" in tags):
if os.path.exists(repre["published_path"]):
published_path = repre["published_path"]
repre_review_path = (
repre.get("published_path") or
os.path.join(repre["stagingDir"], repre["files"])
)
if os.path.exists(repre_review_path):
review_path = repre_review_path
if "burnin" in tags: # burnin has precedence if exists
break
return published_path
return review_path
def _python2_call(self, token, channel, message, publish_files):
from slackclient import SlackClient
@ -263,3 +268,22 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin):
msg = " - application must added to channel '{}'.".format(channel)
error_str += msg + " Ask Slack admin."
return error_str
def _escape_missing_keys(self, message, fill_data):
"""Double escapes placeholder which are missing in 'fill_data'"""
placeholder_keys = re.findall("\{([^}]+)\}", message)
fill_keys = []
for key, value in fill_data.items():
fill_keys.append(key)
if isinstance(value, dict):
for child_key in value.keys():
fill_keys.append("{}[{}]".format(key, child_key))
not_matched = set(placeholder_keys) - set(fill_keys)
for not_matched_item in not_matched:
message = message.replace("{}".format(not_matched_item),
"{{{}}}".format(not_matched_item))
return message

View file

@ -22,7 +22,7 @@ class DropboxHandler(AbstractProvider):
)
return
if not self.presets["enabled"]:
if not self.presets.get("enabled"):
self.log.debug("Sync Server: Site {} not enabled for {}.".
format(site_name, project_name))
return

View file

@ -74,7 +74,7 @@ class GDriveHandler(AbstractProvider):
)
return
if not self.presets["enabled"]:
if not self.presets.get("enabled"):
self.log.debug(
"Sync Server: Site {} not enabled for {}.".format(
site_name, project_name

View file

@ -5,6 +5,7 @@ import threading
import time
from openpype.lib import Logger
from openpype.lib.local_settings import get_local_site_id
from openpype.pipeline import Anatomy
from .abstract_provider import AbstractProvider
@ -220,6 +221,6 @@ class LocalDriveHandler(AbstractProvider):
def _normalize_site_name(self, site_name):
"""Transform user id to 'local' for Local settings"""
if site_name != 'studio':
if site_name == get_local_site_id():
return 'local'
return site_name

View file

@ -72,7 +72,7 @@ class SFTPHandler(AbstractProvider):
Returns:
(boolean)
"""
return self.presets["enabled"] and self.conn is not None
return self.presets.get("enabled") and self.conn is not None
@classmethod
def get_system_settings_schema(cls):

View file

@ -1368,13 +1368,19 @@ class SyncServerModule(OpenPypeModule, ITrayModule):
"""
sync_sett = self.sync_system_settings
project_enabled = True
project_settings = None
if project_name:
project_enabled = project_name in self.get_enabled_projects()
project_settings = self.get_sync_project_setting(project_name)
sync_enabled = sync_sett["enabled"] and project_enabled
system_sites = {}
if sync_enabled:
for site, detail in sync_sett.get("sites", {}).items():
if project_settings:
site_settings = project_settings["sites"].get(site)
if site_settings:
detail.update(site_settings)
system_sites[site] = detail
system_sites.update(self._get_default_site_configs(sync_enabled,
@ -1396,14 +1402,22 @@ class SyncServerModule(OpenPypeModule, ITrayModule):
exclude_locals=True)
roots = {}
for root, config in anatomy_sett["roots"].items():
roots[root] = config[platform.system().lower()]
roots[root] = config
studio_config = {
'enabled': True,
'provider': 'local_drive',
"root": roots
}
all_sites = {self.DEFAULT_SITE: studio_config}
if sync_enabled:
all_sites[get_local_site_id()] = {'provider': 'local_drive'}
all_sites[get_local_site_id()] = {'enabled': True,
'provider': 'local_drive',
"root": roots}
# duplicate values for normalized local name
all_sites["local"] = {
'enabled': True,
'provider': 'local_drive',
"root": roots}
return all_sites
def get_provider_for_site(self, project_name=None, site=None):

View file

@ -24,6 +24,7 @@ from openpype.lib.path_templates import (
FormatObject,
)
from openpype.lib.log import Logger
from openpype.lib import get_local_site_id
log = Logger.get_logger(__name__)
@ -60,6 +61,11 @@ class BaseAnatomy(object):
project_name = project_doc["name"]
self.project_name = project_name
if (site_name and
site_name not in ["studio", "local", get_local_site_id()]):
raise RuntimeError("Anatomy could be created only for default "
"local sites not for {}".format(site_name))
self._site_name = site_name
self._data = self._prepare_anatomy_data(

View file

@ -2,7 +2,7 @@
"deadline_servers": [],
"publish": {
"CollectDefaultDeadlineServer": {
"pass_mongo_url": false
"pass_mongo_url": true
},
"CollectDeadlinePools": {
"primary_pool": "",

View file

@ -248,6 +248,9 @@ class SitesWidget(QtWidgets.QWidget):
main_layout.addWidget(comboboxes_widget, 0)
main_layout.addWidget(content_widget, 1)
active_site_widget.value_changed.connect(self.refresh)
remote_site_widget.value_changed.connect(self.refresh)
self.active_site_widget = active_site_widget
self.remote_site_widget = remote_site_widget
@ -268,25 +271,29 @@ class SitesWidget(QtWidgets.QWidget):
self.modules_manager.modules_by_name["sync_server"]
)
# This is temporary modification
# - whole logic here should be in sync module's providers
site_names = sync_server_module.get_active_sites_from_settings(
self.project_settings["project_settings"].value
)
site_configs = sync_server_module.get_all_site_configs(
self._project_name)
roots_entity = (
self.project_settings[PROJECT_ANATOMY_KEY][LOCAL_ROOTS_KEY]
)
site_names = [self.active_site_widget.current_text(),
self.remote_site_widget.current_text()]
output = []
for site_name in site_names:
if not site_name:
continue
site_inputs = []
for root_name, path_entity in roots_entity.items():
platform_entity = path_entity[platform.system().lower()]
site_config = site_configs[site_name]
for root_name, path_entity in site_config.get("root", {}).items():
if not path_entity:
continue
platform_value = path_entity[platform.system().lower()]
site_inputs.append({
"label": root_name,
"key": root_name,
"value": platform_entity.value
"value": platform_value
})
output.append(
@ -436,6 +443,7 @@ class SitesWidget(QtWidgets.QWidget):
class _SiteCombobox(QtWidgets.QWidget):
input_label = None
value_changed = QtCore.Signal()
def __init__(self, modules_manager, project_settings, parent):
super(_SiteCombobox, self).__init__(parent)
@ -661,6 +669,7 @@ class _SiteCombobox(QtWidgets.QWidget):
self._set_local_settings_value(self.current_text())
self._update_style()
self.value_changed.emit()
def _set_local_settings_value(self, value):
raise NotImplementedError(

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.14.10-nightly.2"
__version__ = "3.14.10-nightly.6"

View file

@ -105,16 +105,19 @@ class ScrollMessageBox(QtWidgets.QDialog):
content_widget = QtWidgets.QWidget(self)
scroll_widget.setWidget(content_widget)
max_len = 0
message_len = 0
content_layout = QtWidgets.QVBoxLayout(content_widget)
for message in messages:
label_widget = QtWidgets.QLabel(message, content_widget)
content_layout.addWidget(label_widget)
max_len = max(max_len, len(message))
message_len = max(message_len, len(message))
# guess size of scrollable area
max_width = QtWidgets.QApplication.desktop().availableGeometry().width
scroll_widget.setMinimumWidth(min(max_width, max_len * 6))
desktop = QtWidgets.QApplication.desktop()
max_width = desktop.availableGeometry().width()
scroll_widget.setMinimumWidth(
min(max_width, message_len * 6)
)
layout.addWidget(scroll_widget)
if not cancelable: # if no specific buttons OK only