mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into bugfix/OP-2865_Loading-maya-reviews-into-resolve
This commit is contained in:
commit
5fcb19bc67
77 changed files with 3245 additions and 1065 deletions
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,8 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.15.8
|
||||
- 3.15.8-nightly.3
|
||||
- 3.15.8-nightly.2
|
||||
- 3.15.8-nightly.1
|
||||
- 3.15.7
|
||||
|
|
@ -133,8 +135,6 @@ body:
|
|||
- 3.14.2-nightly.4
|
||||
- 3.14.2-nightly.3
|
||||
- 3.14.2-nightly.2
|
||||
- 3.14.2-nightly.1
|
||||
- 3.14.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
298
CHANGELOG.md
298
CHANGELOG.md
|
|
@ -1,6 +1,304 @@
|
|||
# Changelog
|
||||
|
||||
|
||||
## [3.15.8](https://github.com/ynput/OpenPype/tree/3.15.8)
|
||||
|
||||
|
||||
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.7...3.15.8)
|
||||
|
||||
### **🆕 New features**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Publisher: Show instances in report page <a href="https://github.com/ynput/OpenPype/pull/4915">#4915</a></summary>
|
||||
|
||||
Show publish instances in report page. Also added basic log view with logs grouped by instance. Validation error detail now have 2 colums, one with erro details second with logs. Crashed state shows fast access to report action buttons. Success will show only logs. Publish frame is shrunked automatically on publish stop.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fusion - Loader plugins updates <a href="https://github.com/ynput/OpenPype/pull/4920">#4920</a></summary>
|
||||
|
||||
Update to some Fusion loader plugins:The sequence loader can now load footage from the image and online family.The FBX loader can now import all formats Fusions FBX node can read.You can now import the content of another workfile into your current comp with the workfile loader.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fusion: deadline farm rendering <a href="https://github.com/ynput/OpenPype/pull/4955">#4955</a></summary>
|
||||
|
||||
Enabling Fusion for deadline farm rendering.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AfterEffects: set frame range and resolution <a href="https://github.com/ynput/OpenPype/pull/4983">#4983</a></summary>
|
||||
|
||||
Frame information (frame start, duration, fps) and resolution (width and height) is applied to selected composition from Asset Management System (Ftrack or DB) automatically when published instance is created.It is also possible explicitly propagate both values from DB to selected composition by newly added menu buttons.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Publish: Enhance automated publish plugin settings <a href="https://github.com/ynput/OpenPype/pull/4986">#4986</a></summary>
|
||||
|
||||
Added plugins option to define settings category where to look for settings of a plugin and added public helper functions to apply settings `get_plugin_settings` and `apply_plugin_settings_automatically`.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🚀 Enhancements**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Load Rig References - Change Rig to Animation in Animation instance <a href="https://github.com/ynput/OpenPype/pull/4877">#4877</a></summary>
|
||||
|
||||
We are using the template builder to build an animation scene. All the rig placeholders are imported correctly, but the automatically created animation instances retain the rig family in their names and subsets. In our example, we need animationMain instead of rigMain, because this name will be used in the following steps like lighting.Here is the result we need. I checked, and it's not a template builder problem, because even if I load a rig as a reference, the result is the same. For me, since we are in the animation instance, it makes more sense to have animation instead of rig in the name. The naming is just fine if we use create from the Openpype menu.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Enhancement: Resolve prelaunch code refactoring and update defaults <a href="https://github.com/ynput/OpenPype/pull/4916">#4916</a></summary>
|
||||
|
||||
The main reason of this PR is wrong default settings in `openpype/settings/defaults/system_settings/applications.json` for Resolve host. The `bin` folder should not be a part of the macos and Linux `RESOLVE_PYTHON3_PATH` variable.The rest of this PR is some code cleanups for Resolve prelaunch hook to simplify further development.Also added a .gitignore for vscode workspace files.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Unreal: 🚚 move Unreal plugin to separate repository <a href="https://github.com/ynput/OpenPype/pull/4980">#4980</a></summary>
|
||||
|
||||
To support Epic Marketplace have to move AYON Unreal integration plugins to separate repository. This is replacing current files with git submodule, so the change should be functionally without impact.New repository lives here: https://github.com/ynput/ayon-unreal-plugin
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>General: Lib code cleanup <a href="https://github.com/ynput/OpenPype/pull/5003">#5003</a></summary>
|
||||
|
||||
Small cleanup in lib files in openpype.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Allow to open with djv by extension instead of representation name <a href="https://github.com/ynput/OpenPype/pull/5004">#5004</a></summary>
|
||||
|
||||
Filter open in djv action by extension instead of representation.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>DJV open action `extensions` as `set` <a href="https://github.com/ynput/OpenPype/pull/5005">#5005</a></summary>
|
||||
|
||||
Change `extensions` attribute to `set`.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: extract thumbnail with multiple reposition nodes <a href="https://github.com/ynput/OpenPype/pull/5011">#5011</a></summary>
|
||||
|
||||
Added support for multiple reposition nodes.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Enhancement: Improve logging levels and messages for artist facing publish reports <a href="https://github.com/ynput/OpenPype/pull/5018">#5018</a></summary>
|
||||
|
||||
Tweak the logging levels and messages to try and only show those logs that an artist should see and could understand. Move anything that's slightly more involved into a "debug" message instead.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🐛 Bug fixes**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bugfix/frame variable fix <a href="https://github.com/ynput/OpenPype/pull/4978">#4978</a></summary>
|
||||
|
||||
Renamed variables to match OpenPype terminology to reduce confusion and add consistency.
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Global: plugins cleanup plugin will leave beauty rendered files <a href="https://github.com/ynput/OpenPype/pull/4790">#4790</a></summary>
|
||||
|
||||
Attempt to mark more files to be cleaned up explicitly in intermediate `renders` folder in work area for farm jobs.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fix: Download last workfile doesn't work if not already downloaded <a href="https://github.com/ynput/OpenPype/pull/4942">#4942</a></summary>
|
||||
|
||||
Some optimization condition is messing with the feature: if the published workfile is not already downloaded, it won't download it...
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Unreal: Fix transform when loading layout to match existing assets <a href="https://github.com/ynput/OpenPype/pull/4972">#4972</a></summary>
|
||||
|
||||
Fixed transform when loading layout to match existing assets.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>fix the bug of fbx loaders in Max <a href="https://github.com/ynput/OpenPype/pull/4977">#4977</a></summary>
|
||||
|
||||
bug fix of fbx loaders for not being able to parent to the CON instances while importing cameras(and models) which is published from other DCCs such as Maya.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AfterEffects: allow returning stub with not saved workfile <a href="https://github.com/ynput/OpenPype/pull/4984">#4984</a></summary>
|
||||
|
||||
Allows to use Workfile app to Save first empty workfile.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Blender: Fix Alembic loading <a href="https://github.com/ynput/OpenPype/pull/4985">#4985</a></summary>
|
||||
|
||||
Fixed problem occurring when trying to load an Alembic model in Blender.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Unreal: Addon Py2 compatibility <a href="https://github.com/ynput/OpenPype/pull/4994">#4994</a></summary>
|
||||
|
||||
Fixed Python 2 compatibility of unreal addon.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: fixed missing files key in representation <a href="https://github.com/ynput/OpenPype/pull/4999">#4999</a></summary>
|
||||
|
||||
Issue with missing keys once rendering target set to existing frames is fixed. Instance has to be evaluated in validation for missing files.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Unreal: Fix the frame range when loading camera <a href="https://github.com/ynput/OpenPype/pull/5002">#5002</a></summary>
|
||||
|
||||
The keyframes of the camera, when loaded, were not using the correct frame range.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fusion: fixing frame range targeting <a href="https://github.com/ynput/OpenPype/pull/5013">#5013</a></summary>
|
||||
|
||||
Frame range targeting at Rendering instances is now following configured options.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Deadline: fix selection from multiple webservices <a href="https://github.com/ynput/OpenPype/pull/5015">#5015</a></summary>
|
||||
|
||||
Multiple different DL webservice could be configured. First they must by configured in System Settings., then they could be configured per project in `project_settings/deadline/deadline_servers`.Only single webservice could be a target of publish though.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **Merged pull requests**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>3dsmax: Refactored publish plugins to use proper implementation of pymxs <a href="https://github.com/ynput/OpenPype/pull/4988">#4988</a></summary>
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.15.7](https://github.com/ynput/OpenPype/tree/3.15.7)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -256,8 +256,11 @@ def switch_item(container,
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def maintained_selection():
|
||||
comp = get_current_comp()
|
||||
def maintained_selection(comp=None):
|
||||
"""Reset comp selection from before the context after the context"""
|
||||
if comp is None:
|
||||
comp = get_current_comp()
|
||||
|
||||
previous_selection = comp.GetToolList(True).values()
|
||||
try:
|
||||
yield
|
||||
|
|
@ -269,6 +272,33 @@ def maintained_selection():
|
|||
flow.Select(tool, True)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def maintained_comp_range(comp=None,
|
||||
global_start=True,
|
||||
global_end=True,
|
||||
render_start=True,
|
||||
render_end=True):
|
||||
"""Reset comp frame ranges from before the context after the context"""
|
||||
if comp is None:
|
||||
comp = get_current_comp()
|
||||
|
||||
comp_attrs = comp.GetAttrs()
|
||||
preserve_attrs = {}
|
||||
if global_start:
|
||||
preserve_attrs["COMPN_GlobalStart"] = comp_attrs["COMPN_GlobalStart"]
|
||||
if global_end:
|
||||
preserve_attrs["COMPN_GlobalEnd"] = comp_attrs["COMPN_GlobalEnd"]
|
||||
if render_start:
|
||||
preserve_attrs["COMPN_RenderStart"] = comp_attrs["COMPN_RenderStart"]
|
||||
if render_end:
|
||||
preserve_attrs["COMPN_RenderEnd"] = comp_attrs["COMPN_RenderEnd"]
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
comp.SetAttrs(preserve_attrs)
|
||||
|
||||
|
||||
def get_frame_path(path):
|
||||
"""Get filename for the Fusion Saver with padded number as '#'
|
||||
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ class CreateSaver(NewCreator):
|
|||
def _get_frame_range_enum(self):
|
||||
frame_range_options = {
|
||||
"asset_db": "Current asset context",
|
||||
"render_range": "From viewer render in/out",
|
||||
"render_range": "From render in/out",
|
||||
"comp_range": "From composition timeline"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,4 +113,4 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin):
|
|||
|
||||
inputs = [c["representation"] for c in containers]
|
||||
instance.data["inputRepresentations"] = inputs
|
||||
self.log.info("Collected inputs: %s" % inputs)
|
||||
self.log.debug("Collected inputs: %s" % inputs)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ class FusionRenderInstance(RenderInstance):
|
|||
tool = attr.ib(default=None)
|
||||
workfileComp = attr.ib(default=None)
|
||||
publish_attributes = attr.ib(default={})
|
||||
frameStartHandle = attr.ib(default=None)
|
||||
frameEndHandle = attr.ib(default=None)
|
||||
|
||||
|
||||
class CollectFusionRender(
|
||||
|
|
@ -83,8 +85,8 @@ class CollectFusionRender(
|
|||
frameEnd=inst.data["frameEnd"],
|
||||
handleStart=inst.data["handleStart"],
|
||||
handleEnd=inst.data["handleEnd"],
|
||||
ignoreFrameHandleCheck=(
|
||||
inst.data["frame_range_source"] == "render_range"),
|
||||
frameStartHandle=inst.data["frameStartHandle"],
|
||||
frameEndHandle=inst.data["frameEndHandle"],
|
||||
frameStep=1,
|
||||
fps=comp_frame_format_prefs.get("Rate"),
|
||||
app_version=comp.GetApp().Version,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import os
|
||||
import logging
|
||||
import contextlib
|
||||
import collections
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.fusion.api import comp_lock_and_undo_chunk
|
||||
from openpype.hosts.fusion.api.lib import get_frame_path
|
||||
from openpype.hosts.fusion.api.lib import get_frame_path, maintained_comp_range
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -52,11 +53,14 @@ class FusionRenderLocal(
|
|||
hosts = ["fusion"]
|
||||
families = ["render.local"]
|
||||
|
||||
is_rendered_key = "_fusionrenderlocal_has_rendered"
|
||||
|
||||
def process(self, instance):
|
||||
context = instance.context
|
||||
|
||||
# Start render
|
||||
self.render_once(context)
|
||||
result = self.render(instance)
|
||||
if result is False:
|
||||
raise RuntimeError(f"Comp render failed for {instance}")
|
||||
|
||||
self._add_representation(instance)
|
||||
|
||||
|
|
@ -69,39 +73,48 @@ class FusionRenderLocal(
|
|||
)
|
||||
)
|
||||
|
||||
def render_once(self, context):
|
||||
"""Render context comp only once, even with more render instances"""
|
||||
def render(self, instance):
|
||||
"""Render instance.
|
||||
|
||||
# This plug-in assumes all render nodes get rendered at the same time
|
||||
# to speed up the rendering. The check below makes sure that we only
|
||||
# execute the rendering once and not for each instance.
|
||||
key = f"__hasRun{self.__class__.__name__}"
|
||||
We try to render the minimal amount of times by combining the instances
|
||||
that have a matching frame range in one Fusion render. Then for the
|
||||
batch of instances we store whether the render succeeded or failed.
|
||||
|
||||
savers_to_render = [
|
||||
# Get the saver tool from the instance
|
||||
instance.data["tool"] for instance in context if
|
||||
# Only active instances
|
||||
instance.data.get("publish", True) and
|
||||
# Only render.local instances
|
||||
"render.local" in instance.data.get("families", [])
|
||||
]
|
||||
"""
|
||||
|
||||
if key not in context.data:
|
||||
# We initialize as false to indicate it wasn't successful yet
|
||||
# so we can keep track of whether Fusion succeeded
|
||||
context.data[key] = False
|
||||
if self.is_rendered_key in instance.data:
|
||||
# This instance was already processed in batch with another
|
||||
# instance, so we just return the render result directly
|
||||
self.log.debug(f"Instance {instance} was already rendered")
|
||||
return instance.data[self.is_rendered_key]
|
||||
|
||||
current_comp = context.data["currentComp"]
|
||||
frame_start = context.data["frameStartHandle"]
|
||||
frame_end = context.data["frameEndHandle"]
|
||||
instances_by_frame_range = self.get_render_instances_by_frame_range(
|
||||
instance.context
|
||||
)
|
||||
|
||||
self.log.info("Starting Fusion render")
|
||||
self.log.info(f"Start frame: {frame_start}")
|
||||
self.log.info(f"End frame: {frame_end}")
|
||||
saver_names = ", ".join(saver.Name for saver in savers_to_render)
|
||||
self.log.info(f"Rendering tools: {saver_names}")
|
||||
# Render matching batch of instances that share the same frame range
|
||||
frame_range = self.get_instance_render_frame_range(instance)
|
||||
render_instances = instances_by_frame_range[frame_range]
|
||||
|
||||
with comp_lock_and_undo_chunk(current_comp):
|
||||
# We initialize render state false to indicate it wasn't successful
|
||||
# yet to keep track of whether Fusion succeeded. This is for cases
|
||||
# where an error below this might cause the comp render result not
|
||||
# to be stored for the instances of this batch
|
||||
for render_instance in render_instances:
|
||||
render_instance.data[self.is_rendered_key] = False
|
||||
|
||||
savers_to_render = [inst.data["tool"] for inst in render_instances]
|
||||
current_comp = instance.context.data["currentComp"]
|
||||
frame_start, frame_end = frame_range
|
||||
|
||||
self.log.info(
|
||||
f"Starting Fusion render frame range {frame_start}-{frame_end}"
|
||||
)
|
||||
saver_names = ", ".join(saver.Name for saver in savers_to_render)
|
||||
self.log.info(f"Rendering tools: {saver_names}")
|
||||
|
||||
with comp_lock_and_undo_chunk(current_comp):
|
||||
with maintained_comp_range(current_comp):
|
||||
with enabled_savers(current_comp, savers_to_render):
|
||||
result = current_comp.Render(
|
||||
{
|
||||
|
|
@ -111,10 +124,11 @@ class FusionRenderLocal(
|
|||
}
|
||||
)
|
||||
|
||||
context.data[key] = bool(result)
|
||||
# Store the render state for all the rendered instances
|
||||
for render_instance in render_instances:
|
||||
render_instance.data[self.is_rendered_key] = bool(result)
|
||||
|
||||
if context.data[key] is False:
|
||||
raise RuntimeError("Comp render failed")
|
||||
return result
|
||||
|
||||
def _add_representation(self, instance):
|
||||
"""Add representation to instance"""
|
||||
|
|
@ -151,3 +165,35 @@ class FusionRenderLocal(
|
|||
instance.data["representations"].append(repre)
|
||||
|
||||
return instance
|
||||
|
||||
def get_render_instances_by_frame_range(self, context):
|
||||
"""Return enabled render.local instances grouped by their frame range.
|
||||
|
||||
Arguments:
|
||||
context (pyblish.Context): The pyblish context
|
||||
|
||||
Returns:
|
||||
dict: (start, end): instances mapping
|
||||
|
||||
"""
|
||||
|
||||
instances_to_render = [
|
||||
instance for instance in context if
|
||||
# Only active instances
|
||||
instance.data.get("publish", True) and
|
||||
# Only render.local instances
|
||||
"render.local" in instance.data.get("families", [])
|
||||
]
|
||||
|
||||
# Instances by frame ranges
|
||||
instances_by_frame_range = collections.defaultdict(list)
|
||||
for instance in instances_to_render:
|
||||
start, end = self.get_instance_render_frame_range(instance)
|
||||
instances_by_frame_range[(start, end)].append(instance)
|
||||
|
||||
return dict(instances_by_frame_range)
|
||||
|
||||
def get_instance_render_frame_range(self, instance):
|
||||
start = instance.data["frameStartHandle"]
|
||||
end = instance.data["frameEndHandle"]
|
||||
return start, end
|
||||
|
|
|
|||
|
|
@ -17,5 +17,5 @@ class FusionSaveComp(pyblish.api.ContextPlugin):
|
|||
current = comp.GetAttrs().get("COMPS_FileName", "")
|
||||
assert context.data['currentFile'] == current
|
||||
|
||||
self.log.info("Saving current file..")
|
||||
self.log.info("Saving current file: {}".format(current))
|
||||
comp.Save()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
|
||||
class ValidateInstanceFrameRange(pyblish.api.InstancePlugin):
|
||||
"""Validate instance frame range is within comp's global render range."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Filename Has Extension"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
context = instance.context
|
||||
global_start = context.data["compFrameStart"]
|
||||
global_end = context.data["compFrameEnd"]
|
||||
|
||||
render_start = instance.data["frameStartHandle"]
|
||||
render_end = instance.data["frameEndHandle"]
|
||||
|
||||
if render_start < global_start or render_end > global_end:
|
||||
|
||||
message = (
|
||||
f"Instance {instance} render frame range "
|
||||
f"({render_start}-{render_end}) is outside of the comp's "
|
||||
f"global render range ({global_start}-{global_end}) and thus "
|
||||
f"can't be rendered. "
|
||||
)
|
||||
description = (
|
||||
f"{message}\n\n"
|
||||
f"Either update the comp's global range or the instance's "
|
||||
f"frame range to ensure the comp's frame range includes the "
|
||||
f"to render frame range for the instance."
|
||||
)
|
||||
raise PublishValidationError(
|
||||
title="Frame range outside of comp range",
|
||||
message=message,
|
||||
description=description
|
||||
)
|
||||
|
|
@ -8,7 +8,6 @@ import pyblish.api
|
|||
from openpype.hosts.houdini.api import lib
|
||||
|
||||
|
||||
|
||||
class CollectFrames(pyblish.api.InstancePlugin):
|
||||
"""Collect all frames which would be saved from the ROP nodes"""
|
||||
|
||||
|
|
@ -34,8 +33,10 @@ class CollectFrames(pyblish.api.InstancePlugin):
|
|||
self.log.warning("Using current frame: {}".format(hou.frame()))
|
||||
output = output_parm.eval()
|
||||
|
||||
_, ext = lib.splitext(output,
|
||||
allowed_multidot_extensions=[".ass.gz"])
|
||||
_, ext = lib.splitext(
|
||||
output,
|
||||
allowed_multidot_extensions=[".ass.gz"]
|
||||
)
|
||||
file_name = os.path.basename(output)
|
||||
result = file_name
|
||||
|
||||
|
|
|
|||
|
|
@ -117,4 +117,4 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin):
|
|||
|
||||
inputs = [c["representation"] for c in containers]
|
||||
instance.data["inputRepresentations"] = inputs
|
||||
self.log.info("Collected inputs: %s" % inputs)
|
||||
self.log.debug("Collected inputs: %s" % inputs)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
has_family = node.evalParm("family")
|
||||
assert has_family, "'%s' is missing 'family'" % node.name()
|
||||
|
||||
self.log.info("processing {}".format(node))
|
||||
self.log.info(
|
||||
"Processing legacy instance node {}".format(node.path())
|
||||
)
|
||||
|
||||
data = lib.read(node)
|
||||
# Check bypass state and reverse
|
||||
|
|
|
|||
|
|
@ -32,5 +32,4 @@ class CollectWorkfile(pyblish.api.InstancePlugin):
|
|||
"stagingDir": folder,
|
||||
}]
|
||||
|
||||
self.log.info('Collected instance: {}'.format(file))
|
||||
self.log.info('staging Dir: {}'.format(folder))
|
||||
self.log.debug('Collected workfile instance: {}'.format(file))
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class SaveCurrentScene(pyblish.api.ContextPlugin):
|
|||
)
|
||||
|
||||
if host.has_unsaved_changes():
|
||||
self.log.info("Saving current file {}...".format(current_file))
|
||||
self.log.info("Saving current file: {}".format(current_file))
|
||||
host.save_workfile(current_file)
|
||||
else:
|
||||
self.log.debug("No unsaved changes, skipping file save..")
|
||||
|
|
|
|||
|
|
@ -28,18 +28,37 @@ class ValidateWorkfilePaths(
|
|||
if not self.is_active(instance.data):
|
||||
return
|
||||
invalid = self.get_invalid()
|
||||
self.log.info(
|
||||
"node types to check: {}".format(", ".join(self.node_types)))
|
||||
self.log.info(
|
||||
"prohibited vars: {}".format(", ".join(self.prohibited_vars))
|
||||
self.log.debug(
|
||||
"Checking node types: {}".format(", ".join(self.node_types)))
|
||||
self.log.debug(
|
||||
"Searching prohibited vars: {}".format(
|
||||
", ".join(self.prohibited_vars)
|
||||
)
|
||||
)
|
||||
if invalid:
|
||||
for param in invalid:
|
||||
self.log.error(
|
||||
"{}: {}".format(param.path(), param.unexpandedString()))
|
||||
|
||||
raise PublishValidationError(
|
||||
"Invalid paths found", title=self.label)
|
||||
if invalid:
|
||||
all_container_vars = set()
|
||||
for param in invalid:
|
||||
value = param.unexpandedString()
|
||||
contained_vars = [
|
||||
var for var in self.prohibited_vars
|
||||
if var in value
|
||||
]
|
||||
all_container_vars.update(contained_vars)
|
||||
|
||||
self.log.error(
|
||||
"Parm {} contains prohibited vars {}: {}".format(
|
||||
param.path(),
|
||||
", ".join(contained_vars),
|
||||
value)
|
||||
)
|
||||
|
||||
message = (
|
||||
"Prohibited vars {} found in parameter values".format(
|
||||
", ".join(all_container_vars)
|
||||
)
|
||||
)
|
||||
raise PublishValidationError(message, title=self.label)
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls):
|
||||
|
|
@ -63,7 +82,7 @@ class ValidateWorkfilePaths(
|
|||
def repair(cls, instance):
|
||||
invalid = cls.get_invalid()
|
||||
for param in invalid:
|
||||
cls.log.info("processing: {}".format(param.path()))
|
||||
cls.log.info("Processing: {}".format(param.path()))
|
||||
cls.log.info("Replacing {} for {}".format(
|
||||
param.unexpandedString(),
|
||||
hou.text.expandString(param.unexpandedString())))
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin):
|
|||
|
||||
inputs = [c["representation"] for c in containers]
|
||||
instance.data["inputRepresentations"] = inputs
|
||||
self.log.info("Collected inputs: %s" % inputs)
|
||||
self.log.debug("Collected inputs: %s" % inputs)
|
||||
|
||||
def _collect_renderlayer_inputs(self, scene_containers, instance):
|
||||
"""Collects inputs from nodes in renderlayer, incl. shaders + camera"""
|
||||
|
|
|
|||
|
|
@ -31,5 +31,5 @@ class SaveCurrentScene(pyblish.api.ContextPlugin):
|
|||
# remove lockfile before saving
|
||||
if is_workfile_lock_enabled("maya", project_name, project_settings):
|
||||
remove_workfile_lock(current)
|
||||
self.log.info("Saving current file..")
|
||||
self.log.info("Saving current file: {}".format(current))
|
||||
cmds.file(save=True, force=True)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import pyblish.api
|
|||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.nuke import api as napi
|
||||
from openpype.hosts.nuke.api.lib import set_node_knobs_from_settings
|
||||
|
||||
|
||||
if sys.version_info[0] >= 3:
|
||||
unicode = str
|
||||
|
|
@ -28,7 +30,7 @@ class ExtractThumbnail(publish.Extractor):
|
|||
bake_viewer_process = True
|
||||
bake_viewer_input_process = True
|
||||
nodes = {}
|
||||
|
||||
reposition_nodes = None
|
||||
|
||||
def process(self, instance):
|
||||
if instance.data.get("farm"):
|
||||
|
|
@ -123,18 +125,32 @@ class ExtractThumbnail(publish.Extractor):
|
|||
temporary_nodes.append(rnode)
|
||||
previous_node = rnode
|
||||
|
||||
reformat_node = nuke.createNode("Reformat")
|
||||
ref_node = self.nodes.get("Reformat", None)
|
||||
if ref_node:
|
||||
for k, v in ref_node:
|
||||
self.log.debug("k, v: {0}:{1}".format(k, v))
|
||||
if isinstance(v, unicode):
|
||||
v = str(v)
|
||||
reformat_node[k].setValue(v)
|
||||
if self.reposition_nodes is None:
|
||||
# [deprecated] create reformat node old way
|
||||
reformat_node = nuke.createNode("Reformat")
|
||||
ref_node = self.nodes.get("Reformat", None)
|
||||
if ref_node:
|
||||
for k, v in ref_node:
|
||||
self.log.debug("k, v: {0}:{1}".format(k, v))
|
||||
if isinstance(v, unicode):
|
||||
v = str(v)
|
||||
reformat_node[k].setValue(v)
|
||||
|
||||
reformat_node.setInput(0, previous_node)
|
||||
previous_node = reformat_node
|
||||
temporary_nodes.append(reformat_node)
|
||||
reformat_node.setInput(0, previous_node)
|
||||
previous_node = reformat_node
|
||||
temporary_nodes.append(reformat_node)
|
||||
else:
|
||||
# create reformat node new way
|
||||
for repo_node in self.reposition_nodes:
|
||||
node_class = repo_node["node_class"]
|
||||
knobs = repo_node["knobs"]
|
||||
node = nuke.createNode(node_class)
|
||||
set_node_knobs_from_settings(node, knobs)
|
||||
|
||||
# connect in order
|
||||
node.setInput(0, previous_node)
|
||||
previous_node = node
|
||||
temporary_nodes.append(node)
|
||||
|
||||
# only create colorspace baking if toggled on
|
||||
if bake_viewer_process:
|
||||
|
|
|
|||
|
|
@ -16,11 +16,12 @@ class SaveCurrentWorkfile(pyblish.api.ContextPlugin):
|
|||
def process(self, context):
|
||||
|
||||
host = registered_host()
|
||||
if context.data["currentFile"] != host.get_current_workfile():
|
||||
current = host.get_current_workfile()
|
||||
if context.data["currentFile"] != current:
|
||||
raise KnownPublishError("Workfile has changed during publishing!")
|
||||
|
||||
if host.has_unsaved_changes():
|
||||
self.log.info("Saving current file..")
|
||||
self.log.info("Saving current file: {}".format(current))
|
||||
host.save_workfile()
|
||||
else:
|
||||
self.log.debug("Skipping workfile save because there are no "
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ class AnimationFBXLoader(plugin.Loader):
|
|||
package_paths=[f"{root}/{hierarchy[0]}"],
|
||||
recursive_paths=False)
|
||||
levels = ar.get_assets(_filter)
|
||||
master_level = levels[0].get_full_name()
|
||||
master_level = levels[0].get_asset().get_path_name()
|
||||
|
||||
hierarchy_dir = root
|
||||
for h in hierarchy:
|
||||
|
|
@ -168,7 +168,7 @@ class AnimationFBXLoader(plugin.Loader):
|
|||
package_paths=[f"{hierarchy_dir}/"],
|
||||
recursive_paths=True)
|
||||
levels = ar.get_assets(_filter)
|
||||
level = levels[0].get_full_name()
|
||||
level = levels[0].get_asset().get_path_name()
|
||||
|
||||
unreal.EditorLevelLibrary.save_all_dirty_levels()
|
||||
unreal.EditorLevelLibrary.load_level(level)
|
||||
|
|
|
|||
|
|
@ -365,7 +365,7 @@ class CameraLoader(plugin.Loader):
|
|||
maps = ar.get_assets(filter)
|
||||
|
||||
# There should be only one map in the list
|
||||
EditorLevelLibrary.load_level(maps[0].get_full_name())
|
||||
EditorLevelLibrary.load_level(maps[0].get_asset().get_path_name())
|
||||
|
||||
level_sequence = sequences[0].get_asset()
|
||||
|
||||
|
|
@ -513,7 +513,7 @@ class CameraLoader(plugin.Loader):
|
|||
map = maps[0]
|
||||
|
||||
EditorLevelLibrary.save_all_dirty_levels()
|
||||
EditorLevelLibrary.load_level(map.get_full_name())
|
||||
EditorLevelLibrary.load_level(map.get_asset().get_path_name())
|
||||
|
||||
# Remove the camera from the level.
|
||||
actors = EditorLevelLibrary.get_all_level_actors()
|
||||
|
|
@ -523,7 +523,7 @@ class CameraLoader(plugin.Loader):
|
|||
EditorLevelLibrary.destroy_actor(a)
|
||||
|
||||
EditorLevelLibrary.save_all_dirty_levels()
|
||||
EditorLevelLibrary.load_level(world.get_full_name())
|
||||
EditorLevelLibrary.load_level(world.get_asset().get_path_name())
|
||||
|
||||
# There should be only one sequence in the path.
|
||||
sequence_name = sequences[0].asset_name
|
||||
|
|
|
|||
|
|
@ -740,7 +740,7 @@ class LayoutLoader(plugin.Loader):
|
|||
loaded_assets = self._process(self.fname, asset_dir, shot)
|
||||
|
||||
for s in sequences:
|
||||
EditorAssetLibrary.save_asset(s.get_full_name())
|
||||
EditorAssetLibrary.save_asset(s.get_path_name())
|
||||
|
||||
EditorLevelLibrary.save_current_level()
|
||||
|
||||
|
|
@ -819,7 +819,7 @@ class LayoutLoader(plugin.Loader):
|
|||
recursive_paths=False)
|
||||
levels = ar.get_assets(filter)
|
||||
|
||||
layout_level = levels[0].get_full_name()
|
||||
layout_level = levels[0].get_asset().get_path_name()
|
||||
|
||||
EditorLevelLibrary.save_all_dirty_levels()
|
||||
EditorLevelLibrary.load_level(layout_level)
|
||||
|
|
@ -919,7 +919,7 @@ class LayoutLoader(plugin.Loader):
|
|||
package_paths=[f"{root}/{ms_asset}"],
|
||||
recursive_paths=False)
|
||||
levels = ar.get_assets(_filter)
|
||||
master_level = levels[0].get_full_name()
|
||||
master_level = levels[0].get_asset().get_path_name()
|
||||
|
||||
sequences = [master_sequence]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# flake8: noqa E402
|
||||
"""Pype module API."""
|
||||
"""OpenPype lib functions."""
|
||||
# add vendor to sys path based on Python version
|
||||
import sys
|
||||
import os
|
||||
|
|
@ -94,7 +94,8 @@ from .python_module_tools import (
|
|||
modules_from_path,
|
||||
recursive_bases_from_class,
|
||||
classes_from_module,
|
||||
import_module_from_dirpath
|
||||
import_module_from_dirpath,
|
||||
is_func_signature_supported,
|
||||
)
|
||||
|
||||
from .profiles_filtering import (
|
||||
|
|
@ -243,6 +244,7 @@ __all__ = [
|
|||
"recursive_bases_from_class",
|
||||
"classes_from_module",
|
||||
"import_module_from_dirpath",
|
||||
"is_func_signature_supported",
|
||||
|
||||
"get_transcode_temp_directory",
|
||||
"should_convert_for_ffmpeg",
|
||||
|
|
|
|||
|
|
@ -6,10 +6,9 @@ import inspect
|
|||
import logging
|
||||
import weakref
|
||||
from uuid import uuid4
|
||||
try:
|
||||
from weakref import WeakMethod
|
||||
except Exception:
|
||||
from openpype.lib.python_2_comp import WeakMethod
|
||||
|
||||
from .python_2_comp import WeakMethod
|
||||
from .python_module_tools import is_func_signature_supported
|
||||
|
||||
|
||||
class MissingEventSystem(Exception):
|
||||
|
|
@ -80,40 +79,8 @@ class EventCallback(object):
|
|||
|
||||
# Get expected arguments from function spec
|
||||
# - positional arguments are always preferred
|
||||
expect_args = False
|
||||
expect_kwargs = False
|
||||
fake_event = "fake"
|
||||
if hasattr(inspect, "signature"):
|
||||
# Python 3 using 'Signature' object where we try to bind arg
|
||||
# or kwarg. Using signature is recommended approach based on
|
||||
# documentation.
|
||||
sig = inspect.signature(func)
|
||||
try:
|
||||
sig.bind(fake_event)
|
||||
expect_args = True
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
sig.bind(event=fake_event)
|
||||
expect_kwargs = True
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
else:
|
||||
# In Python 2 'signature' is not available so 'getcallargs' is used
|
||||
# - 'getcallargs' is marked as deprecated since Python 3.0
|
||||
try:
|
||||
inspect.getcallargs(func, fake_event)
|
||||
expect_args = True
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
inspect.getcallargs(func, event=fake_event)
|
||||
expect_kwargs = True
|
||||
except TypeError:
|
||||
pass
|
||||
expect_args = is_func_signature_supported(func, "fake")
|
||||
expect_kwargs = is_func_signature_supported(func, event="fake")
|
||||
|
||||
self._func_ref = func_ref
|
||||
self._func_name = func_name
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ def run_openpype_process(*args, **kwargs):
|
|||
|
||||
Example:
|
||||
```
|
||||
run_openpype_process("run", "<path to .py script>")
|
||||
run_detached_process("run", "<path to .py script>")
|
||||
```
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -1,41 +1,44 @@
|
|||
import weakref
|
||||
|
||||
|
||||
class _weak_callable:
|
||||
def __init__(self, obj, func):
|
||||
self.im_self = obj
|
||||
self.im_func = func
|
||||
WeakMethod = getattr(weakref, "WeakMethod", None)
|
||||
|
||||
def __call__(self, *args, **kws):
|
||||
if self.im_self is None:
|
||||
return self.im_func(*args, **kws)
|
||||
else:
|
||||
return self.im_func(self.im_self, *args, **kws)
|
||||
if WeakMethod is None:
|
||||
class _WeakCallable:
|
||||
def __init__(self, obj, func):
|
||||
self.im_self = obj
|
||||
self.im_func = func
|
||||
|
||||
def __call__(self, *args, **kws):
|
||||
if self.im_self is None:
|
||||
return self.im_func(*args, **kws)
|
||||
else:
|
||||
return self.im_func(self.im_self, *args, **kws)
|
||||
|
||||
|
||||
class WeakMethod:
|
||||
""" Wraps a function or, more importantly, a bound method in
|
||||
a way that allows a bound method's object to be GCed, while
|
||||
providing the same interface as a normal weak reference. """
|
||||
class WeakMethod:
|
||||
""" Wraps a function or, more importantly, a bound method in
|
||||
a way that allows a bound method's object to be GCed, while
|
||||
providing the same interface as a normal weak reference. """
|
||||
|
||||
def __init__(self, fn):
|
||||
try:
|
||||
self._obj = weakref.ref(fn.im_self)
|
||||
self._meth = fn.im_func
|
||||
except AttributeError:
|
||||
# It's not a bound method
|
||||
self._obj = None
|
||||
self._meth = fn
|
||||
def __init__(self, fn):
|
||||
try:
|
||||
self._obj = weakref.ref(fn.im_self)
|
||||
self._meth = fn.im_func
|
||||
except AttributeError:
|
||||
# It's not a bound method
|
||||
self._obj = None
|
||||
self._meth = fn
|
||||
|
||||
def __call__(self):
|
||||
if self._dead():
|
||||
return None
|
||||
return _weak_callable(self._getobj(), self._meth)
|
||||
def __call__(self):
|
||||
if self._dead():
|
||||
return None
|
||||
return _WeakCallable(self._getobj(), self._meth)
|
||||
|
||||
def _dead(self):
|
||||
return self._obj is not None and self._obj() is None
|
||||
def _dead(self):
|
||||
return self._obj is not None and self._obj() is None
|
||||
|
||||
def _getobj(self):
|
||||
if self._obj is None:
|
||||
return None
|
||||
return self._obj()
|
||||
def _getobj(self):
|
||||
if self._obj is None:
|
||||
return None
|
||||
return self._obj()
|
||||
|
|
|
|||
|
|
@ -230,3 +230,70 @@ def import_module_from_dirpath(dirpath, folder_name, dst_module_name=None):
|
|||
dirpath, folder_name, dst_module_name
|
||||
)
|
||||
return module
|
||||
|
||||
|
||||
def is_func_signature_supported(func, *args, **kwargs):
|
||||
"""Check if a function signature supports passed args and kwargs.
|
||||
|
||||
This check does not actually call the function, just look if function can
|
||||
be called with the arguments.
|
||||
|
||||
Notes:
|
||||
This does NOT check if the function would work with passed arguments
|
||||
only if they can be passed in. If function have *args, **kwargs
|
||||
in paramaters, this will always return 'True'.
|
||||
|
||||
Example:
|
||||
>>> def my_function(my_number):
|
||||
... return my_number + 1
|
||||
...
|
||||
>>> is_func_signature_supported(my_function, 1)
|
||||
True
|
||||
>>> is_func_signature_supported(my_function, 1, 2)
|
||||
False
|
||||
>>> is_func_signature_supported(my_function, my_number=1)
|
||||
True
|
||||
>>> is_func_signature_supported(my_function, number=1)
|
||||
False
|
||||
>>> is_func_signature_supported(my_function, "string")
|
||||
True
|
||||
>>> def my_other_function(*args, **kwargs):
|
||||
... my_function(*args, **kwargs)
|
||||
...
|
||||
>>> is_func_signature_supported(
|
||||
... my_other_function,
|
||||
... "string",
|
||||
... 1,
|
||||
... other=None
|
||||
... )
|
||||
True
|
||||
|
||||
Args:
|
||||
func (function): A function where the signature should be tested.
|
||||
*args (tuple[Any]): Positional arguments for function signature.
|
||||
**kwargs (dict[str, Any]): Keyword arguments for function signature.
|
||||
|
||||
Returns:
|
||||
bool: Function can pass in arguments.
|
||||
"""
|
||||
|
||||
if hasattr(inspect, "signature"):
|
||||
# Python 3 using 'Signature' object where we try to bind arg
|
||||
# or kwarg. Using signature is recommended approach based on
|
||||
# documentation.
|
||||
sig = inspect.signature(func)
|
||||
try:
|
||||
sig.bind(*args, **kwargs)
|
||||
return True
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
else:
|
||||
# In Python 2 'signature' is not available so 'getcallargs' is used
|
||||
# - 'getcallargs' is marked as deprecated since Python 3.0
|
||||
try:
|
||||
inspect.getcallargs(func, *args, **kwargs)
|
||||
return True
|
||||
except TypeError:
|
||||
pass
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -4,7 +4,18 @@ import pyblish.api
|
|||
|
||||
|
||||
class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin):
|
||||
"""Collect default Deadline Webservice URL."""
|
||||
"""Collect default Deadline Webservice URL.
|
||||
|
||||
DL webservice addresses must be configured first in System Settings for
|
||||
project settings enum to work.
|
||||
|
||||
Default webservice could be overriden by
|
||||
`project_settings/deadline/deadline_servers`. Currently only single url
|
||||
is expected.
|
||||
|
||||
This url could be overriden by some hosts directly on instances with
|
||||
`CollectDeadlineServerFromInstance`.
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.410
|
||||
label = "Default Deadline Webservice"
|
||||
|
|
@ -23,3 +34,16 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin):
|
|||
context.data["defaultDeadline"] = deadline_module.deadline_urls["default"] # noqa: E501
|
||||
|
||||
context.data["deadlinePassMongoUrl"] = self.pass_mongo_url
|
||||
|
||||
deadline_servers = (context.data
|
||||
["project_settings"]
|
||||
["deadline"]
|
||||
["deadline_servers"])
|
||||
if deadline_servers:
|
||||
deadline_server_name = deadline_servers[0]
|
||||
deadline_webservice = deadline_module.deadline_urls.get(
|
||||
deadline_server_name)
|
||||
if deadline_webservice:
|
||||
context.data["defaultDeadline"] = deadline_webservice
|
||||
self.log.debug("Overriding from project settings with {}".format( # noqa: E501
|
||||
deadline_webservice))
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class FusionSubmitDeadline(
|
|||
|
||||
def process(self, instance):
|
||||
if not instance.data.get("farm"):
|
||||
self.log.info("Skipping local instance.")
|
||||
self.log.debug("Skipping local instance.")
|
||||
return
|
||||
|
||||
attribute_values = self.get_attr_values_from_data(
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin,
|
|||
|
||||
def process(self, instance):
|
||||
if not instance.data.get("farm"):
|
||||
self.log.info("Skipping local instance.")
|
||||
self.log.debug("Skipping local instance.")
|
||||
return
|
||||
|
||||
instance.data["attributeValues"] = self.get_attr_values_from_data(
|
||||
|
|
|
|||
|
|
@ -762,7 +762,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
|
||||
"""
|
||||
if not instance.data.get("farm"):
|
||||
self.log.info("Skipping local instance.")
|
||||
self.log.debug("Skipping local instance.")
|
||||
return
|
||||
|
||||
data = instance.data.copy()
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin,
|
|||
|
||||
def process(self, instance):
|
||||
if not instance.data.get("farm"):
|
||||
self.log.info("Skipping local instance.")
|
||||
self.log.debug("Skipping local instance.")
|
||||
return
|
||||
|
||||
# get default deadline webservice url from deadline module
|
||||
|
|
|
|||
|
|
@ -167,16 +167,25 @@ class AbstractCollectRender(pyblish.api.ContextPlugin):
|
|||
|
||||
frame_start_render = int(render_instance.frameStart)
|
||||
frame_end_render = int(render_instance.frameEnd)
|
||||
# TODO: Refactor hacky frame range workaround below
|
||||
if (render_instance.ignoreFrameHandleCheck or
|
||||
int(context.data['frameStartHandle']) == frame_start_render
|
||||
and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501
|
||||
|
||||
# only for Harmony where frame range cannot be set by DB
|
||||
handle_start = context.data['handleStart']
|
||||
handle_end = context.data['handleEnd']
|
||||
frame_start = context.data['frameStart']
|
||||
frame_end = context.data['frameEnd']
|
||||
frame_start_handle = context.data['frameStartHandle']
|
||||
frame_end_handle = context.data['frameEndHandle']
|
||||
elif (hasattr(render_instance, "frameStartHandle")
|
||||
and hasattr(render_instance, "frameEndHandle")):
|
||||
handle_start = int(render_instance.handleStart)
|
||||
handle_end = int(render_instance.handleEnd)
|
||||
frame_start = int(render_instance.frameStart)
|
||||
frame_end = int(render_instance.frameEnd)
|
||||
frame_start_handle = int(render_instance.frameStartHandle)
|
||||
frame_end_handle = int(render_instance.frameEndHandle)
|
||||
else:
|
||||
handle_start = 0
|
||||
handle_end = 0
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import os
|
||||
import sys
|
||||
import types
|
||||
import inspect
|
||||
import copy
|
||||
import tempfile
|
||||
import xml.etree.ElementTree
|
||||
|
||||
import six
|
||||
import pyblish.util
|
||||
import pyblish.plugin
|
||||
import pyblish.api
|
||||
|
|
@ -42,7 +40,9 @@ def get_template_name_profiles(
|
|||
|
||||
Args:
|
||||
project_name (str): Name of project where to look for templates.
|
||||
project_settings(Dic[str, Any]): Prepared project settings.
|
||||
project_settings (Dict[str, Any]): Prepared project settings.
|
||||
logger (Optional[logging.Logger]): Logger object to be used instead
|
||||
of default logger.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Publish template profiles.
|
||||
|
|
@ -103,7 +103,9 @@ def get_hero_template_name_profiles(
|
|||
|
||||
Args:
|
||||
project_name (str): Name of project where to look for templates.
|
||||
project_settings(Dic[str, Any]): Prepared project settings.
|
||||
project_settings (Dict[str, Any]): Prepared project settings.
|
||||
logger (Optional[logging.Logger]): Logger object to be used instead
|
||||
of default logger.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Publish template profiles.
|
||||
|
|
@ -172,9 +174,10 @@ def get_publish_template_name(
|
|||
project_name (str): Name of project where to look for settings.
|
||||
host_name (str): Name of host integration.
|
||||
family (str): Family for which should be found template.
|
||||
task_name (str): Task name on which is intance working.
|
||||
task_type (str): Task type on which is intance working.
|
||||
project_setting (Dict[str, Any]): Prepared project settings.
|
||||
task_name (str): Task name on which is instance working.
|
||||
task_type (str): Task type on which is instance working.
|
||||
project_settings (Dict[str, Any]): Prepared project settings.
|
||||
hero (bool): Template is for hero version publishing.
|
||||
logger (logging.Logger): Custom logger used for 'filter_profiles'
|
||||
function.
|
||||
|
||||
|
|
@ -264,19 +267,18 @@ def load_help_content_from_plugin(plugin):
|
|||
def publish_plugins_discover(paths=None):
|
||||
"""Find and return available pyblish plug-ins
|
||||
|
||||
Overridden function from `pyblish` module to be able collect crashed files
|
||||
and reason of their crash.
|
||||
Overridden function from `pyblish` module to be able to collect
|
||||
crashed files and reason of their crash.
|
||||
|
||||
Arguments:
|
||||
paths (list, optional): Paths to discover plug-ins from.
|
||||
If no paths are provided, all paths are searched.
|
||||
|
||||
"""
|
||||
|
||||
# The only difference with `pyblish.api.discover`
|
||||
result = DiscoverResult(pyblish.api.Plugin)
|
||||
|
||||
plugins = dict()
|
||||
plugins = {}
|
||||
plugin_names = []
|
||||
|
||||
allow_duplicates = pyblish.plugin.ALLOW_DUPLICATES
|
||||
|
|
@ -302,7 +304,7 @@ def publish_plugins_discover(paths=None):
|
|||
|
||||
mod_name, mod_ext = os.path.splitext(fname)
|
||||
|
||||
if not mod_ext == ".py":
|
||||
if mod_ext != ".py":
|
||||
continue
|
||||
|
||||
try:
|
||||
|
|
@ -320,6 +322,14 @@ def publish_plugins_discover(paths=None):
|
|||
continue
|
||||
|
||||
for plugin in pyblish.plugin.plugins_from_module(module):
|
||||
# Ignore base plugin classes
|
||||
# NOTE 'pyblish.api.discover' does not ignore them!
|
||||
if (
|
||||
plugin is pyblish.api.Plugin
|
||||
or plugin is pyblish.api.ContextPlugin
|
||||
or plugin is pyblish.api.InstancePlugin
|
||||
):
|
||||
continue
|
||||
if not allow_duplicates and plugin.__name__ in plugin_names:
|
||||
result.duplicated_plugins.append(plugin)
|
||||
log.debug("Duplicate plug-in found: %s", plugin)
|
||||
|
|
@ -525,10 +535,10 @@ def find_close_plugin(close_plugin_name, log):
|
|||
def remote_publish(log, close_plugin_name=None, raise_error=False):
|
||||
"""Loops through all plugins, logs to console. Used for tests.
|
||||
|
||||
Args:
|
||||
log (openpype.lib.Logger)
|
||||
close_plugin_name (str): name of plugin with responsibility to
|
||||
close host app
|
||||
Args:
|
||||
log (Logger)
|
||||
close_plugin_name (str): name of plugin with responsibility to
|
||||
close host app
|
||||
"""
|
||||
# Error exit as soon as any error occurs.
|
||||
error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}"
|
||||
|
|
@ -837,3 +847,22 @@ def _validate_transient_template(project_name, template_name, anatomy):
|
|||
raise ValueError(("There is not set \"folder\" template in \"{}\" anatomy" # noqa
|
||||
" for project \"{}\"."
|
||||
).format(template_name, project_name))
|
||||
|
||||
|
||||
def add_repre_files_for_cleanup(instance, repre):
|
||||
""" Explicitly mark repre files to be deleted.
|
||||
|
||||
Should be used on intermediate files (eg. review, thumbnails) to be
|
||||
explicitly deleted.
|
||||
"""
|
||||
files = repre["files"]
|
||||
staging_dir = repre.get("stagingDir")
|
||||
if not staging_dir:
|
||||
return
|
||||
|
||||
if isinstance(files, str):
|
||||
files = [files]
|
||||
|
||||
for file_name in files:
|
||||
expected_file = os.path.join(staging_dir, file_name)
|
||||
instance.context.data["cleanupFullPaths"].append(expected_file)
|
||||
|
|
|
|||
|
|
@ -379,7 +379,9 @@ class ColormanagedPyblishPluginMixin(object):
|
|||
|
||||
# check if ext in lower case is in self.allowed_ext
|
||||
if ext.lstrip(".").lower() not in self.allowed_ext:
|
||||
self.log.debug("Extension is not in allowed extensions.")
|
||||
self.log.debug(
|
||||
"Extension '{}' is not in allowed extensions.".format(ext)
|
||||
)
|
||||
return
|
||||
|
||||
if colorspace_settings is None:
|
||||
|
|
@ -393,8 +395,7 @@ class ColormanagedPyblishPluginMixin(object):
|
|||
self.log.warning("No colorspace management was defined")
|
||||
return
|
||||
|
||||
self.log.info("Config data is : `{}`".format(
|
||||
config_data))
|
||||
self.log.debug("Config data is: `{}`".format(config_data))
|
||||
|
||||
project_name = context.data["projectName"]
|
||||
host_name = context.data["hostName"]
|
||||
|
|
@ -405,8 +406,7 @@ class ColormanagedPyblishPluginMixin(object):
|
|||
if isinstance(filename, list):
|
||||
filename = filename[0]
|
||||
|
||||
self.log.debug("__ filename: `{}`".format(
|
||||
filename))
|
||||
self.log.debug("__ filename: `{}`".format(filename))
|
||||
|
||||
# get matching colorspace from rules
|
||||
colorspace = colorspace or get_imageio_colorspace_from_filepath(
|
||||
|
|
@ -415,8 +415,7 @@ class ColormanagedPyblishPluginMixin(object):
|
|||
file_rules=file_rules,
|
||||
project_settings=project_settings
|
||||
)
|
||||
self.log.debug("__ colorspace: `{}`".format(
|
||||
colorspace))
|
||||
self.log.debug("__ colorspace: `{}`".format(colorspace))
|
||||
|
||||
# infuse data to representation
|
||||
if colorspace:
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ class CleanUp(pyblish.api.InstancePlugin):
|
|||
staging_dir = instance.data.get("stagingDir", None)
|
||||
|
||||
if not staging_dir:
|
||||
self.log.info("Staging dir not set.")
|
||||
self.log.debug("Skipping cleanup. Staging dir not set "
|
||||
"on instance: {}.".format(instance))
|
||||
return
|
||||
|
||||
if not os.path.normpath(staging_dir).startswith(temp_root):
|
||||
|
|
@ -90,7 +91,7 @@ class CleanUp(pyblish.api.InstancePlugin):
|
|||
return
|
||||
|
||||
if not os.path.exists(staging_dir):
|
||||
self.log.info("No staging directory found: %s" % staging_dir)
|
||||
self.log.debug("No staging directory found at: %s" % staging_dir)
|
||||
return
|
||||
|
||||
if instance.data.get("stagingDir_persistent"):
|
||||
|
|
@ -131,7 +132,9 @@ class CleanUp(pyblish.api.InstancePlugin):
|
|||
try:
|
||||
os.remove(src)
|
||||
except PermissionError:
|
||||
self.log.warning("Insufficient permission to delete {}".format(src))
|
||||
self.log.warning(
|
||||
"Insufficient permission to delete {}".format(src)
|
||||
)
|
||||
continue
|
||||
|
||||
# add dir for cleanup
|
||||
|
|
|
|||
|
|
@ -67,5 +67,6 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin):
|
|||
# Store
|
||||
context.data["anatomyData"] = anatomy_data
|
||||
|
||||
self.log.info("Global anatomy Data collected")
|
||||
self.log.debug(json.dumps(anatomy_data, indent=4))
|
||||
self.log.debug("Global Anatomy Context Data collected:\n{}".format(
|
||||
json.dumps(anatomy_data, indent=4)
|
||||
))
|
||||
|
|
|
|||
|
|
@ -46,17 +46,17 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
follow_workfile_version = False
|
||||
|
||||
def process(self, context):
|
||||
self.log.info("Collecting anatomy data for all instances.")
|
||||
self.log.debug("Collecting anatomy data for all instances.")
|
||||
|
||||
project_name = context.data["projectName"]
|
||||
self.fill_missing_asset_docs(context, project_name)
|
||||
self.fill_latest_versions(context, project_name)
|
||||
self.fill_anatomy_data(context)
|
||||
|
||||
self.log.info("Anatomy Data collection finished.")
|
||||
self.log.debug("Anatomy Data collection finished.")
|
||||
|
||||
def fill_missing_asset_docs(self, context, project_name):
|
||||
self.log.debug("Qeurying asset documents for instances.")
|
||||
self.log.debug("Querying asset documents for instances.")
|
||||
|
||||
context_asset_doc = context.data.get("assetEntity")
|
||||
|
||||
|
|
@ -271,7 +271,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
instance_name = instance.data["name"]
|
||||
instance_label = instance.data.get("label")
|
||||
if instance_label:
|
||||
instance_name += "({})".format(instance_label)
|
||||
instance_name += " ({})".format(instance_label)
|
||||
self.log.debug("Anatomy data for instance {}: {}".format(
|
||||
instance_name,
|
||||
json.dumps(anatomy_data, indent=4)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,6 @@ class CollectAnatomyObject(pyblish.api.ContextPlugin):
|
|||
|
||||
context.data["anatomy"] = Anatomy(project_name)
|
||||
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
"Anatomy object collected for project \"{}\".".format(project_name)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,6 @@ class CollectCustomStagingDir(pyblish.api.InstancePlugin):
|
|||
else:
|
||||
result_str = "Not adding"
|
||||
|
||||
self.log.info("{} custom staging dir for instance with '{}'".format(
|
||||
self.log.debug("{} custom staging dir for instance with '{}'".format(
|
||||
result_str, family
|
||||
))
|
||||
|
|
|
|||
|
|
@ -92,5 +92,5 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
|
|||
|
||||
instance.data["transientData"] = transient_data
|
||||
|
||||
self.log.info("collected instance: {}".format(instance.data))
|
||||
self.log.info("parsing data: {}".format(in_data))
|
||||
self.log.debug("collected instance: {}".format(instance.data))
|
||||
self.log.debug("parsing data: {}".format(in_data))
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import json
|
|||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import legacy_io, KnownPublishError
|
||||
from openpype.pipeline.publish.lib import add_repre_files_for_cleanup
|
||||
|
||||
|
||||
class CollectRenderedFiles(pyblish.api.ContextPlugin):
|
||||
|
|
@ -89,6 +90,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
|
|||
|
||||
# now we can just add instances from json file and we are done
|
||||
for instance_data in data.get("instances"):
|
||||
|
||||
self.log.info(" - processing instance for {}".format(
|
||||
instance_data.get("subset")))
|
||||
instance = self._context.create_instance(
|
||||
|
|
@ -107,6 +109,8 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
|
|||
self._fill_staging_dir(repre_data, anatomy)
|
||||
representations.append(repre_data)
|
||||
|
||||
add_repre_files_for_cleanup(instance, repre_data)
|
||||
|
||||
instance.data["representations"] = representations
|
||||
|
||||
# add audio if in metadata data
|
||||
|
|
@ -157,6 +161,8 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
|
|||
os.environ.update(session_data)
|
||||
session_is_set = True
|
||||
self._process_path(data, anatomy)
|
||||
context.data["cleanupFullPaths"].append(path)
|
||||
context.data["cleanupEmptyDirs"].append(os.path.dirname(path))
|
||||
except Exception as e:
|
||||
self.log.error(e, exc_info=True)
|
||||
raise Exception("Error") from e
|
||||
|
|
|
|||
|
|
@ -48,10 +48,13 @@ class CollectSceneVersion(pyblish.api.ContextPlugin):
|
|||
if '<shell>' in filename:
|
||||
return
|
||||
|
||||
self.log.debug(
|
||||
"Collecting scene version from filename: {}".format(filename)
|
||||
)
|
||||
|
||||
version = get_version_from_path(filename)
|
||||
assert version, "Cannot determine version"
|
||||
|
||||
rootVersion = int(version)
|
||||
context.data['version'] = rootVersion
|
||||
self.log.info("{}".format(type(rootVersion)))
|
||||
self.log.info('Scene Version: %s' % context.data.get('version'))
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from openpype.lib import (
|
|||
should_convert_for_ffmpeg
|
||||
)
|
||||
from openpype.lib.profiles_filtering import filter_profiles
|
||||
from openpype.pipeline.publish.lib import add_repre_files_for_cleanup
|
||||
|
||||
|
||||
class ExtractBurnin(publish.Extractor):
|
||||
|
|
@ -353,6 +354,8 @@ class ExtractBurnin(publish.Extractor):
|
|||
# Add new representation to instance
|
||||
instance.data["representations"].append(new_repre)
|
||||
|
||||
add_repre_files_for_cleanup(instance, new_repre)
|
||||
|
||||
# Cleanup temp staging dir after procesisng of output definitions
|
||||
if do_convert:
|
||||
temp_dir = repre["stagingDir"]
|
||||
|
|
@ -517,8 +520,8 @@ class ExtractBurnin(publish.Extractor):
|
|||
"""
|
||||
|
||||
if "burnin" not in (repre.get("tags") or []):
|
||||
self.log.info((
|
||||
"Representation \"{}\" don't have \"burnin\" tag. Skipped."
|
||||
self.log.debug((
|
||||
"Representation \"{}\" does not have \"burnin\" tag. Skipped."
|
||||
).format(repre["name"]))
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -336,13 +336,13 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
|
||||
if repre.get("ext") not in self.supported_exts:
|
||||
self.log.debug((
|
||||
"Representation '{}' of unsupported extension. Skipped."
|
||||
).format(repre["name"]))
|
||||
"Representation '{}' has unsupported extension: '{}'. Skipped."
|
||||
).format(repre["name"], repre.get("ext")))
|
||||
return False
|
||||
|
||||
if not repre.get("files"):
|
||||
self.log.debug((
|
||||
"Representation '{}' have empty files. Skipped."
|
||||
"Representation '{}' has empty files. Skipped."
|
||||
).format(repre["name"]))
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from openpype.lib.transcoding import (
|
|||
get_transcode_temp_directory,
|
||||
)
|
||||
from openpype.pipeline.publish import KnownPublishError
|
||||
from openpype.pipeline.publish.lib import add_repre_files_for_cleanup
|
||||
|
||||
|
||||
class ExtractReview(pyblish.api.InstancePlugin):
|
||||
|
|
@ -92,8 +93,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
host_name = instance.context.data["hostName"]
|
||||
family = self.main_family_from_instance(instance)
|
||||
|
||||
self.log.info("Host: \"{}\"".format(host_name))
|
||||
self.log.info("Family: \"{}\"".format(family))
|
||||
self.log.debug("Host: \"{}\"".format(host_name))
|
||||
self.log.debug("Family: \"{}\"".format(family))
|
||||
|
||||
profile = filter_profiles(
|
||||
self.profiles,
|
||||
|
|
@ -351,7 +352,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
temp_data = self.prepare_temp_data(instance, repre, output_def)
|
||||
files_to_clean = []
|
||||
if temp_data["input_is_sequence"]:
|
||||
self.log.info("Filling gaps in sequence.")
|
||||
self.log.debug("Checking sequence to fill gaps in sequence..")
|
||||
files_to_clean = self.fill_sequence_gaps(
|
||||
files=temp_data["origin_repre"]["files"],
|
||||
staging_dir=new_repre["stagingDir"],
|
||||
|
|
@ -425,6 +426,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
)
|
||||
instance.data["representations"].append(new_repre)
|
||||
|
||||
add_repre_files_for_cleanup(instance, new_repre)
|
||||
|
||||
def input_is_sequence(self, repre):
|
||||
"""Deduce from representation data if input is sequence."""
|
||||
# TODO GLOBAL ISSUE - Find better way how to find out if input
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
).format(subset_name))
|
||||
return
|
||||
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
"Processing instance with subset name {}".format(subset_name)
|
||||
)
|
||||
|
||||
|
|
@ -89,13 +89,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
|
||||
src_staging = os.path.normpath(repre["stagingDir"])
|
||||
full_input_path = os.path.join(src_staging, input_file)
|
||||
self.log.info("input {}".format(full_input_path))
|
||||
self.log.debug("input {}".format(full_input_path))
|
||||
filename = os.path.splitext(input_file)[0]
|
||||
jpeg_file = filename + "_thumb.jpg"
|
||||
full_output_path = os.path.join(dst_staging, jpeg_file)
|
||||
|
||||
if oiio_supported:
|
||||
self.log.info("Trying to convert with OIIO")
|
||||
self.log.debug("Trying to convert with OIIO")
|
||||
# If the input can read by OIIO then use OIIO method for
|
||||
# conversion otherwise use ffmpeg
|
||||
thumbnail_created = self.create_thumbnail_oiio(
|
||||
|
|
@ -148,7 +148,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
|
||||
def _already_has_thumbnail(self, repres):
|
||||
for repre in repres:
|
||||
self.log.info("repre {}".format(repre))
|
||||
self.log.debug("repre {}".format(repre))
|
||||
if repre["name"] == "thumbnail":
|
||||
return True
|
||||
return False
|
||||
|
|
@ -173,20 +173,20 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
return filtered_repres
|
||||
|
||||
def create_thumbnail_oiio(self, src_path, dst_path):
|
||||
self.log.info("outputting {}".format(dst_path))
|
||||
self.log.info("Extracting thumbnail {}".format(dst_path))
|
||||
oiio_tool_path = get_oiio_tools_path()
|
||||
oiio_cmd = [
|
||||
oiio_tool_path,
|
||||
"-a", src_path,
|
||||
"-o", dst_path
|
||||
]
|
||||
self.log.info("running: {}".format(" ".join(oiio_cmd)))
|
||||
self.log.debug("running: {}".format(" ".join(oiio_cmd)))
|
||||
try:
|
||||
run_subprocess(oiio_cmd, logger=self.log)
|
||||
return True
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to create thubmnail using oiiotool",
|
||||
"Failed to create thumbnail using oiiotool",
|
||||
exc_info=True
|
||||
)
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
self._create_context_thumbnail(instance.context)
|
||||
|
||||
subset_name = instance.data["subset"]
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
"Processing instance with subset name {}".format(subset_name)
|
||||
)
|
||||
thumbnail_source = instance.data.get("thumbnailSource")
|
||||
|
|
@ -104,7 +104,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
full_output_path = os.path.join(dst_staging, dst_filename)
|
||||
|
||||
if oiio_supported:
|
||||
self.log.info("Trying to convert with OIIO")
|
||||
self.log.debug("Trying to convert with OIIO")
|
||||
# If the input can read by OIIO then use OIIO method for
|
||||
# conversion otherwise use ffmpeg
|
||||
thumbnail_created = self.create_thumbnail_oiio(
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
|
||||
instance_stagingdir = instance.data.get("stagingDir")
|
||||
if not instance_stagingdir:
|
||||
self.log.info((
|
||||
self.log.debug((
|
||||
"{0} is missing reference to staging directory."
|
||||
" Will try to get it from representation."
|
||||
).format(instance))
|
||||
|
|
@ -480,7 +480,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
update_data
|
||||
)
|
||||
|
||||
self.log.info("Prepared subset: {}".format(subset_name))
|
||||
self.log.debug("Prepared subset: {}".format(subset_name))
|
||||
return subset_doc
|
||||
|
||||
def prepare_version(self, instance, op_session, subset_doc, project_name):
|
||||
|
|
@ -521,7 +521,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
project_name, version_doc["type"], version_doc
|
||||
)
|
||||
|
||||
self.log.info("Prepared version: v{0:03d}".format(version_doc["name"]))
|
||||
self.log.debug(
|
||||
"Prepared version: v{0:03d}".format(version_doc["name"])
|
||||
)
|
||||
|
||||
return version_doc
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
|
||||
def process(self, instance):
|
||||
if instance.data.get("processedWithNewIntegrator"):
|
||||
self.log.info("Instance was already processed with new integrator")
|
||||
self.log.debug(
|
||||
"Instance was already processed with new integrator"
|
||||
)
|
||||
return
|
||||
|
||||
for ef in self.exclude_families:
|
||||
|
|
@ -274,7 +276,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
|
||||
stagingdir = instance.data.get("stagingDir")
|
||||
if not stagingdir:
|
||||
self.log.info((
|
||||
self.log.debug((
|
||||
"{0} is missing reference to staging directory."
|
||||
" Will try to get it from representation."
|
||||
).format(instance))
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin):
|
|||
# Filter instances which can be used for integration
|
||||
filtered_instance_items = self._prepare_instances(context)
|
||||
if not filtered_instance_items:
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
"All instances were filtered. Thumbnail integration skipped."
|
||||
)
|
||||
return
|
||||
|
|
@ -162,7 +162,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin):
|
|||
|
||||
# Skip instance if thumbnail path is not available for it
|
||||
if not thumbnail_path:
|
||||
self.log.info((
|
||||
self.log.debug((
|
||||
"Skipping thumbnail integration for instance \"{}\"."
|
||||
" Instance and context"
|
||||
" thumbnail paths are not available."
|
||||
|
|
|
|||
|
|
@ -358,12 +358,12 @@
|
|||
"optional": true,
|
||||
"active": true
|
||||
},
|
||||
"ValidateGizmo": {
|
||||
"ValidateBackdrop": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
},
|
||||
"ValidateBackdrop": {
|
||||
"ValidateGizmo": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
|
|
@ -401,7 +401,39 @@
|
|||
false
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"reposition_nodes": [
|
||||
{
|
||||
"node_class": "Reformat",
|
||||
"knobs": [
|
||||
{
|
||||
"type": "text",
|
||||
"name": "type",
|
||||
"value": "to format"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "format",
|
||||
"value": "HD_1080"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "filter",
|
||||
"value": "Lanczos6"
|
||||
},
|
||||
{
|
||||
"type": "bool",
|
||||
"name": "black_outside",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "bool",
|
||||
"name": "pbb",
|
||||
"value": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"ExtractReviewData": {
|
||||
"enabled": false
|
||||
|
|
|
|||
|
|
@ -323,7 +323,10 @@ class SchemasHub:
|
|||
filled_template = self._fill_template(
|
||||
schema_data, template_def
|
||||
)
|
||||
return filled_template
|
||||
new_template_def = []
|
||||
for item in filled_template:
|
||||
new_template_def.extend(self.resolve_schema_data(item))
|
||||
return new_template_def
|
||||
|
||||
def create_schema_object(self, schema_data, *args, **kwargs):
|
||||
"""Create entity for passed schema data.
|
||||
|
|
|
|||
|
|
@ -158,10 +158,43 @@
|
|||
"label": "Nodes",
|
||||
"collapsible": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Nodes attribute will be deprecated in future releases. Use reposition_nodes instead."
|
||||
},
|
||||
{
|
||||
"type": "raw-json",
|
||||
"key": "nodes",
|
||||
"label": "Nodes"
|
||||
"label": "Nodes [depricated]"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Reposition knobs supported only. You can add multiple reformat nodes <br/>and set their knobs. Order of reformat nodes is important. First reformat node <br/>will be applied first and last reformat node will be applied last."
|
||||
},
|
||||
{
|
||||
"key": "reposition_nodes",
|
||||
"type": "list",
|
||||
"label": "Reposition nodes",
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"key": "node_class",
|
||||
"label": "Node class",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_nuke_knob_inputs",
|
||||
"template_data": [
|
||||
{
|
||||
"label": "Node knobs",
|
||||
"key": "knobs"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@
|
|||
|
||||
"bg": "#2C313A",
|
||||
"bg-inputs": "#21252B",
|
||||
"bg-buttons": "#434a56",
|
||||
"bg-button-hover": "rgb(81, 86, 97)",
|
||||
"bg-buttons": "rgb(67, 74, 86)",
|
||||
"bg-buttons-hover": "rgb(81, 86, 97)",
|
||||
"bg-inputs-disabled": "#2C313A",
|
||||
"bg-buttons-disabled": "#434a56",
|
||||
|
||||
|
|
@ -66,7 +66,9 @@
|
|||
"bg-success": "#458056",
|
||||
"bg-success-hover": "#55a066",
|
||||
"bg-error": "#AD2E2E",
|
||||
"bg-error-hover": "#C93636"
|
||||
"bg-error-hover": "#C93636",
|
||||
"bg-info": "rgb(63, 98, 121)",
|
||||
"bg-info-hover": "rgb(81, 146, 181)"
|
||||
},
|
||||
"tab-widget": {
|
||||
"bg": "#21252B",
|
||||
|
|
@ -94,6 +96,7 @@
|
|||
"crash": "#FF6432",
|
||||
"success": "#458056",
|
||||
"warning": "#ffc671",
|
||||
"progress": "rgb(194, 226, 236)",
|
||||
"tab-bg": "#16191d",
|
||||
"list-view-group": {
|
||||
"bg": "#434a56",
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ QPushButton {
|
|||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
color: {color:font-hover};
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +166,7 @@ QToolButton {
|
|||
}
|
||||
|
||||
QToolButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
color: {color:font-hover};
|
||||
}
|
||||
|
||||
|
|
@ -722,6 +722,13 @@ OverlayMessageWidget[type="error"]:hover {
|
|||
background: {color:overlay-messages:bg-error-hover};
|
||||
}
|
||||
|
||||
OverlayMessageWidget[type="info"] {
|
||||
background: {color:overlay-messages:bg-info};
|
||||
}
|
||||
OverlayMessageWidget[type="info"]:hover {
|
||||
background: {color:overlay-messages:bg-info-hover};
|
||||
}
|
||||
|
||||
OverlayMessageWidget QWidget {
|
||||
background: transparent;
|
||||
}
|
||||
|
|
@ -749,10 +756,11 @@ OverlayMessageWidget QWidget {
|
|||
}
|
||||
|
||||
#InfoText {
|
||||
padding-left: 30px;
|
||||
padding-top: 20px;
|
||||
padding-left: 0px;
|
||||
padding-top: 0px;
|
||||
padding-right: 20px;
|
||||
background: transparent;
|
||||
border: 1px solid {color:border};
|
||||
border: none;
|
||||
}
|
||||
|
||||
#TypeEditor, #ToolEditor, #NameEditor, #NumberEditor {
|
||||
|
|
@ -914,7 +922,7 @@ PixmapButton{
|
|||
background: {color:bg-buttons};
|
||||
}
|
||||
PixmapButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
}
|
||||
PixmapButton:disabled {
|
||||
background: {color:bg-buttons-disabled};
|
||||
|
|
@ -925,7 +933,7 @@ PixmapButton:disabled {
|
|||
background: {color:bg-view};
|
||||
}
|
||||
#ThumbnailPixmapHoverButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
}
|
||||
|
||||
#CreatorDetailedDescription {
|
||||
|
|
@ -946,7 +954,7 @@ PixmapButton:disabled {
|
|||
}
|
||||
|
||||
#CreateDialogHelpButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
}
|
||||
#CreateDialogHelpButton QWidget {
|
||||
background: transparent;
|
||||
|
|
@ -1005,7 +1013,7 @@ PixmapButton:disabled {
|
|||
border-radius: 0.2em;
|
||||
}
|
||||
#CardViewWidget:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
}
|
||||
#CardViewWidget[state="selected"] {
|
||||
background: {color:bg-view-selection};
|
||||
|
|
@ -1032,7 +1040,7 @@ PixmapButton:disabled {
|
|||
}
|
||||
|
||||
#PublishInfoFrame[state="3"], #PublishInfoFrame[state="4"] {
|
||||
background: rgb(194, 226, 236);
|
||||
background: {color:publisher:progress};
|
||||
}
|
||||
|
||||
#PublishInfoFrame QLabel {
|
||||
|
|
@ -1040,6 +1048,11 @@ PixmapButton:disabled {
|
|||
font-style: bold;
|
||||
}
|
||||
|
||||
#PublishReportHeader {
|
||||
font-size: 14pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#PublishInfoMainLabel {
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
|
@ -1060,7 +1073,7 @@ ValidationArtistMessage QLabel {
|
|||
}
|
||||
|
||||
#ValidationActionButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
color: {color:font-hover};
|
||||
}
|
||||
|
||||
|
|
@ -1090,6 +1103,35 @@ ValidationArtistMessage QLabel {
|
|||
border-left: 1px solid {color:border};
|
||||
}
|
||||
|
||||
#PublishInstancesDetails {
|
||||
border: 1px solid {color:border};
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
#InstancesLogsView {
|
||||
border: 1px solid {color:border};
|
||||
background: {color:bg-view};
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
#PublishLogMessage {
|
||||
font-family: "Noto Sans Mono";
|
||||
}
|
||||
|
||||
#PublishInstanceLogsLabel {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#PublishCrashMainLabel{
|
||||
font-weight: bold;
|
||||
font-size: 16pt;
|
||||
}
|
||||
|
||||
#PublishCrashReportLabel {
|
||||
font-weight: bold;
|
||||
font-size: 13pt;
|
||||
}
|
||||
|
||||
#AssetNameInputWidget {
|
||||
background: {color:bg-inputs};
|
||||
border: 1px solid {color:border};
|
||||
|
|
|
|||
|
|
@ -198,29 +198,33 @@ class DropEmpty(QtWidgets.QWidget):
|
|||
|
||||
def paintEvent(self, event):
|
||||
super(DropEmpty, self).paintEvent(event)
|
||||
painter = QtGui.QPainter(self)
|
||||
|
||||
pen = QtGui.QPen()
|
||||
pen.setWidth(1)
|
||||
pen.setBrush(QtCore.Qt.darkGray)
|
||||
pen.setStyle(QtCore.Qt.DashLine)
|
||||
painter.setPen(pen)
|
||||
content_margins = self.layout().contentsMargins()
|
||||
pen.setWidth(1)
|
||||
|
||||
left_m = content_margins.left()
|
||||
top_m = content_margins.top()
|
||||
rect = QtCore.QRect(
|
||||
content_margins = self.layout().contentsMargins()
|
||||
rect = self.rect()
|
||||
left_m = content_margins.left() + pen.width()
|
||||
top_m = content_margins.top() + pen.width()
|
||||
new_rect = QtCore.QRect(
|
||||
left_m,
|
||||
top_m,
|
||||
(
|
||||
self.rect().width()
|
||||
rect.width()
|
||||
- (left_m + content_margins.right() + pen.width())
|
||||
),
|
||||
(
|
||||
self.rect().height()
|
||||
rect.height()
|
||||
- (top_m + content_margins.bottom() + pen.width())
|
||||
)
|
||||
)
|
||||
painter.drawRect(rect)
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
painter.setPen(pen)
|
||||
painter.drawRect(new_rect)
|
||||
|
||||
|
||||
class FilesModel(QtGui.QStandardItemModel):
|
||||
|
|
|
|||
|
|
@ -35,9 +35,13 @@ ResetKeySequence = QtGui.QKeySequence(
|
|||
|
||||
__all__ = (
|
||||
"CONTEXT_ID",
|
||||
"CONTEXT_LABEL",
|
||||
|
||||
"VARIANT_TOOLTIP",
|
||||
|
||||
"INPUTS_LAYOUT_HSPACING",
|
||||
"INPUTS_LAYOUT_VSPACING",
|
||||
|
||||
"INSTANCE_ID_ROLE",
|
||||
"SORT_VALUE_ROLE",
|
||||
"IS_GROUP_ROLE",
|
||||
|
|
@ -47,4 +51,6 @@ __all__ = (
|
|||
"FAMILY_ROLE",
|
||||
"GROUP_ROLE",
|
||||
"CONVERTER_IDENTIFIER_ROLE",
|
||||
|
||||
"ResetKeySequence",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ PLUGIN_ORDER_OFFSET = 0.5
|
|||
|
||||
class CardMessageTypes:
|
||||
standard = None
|
||||
info = "info"
|
||||
error = "error"
|
||||
|
||||
|
||||
|
|
@ -220,7 +221,12 @@ class PublishReportMaker:
|
|||
|
||||
def _add_plugin_data_item(self, plugin):
|
||||
if plugin in self._stored_plugins:
|
||||
raise ValueError("Plugin is already stored")
|
||||
# A plugin would be processed more than once. What can cause it:
|
||||
# - there is a bug in controller
|
||||
# - plugin class is imported into multiple files
|
||||
# - this can happen even with base classes from 'pyblish'
|
||||
raise ValueError(
|
||||
"Plugin '{}' is already stored".format(str(plugin)))
|
||||
|
||||
self._stored_plugins.append(plugin)
|
||||
|
||||
|
|
@ -239,6 +245,7 @@ class PublishReportMaker:
|
|||
label = plugin.label
|
||||
|
||||
return {
|
||||
"id": plugin.id,
|
||||
"name": plugin.__name__,
|
||||
"label": label,
|
||||
"order": plugin.order,
|
||||
|
|
@ -324,7 +331,7 @@ class PublishReportMaker:
|
|||
"instances": instances_details,
|
||||
"context": self._extract_context_data(self._current_context),
|
||||
"crashed_file_paths": crashed_file_paths,
|
||||
"id": str(uuid.uuid4()),
|
||||
"id": uuid.uuid4().hex,
|
||||
"report_version": "1.0.0"
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +349,9 @@ class PublishReportMaker:
|
|||
"label": instance.data.get("label"),
|
||||
"family": instance.data["family"],
|
||||
"families": instance.data.get("families") or [],
|
||||
"exists": exists
|
||||
"exists": exists,
|
||||
"creator_identifier": instance.data.get("creator_identifier"),
|
||||
"instance_id": instance.data.get("instance_id"),
|
||||
}
|
||||
|
||||
def _extract_instance_log_items(self, result):
|
||||
|
|
@ -388,8 +397,11 @@ class PublishReportMaker:
|
|||
exception = result.get("error")
|
||||
if exception:
|
||||
fname, line_no, func, exc = exception.traceback
|
||||
# Action result does not have 'is_validation_error'
|
||||
is_validation_error = result.get("is_validation_error", False)
|
||||
output.append({
|
||||
"type": "error",
|
||||
"is_validation_error": is_validation_error,
|
||||
"msg": str(exception),
|
||||
"filename": str(fname),
|
||||
"lineno": str(line_no),
|
||||
|
|
@ -426,13 +438,15 @@ class PublishPluginsProxy:
|
|||
plugin_id = plugin.id
|
||||
plugins_by_id[plugin_id] = plugin
|
||||
|
||||
action_ids = set()
|
||||
action_ids = []
|
||||
action_ids_by_plugin_id[plugin_id] = action_ids
|
||||
|
||||
actions = getattr(plugin, "actions", None) or []
|
||||
for action in actions:
|
||||
action_id = action.id
|
||||
action_ids.add(action_id)
|
||||
if action_id in actions_by_id:
|
||||
continue
|
||||
action_ids.append(action_id)
|
||||
actions_by_id[action_id] = action
|
||||
|
||||
self._plugins_by_id = plugins_by_id
|
||||
|
|
@ -461,7 +475,7 @@ class PublishPluginsProxy:
|
|||
return plugin.id
|
||||
|
||||
def get_plugin_action_items(self, plugin_id):
|
||||
"""Get plugin action items for plugin by it's id.
|
||||
"""Get plugin action items for plugin by its id.
|
||||
|
||||
Args:
|
||||
plugin_id (str): Publish plugin id.
|
||||
|
|
@ -568,7 +582,7 @@ class ValidationErrorItem:
|
|||
context_validation,
|
||||
title,
|
||||
description,
|
||||
detail,
|
||||
detail
|
||||
):
|
||||
self.instance_id = instance_id
|
||||
self.instance_label = instance_label
|
||||
|
|
@ -677,6 +691,8 @@ class PublishValidationErrorsReport:
|
|||
|
||||
for title in titles:
|
||||
grouped_error_items.append({
|
||||
"id": uuid.uuid4().hex,
|
||||
"plugin_id": plugin_id,
|
||||
"plugin_action_items": list(plugin_action_items),
|
||||
"error_items": error_items_by_title[title],
|
||||
"title": title
|
||||
|
|
@ -2379,7 +2395,8 @@ class PublisherController(BasePublisherController):
|
|||
yield MainThreadItem(self.stop_publish)
|
||||
|
||||
# Add plugin to publish report
|
||||
self._publish_report.add_plugin_iter(plugin, self._publish_context)
|
||||
self._publish_report.add_plugin_iter(
|
||||
plugin, self._publish_context)
|
||||
|
||||
# WARNING This is hack fix for optional plugins
|
||||
if not self._is_publish_plugin_active(plugin):
|
||||
|
|
@ -2461,14 +2478,14 @@ class PublisherController(BasePublisherController):
|
|||
plugin, self._publish_context, instance
|
||||
)
|
||||
|
||||
self._publish_report.add_result(result)
|
||||
|
||||
exception = result.get("error")
|
||||
if exception:
|
||||
has_validation_error = False
|
||||
if (
|
||||
isinstance(exception, PublishValidationError)
|
||||
and not self.publish_has_validated
|
||||
):
|
||||
has_validation_error = True
|
||||
self._add_validation_error(result)
|
||||
|
||||
else:
|
||||
|
|
@ -2482,6 +2499,10 @@ class PublisherController(BasePublisherController):
|
|||
self.publish_error_msg = msg
|
||||
self.publish_has_crashed = True
|
||||
|
||||
result["is_validation_error"] = has_validation_error
|
||||
|
||||
self._publish_report.add_result(result)
|
||||
|
||||
self._publish_next_process()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -163,7 +163,11 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit):
|
|||
super(ZoomPlainText, self).wheelEvent(event)
|
||||
return
|
||||
|
||||
degrees = float(event.delta()) / 8
|
||||
if hasattr(event, "angleDelta"):
|
||||
delta = event.angleDelta().y()
|
||||
else:
|
||||
delta = event.delta()
|
||||
degrees = float(delta) / 8
|
||||
steps = int(ceil(degrees / 5))
|
||||
self._scheduled_scalings += steps
|
||||
if (self._scheduled_scalings * steps < 0):
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from .help_widget import (
|
|||
from .publish_frame import PublishFrame
|
||||
from .tabs_widget import PublisherTabsWidget
|
||||
from .overview_widget import OverviewWidget
|
||||
from .validations_widget import ValidationsWidget
|
||||
from .report_page import ReportPageWidget
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
@ -40,5 +40,5 @@ __all__ = (
|
|||
|
||||
"PublisherTabsWidget",
|
||||
"OverviewWidget",
|
||||
"ValidationsWidget",
|
||||
"ReportPageWidget",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class BaseGroupWidget(QtWidgets.QWidget):
|
|||
return self._group
|
||||
|
||||
def get_widget_by_item_id(self, item_id):
|
||||
"""Get instance widget by it's id."""
|
||||
"""Get instance widget by its id."""
|
||||
|
||||
return self._widgets_by_id.get(item_id)
|
||||
|
||||
|
|
@ -702,8 +702,8 @@ class InstanceCardView(AbstractInstanceView):
|
|||
|
||||
for group_name in sorted_group_names:
|
||||
group_icons = {
|
||||
idenfier: self._controller.get_creator_icon(idenfier)
|
||||
for idenfier in identifiers_by_group[group_name]
|
||||
identifier: self._controller.get_creator_icon(identifier)
|
||||
for identifier in identifiers_by_group[group_name]
|
||||
}
|
||||
if group_name in self._widgets_by_group:
|
||||
group_widget = self._widgets_by_group[group_name]
|
||||
|
|
|
|||
BIN
openpype/tools/publisher/widgets/images/error.png
Normal file
BIN
openpype/tools/publisher/widgets/images/error.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
openpype/tools/publisher/widgets/images/success.png
Normal file
BIN
openpype/tools/publisher/widgets/images/success.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 11 KiB |
|
|
@ -468,45 +468,14 @@ class PublishFrame(QtWidgets.QWidget):
|
|||
widget.setProperty("state", state)
|
||||
widget.style().polish(widget)
|
||||
|
||||
def _copy_report(self):
|
||||
logs = self._controller.get_publish_report()
|
||||
logs_string = json.dumps(logs, indent=4)
|
||||
|
||||
mime_data = QtCore.QMimeData()
|
||||
mime_data.setText(logs_string)
|
||||
QtWidgets.QApplication.instance().clipboard().setMimeData(
|
||||
mime_data
|
||||
)
|
||||
|
||||
def _export_report(self):
|
||||
default_filename = "publish-report-{}".format(
|
||||
time.strftime("%y%m%d-%H-%M")
|
||||
)
|
||||
default_filepath = os.path.join(
|
||||
os.path.expanduser("~"),
|
||||
default_filename
|
||||
)
|
||||
new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self, "Save report", default_filepath, ".json"
|
||||
)
|
||||
if not ext or not new_filepath:
|
||||
return
|
||||
|
||||
logs = self._controller.get_publish_report()
|
||||
full_path = new_filepath + ext
|
||||
dir_path = os.path.dirname(full_path)
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
|
||||
with open(full_path, "w") as file_stream:
|
||||
json.dump(logs, file_stream)
|
||||
|
||||
def _on_report_triggered(self, identifier):
|
||||
if identifier == "export_report":
|
||||
self._export_report()
|
||||
self._controller.event_system.emit(
|
||||
"export_report.request", {}, "publish_frame")
|
||||
|
||||
elif identifier == "copy_report":
|
||||
self._copy_report()
|
||||
self._controller.event_system.emit(
|
||||
"copy_report.request", {}, "publish_frame")
|
||||
|
||||
elif identifier == "go_to_report":
|
||||
self.details_page_requested.emit()
|
||||
|
|
|
|||
1876
openpype/tools/publisher/widgets/report_page.py
Normal file
1876
openpype/tools/publisher/widgets/report_page.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -75,6 +75,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
|
|||
|
||||
painter = QtGui.QPainter()
|
||||
painter.begin(self)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
painter.drawPixmap(0, 0, self._cached_pix)
|
||||
painter.end()
|
||||
|
||||
|
|
@ -183,6 +184,18 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
|
|||
backgrounded_images.append(new_pix)
|
||||
return backgrounded_images
|
||||
|
||||
def _paint_dash_line(self, painter, rect):
|
||||
pen = QtGui.QPen()
|
||||
pen.setWidth(1)
|
||||
pen.setBrush(QtCore.Qt.darkGray)
|
||||
pen.setStyle(QtCore.Qt.DashLine)
|
||||
|
||||
new_rect = rect.adjusted(1, 1, -1, -1)
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(QtCore.Qt.transparent)
|
||||
# painter.drawRect(rect)
|
||||
painter.drawRect(new_rect)
|
||||
|
||||
def _cache_pix(self):
|
||||
rect = self.rect()
|
||||
rect_width = rect.width()
|
||||
|
|
@ -264,13 +277,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
|
|||
|
||||
# Draw drop enabled dashes
|
||||
if used_default_pix:
|
||||
pen = QtGui.QPen()
|
||||
pen.setWidth(1)
|
||||
pen.setBrush(QtCore.Qt.darkGray)
|
||||
pen.setStyle(QtCore.Qt.DashLine)
|
||||
final_painter.setPen(pen)
|
||||
final_painter.setBrush(QtCore.Qt.transparent)
|
||||
final_painter.drawRect(rect)
|
||||
self._paint_dash_line(final_painter, rect)
|
||||
|
||||
final_painter.end()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,715 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
try:
|
||||
import commonmark
|
||||
except Exception:
|
||||
commonmark = None
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.tools.utils import BaseClickableFrame, ClickableFrame
|
||||
from .widgets import (
|
||||
IconValuePixmapLabel
|
||||
)
|
||||
from ..constants import (
|
||||
INSTANCE_ID_ROLE
|
||||
)
|
||||
|
||||
|
||||
class ValidationErrorInstanceList(QtWidgets.QListView):
|
||||
"""List of publish instances that caused a validation error.
|
||||
|
||||
Instances are collected per plugin's validation error title.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ValidationErrorInstanceList, self).__init__(*args, **kwargs)
|
||||
|
||||
self.setObjectName("ValidationErrorInstanceList")
|
||||
|
||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
|
||||
def minimumSizeHint(self):
|
||||
return self.sizeHint()
|
||||
|
||||
def sizeHint(self):
|
||||
result = super(ValidationErrorInstanceList, self).sizeHint()
|
||||
row_count = self.model().rowCount()
|
||||
height = 0
|
||||
if row_count > 0:
|
||||
height = self.sizeHintForRow(0) * row_count
|
||||
result.setHeight(height)
|
||||
return result
|
||||
|
||||
|
||||
class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
||||
"""Title of validation error.
|
||||
|
||||
Widget is used as radio button so requires clickable functionality and
|
||||
changing style on selection/deselection.
|
||||
|
||||
Has toggle button to show/hide instances on which validation error happened
|
||||
if there is a list (Valdation error may happen on context).
|
||||
"""
|
||||
|
||||
selected = QtCore.Signal(int)
|
||||
instance_changed = QtCore.Signal(int)
|
||||
|
||||
def __init__(self, index, error_info, parent):
|
||||
super(ValidationErrorTitleWidget, self).__init__(parent)
|
||||
|
||||
self._index = index
|
||||
self._error_info = error_info
|
||||
self._selected = False
|
||||
|
||||
title_frame = ClickableFrame(self)
|
||||
title_frame.setObjectName("ValidationErrorTitleFrame")
|
||||
|
||||
toggle_instance_btn = QtWidgets.QToolButton(title_frame)
|
||||
toggle_instance_btn.setObjectName("ArrowBtn")
|
||||
toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)
|
||||
toggle_instance_btn.setMaximumWidth(14)
|
||||
|
||||
label_widget = QtWidgets.QLabel(error_info["title"], title_frame)
|
||||
|
||||
title_frame_layout = QtWidgets.QHBoxLayout(title_frame)
|
||||
title_frame_layout.addWidget(label_widget, 1)
|
||||
title_frame_layout.addWidget(toggle_instance_btn, 0)
|
||||
|
||||
instances_model = QtGui.QStandardItemModel()
|
||||
|
||||
help_text_by_instance_id = {}
|
||||
|
||||
items = []
|
||||
context_validation = False
|
||||
for error_item in error_info["error_items"]:
|
||||
context_validation = error_item.context_validation
|
||||
if context_validation:
|
||||
toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow)
|
||||
description = self._prepare_description(error_item)
|
||||
help_text_by_instance_id[None] = description
|
||||
# Add fake item to have minimum size hint of view widget
|
||||
items.append(QtGui.QStandardItem("Context"))
|
||||
continue
|
||||
|
||||
label = error_item.instance_label
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setFlags(
|
||||
QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||
)
|
||||
item.setData(label, QtCore.Qt.ToolTipRole)
|
||||
item.setData(error_item.instance_id, INSTANCE_ID_ROLE)
|
||||
items.append(item)
|
||||
description = self._prepare_description(error_item)
|
||||
help_text_by_instance_id[error_item.instance_id] = description
|
||||
|
||||
if items:
|
||||
root_item = instances_model.invisibleRootItem()
|
||||
root_item.appendRows(items)
|
||||
|
||||
instances_view = ValidationErrorInstanceList(self)
|
||||
instances_view.setModel(instances_model)
|
||||
|
||||
self.setLayoutDirection(QtCore.Qt.LeftToRight)
|
||||
|
||||
view_widget = QtWidgets.QWidget(self)
|
||||
view_layout = QtWidgets.QHBoxLayout(view_widget)
|
||||
view_layout.setContentsMargins(0, 0, 0, 0)
|
||||
view_layout.setSpacing(0)
|
||||
view_layout.addSpacing(14)
|
||||
view_layout.addWidget(instances_view, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(title_frame, 0)
|
||||
layout.addWidget(view_widget, 0)
|
||||
view_widget.setVisible(False)
|
||||
|
||||
if not context_validation:
|
||||
toggle_instance_btn.clicked.connect(self._on_toggle_btn_click)
|
||||
|
||||
title_frame.clicked.connect(self._mouse_release_callback)
|
||||
instances_view.selectionModel().selectionChanged.connect(
|
||||
self._on_seleciton_change
|
||||
)
|
||||
|
||||
self._title_frame = title_frame
|
||||
|
||||
self._toggle_instance_btn = toggle_instance_btn
|
||||
|
||||
self._view_widget = view_widget
|
||||
|
||||
self._instances_model = instances_model
|
||||
self._instances_view = instances_view
|
||||
|
||||
self._context_validation = context_validation
|
||||
self._help_text_by_instance_id = help_text_by_instance_id
|
||||
|
||||
self._expanded = False
|
||||
|
||||
def sizeHint(self):
|
||||
result = super(ValidationErrorTitleWidget, self).sizeHint()
|
||||
expected_width = max(
|
||||
self._view_widget.minimumSizeHint().width(),
|
||||
self._view_widget.sizeHint().width()
|
||||
)
|
||||
|
||||
if expected_width < 200:
|
||||
expected_width = 200
|
||||
|
||||
if result.width() < expected_width:
|
||||
result.setWidth(expected_width)
|
||||
|
||||
return result
|
||||
|
||||
def minimumSizeHint(self):
|
||||
return self.sizeHint()
|
||||
|
||||
def _prepare_description(self, error_item):
|
||||
"""Prepare description text for detail intput.
|
||||
|
||||
Args:
|
||||
error_item (ValidationErrorItem): Item which hold information about
|
||||
validation error.
|
||||
|
||||
Returns:
|
||||
str: Prepared detailed description.
|
||||
"""
|
||||
|
||||
dsc = error_item.description
|
||||
detail = error_item.detail
|
||||
if detail:
|
||||
dsc += "<br/><br/>{}".format(detail)
|
||||
|
||||
description = dsc
|
||||
if commonmark:
|
||||
description = commonmark.commonmark(dsc)
|
||||
return description
|
||||
|
||||
def _mouse_release_callback(self):
|
||||
"""Mark this widget as selected on click."""
|
||||
|
||||
self.set_selected(True)
|
||||
|
||||
def current_description_text(self):
|
||||
if self._context_validation:
|
||||
return self._help_text_by_instance_id[None]
|
||||
index = self._instances_view.currentIndex()
|
||||
# TODO make sure instance is selected
|
||||
if not index.isValid():
|
||||
index = self._instances_model.index(0, 0)
|
||||
|
||||
indence_id = index.data(INSTANCE_ID_ROLE)
|
||||
return self._help_text_by_instance_id[indence_id]
|
||||
|
||||
@property
|
||||
def is_selected(self):
|
||||
"""Is widget marked a selected.
|
||||
|
||||
Returns:
|
||||
bool: Item is selected or not.
|
||||
"""
|
||||
|
||||
return self._selected
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
"""Widget's index set by parent.
|
||||
|
||||
Returns:
|
||||
int: Index of widget.
|
||||
"""
|
||||
|
||||
return self._index
|
||||
|
||||
def set_index(self, index):
|
||||
"""Set index of widget (called by parent).
|
||||
|
||||
Args:
|
||||
int: New index of widget.
|
||||
"""
|
||||
|
||||
self._index = index
|
||||
|
||||
def _change_style_property(self, selected):
|
||||
"""Change style of widget based on selection."""
|
||||
|
||||
value = "1" if selected else ""
|
||||
self._title_frame.setProperty("selected", value)
|
||||
self._title_frame.style().polish(self._title_frame)
|
||||
|
||||
def set_selected(self, selected=None):
|
||||
"""Change selected state of widget."""
|
||||
|
||||
if selected is None:
|
||||
selected = not self._selected
|
||||
|
||||
# Clear instance view selection on deselect
|
||||
if not selected:
|
||||
self._instances_view.clearSelection()
|
||||
|
||||
# Skip if has same value
|
||||
if selected == self._selected:
|
||||
return
|
||||
|
||||
self._selected = selected
|
||||
self._change_style_property(selected)
|
||||
if selected:
|
||||
self.selected.emit(self._index)
|
||||
self._set_expanded(True)
|
||||
|
||||
def _on_toggle_btn_click(self):
|
||||
"""Show/hide instances list."""
|
||||
|
||||
self._set_expanded()
|
||||
|
||||
def _set_expanded(self, expanded=None):
|
||||
if expanded is None:
|
||||
expanded = not self._expanded
|
||||
|
||||
elif expanded is self._expanded:
|
||||
return
|
||||
|
||||
if expanded and self._context_validation:
|
||||
return
|
||||
|
||||
self._expanded = expanded
|
||||
self._view_widget.setVisible(expanded)
|
||||
if expanded:
|
||||
self._toggle_instance_btn.setArrowType(QtCore.Qt.DownArrow)
|
||||
else:
|
||||
self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)
|
||||
|
||||
def _on_seleciton_change(self):
|
||||
sel_model = self._instances_view.selectionModel()
|
||||
if sel_model.selectedIndexes():
|
||||
self.instance_changed.emit(self._index)
|
||||
|
||||
|
||||
class ActionButton(BaseClickableFrame):
|
||||
"""Plugin's action callback button.
|
||||
|
||||
Action may have label or icon or both.
|
||||
|
||||
Args:
|
||||
plugin_action_item (PublishPluginActionItem): Action item that can be
|
||||
triggered by it's id.
|
||||
"""
|
||||
|
||||
action_clicked = QtCore.Signal(str, str)
|
||||
|
||||
def __init__(self, plugin_action_item, parent):
|
||||
super(ActionButton, self).__init__(parent)
|
||||
|
||||
self.setObjectName("ValidationActionButton")
|
||||
|
||||
self.plugin_action_item = plugin_action_item
|
||||
|
||||
action_label = plugin_action_item.label
|
||||
action_icon = plugin_action_item.icon
|
||||
label_widget = QtWidgets.QLabel(action_label, self)
|
||||
icon_label = None
|
||||
if action_icon:
|
||||
icon_label = IconValuePixmapLabel(action_icon, self)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(5, 0, 5, 0)
|
||||
layout.addWidget(label_widget, 1)
|
||||
if icon_label:
|
||||
layout.addWidget(icon_label, 0)
|
||||
|
||||
self.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Minimum,
|
||||
self.sizePolicy().verticalPolicy()
|
||||
)
|
||||
|
||||
def _mouse_release_callback(self):
|
||||
self.action_clicked.emit(
|
||||
self.plugin_action_item.plugin_id,
|
||||
self.plugin_action_item.action_id
|
||||
)
|
||||
|
||||
|
||||
class ValidateActionsWidget(QtWidgets.QFrame):
|
||||
"""Wrapper widget for plugin actions.
|
||||
|
||||
Change actions based on selected validation error.
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(ValidateActionsWidget, self).__init__(parent)
|
||||
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
content_widget = QtWidgets.QWidget(self)
|
||||
content_layout = QtWidgets.QVBoxLayout(content_widget)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(content_widget)
|
||||
|
||||
self._controller = controller
|
||||
self._content_widget = content_widget
|
||||
self._content_layout = content_layout
|
||||
self._actions_mapping = {}
|
||||
|
||||
def clear(self):
|
||||
"""Remove actions from widget."""
|
||||
while self._content_layout.count():
|
||||
item = self._content_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget:
|
||||
widget.setVisible(False)
|
||||
widget.deleteLater()
|
||||
self._actions_mapping = {}
|
||||
|
||||
def set_error_item(self, error_item):
|
||||
"""Set selected plugin and show it's actions.
|
||||
|
||||
Clears current actions from widget and recreate them from the plugin.
|
||||
|
||||
Args:
|
||||
Dict[str, Any]: Object holding error items, title and possible
|
||||
actions to run.
|
||||
"""
|
||||
|
||||
self.clear()
|
||||
|
||||
if not error_item:
|
||||
self.setVisible(False)
|
||||
return
|
||||
|
||||
plugin_action_items = error_item["plugin_action_items"]
|
||||
for plugin_action_item in plugin_action_items:
|
||||
if not plugin_action_item.active:
|
||||
continue
|
||||
|
||||
if plugin_action_item.on_filter not in ("failed", "all"):
|
||||
continue
|
||||
|
||||
action_id = plugin_action_item.action_id
|
||||
self._actions_mapping[action_id] = plugin_action_item
|
||||
|
||||
action_btn = ActionButton(plugin_action_item, self._content_widget)
|
||||
action_btn.action_clicked.connect(self._on_action_click)
|
||||
self._content_layout.addWidget(action_btn)
|
||||
|
||||
if self._content_layout.count() > 0:
|
||||
self.setVisible(True)
|
||||
self._content_layout.addStretch(1)
|
||||
else:
|
||||
self.setVisible(False)
|
||||
|
||||
def _on_action_click(self, plugin_id, action_id):
|
||||
self._controller.run_action(plugin_id, action_id)
|
||||
|
||||
|
||||
class VerticallScrollArea(QtWidgets.QScrollArea):
|
||||
"""Scroll area for validation error titles.
|
||||
|
||||
The biggest difference is that the scroll area has scroll bar on left side
|
||||
and resize of content will also resize scrollarea itself.
|
||||
|
||||
Resize if deferred by 100ms because at the moment of resize are not yet
|
||||
propagated sizes and visibility of scroll bars.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(VerticallScrollArea, self).__init__(*args, **kwargs)
|
||||
|
||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||
self.setLayoutDirection(QtCore.Qt.RightToLeft)
|
||||
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
# Background of scrollbar will be transparent
|
||||
scrollbar_bg = self.verticalScrollBar().parent()
|
||||
if scrollbar_bg:
|
||||
scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
self.setViewportMargins(0, 0, 0, 0)
|
||||
|
||||
self.verticalScrollBar().installEventFilter(self)
|
||||
|
||||
# Timer with 100ms offset after changing size
|
||||
size_changed_timer = QtCore.QTimer()
|
||||
size_changed_timer.setInterval(100)
|
||||
size_changed_timer.setSingleShot(True)
|
||||
|
||||
size_changed_timer.timeout.connect(self._on_timer_timeout)
|
||||
self._size_changed_timer = size_changed_timer
|
||||
|
||||
def setVerticalScrollBar(self, widget):
|
||||
old_widget = self.verticalScrollBar()
|
||||
if old_widget:
|
||||
old_widget.removeEventFilter(self)
|
||||
|
||||
super(VerticallScrollArea, self).setVerticalScrollBar(widget)
|
||||
if widget:
|
||||
widget.installEventFilter(self)
|
||||
|
||||
def setWidget(self, widget):
|
||||
old_widget = self.widget()
|
||||
if old_widget:
|
||||
old_widget.removeEventFilter(self)
|
||||
|
||||
super(VerticallScrollArea, self).setWidget(widget)
|
||||
if widget:
|
||||
widget.installEventFilter(self)
|
||||
|
||||
def _on_timer_timeout(self):
|
||||
width = self.widget().width()
|
||||
if self.verticalScrollBar().isVisible():
|
||||
width += self.verticalScrollBar().width()
|
||||
self.setMinimumWidth(width)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if (
|
||||
event.type() == QtCore.QEvent.Resize
|
||||
and (obj is self.widget() or obj is self.verticalScrollBar())
|
||||
):
|
||||
self._size_changed_timer.start()
|
||||
return super(VerticallScrollArea, self).eventFilter(obj, event)
|
||||
|
||||
|
||||
class ValidationArtistMessage(QtWidgets.QWidget):
|
||||
def __init__(self, message, parent):
|
||||
super(ValidationArtistMessage, self).__init__(parent)
|
||||
|
||||
artist_msg_label = QtWidgets.QLabel(message, self)
|
||||
artist_msg_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(
|
||||
artist_msg_label, 1, QtCore.Qt.AlignCenter
|
||||
)
|
||||
|
||||
|
||||
class ValidationsWidget(QtWidgets.QFrame):
|
||||
"""Widgets showing validation error.
|
||||
|
||||
This widget is shown if validation error/s happened during validation part.
|
||||
|
||||
Shows validation error titles with instances on which happened and
|
||||
validation error detail with possible actions (repair).
|
||||
|
||||
┌──────┬────────────────┬───────┐
|
||||
│titles│ │actions│
|
||||
│ │ │ │
|
||||
│ │ Error detail │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
└──────┴────────────────┴───────┘
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(ValidationsWidget, self).__init__(parent)
|
||||
|
||||
# Before publishing
|
||||
before_publish_widget = ValidationArtistMessage(
|
||||
"Nothing to report until you run publish", self
|
||||
)
|
||||
# After success publishing
|
||||
publish_started_widget = ValidationArtistMessage(
|
||||
"So far so good", self
|
||||
)
|
||||
# After success publishing
|
||||
publish_stop_ok_widget = ValidationArtistMessage(
|
||||
"Publishing finished successfully", self
|
||||
)
|
||||
# After failed publishing (not with validation error)
|
||||
publish_stop_fail_widget = ValidationArtistMessage(
|
||||
"This is not your fault...", self
|
||||
)
|
||||
|
||||
# Validation errors
|
||||
validations_widget = QtWidgets.QWidget(self)
|
||||
|
||||
content_widget = QtWidgets.QWidget(validations_widget)
|
||||
|
||||
errors_scroll = VerticallScrollArea(content_widget)
|
||||
errors_scroll.setWidgetResizable(True)
|
||||
|
||||
errors_widget = QtWidgets.QWidget(errors_scroll)
|
||||
errors_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
errors_layout = QtWidgets.QVBoxLayout(errors_widget)
|
||||
errors_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
errors_scroll.setWidget(errors_widget)
|
||||
|
||||
error_details_frame = QtWidgets.QFrame(content_widget)
|
||||
error_details_input = QtWidgets.QTextEdit(error_details_frame)
|
||||
error_details_input.setObjectName("InfoText")
|
||||
error_details_input.setTextInteractionFlags(
|
||||
QtCore.Qt.TextBrowserInteraction
|
||||
)
|
||||
|
||||
actions_widget = ValidateActionsWidget(controller, content_widget)
|
||||
actions_widget.setMinimumWidth(140)
|
||||
|
||||
error_details_layout = QtWidgets.QHBoxLayout(error_details_frame)
|
||||
error_details_layout.addWidget(error_details_input, 1)
|
||||
error_details_layout.addWidget(actions_widget, 0)
|
||||
|
||||
content_layout = QtWidgets.QHBoxLayout(content_widget)
|
||||
content_layout.setSpacing(0)
|
||||
content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
content_layout.addWidget(errors_scroll, 0)
|
||||
content_layout.addWidget(error_details_frame, 1)
|
||||
|
||||
top_label = QtWidgets.QLabel(
|
||||
"Publish validation report", content_widget
|
||||
)
|
||||
top_label.setObjectName("PublishInfoMainLabel")
|
||||
top_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
validation_layout = QtWidgets.QVBoxLayout(validations_widget)
|
||||
validation_layout.setContentsMargins(0, 0, 0, 0)
|
||||
validation_layout.addWidget(top_label, 0)
|
||||
validation_layout.addWidget(content_widget, 1)
|
||||
|
||||
main_layout = QtWidgets.QStackedLayout(self)
|
||||
main_layout.addWidget(before_publish_widget)
|
||||
main_layout.addWidget(publish_started_widget)
|
||||
main_layout.addWidget(publish_stop_ok_widget)
|
||||
main_layout.addWidget(publish_stop_fail_widget)
|
||||
main_layout.addWidget(validations_widget)
|
||||
|
||||
main_layout.setCurrentWidget(before_publish_widget)
|
||||
|
||||
controller.event_system.add_callback(
|
||||
"publish.process.started", self._on_publish_start
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"publish.reset.finished", self._on_publish_reset
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"publish.process.stopped", self._on_publish_stop
|
||||
)
|
||||
|
||||
self._main_layout = main_layout
|
||||
|
||||
self._before_publish_widget = before_publish_widget
|
||||
self._publish_started_widget = publish_started_widget
|
||||
self._publish_stop_ok_widget = publish_stop_ok_widget
|
||||
self._publish_stop_fail_widget = publish_stop_fail_widget
|
||||
self._validations_widget = validations_widget
|
||||
|
||||
self._top_label = top_label
|
||||
self._errors_widget = errors_widget
|
||||
self._errors_layout = errors_layout
|
||||
self._error_details_frame = error_details_frame
|
||||
self._error_details_input = error_details_input
|
||||
self._actions_widget = actions_widget
|
||||
|
||||
self._title_widgets = {}
|
||||
self._error_info = {}
|
||||
self._previous_select = None
|
||||
|
||||
self._controller = controller
|
||||
|
||||
def clear(self):
|
||||
"""Delete all dynamic widgets and hide all wrappers."""
|
||||
self._title_widgets = {}
|
||||
self._error_info = {}
|
||||
self._previous_select = None
|
||||
while self._errors_layout.count():
|
||||
item = self._errors_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget:
|
||||
widget.deleteLater()
|
||||
|
||||
self._top_label.setVisible(False)
|
||||
self._error_details_frame.setVisible(False)
|
||||
self._errors_widget.setVisible(False)
|
||||
self._actions_widget.setVisible(False)
|
||||
|
||||
def _set_errors(self, validation_error_report):
|
||||
"""Set errors into context and created titles.
|
||||
|
||||
Args:
|
||||
validation_error_report (PublishValidationErrorsReport): Report
|
||||
with information about validation errors and publish plugin
|
||||
actions.
|
||||
"""
|
||||
|
||||
self.clear()
|
||||
if not validation_error_report:
|
||||
return
|
||||
|
||||
self._top_label.setVisible(True)
|
||||
self._error_details_frame.setVisible(True)
|
||||
self._errors_widget.setVisible(True)
|
||||
|
||||
grouped_error_items = validation_error_report.group_items_by_title()
|
||||
for idx, error_info in enumerate(grouped_error_items):
|
||||
widget = ValidationErrorTitleWidget(idx, error_info, self)
|
||||
widget.selected.connect(self._on_select)
|
||||
widget.instance_changed.connect(self._on_instance_change)
|
||||
self._errors_layout.addWidget(widget)
|
||||
self._title_widgets[idx] = widget
|
||||
self._error_info[idx] = error_info
|
||||
|
||||
self._errors_layout.addStretch(1)
|
||||
|
||||
if self._title_widgets:
|
||||
self._title_widgets[0].set_selected(True)
|
||||
|
||||
self.updateGeometry()
|
||||
|
||||
def _set_current_widget(self, widget):
|
||||
self._main_layout.setCurrentWidget(widget)
|
||||
|
||||
def _on_publish_start(self):
|
||||
self._set_current_widget(self._publish_started_widget)
|
||||
|
||||
def _on_publish_reset(self):
|
||||
self._set_current_widget(self._before_publish_widget)
|
||||
|
||||
def _on_publish_stop(self):
|
||||
if self._controller.publish_has_crashed:
|
||||
self._set_current_widget(self._publish_stop_fail_widget)
|
||||
return
|
||||
|
||||
if self._controller.publish_has_validation_errors:
|
||||
validation_errors = self._controller.get_validation_errors()
|
||||
self._set_current_widget(self._validations_widget)
|
||||
self._set_errors(validation_errors)
|
||||
return
|
||||
|
||||
if self._controller.publish_has_finished:
|
||||
self._set_current_widget(self._publish_stop_ok_widget)
|
||||
return
|
||||
|
||||
self._set_current_widget(self._publish_started_widget)
|
||||
|
||||
def _on_select(self, index):
|
||||
if self._previous_select:
|
||||
if self._previous_select.index == index:
|
||||
return
|
||||
self._previous_select.set_selected(False)
|
||||
|
||||
self._previous_select = self._title_widgets[index]
|
||||
|
||||
error_item = self._error_info[index]
|
||||
|
||||
self._actions_widget.set_error_item(error_item)
|
||||
|
||||
self._update_description()
|
||||
|
||||
def _on_instance_change(self, index):
|
||||
if self._previous_select and self._previous_select.index != index:
|
||||
self._title_widgets[index].set_selected(True)
|
||||
else:
|
||||
self._update_description()
|
||||
|
||||
def _update_description(self):
|
||||
description = self._previous_select.current_description_text()
|
||||
if commonmark:
|
||||
html = commonmark.commonmark(description)
|
||||
self._error_details_input.setHtml(html)
|
||||
elif hasattr(self._error_details_input, "setMarkdown"):
|
||||
self._error_details_input.setMarkdown(description)
|
||||
else:
|
||||
self._error_details_input.setText(description)
|
||||
|
|
@ -40,6 +40,41 @@ from ..constants import (
|
|||
INPUTS_LAYOUT_VSPACING,
|
||||
)
|
||||
|
||||
FA_PREFIXES = ["", "fa.", "fa5.", "fa5b.", "fa5s.", "ei.", "mdi."]
|
||||
|
||||
|
||||
def parse_icon_def(
|
||||
icon_def, default_width=None, default_height=None, color=None
|
||||
):
|
||||
if not icon_def:
|
||||
return None
|
||||
|
||||
if isinstance(icon_def, QtGui.QPixmap):
|
||||
return icon_def
|
||||
|
||||
color = color or "white"
|
||||
default_width = default_width or 512
|
||||
default_height = default_height or 512
|
||||
|
||||
if isinstance(icon_def, QtGui.QIcon):
|
||||
return icon_def.pixmap(default_width, default_height)
|
||||
|
||||
try:
|
||||
if os.path.exists(icon_def):
|
||||
return QtGui.QPixmap(icon_def)
|
||||
except Exception:
|
||||
# TODO logging
|
||||
pass
|
||||
|
||||
for prefix in FA_PREFIXES:
|
||||
try:
|
||||
icon_name = "{}{}".format(prefix, icon_def)
|
||||
icon = qtawesome.icon(icon_name, color=color)
|
||||
return icon.pixmap(default_width, default_height)
|
||||
except Exception:
|
||||
# TODO logging
|
||||
continue
|
||||
|
||||
|
||||
class PublishPixmapLabel(PixmapLabel):
|
||||
def _get_pix_size(self):
|
||||
|
|
@ -54,7 +89,6 @@ class IconValuePixmapLabel(PublishPixmapLabel):
|
|||
Handle icon parsing from creators/instances. Using of QAwesome module
|
||||
of path to images.
|
||||
"""
|
||||
fa_prefixes = ["", "fa."]
|
||||
default_size = 200
|
||||
|
||||
def __init__(self, icon_def, parent):
|
||||
|
|
@ -77,31 +111,9 @@ class IconValuePixmapLabel(PublishPixmapLabel):
|
|||
return pix
|
||||
|
||||
def _parse_icon_def(self, icon_def):
|
||||
if not icon_def:
|
||||
return self._default_pixmap()
|
||||
|
||||
if isinstance(icon_def, QtGui.QPixmap):
|
||||
return icon_def
|
||||
|
||||
if isinstance(icon_def, QtGui.QIcon):
|
||||
return icon_def.pixmap(self.default_size, self.default_size)
|
||||
|
||||
try:
|
||||
if os.path.exists(icon_def):
|
||||
return QtGui.QPixmap(icon_def)
|
||||
except Exception:
|
||||
# TODO logging
|
||||
pass
|
||||
|
||||
for prefix in self.fa_prefixes:
|
||||
try:
|
||||
icon_name = "{}{}".format(prefix, icon_def)
|
||||
icon = qtawesome.icon(icon_name, color="white")
|
||||
return icon.pixmap(self.default_size, self.default_size)
|
||||
except Exception:
|
||||
# TODO logging
|
||||
continue
|
||||
|
||||
icon = parse_icon_def(icon_def, self.default_size, self.default_size)
|
||||
if icon:
|
||||
return icon
|
||||
return self._default_pixmap()
|
||||
|
||||
|
||||
|
|
@ -692,6 +704,7 @@ class TasksCombobox(QtWidgets.QComboBox):
|
|||
style.drawControl(
|
||||
QtWidgets.QStyle.CE_ComboBoxLabel, opt, painter, self
|
||||
)
|
||||
painter.end()
|
||||
|
||||
def is_valid(self):
|
||||
"""Are all selected items valid."""
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import os
|
||||
import json
|
||||
import time
|
||||
import collections
|
||||
import copy
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
|
@ -15,10 +18,11 @@ from openpype.tools.utils import (
|
|||
|
||||
from .constants import ResetKeySequence
|
||||
from .publish_report_viewer import PublishReportViewerWidget
|
||||
from .control import CardMessageTypes
|
||||
from .control_qt import QtPublisherController
|
||||
from .widgets import (
|
||||
OverviewWidget,
|
||||
ValidationsWidget,
|
||||
ReportPageWidget,
|
||||
PublishFrame,
|
||||
|
||||
PublisherTabsWidget,
|
||||
|
|
@ -182,7 +186,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
controller, content_stacked_widget
|
||||
)
|
||||
|
||||
report_widget = ValidationsWidget(controller, parent)
|
||||
report_widget = ReportPageWidget(controller, parent)
|
||||
|
||||
# Details - Publish details
|
||||
publish_details_widget = PublishReportViewerWidget(
|
||||
|
|
@ -313,6 +317,13 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
controller.event_system.add_callback(
|
||||
"convertors.find.failed", self._on_convertor_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"export_report.request", self._export_report
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"copy_report.request", self._copy_report
|
||||
)
|
||||
|
||||
|
||||
# Store extra header widget for TrayPublisher
|
||||
# - can be used to add additional widgets to header between context
|
||||
|
|
@ -825,6 +836,9 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._validate_btn.setEnabled(validate_enabled)
|
||||
self._publish_btn.setEnabled(publish_enabled)
|
||||
|
||||
if not publish_enabled:
|
||||
self._publish_frame.set_shrunk_state(True)
|
||||
|
||||
self._update_publish_details_widget()
|
||||
|
||||
def _validate_create_instances(self):
|
||||
|
|
@ -941,6 +955,46 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
under_mouse = widget_x < global_pos.x()
|
||||
self._create_overlay_button.set_under_mouse(under_mouse)
|
||||
|
||||
def _copy_report(self):
|
||||
logs = self._controller.get_publish_report()
|
||||
logs_string = json.dumps(logs, indent=4)
|
||||
|
||||
mime_data = QtCore.QMimeData()
|
||||
mime_data.setText(logs_string)
|
||||
QtWidgets.QApplication.instance().clipboard().setMimeData(
|
||||
mime_data
|
||||
)
|
||||
self._controller.emit_card_message(
|
||||
"Report added to clipboard",
|
||||
CardMessageTypes.info)
|
||||
|
||||
def _export_report(self):
|
||||
default_filename = "publish-report-{}".format(
|
||||
time.strftime("%y%m%d-%H-%M")
|
||||
)
|
||||
default_filepath = os.path.join(
|
||||
os.path.expanduser("~"),
|
||||
default_filename
|
||||
)
|
||||
new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self, "Save report", default_filepath, ".json"
|
||||
)
|
||||
if not ext or not new_filepath:
|
||||
return
|
||||
|
||||
logs = self._controller.get_publish_report()
|
||||
full_path = new_filepath + ext
|
||||
dir_path = os.path.dirname(full_path)
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
|
||||
with open(full_path, "w") as file_stream:
|
||||
json.dump(logs, file_stream)
|
||||
|
||||
self._controller.emit_card_message(
|
||||
"Report saved",
|
||||
CardMessageTypes.info)
|
||||
|
||||
|
||||
class ErrorsMessageBox(ErrorMessageBox):
|
||||
def __init__(self, error_title, failed_info, message_start, parent):
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
from .layouts import FlowLayout
|
||||
from .widgets import (
|
||||
FocusSpinBox,
|
||||
FocusDoubleSpinBox,
|
||||
ComboBox,
|
||||
CustomTextComboBox,
|
||||
PlaceholderLineEdit,
|
||||
ExpandingTextEdit,
|
||||
BaseClickableFrame,
|
||||
ClickableFrame,
|
||||
ClickableLabel,
|
||||
ExpandBtn,
|
||||
ClassicExpandBtn,
|
||||
PixmapLabel,
|
||||
IconButton,
|
||||
PixmapButton,
|
||||
|
|
@ -37,15 +40,19 @@ from .overlay_messages import (
|
|||
|
||||
|
||||
__all__ = (
|
||||
"FlowLayout",
|
||||
|
||||
"FocusSpinBox",
|
||||
"FocusDoubleSpinBox",
|
||||
"ComboBox",
|
||||
"CustomTextComboBox",
|
||||
"PlaceholderLineEdit",
|
||||
"ExpandingTextEdit",
|
||||
"BaseClickableFrame",
|
||||
"ClickableFrame",
|
||||
"ClickableLabel",
|
||||
"ExpandBtn",
|
||||
"ClassicExpandBtn",
|
||||
"PixmapLabel",
|
||||
"IconButton",
|
||||
"PixmapButton",
|
||||
|
|
|
|||
150
openpype/tools/utils/layouts.py
Normal file
150
openpype/tools/utils/layouts.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
|
||||
class FlowLayout(QtWidgets.QLayout):
|
||||
"""Layout that organize widgets by minimum size into a flow layout.
|
||||
|
||||
Layout is putting widget from left to right and top to bottom. When widget
|
||||
can't fit a row it is added to next line. Minimum size matches widget with
|
||||
biggest 'sizeHint' width and height using calculated geometry.
|
||||
|
||||
Content margins are part of calculations. It is possible to define
|
||||
horizontal and vertical spacing.
|
||||
|
||||
Layout does not support stretch and spacing items.
|
||||
|
||||
Todos:
|
||||
Unified width concept -> use width of largest item so all of them are
|
||||
same. This could allow to have minimum columns option too.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(FlowLayout, self).__init__(parent)
|
||||
|
||||
# spaces between each item
|
||||
self._horizontal_spacing = 5
|
||||
self._vertical_spacing = 5
|
||||
|
||||
self._items = []
|
||||
|
||||
def __del__(self):
|
||||
while self.count():
|
||||
self.takeAt(0, False)
|
||||
|
||||
def isEmpty(self):
|
||||
for item in self._items:
|
||||
if not item.isEmpty():
|
||||
return False
|
||||
return True
|
||||
|
||||
def setSpacing(self, spacing):
|
||||
self._horizontal_spacing = spacing
|
||||
self._vertical_spacing = spacing
|
||||
self.invalidate()
|
||||
|
||||
def setHorizontalSpacing(self, spacing):
|
||||
self._horizontal_spacing = spacing
|
||||
self.invalidate()
|
||||
|
||||
def setVerticalSpacing(self, spacing):
|
||||
self._vertical_spacing = spacing
|
||||
self.invalidate()
|
||||
|
||||
def addItem(self, item):
|
||||
self._items.append(item)
|
||||
self.invalidate()
|
||||
|
||||
def count(self):
|
||||
return len(self._items)
|
||||
|
||||
def itemAt(self, index):
|
||||
if 0 <= index < len(self._items):
|
||||
return self._items[index]
|
||||
return None
|
||||
|
||||
def takeAt(self, index, invalidate=True):
|
||||
if 0 <= index < len(self._items):
|
||||
item = self._items.pop(index)
|
||||
if invalidate:
|
||||
self.invalidate()
|
||||
return item
|
||||
return None
|
||||
|
||||
def expandingDirections(self):
|
||||
return QtCore.Qt.Orientations(QtCore.Qt.Vertical)
|
||||
|
||||
def hasHeightForWidth(self):
|
||||
return True
|
||||
|
||||
def heightForWidth(self, width):
|
||||
return self._setup_geometry(QtCore.QRect(0, 0, width, 0), True)
|
||||
|
||||
def setGeometry(self, rect):
|
||||
super(FlowLayout, self).setGeometry(rect)
|
||||
self._setup_geometry(rect)
|
||||
|
||||
def sizeHint(self):
|
||||
return self.minimumSize()
|
||||
|
||||
def minimumSize(self):
|
||||
size = QtCore.QSize(0, 0)
|
||||
for item in self._items:
|
||||
widget = item.widget()
|
||||
if widget is not None:
|
||||
parent = widget.parent()
|
||||
if not widget.isVisibleTo(parent):
|
||||
continue
|
||||
size = size.expandedTo(item.minimumSize())
|
||||
|
||||
if size.width() < 1 or size.height() < 1:
|
||||
return size
|
||||
l_margin, t_margin, r_margin, b_margin = self.getContentsMargins()
|
||||
size += QtCore.QSize(l_margin + r_margin, t_margin + b_margin)
|
||||
return size
|
||||
|
||||
def _setup_geometry(self, rect, only_calculate=False):
|
||||
h_spacing = self._horizontal_spacing
|
||||
v_spacing = self._vertical_spacing
|
||||
l_margin, t_margin, r_margin, b_margin = self.getContentsMargins()
|
||||
|
||||
left_x = rect.x() + l_margin
|
||||
top_y = rect.y() + t_margin
|
||||
pos_x = left_x
|
||||
pos_y = top_y
|
||||
row_height = 0
|
||||
for item in self._items:
|
||||
item_hint = item.sizeHint()
|
||||
item_width = item_hint.width()
|
||||
item_height = item_hint.height()
|
||||
if item_width < 1 or item_height < 1:
|
||||
continue
|
||||
|
||||
end_x = pos_x + item_width
|
||||
|
||||
wrap = (
|
||||
row_height > 0
|
||||
and (
|
||||
end_x > rect.right()
|
||||
or (end_x + r_margin) > rect.right()
|
||||
)
|
||||
)
|
||||
if not wrap:
|
||||
next_pos_x = end_x + h_spacing
|
||||
else:
|
||||
pos_x = left_x
|
||||
pos_y += row_height + v_spacing
|
||||
next_pos_x = pos_x + item_width + h_spacing
|
||||
row_height = 0
|
||||
|
||||
if not only_calculate:
|
||||
item.setGeometry(
|
||||
QtCore.QRect(pos_x, pos_y, item_width, item_height)
|
||||
)
|
||||
|
||||
pos_x = next_pos_x
|
||||
row_height = max(row_height, item_height)
|
||||
|
||||
height = (pos_y - top_y) + row_height
|
||||
if height > 0:
|
||||
height += b_margin
|
||||
return height
|
||||
|
|
@ -101,6 +101,46 @@ class PlaceholderLineEdit(QtWidgets.QLineEdit):
|
|||
self.setPalette(filter_palette)
|
||||
|
||||
|
||||
class ExpandingTextEdit(QtWidgets.QTextEdit):
|
||||
"""QTextEdit which does not have sroll area but expands height."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ExpandingTextEdit, self).__init__(parent)
|
||||
|
||||
size_policy = self.sizePolicy()
|
||||
size_policy.setHeightForWidth(True)
|
||||
size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Preferred)
|
||||
self.setSizePolicy(size_policy)
|
||||
|
||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
|
||||
doc = self.document()
|
||||
doc.contentsChanged.connect(self._on_doc_change)
|
||||
|
||||
def _on_doc_change(self):
|
||||
self.updateGeometry()
|
||||
|
||||
def hasHeightForWidth(self):
|
||||
return True
|
||||
|
||||
def heightForWidth(self, width):
|
||||
margins = self.contentsMargins()
|
||||
|
||||
document_width = 0
|
||||
if width >= margins.left() + margins.right():
|
||||
document_width = width - margins.left() - margins.right()
|
||||
|
||||
document = self.document().clone()
|
||||
document.setTextWidth(document_width)
|
||||
|
||||
return margins.top() + document.size().height() + margins.bottom()
|
||||
|
||||
def sizeHint(self):
|
||||
width = super(ExpandingTextEdit, self).sizeHint().width()
|
||||
return QtCore.QSize(width, self.heightForWidth(width))
|
||||
|
||||
|
||||
class BaseClickableFrame(QtWidgets.QFrame):
|
||||
"""Widget that catch left mouse click and can trigger a callback.
|
||||
|
||||
|
|
@ -161,19 +201,34 @@ class ClickableLabel(QtWidgets.QLabel):
|
|||
|
||||
class ExpandBtnLabel(QtWidgets.QLabel):
|
||||
"""Label showing expand icon meant for ExpandBtn."""
|
||||
state_changed = QtCore.Signal()
|
||||
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ExpandBtnLabel, self).__init__(parent)
|
||||
self._source_collapsed_pix = QtGui.QPixmap(
|
||||
get_style_image_path("branch_closed")
|
||||
)
|
||||
self._source_expanded_pix = QtGui.QPixmap(
|
||||
get_style_image_path("branch_open")
|
||||
)
|
||||
self._source_collapsed_pix = self._create_collapsed_pixmap()
|
||||
self._source_expanded_pix = self._create_expanded_pixmap()
|
||||
|
||||
self._current_image = self._source_collapsed_pix
|
||||
self._collapsed = True
|
||||
|
||||
def set_collapsed(self, collapsed):
|
||||
def _create_collapsed_pixmap(self):
|
||||
return QtGui.QPixmap(
|
||||
get_style_image_path("branch_closed")
|
||||
)
|
||||
|
||||
def _create_expanded_pixmap(self):
|
||||
return QtGui.QPixmap(
|
||||
get_style_image_path("branch_open")
|
||||
)
|
||||
|
||||
@property
|
||||
def collapsed(self):
|
||||
return self._collapsed
|
||||
|
||||
def set_collapsed(self, collapsed=None):
|
||||
if collapsed is None:
|
||||
collapsed = not self._collapsed
|
||||
if self._collapsed == collapsed:
|
||||
return
|
||||
self._collapsed = collapsed
|
||||
|
|
@ -182,6 +237,7 @@ class ExpandBtnLabel(QtWidgets.QLabel):
|
|||
else:
|
||||
self._current_image = self._source_expanded_pix
|
||||
self._set_resized_pix()
|
||||
self.state_changed.emit()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self._set_resized_pix()
|
||||
|
|
@ -203,21 +259,55 @@ class ExpandBtnLabel(QtWidgets.QLabel):
|
|||
|
||||
|
||||
class ExpandBtn(ClickableFrame):
|
||||
state_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ExpandBtn, self).__init__(parent)
|
||||
|
||||
pixmap_label = ExpandBtnLabel(self)
|
||||
pixmap_label = self._create_pix_widget(self)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(pixmap_label)
|
||||
|
||||
pixmap_label.state_changed.connect(self.state_changed)
|
||||
|
||||
self._pixmap_label = pixmap_label
|
||||
|
||||
def set_collapsed(self, collapsed):
|
||||
def _create_pix_widget(self, parent=None):
|
||||
if parent is None:
|
||||
parent = self
|
||||
return ExpandBtnLabel(parent)
|
||||
|
||||
@property
|
||||
def collapsed(self):
|
||||
return self._pixmap_label.collapsed
|
||||
|
||||
def set_collapsed(self, collapsed=None):
|
||||
self._pixmap_label.set_collapsed(collapsed)
|
||||
|
||||
|
||||
class ClassicExpandBtnLabel(ExpandBtnLabel):
|
||||
def _create_collapsed_pixmap(self):
|
||||
return QtGui.QPixmap(
|
||||
get_style_image_path("right_arrow")
|
||||
)
|
||||
|
||||
def _create_expanded_pixmap(self):
|
||||
return QtGui.QPixmap(
|
||||
get_style_image_path("down_arrow")
|
||||
)
|
||||
|
||||
|
||||
class ClassicExpandBtn(ExpandBtn):
|
||||
"""Same as 'ExpandBtn' but with arrow images."""
|
||||
|
||||
def _create_pix_widget(self, parent=None):
|
||||
if parent is None:
|
||||
parent = self
|
||||
return ClassicExpandBtnLabel(parent)
|
||||
|
||||
|
||||
class ImageButton(QtWidgets.QPushButton):
|
||||
"""PushButton with icon and size of font.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.15.8-nightly.2"
|
||||
__version__ = "3.15.8"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "OpenPype"
|
||||
version = "3.15.7" # OpenPype
|
||||
version = "3.15.8" # OpenPype
|
||||
description = "Open VFX and Animation pipeline with support."
|
||||
authors = ["OpenPype Team <info@openpype.io>"]
|
||||
license = "MIT License"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ For [AWS Thinkbox Deadline](https://www.awsthinkbox.com/deadline) support you ne
|
|||
|
||||
5. Install our custom plugin and scripts to your deadline repository. It should be as simple as copying content of `openpype/modules/deadline/repository/custom` to `path/to/your/deadline/repository/custom`.
|
||||
|
||||
Multiple different DL webservice could be configured. First set them in point 4., then they could be configured per project in `project_settings/deadline/deadline_servers`.
|
||||
Only single webservice could be a target of publish though.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue