mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 21:32:15 +01:00
Merge branch 'develop' into enhancement/OP-6943_Display-mode-or-Viewport-style-not-shown-correctly-when-creating-review-in-3dsMax
This commit is contained in:
commit
1ffd1f2fed
97 changed files with 5496 additions and 969 deletions
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,9 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.17.3-nightly.2
|
||||
- 3.17.3-nightly.1
|
||||
- 3.17.2
|
||||
- 3.17.2-nightly.4
|
||||
- 3.17.2-nightly.3
|
||||
- 3.17.2-nightly.2
|
||||
|
|
@ -132,9 +135,6 @@ body:
|
|||
- 3.14.11-nightly.4
|
||||
- 3.14.11-nightly.3
|
||||
- 3.14.11-nightly.2
|
||||
- 3.14.11-nightly.1
|
||||
- 3.14.10
|
||||
- 3.14.10-nightly.9
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
471
CHANGELOG.md
471
CHANGELOG.md
|
|
@ -1,6 +1,477 @@
|
|||
# Changelog
|
||||
|
||||
|
||||
## [3.17.2](https://github.com/ynput/OpenPype/tree/3.17.2)
|
||||
|
||||
|
||||
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.1...3.17.2)
|
||||
|
||||
### **🆕 New features**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Add MayaPy application. <a href="https://github.com/ynput/OpenPype/pull/5705">#5705</a></summary>
|
||||
|
||||
This adds mayapy to the application to be launched from a task.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Feature: Copy resources when downloading last workfile <a href="https://github.com/ynput/OpenPype/pull/4944">#4944</a></summary>
|
||||
|
||||
When the last published workfile is downloaded as a prelaunch hook, all resource files referenced in the workfile representation are copied to the `resources` folder, which is inside the local workfile folder.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Blender: Deadline support <a href="https://github.com/ynput/OpenPype/pull/5438">#5438</a></summary>
|
||||
|
||||
Add Deadline support for Blender.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fusion: implement toggle to use Deadline plugin FusionCmd <a href="https://github.com/ynput/OpenPype/pull/5678">#5678</a></summary>
|
||||
|
||||
Fusion 17 doesn't work in DL 10.3, but FusionCmd does. It might be probably better option as headless variant.Fusion plugin seems to be closing and reopening application when worker is running on artist machine, not so with FusionCmdAdded configuration to Project Settings for admin to select appropriate Deadline plugin:
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Loader tool: Refactor loader tool (for AYON) <a href="https://github.com/ynput/OpenPype/pull/5729">#5729</a></summary>
|
||||
|
||||
Refactored loader tool to new tool. Separated backend and frontend logic. Refactored logic is AYON-centric and is used only in AYON mode, so it does not affect OpenPype. The tool is also replacing library loader.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🚀 Enhancements**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: implement matchmove publishing <a href="https://github.com/ynput/OpenPype/pull/5445">#5445</a></summary>
|
||||
|
||||
Add possibility to export multiple cameras in single `matchmove` family instance, both in `abc` and `ma`.Exposed flag 'Keep image planes' to control export of image planes.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Add optional Fbx extractors in Rig and Animation family <a href="https://github.com/ynput/OpenPype/pull/5589">#5589</a></summary>
|
||||
|
||||
This PR allows user to export control rigs(optionally with mesh) and animated rig in fbx optionally by attaching the rig objects to the two newly introduced sets.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Optional Resolution Validator for Render <a href="https://github.com/ynput/OpenPype/pull/5693">#5693</a></summary>
|
||||
|
||||
Adding optional resolution validator for maya in render family, similar to the one in Max.It checks if the resolution in render setting aligns with that in setting from the db.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Use host's node uniqueness for instance id in new publisher <a href="https://github.com/ynput/OpenPype/pull/5490">#5490</a></summary>
|
||||
|
||||
Instead of writing `instance_id` as parm or attributes on the publish instances we can, for some hosts, just rely on a unique name or path within the scene to refer to that particular instance. By doing so we fix #4820 because upon duplicating such a publish instance using the host's (DCC) functionality the uniqueness for the duplicate is then already ensured instead of attributes remaining exact same value as where to were duplicated from, making `instance_id` a non-unique value.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Max: Implementation of OCIO configuration <a href="https://github.com/ynput/OpenPype/pull/5499">#5499</a></summary>
|
||||
|
||||
Resolve #5473 Implementation of OCIO configuration for Max 2024 regarding to the update of Max 2024
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: Multiple format supports for ExtractReviewDataMov <a href="https://github.com/ynput/OpenPype/pull/5623">#5623</a></summary>
|
||||
|
||||
This PR would fix the bug of the plugin `ExtractReviewDataMov` not being able to support extensions other than `mov`. The plugin is also renamed to `ExtractReviewDataBakingStreams` as i provides multiple format supoort.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bugfix: houdini switching context doesnt update variables <a href="https://github.com/ynput/OpenPype/pull/5651">#5651</a></summary>
|
||||
|
||||
Allows admins to have a list of vars (e.g. JOB) with (dynamic) values that will be updated on context changes, e.g. when switching to another asset or task.Using template keys is supported but formatting keys capitalization variants is not, e.g. {Asset} and {ASSET} won't workDisabling Update Houdini vars on context change feature will leave all Houdini vars unmanaged and thus no context update changes will occur.Also, this PR adds a new button in menu to update vars on demand.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Publisher: Fix report maker memory leak + optimize lookups using set <a href="https://github.com/ynput/OpenPype/pull/5667">#5667</a></summary>
|
||||
|
||||
Fixes a memory leak where resetting publisher does not clear the stored plugins for the Publish Report Maker.Also changes the stored plugins to a `set` to optimize the lookup speeds.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Add openpype_mongo command flag for testing. <a href="https://github.com/ynput/OpenPype/pull/5676">#5676</a></summary>
|
||||
|
||||
Instead of changing the environment, this command flag allows for changing the database.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: minor docstring and code tweaks for ExtractReviewMov <a href="https://github.com/ynput/OpenPype/pull/5695">#5695</a></summary>
|
||||
|
||||
Code and docstring tweaks on https://github.com/ynput/OpenPype/pull/5623
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON: Small settings fixes <a href="https://github.com/ynput/OpenPype/pull/5699">#5699</a></summary>
|
||||
|
||||
Small changes/fixes related to AYON settings. All foundry apps variant `13-0` has label `13.0`. Key `"ExtractReviewIntermediates"` is not mandatory in settings.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Blender: Alembic Animation loader <a href="https://github.com/ynput/OpenPype/pull/5711">#5711</a></summary>
|
||||
|
||||
Implemented loading Alembic Animations in Blender.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🐛 Bug fixes**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Missing "data" field and enabling of audio <a href="https://github.com/ynput/OpenPype/pull/5618">#5618</a></summary>
|
||||
|
||||
When updating audio containers, the field "data" was missing and the audio node was not enabled on the timeline.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Bug in validate Plug-in Path Attribute <a href="https://github.com/ynput/OpenPype/pull/5687">#5687</a></summary>
|
||||
|
||||
Overwriting list with string is causing `TypeError: string indices must be integers` in subsequent iterations, crashing the validator plugin.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>General: Avoid fallback if value is 0 for handle start/end <a href="https://github.com/ynput/OpenPype/pull/5652">#5652</a></summary>
|
||||
|
||||
There's a bug on the `pyblish_functions.get_time_data_from_instance_or_context` where if `handleStart` or `handleEnd` on the instance are set to value 0 it's falling back to grabbing the handles from the instance context. Instead, the logic should be that it only falls back to the `instance.context` if the key doesn't exist.This change was only affecting me on the `handleStart`/`handleEnd` and it's unlikely it could cause issues on `frameStart`, `frameEnd` or `fps` but regardless, the `get` logic is wrong.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fusion: added missing env vars to Deadline submission <a href="https://github.com/ynput/OpenPype/pull/5659">#5659</a></summary>
|
||||
|
||||
Environment variables discerning type of job was missing. Without this injection of environment variables won't start.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: workfile version synchronization settings fixed <a href="https://github.com/ynput/OpenPype/pull/5662">#5662</a></summary>
|
||||
|
||||
Settings for synchronizing workfile version to published products is fixed.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON Workfiles Tool: Open workfile changes context <a href="https://github.com/ynput/OpenPype/pull/5671">#5671</a></summary>
|
||||
|
||||
Change context when workfile is opened.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Blender: Fix remove/update in new layout instance <a href="https://github.com/ynput/OpenPype/pull/5679">#5679</a></summary>
|
||||
|
||||
Fixes an error that occurs when removing or updating an asset in a new layout instance.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON Launcher tool: Fix refresh btn <a href="https://github.com/ynput/OpenPype/pull/5685">#5685</a></summary>
|
||||
|
||||
Refresh button does propagate refreshed content properly. Folders and tasks are cached for 60 seconds instead of 10 seconds. Auto-refresh in launcher will refresh only actions and related data which is project and project settings.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Deadline: handle all valid paths in RenderExecutable <a href="https://github.com/ynput/OpenPype/pull/5694">#5694</a></summary>
|
||||
|
||||
This commit enhances the path resolution mechanism in the RenderExecutable function of the Ayon plugin. Previously, the function only considered paths starting with a tilde (~), ignoring other valid paths listed in exe_list. This limitation led to an empty expanded_paths list when none of the paths in exe_list started with a tilde, causing the function to fail in finding the Ayon executable.With this fix, the RenderExecutable function now correctly processes and includes all valid paths from exe_list, improving its reliability and preventing unnecessary errors related to Ayon executable location.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON Launcher tool: Fix skip last workfile boolean <a href="https://github.com/ynput/OpenPype/pull/5700">#5700</a></summary>
|
||||
|
||||
Skip last workfile boolean works as expected.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: Explore here action can work without task <a href="https://github.com/ynput/OpenPype/pull/5703">#5703</a></summary>
|
||||
|
||||
Explore here action does not crash when task is not selected, and change error message a little.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Testing: Inject mongo_url argument earlier <a href="https://github.com/ynput/OpenPype/pull/5706">#5706</a></summary>
|
||||
|
||||
Fix for https://github.com/ynput/OpenPype/pull/5676The Mongo url is used earlier in the execution.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Blender: Add support to auto-install PySide2 in blender 4 <a href="https://github.com/ynput/OpenPype/pull/5723">#5723</a></summary>
|
||||
|
||||
Change version regex to support blender 4 subfolder.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fix: Hardcoded main site and wrongly copied workfile <a href="https://github.com/ynput/OpenPype/pull/5733">#5733</a></summary>
|
||||
|
||||
Fixing these two issues:
|
||||
- Hardcoded main site -> Replaced by `anatomy.fill_root`.
|
||||
- Workfiles can sometimes be copied while they shouldn't.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bugfix: ServerDeleteOperation asset -> folder conversion typo <a href="https://github.com/ynput/OpenPype/pull/5735">#5735</a></summary>
|
||||
|
||||
Fix ServerDeleteOperation asset -> folder conversion typo
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: loaders are filtering correctly <a href="https://github.com/ynput/OpenPype/pull/5739">#5739</a></summary>
|
||||
|
||||
Variable name for filtering by extensions were not correct - it suppose to be plural. It is fixed now and filtering is working as suppose to.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: failing multiple thumbnails integration <a href="https://github.com/ynput/OpenPype/pull/5741">#5741</a></summary>
|
||||
|
||||
This handles the situation when `ExtractReviewIntermediates` (previously `ExtractReviewDataMov`) has multiple outputs, including thumbnails that need to be integrated. Previously, integrating the thumbnail representation was causing an issue in the integration process. However, we have now resolved this issue by no longer integrating thumbnails as loadable representations.NOW default is that thumbnail representation are NOT integrated (eg. they will not show up in DB > couldn't be Loaded in Loader) and no `_thumb.jpg` will be left in `render` (most likely) publish folder.IF there would be need to override this behavior, please use `project_settings/global/publish/PreIntegrateThumbnails`
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON Settings: Fix global overrides <a href="https://github.com/ynput/OpenPype/pull/5745">#5745</a></summary>
|
||||
|
||||
The `output` dictionary that gets passed into `ayon_settings._convert_global_project_settings` gets replaced when converting the settings for `ExtractOIIOTranscode`. This results in `global` not being in the output dictionary and thus the defaults being used and not the project overrides.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: AYON query functions arguments <a href="https://github.com/ynput/OpenPype/pull/5752">#5752</a></summary>
|
||||
|
||||
Fixed how `archived` argument is handled in get subsets/assets function.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🔀 Refactored code**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Publisher: Refactor Report Maker plugin data storage to be a dict by plugin.id <a href="https://github.com/ynput/OpenPype/pull/5668">#5668</a></summary>
|
||||
|
||||
Refactor Report Maker plugin data storage to be a dict by `plugin.id`Also fixes `_current_plugin_data` type on `__init__`
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: Refactor Resolve into new style HostBase, IWorkfileHost, ILoadHost <a href="https://github.com/ynput/OpenPype/pull/5701">#5701</a></summary>
|
||||
|
||||
Refactor Resolve into new style HostBase, IWorkfileHost, ILoadHost
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **Merged pull requests**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: Maya reduce get project settings calls <a href="https://github.com/ynput/OpenPype/pull/5669">#5669</a></summary>
|
||||
|
||||
Re-use system settings / project settings where we can instead of requerying.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Extended error message when getting subset name <a href="https://github.com/ynput/OpenPype/pull/5649">#5649</a></summary>
|
||||
|
||||
Each Creator is using `get_subset_name` functions which collects context data and fills configured template with placeholders.If any key is missing in the template, non descriptive error is thrown.This should provide more verbose message:
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Tests: Remove checks for env var <a href="https://github.com/ynput/OpenPype/pull/5696">#5696</a></summary>
|
||||
|
||||
Env var will be filled in `env_var` fixture, here it is too early to check
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.17.1](https://github.com/ynput/OpenPype/tree/3.17.1)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -75,9 +75,9 @@ def _get_subsets(
|
|||
):
|
||||
fields.add(key)
|
||||
|
||||
active = None
|
||||
active = True
|
||||
if archived:
|
||||
active = False
|
||||
active = None
|
||||
|
||||
for subset in con.get_products(
|
||||
project_name,
|
||||
|
|
@ -196,7 +196,7 @@ def get_assets(
|
|||
|
||||
active = True
|
||||
if archived:
|
||||
active = False
|
||||
active = None
|
||||
|
||||
con = get_server_api_connection()
|
||||
fields = folder_fields_v3_to_v4(fields, con)
|
||||
|
|
|
|||
|
|
@ -460,36 +460,6 @@ def ls() -> Iterator:
|
|||
yield parse_container(container)
|
||||
|
||||
|
||||
def update_hierarchy(containers):
|
||||
"""Hierarchical container support
|
||||
|
||||
This is the function to support Scene Inventory to draw hierarchical
|
||||
view for containers.
|
||||
|
||||
We need both parent and children to visualize the graph.
|
||||
|
||||
"""
|
||||
|
||||
all_containers = set(ls()) # lookup set
|
||||
|
||||
for container in containers:
|
||||
# Find parent
|
||||
# FIXME (jasperge): re-evaluate this. How would it be possible
|
||||
# to 'nest' assets? Collections can have several parents, for
|
||||
# now assume it has only 1 parent
|
||||
parent = [
|
||||
coll for coll in bpy.data.collections if container in coll.children
|
||||
]
|
||||
for node in parent:
|
||||
if node in all_containers:
|
||||
container["parent"] = node
|
||||
break
|
||||
|
||||
log.debug("Container: %s", container)
|
||||
|
||||
yield container
|
||||
|
||||
|
||||
def publish():
|
||||
"""Shorthand to publish from within host."""
|
||||
|
||||
|
|
|
|||
|
|
@ -165,7 +165,8 @@ class CreateSaver(NewCreator):
|
|||
filepath = self.temp_rendering_path_template.format(
|
||||
**formatting_data)
|
||||
|
||||
tool["Clip"] = os.path.normpath(filepath)
|
||||
comp = get_current_comp()
|
||||
tool["Clip"] = comp.ReverseMapPath(os.path.normpath(filepath))
|
||||
|
||||
# Rename tool
|
||||
if tool.Name != subset:
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
with comp_lock_and_undo_chunk(comp, "Create Loader"):
|
||||
args = (-32768, -32768)
|
||||
tool = comp.AddTool("Loader", *args)
|
||||
tool["Clip"] = path
|
||||
tool["Clip"] = comp.ReverseMapPath(path)
|
||||
|
||||
# Set global in point to start frame (if in version.data)
|
||||
start = self._get_start(context["version"], tool)
|
||||
|
|
@ -244,7 +244,7 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
"TimeCodeOffset",
|
||||
),
|
||||
):
|
||||
tool["Clip"] = path
|
||||
tool["Clip"] = comp.ReverseMapPath(path)
|
||||
|
||||
# Set the global in to the start frame of the sequence
|
||||
global_in_changed = loader_shift(tool, start, relative=False)
|
||||
|
|
|
|||
|
|
@ -145,9 +145,11 @@ class CollectFusionRender(
|
|||
start = render_instance.frameStart - render_instance.handleStart
|
||||
end = render_instance.frameEnd + render_instance.handleEnd
|
||||
|
||||
path = (
|
||||
render_instance.tool["Clip"]
|
||||
[render_instance.workfileComp.TIME_UNDEFINED]
|
||||
comp = render_instance.workfileComp
|
||||
path = comp.MapPath(
|
||||
render_instance.tool["Clip"][
|
||||
render_instance.workfileComp.TIME_UNDEFINED
|
||||
]
|
||||
)
|
||||
output_dir = os.path.dirname(path)
|
||||
render_instance.outputDir = output_dir
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ class ExtractPointCloud(publish.Extractor):
|
|||
label = "Extract Point Cloud"
|
||||
hosts = ["max"]
|
||||
families = ["pointcloud"]
|
||||
settings = []
|
||||
|
||||
def process(self, instance):
|
||||
self.settings = self.get_setting(instance)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class ValidateMaxContents(pyblish.api.InstancePlugin):
|
||||
"""Validates Max contents.
|
||||
class ValidateInstanceHasMembers(pyblish.api.InstancePlugin):
|
||||
"""Validates Instance has members.
|
||||
|
||||
Check if MaxScene container includes any contents underneath.
|
||||
Check if MaxScene containers includes any contents underneath.
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["camera",
|
||||
"model",
|
||||
"maxScene",
|
||||
"review"]
|
||||
"review",
|
||||
"pointcache",
|
||||
"pointcloud",
|
||||
"redshiftproxy"]
|
||||
hosts = ["max"]
|
||||
label = "Max Scene Contents"
|
||||
label = "Container Contents"
|
||||
|
||||
def process(self, instance):
|
||||
if not instance.data["members"]:
|
||||
|
|
@ -100,8 +100,8 @@ class ValidatePointCloud(pyblish.api.InstancePlugin):
|
|||
|
||||
selection_list = instance.data["members"]
|
||||
|
||||
project_setting = instance.data["project_setting"]
|
||||
attr_settings = project_setting["max"]["PointCloud"]["attribute"]
|
||||
project_settings = instance.context.data["project_settings"]
|
||||
attr_settings = project_settings["max"]["PointCloud"]["attribute"]
|
||||
for sel in selection_list:
|
||||
obj = sel.baseobject
|
||||
anim_names = rt.GetSubAnimNames(obj)
|
||||
|
|
|
|||
|
|
@ -146,6 +146,10 @@ def suspended_refresh(suspend=True):
|
|||
|
||||
cmds.ogs(pause=True) is a toggle so we cant pass False.
|
||||
"""
|
||||
if IS_HEADLESS:
|
||||
yield
|
||||
return
|
||||
|
||||
original_state = cmds.ogs(query=True, pause=True)
|
||||
try:
|
||||
if suspend and not original_state:
|
||||
|
|
|
|||
211
openpype/hosts/maya/plugins/create/create_multishot_layout.py
Normal file
211
openpype/hosts/maya/plugins/create/create_multishot_layout.py
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
from ayon_api import (
|
||||
get_folder_by_name,
|
||||
get_folder_by_path,
|
||||
get_folders,
|
||||
)
|
||||
from maya import cmds # noqa: F401
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.client import get_assets
|
||||
from openpype.hosts.maya.api import plugin
|
||||
from openpype.lib import BoolDef, EnumDef, TextDef
|
||||
from openpype.pipeline import (
|
||||
Creator,
|
||||
get_current_asset_name,
|
||||
get_current_project_name,
|
||||
)
|
||||
from openpype.pipeline.create import CreatorError
|
||||
|
||||
|
||||
class CreateMultishotLayout(plugin.MayaCreator):
|
||||
"""Create a multi-shot layout in the Maya scene.
|
||||
|
||||
This creator will create a Camera Sequencer in the Maya scene based on
|
||||
the shots found under the specified folder. The shots will be added to
|
||||
the sequencer in the order of their clipIn and clipOut values. For each
|
||||
shot a Layout will be created.
|
||||
|
||||
"""
|
||||
identifier = "io.openpype.creators.maya.multishotlayout"
|
||||
label = "Multi-shot Layout"
|
||||
family = "layout"
|
||||
icon = "project-diagram"
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
# Present artist with a list of parents of the current context
|
||||
# to choose from. This will be used to get the shots under the
|
||||
# selected folder to create the Camera Sequencer.
|
||||
|
||||
"""
|
||||
Todo: `get_folder_by_name` should be switched to `get_folder_by_path`
|
||||
once the fork to pure AYON is done.
|
||||
|
||||
Warning: this will not work for projects where the asset name
|
||||
is not unique across the project until the switch mentioned
|
||||
above is done.
|
||||
"""
|
||||
|
||||
current_folder = get_folder_by_name(
|
||||
project_name=get_current_project_name(),
|
||||
folder_name=get_current_asset_name(),
|
||||
)
|
||||
|
||||
current_path_parts = current_folder["path"].split("/")
|
||||
|
||||
# populate the list with parents of the current folder
|
||||
# this will create menu items like:
|
||||
# [
|
||||
# {
|
||||
# "value": "",
|
||||
# "label": "project (shots directly under the project)"
|
||||
# }, {
|
||||
# "value": "shots/shot_01", "label": "shot_01 (current)"
|
||||
# }, {
|
||||
# "value": "shots", "label": "shots"
|
||||
# }
|
||||
# ]
|
||||
|
||||
# add the project as the first item
|
||||
items_with_label = [
|
||||
{
|
||||
"label": f"{self.project_name} "
|
||||
"(shots directly under the project)",
|
||||
"value": ""
|
||||
}
|
||||
]
|
||||
|
||||
# go through the current folder path and add each part to the list,
|
||||
# but mark the current folder.
|
||||
for part_idx, part in enumerate(current_path_parts):
|
||||
label = part
|
||||
if label == current_folder["name"]:
|
||||
label = f"{label} (current)"
|
||||
|
||||
value = "/".join(current_path_parts[:part_idx + 1])
|
||||
|
||||
items_with_label.append({"label": label, "value": value})
|
||||
|
||||
return [
|
||||
EnumDef("shotParent",
|
||||
default=current_folder["name"],
|
||||
label="Shot Parent Folder",
|
||||
items=items_with_label,
|
||||
),
|
||||
BoolDef("groupLoadedAssets",
|
||||
label="Group Loaded Assets",
|
||||
tooltip="Enable this when you want to publish group of "
|
||||
"loaded asset",
|
||||
default=False),
|
||||
TextDef("taskName",
|
||||
label="Associated Task Name",
|
||||
tooltip=("Task name to be associated "
|
||||
"with the created Layout"),
|
||||
default="layout"),
|
||||
]
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
shots = list(
|
||||
self.get_related_shots(folder_path=pre_create_data["shotParent"])
|
||||
)
|
||||
if not shots:
|
||||
# There are no shot folders under the specified folder.
|
||||
# We are raising an error here but in the future we might
|
||||
# want to create a new shot folders by publishing the layouts
|
||||
# and shot defined in the sequencer. Sort of editorial publish
|
||||
# in side of Maya.
|
||||
raise CreatorError((
|
||||
"No shots found under the specified "
|
||||
f"folder: {pre_create_data['shotParent']}."))
|
||||
|
||||
# Get layout creator
|
||||
layout_creator_id = "io.openpype.creators.maya.layout"
|
||||
layout_creator: Creator = self.create_context.creators.get(
|
||||
layout_creator_id)
|
||||
if not layout_creator:
|
||||
raise CreatorError(
|
||||
f"Creator {layout_creator_id} not found.")
|
||||
|
||||
# Get OpenPype style asset documents for the shots
|
||||
op_asset_docs = get_assets(
|
||||
self.project_name, [s["id"] for s in shots])
|
||||
asset_docs_by_id = {doc["_id"]: doc for doc in op_asset_docs}
|
||||
for shot in shots:
|
||||
# we are setting shot name to be displayed in the sequencer to
|
||||
# `shot name (shot label)` if the label is set, otherwise just
|
||||
# `shot name`. So far, labels are used only when the name is set
|
||||
# with characters that are not allowed in the shot name.
|
||||
if not shot["active"]:
|
||||
continue
|
||||
|
||||
# get task for shot
|
||||
asset_doc = asset_docs_by_id[shot["id"]]
|
||||
|
||||
tasks = asset_doc.get("data").get("tasks").keys()
|
||||
layout_task = None
|
||||
if pre_create_data["taskName"] in tasks:
|
||||
layout_task = pre_create_data["taskName"]
|
||||
|
||||
shot_name = f"{shot['name']}%s" % (
|
||||
f" ({shot['label']})" if shot["label"] else "")
|
||||
cmds.shot(sequenceStartTime=shot["attrib"]["clipIn"],
|
||||
sequenceEndTime=shot["attrib"]["clipOut"],
|
||||
shotName=shot_name)
|
||||
|
||||
# Create layout instance by the layout creator
|
||||
|
||||
instance_data = {
|
||||
"asset": shot["name"],
|
||||
"variant": layout_creator.get_default_variant()
|
||||
}
|
||||
if layout_task:
|
||||
instance_data["task"] = layout_task
|
||||
|
||||
layout_creator.create(
|
||||
subset_name=layout_creator.get_subset_name(
|
||||
layout_creator.get_default_variant(),
|
||||
self.create_context.get_current_task_name(),
|
||||
asset_doc,
|
||||
self.project_name),
|
||||
instance_data=instance_data,
|
||||
pre_create_data={
|
||||
"groupLoadedAssets": pre_create_data["groupLoadedAssets"]
|
||||
}
|
||||
)
|
||||
|
||||
def get_related_shots(self, folder_path: str):
|
||||
"""Get all shots related to the current asset.
|
||||
|
||||
Get all folders of type Shot under specified folder.
|
||||
|
||||
Args:
|
||||
folder_path (str): Path of the folder.
|
||||
|
||||
Returns:
|
||||
list: List of dicts with folder data.
|
||||
|
||||
"""
|
||||
# if folder_path is None, project is selected as a root
|
||||
# and its name is used as a parent id
|
||||
parent_id = self.project_name
|
||||
if folder_path:
|
||||
current_folder = get_folder_by_path(
|
||||
project_name=self.project_name,
|
||||
folder_path=folder_path,
|
||||
)
|
||||
parent_id = current_folder["id"]
|
||||
|
||||
# get all child folders of the current one
|
||||
return get_folders(
|
||||
project_name=self.project_name,
|
||||
parent_ids=[parent_id],
|
||||
fields=[
|
||||
"attrib.clipIn", "attrib.clipOut",
|
||||
"attrib.frameStart", "attrib.frameEnd",
|
||||
"name", "label", "path", "folderType", "id"
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# blast this creator if Ayon server is not enabled
|
||||
if not AYON_SERVER_ENABLED:
|
||||
del CreateMultishotLayout
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Maya look extractor."""
|
||||
import sys
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections import OrderedDict
|
||||
import contextlib
|
||||
|
|
@ -176,6 +177,24 @@ class MakeRSTexBin(TextureProcessor):
|
|||
source
|
||||
]
|
||||
|
||||
# if color management is enabled we pass color space information
|
||||
if color_management["enabled"]:
|
||||
config_path = color_management["config"]
|
||||
if not os.path.exists(config_path):
|
||||
raise RuntimeError("OCIO config not found at: "
|
||||
"{}".format(config_path))
|
||||
|
||||
if not os.getenv("OCIO"):
|
||||
self.log.debug(
|
||||
"OCIO environment variable not set."
|
||||
"Setting it with OCIO config from Maya."
|
||||
)
|
||||
os.environ["OCIO"] = config_path
|
||||
|
||||
self.log.debug("converting colorspace {0} to redshift render "
|
||||
"colorspace".format(colorspace))
|
||||
subprocess_args.extend(["-cs", colorspace])
|
||||
|
||||
hash_args = ["rstex"]
|
||||
texture_hash = source_hash(source, *hash_args)
|
||||
|
||||
|
|
@ -186,11 +205,11 @@ class MakeRSTexBin(TextureProcessor):
|
|||
|
||||
self.log.debug(" ".join(subprocess_args))
|
||||
try:
|
||||
run_subprocess(subprocess_args)
|
||||
run_subprocess(subprocess_args, logger=self.log)
|
||||
except Exception:
|
||||
self.log.error("Texture .rstexbin conversion failed",
|
||||
exc_info=True)
|
||||
raise
|
||||
six.reraise(*sys.exc_info())
|
||||
|
||||
return TextureResult(
|
||||
path=destination,
|
||||
|
|
@ -472,7 +491,7 @@ class ExtractLook(publish.Extractor):
|
|||
"rstex": MakeRSTexBin
|
||||
}.items():
|
||||
if instance.data.get(key, False):
|
||||
processor = Processor()
|
||||
processor = Processor(log=self.log)
|
||||
processor.apply_settings(context.data["system_settings"],
|
||||
context.data["project_settings"])
|
||||
processors.append(processor)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ from .utils import (
|
|||
get_colorspace_list
|
||||
)
|
||||
|
||||
from .actions import (
|
||||
SelectInvalidAction,
|
||||
SelectInstanceNodeAction
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"file_extensions",
|
||||
"has_unsaved_changes",
|
||||
|
|
@ -92,5 +97,8 @@ __all__ = (
|
|||
"create_write_node",
|
||||
|
||||
"colorspace_exists_on_node",
|
||||
"get_colorspace_list"
|
||||
"get_colorspace_list",
|
||||
|
||||
"SelectInvalidAction",
|
||||
"SelectInstanceNodeAction"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,33 +20,58 @@ class SelectInvalidAction(pyblish.api.Action):
|
|||
|
||||
def process(self, context, plugin):
|
||||
|
||||
try:
|
||||
import nuke
|
||||
except ImportError:
|
||||
raise ImportError("Current host is not Nuke")
|
||||
|
||||
errored_instances = get_errored_instances_from_context(context,
|
||||
plugin=plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding invalid nodes..")
|
||||
invalid = list()
|
||||
invalid = set()
|
||||
for instance in errored_instances:
|
||||
invalid_nodes = plugin.get_invalid(instance)
|
||||
|
||||
if invalid_nodes:
|
||||
if isinstance(invalid_nodes, (list, tuple)):
|
||||
invalid.append(invalid_nodes[0])
|
||||
invalid.update(invalid_nodes)
|
||||
else:
|
||||
self.log.warning("Plug-in returned to be invalid, "
|
||||
"but has no selectable nodes.")
|
||||
|
||||
# Ensure unique (process each node only once)
|
||||
invalid = list(set(invalid))
|
||||
|
||||
if invalid:
|
||||
self.log.info("Selecting invalid nodes: {}".format(invalid))
|
||||
reset_selection()
|
||||
select_nodes(invalid)
|
||||
else:
|
||||
self.log.info("No invalid nodes found.")
|
||||
|
||||
|
||||
class SelectInstanceNodeAction(pyblish.api.Action):
|
||||
"""Select instance node for failed plugin."""
|
||||
label = "Select instance node"
|
||||
on = "failed" # This action is only available on a failed plug-in
|
||||
icon = "mdi.cursor-default-click"
|
||||
|
||||
def process(self, context, plugin):
|
||||
|
||||
# Get the errored instances for the plug-in
|
||||
errored_instances = get_errored_instances_from_context(
|
||||
context, plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding instance nodes..")
|
||||
nodes = set()
|
||||
for instance in errored_instances:
|
||||
instance_node = instance.data.get("transientData", {}).get("node")
|
||||
if not instance_node:
|
||||
raise RuntimeError(
|
||||
"No transientData['node'] found on instance: {}".format(
|
||||
instance
|
||||
)
|
||||
)
|
||||
nodes.add(instance_node)
|
||||
|
||||
if nodes:
|
||||
self.log.info("Selecting instance nodes: {}".format(nodes))
|
||||
reset_selection()
|
||||
select_nodes(nodes)
|
||||
else:
|
||||
self.log.info("No instance nodes found.")
|
||||
|
|
|
|||
|
|
@ -48,20 +48,15 @@ from openpype.pipeline import (
|
|||
get_current_asset_name,
|
||||
)
|
||||
from openpype.pipeline.context_tools import (
|
||||
get_current_project_asset,
|
||||
get_custom_workfile_template_from_session
|
||||
)
|
||||
from openpype.pipeline.colorspace import (
|
||||
get_imageio_config
|
||||
)
|
||||
from openpype.pipeline.colorspace import get_imageio_config
|
||||
from openpype.pipeline.workfile import BuildWorkfile
|
||||
from . import gizmo_menu
|
||||
from .constants import ASSIST
|
||||
|
||||
from .workio import (
|
||||
save_file,
|
||||
open_file
|
||||
)
|
||||
from .workio import save_file
|
||||
from .utils import get_node_outputs
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
|
@ -2802,16 +2797,28 @@ def find_free_space_to_paste_nodes(
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def maintained_selection():
|
||||
def maintained_selection(exclude_nodes=None):
|
||||
"""Maintain selection during context
|
||||
|
||||
Maintain selection during context and unselect
|
||||
all nodes after context is done.
|
||||
|
||||
Arguments:
|
||||
exclude_nodes (list[nuke.Node]): list of nodes to be unselected
|
||||
before context is done
|
||||
|
||||
Example:
|
||||
>>> with maintained_selection():
|
||||
... node["selected"].setValue(True)
|
||||
>>> print(node["selected"].value())
|
||||
False
|
||||
"""
|
||||
if exclude_nodes:
|
||||
for node in exclude_nodes:
|
||||
node["selected"].setValue(False)
|
||||
|
||||
previous_selection = nuke.selectedNodes()
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
|
|
@ -2823,6 +2830,51 @@ def maintained_selection():
|
|||
select_nodes(previous_selection)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def swap_node_with_dependency(old_node, new_node):
|
||||
""" Swap node with dependency
|
||||
|
||||
Swap node with dependency and reconnect all inputs and outputs.
|
||||
It removes old node.
|
||||
|
||||
Arguments:
|
||||
old_node (nuke.Node): node to be replaced
|
||||
new_node (nuke.Node): node to replace with
|
||||
|
||||
Example:
|
||||
>>> old_node_name = old_node["name"].value()
|
||||
>>> print(old_node_name)
|
||||
old_node_name_01
|
||||
>>> with swap_node_with_dependency(old_node, new_node) as node_name:
|
||||
... new_node["name"].setValue(node_name)
|
||||
>>> print(new_node["name"].value())
|
||||
old_node_name_01
|
||||
"""
|
||||
# preserve position
|
||||
xpos, ypos = old_node.xpos(), old_node.ypos()
|
||||
# preserve selection after all is done
|
||||
outputs = get_node_outputs(old_node)
|
||||
inputs = old_node.dependencies()
|
||||
node_name = old_node["name"].value()
|
||||
|
||||
try:
|
||||
nuke.delete(old_node)
|
||||
|
||||
yield node_name
|
||||
finally:
|
||||
|
||||
# Reconnect inputs
|
||||
for i, node in enumerate(inputs):
|
||||
new_node.setInput(i, node)
|
||||
# Reconnect outputs
|
||||
if outputs:
|
||||
for n, pipes in outputs.items():
|
||||
for i in pipes:
|
||||
n.setInput(i, new_node)
|
||||
# return to original position
|
||||
new_node.setXYpos(xpos, ypos)
|
||||
|
||||
|
||||
def reset_selection():
|
||||
"""Deselect all selected nodes"""
|
||||
for node in nuke.selectedNodes():
|
||||
|
|
@ -2833,9 +2885,10 @@ def select_nodes(nodes):
|
|||
"""Selects all inputted nodes
|
||||
|
||||
Arguments:
|
||||
nodes (list): nuke nodes to be selected
|
||||
nodes (Union[list, tuple, set]): nuke nodes to be selected
|
||||
"""
|
||||
assert isinstance(nodes, (list, tuple)), "nodes has to be list or tuple"
|
||||
assert isinstance(nodes, (list, tuple, set)), \
|
||||
"nodes has to be list, tuple or set"
|
||||
|
||||
for node in nodes:
|
||||
node["selected"].setValue(True)
|
||||
|
|
@ -2919,13 +2972,13 @@ def process_workfile_builder():
|
|||
"workfile_builder", {})
|
||||
|
||||
# get settings
|
||||
createfv_on = workfile_builder.get("create_first_version") or None
|
||||
create_fv_on = workfile_builder.get("create_first_version") or None
|
||||
builder_on = workfile_builder.get("builder_on_start") or None
|
||||
|
||||
last_workfile_path = os.environ.get("AVALON_LAST_WORKFILE")
|
||||
|
||||
# generate first version in file not existing and feature is enabled
|
||||
if createfv_on and not os.path.exists(last_workfile_path):
|
||||
if create_fv_on and not os.path.exists(last_workfile_path):
|
||||
# get custom template path if any
|
||||
custom_template_path = get_custom_workfile_template_from_session(
|
||||
project_settings=project_settings
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ from openpype.pipeline import (
|
|||
from openpype.hosts.nuke.api.lib import (
|
||||
maintained_selection,
|
||||
get_avalon_knob_data,
|
||||
set_avalon_knob_data
|
||||
set_avalon_knob_data,
|
||||
swap_node_with_dependency,
|
||||
)
|
||||
from openpype.hosts.nuke.api import (
|
||||
containerise,
|
||||
|
|
@ -26,7 +27,7 @@ class LoadGizmo(load.LoaderPlugin):
|
|||
|
||||
families = ["gizmo"]
|
||||
representations = ["*"]
|
||||
extensions = {"gizmo"}
|
||||
extensions = {"nk"}
|
||||
|
||||
label = "Load Gizmo"
|
||||
order = 0
|
||||
|
|
@ -45,7 +46,7 @@ class LoadGizmo(load.LoaderPlugin):
|
|||
data (dict): compulsory attribute > not used
|
||||
|
||||
Returns:
|
||||
nuke node: containerised nuke node object
|
||||
nuke node: containerized nuke node object
|
||||
"""
|
||||
|
||||
# get main variables
|
||||
|
|
@ -83,12 +84,12 @@ class LoadGizmo(load.LoaderPlugin):
|
|||
# add group from nk
|
||||
nuke.nodePaste(file)
|
||||
|
||||
GN = nuke.selectedNode()
|
||||
group_node = nuke.selectedNode()
|
||||
|
||||
GN["name"].setValue(object_name)
|
||||
group_node["name"].setValue(object_name)
|
||||
|
||||
return containerise(
|
||||
node=GN,
|
||||
node=group_node,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
context=context,
|
||||
|
|
@ -110,7 +111,7 @@ class LoadGizmo(load.LoaderPlugin):
|
|||
version_doc = get_version_by_id(project_name, representation["parent"])
|
||||
|
||||
# get corresponding node
|
||||
GN = nuke.toNode(container['objectName'])
|
||||
group_node = nuke.toNode(container['objectName'])
|
||||
|
||||
file = get_representation_path(representation).replace("\\", "/")
|
||||
name = container['name']
|
||||
|
|
@ -135,22 +136,24 @@ class LoadGizmo(load.LoaderPlugin):
|
|||
for k in add_keys:
|
||||
data_imprint.update({k: version_data[k]})
|
||||
|
||||
# capture pipeline metadata
|
||||
avalon_data = get_avalon_knob_data(group_node)
|
||||
|
||||
# adding nodes to node graph
|
||||
# just in case we are in group lets jump out of it
|
||||
nuke.endGroup()
|
||||
|
||||
with maintained_selection():
|
||||
xpos = GN.xpos()
|
||||
ypos = GN.ypos()
|
||||
avalon_data = get_avalon_knob_data(GN)
|
||||
nuke.delete(GN)
|
||||
# add group from nk
|
||||
with maintained_selection([group_node]):
|
||||
# insert nuke script to the script
|
||||
nuke.nodePaste(file)
|
||||
|
||||
GN = nuke.selectedNode()
|
||||
set_avalon_knob_data(GN, avalon_data)
|
||||
GN.setXYpos(xpos, ypos)
|
||||
GN["name"].setValue(object_name)
|
||||
# convert imported to selected node
|
||||
new_group_node = nuke.selectedNode()
|
||||
# swap nodes with maintained connections
|
||||
with swap_node_with_dependency(
|
||||
group_node, new_group_node) as node_name:
|
||||
new_group_node["name"].setValue(node_name)
|
||||
# set updated pipeline metadata
|
||||
set_avalon_knob_data(new_group_node, avalon_data)
|
||||
|
||||
last_version_doc = get_last_version_by_subset_id(
|
||||
project_name, version_doc["parent"], fields=["_id"]
|
||||
|
|
@ -161,11 +164,12 @@ class LoadGizmo(load.LoaderPlugin):
|
|||
color_value = self.node_color
|
||||
else:
|
||||
color_value = "0xd88467ff"
|
||||
GN["tile_color"].setValue(int(color_value, 16))
|
||||
|
||||
new_group_node["tile_color"].setValue(int(color_value, 16))
|
||||
|
||||
self.log.info("updated to version: {}".format(version_doc.get("name")))
|
||||
|
||||
return update_container(GN, data_imprint)
|
||||
return update_container(new_group_node, data_imprint)
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ from openpype.hosts.nuke.api.lib import (
|
|||
maintained_selection,
|
||||
create_backdrop,
|
||||
get_avalon_knob_data,
|
||||
set_avalon_knob_data
|
||||
set_avalon_knob_data,
|
||||
swap_node_with_dependency,
|
||||
)
|
||||
from openpype.hosts.nuke.api import (
|
||||
containerise,
|
||||
|
|
@ -28,7 +29,7 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
|
|||
|
||||
families = ["gizmo"]
|
||||
representations = ["*"]
|
||||
extensions = {"gizmo"}
|
||||
extensions = {"nk"}
|
||||
|
||||
label = "Load Gizmo - Input Process"
|
||||
order = 0
|
||||
|
|
@ -47,7 +48,7 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
|
|||
data (dict): compulsory attribute > not used
|
||||
|
||||
Returns:
|
||||
nuke node: containerised nuke node object
|
||||
nuke node: containerized nuke node object
|
||||
"""
|
||||
|
||||
# get main variables
|
||||
|
|
@ -85,17 +86,17 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
|
|||
# add group from nk
|
||||
nuke.nodePaste(file)
|
||||
|
||||
GN = nuke.selectedNode()
|
||||
group_node = nuke.selectedNode()
|
||||
|
||||
GN["name"].setValue(object_name)
|
||||
group_node["name"].setValue(object_name)
|
||||
|
||||
# try to place it under Viewer1
|
||||
if not self.connect_active_viewer(GN):
|
||||
nuke.delete(GN)
|
||||
if not self.connect_active_viewer(group_node):
|
||||
nuke.delete(group_node)
|
||||
return
|
||||
|
||||
return containerise(
|
||||
node=GN,
|
||||
node=group_node,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
context=context,
|
||||
|
|
@ -117,7 +118,7 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
|
|||
version_doc = get_version_by_id(project_name, representation["parent"])
|
||||
|
||||
# get corresponding node
|
||||
GN = nuke.toNode(container['objectName'])
|
||||
group_node = nuke.toNode(container['objectName'])
|
||||
|
||||
file = get_representation_path(representation).replace("\\", "/")
|
||||
name = container['name']
|
||||
|
|
@ -142,22 +143,24 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
|
|||
for k in add_keys:
|
||||
data_imprint.update({k: version_data[k]})
|
||||
|
||||
# capture pipeline metadata
|
||||
avalon_data = get_avalon_knob_data(group_node)
|
||||
|
||||
# adding nodes to node graph
|
||||
# just in case we are in group lets jump out of it
|
||||
nuke.endGroup()
|
||||
|
||||
with maintained_selection():
|
||||
xpos = GN.xpos()
|
||||
ypos = GN.ypos()
|
||||
avalon_data = get_avalon_knob_data(GN)
|
||||
nuke.delete(GN)
|
||||
# add group from nk
|
||||
with maintained_selection([group_node]):
|
||||
# insert nuke script to the script
|
||||
nuke.nodePaste(file)
|
||||
|
||||
GN = nuke.selectedNode()
|
||||
set_avalon_knob_data(GN, avalon_data)
|
||||
GN.setXYpos(xpos, ypos)
|
||||
GN["name"].setValue(object_name)
|
||||
# convert imported to selected node
|
||||
new_group_node = nuke.selectedNode()
|
||||
# swap nodes with maintained connections
|
||||
with swap_node_with_dependency(
|
||||
group_node, new_group_node) as node_name:
|
||||
new_group_node["name"].setValue(node_name)
|
||||
# set updated pipeline metadata
|
||||
set_avalon_knob_data(new_group_node, avalon_data)
|
||||
|
||||
last_version_doc = get_last_version_by_subset_id(
|
||||
project_name, version_doc["parent"], fields=["_id"]
|
||||
|
|
@ -168,11 +171,11 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
|
|||
color_value = self.node_color
|
||||
else:
|
||||
color_value = "0xd88467ff"
|
||||
GN["tile_color"].setValue(int(color_value, 16))
|
||||
new_group_node["tile_color"].setValue(int(color_value, 16))
|
||||
|
||||
self.log.info("updated to version: {}".format(version_doc.get("name")))
|
||||
|
||||
return update_container(GN, data_imprint)
|
||||
return update_container(new_group_node, data_imprint)
|
||||
|
||||
def connect_active_viewer(self, group_node):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -57,4 +57,4 @@ class CollectBackdrops(pyblish.api.InstancePlugin):
|
|||
if version:
|
||||
instance.data['version'] = version
|
||||
|
||||
self.log.info("Backdrop instance collected: `{}`".format(instance))
|
||||
self.log.debug("Backdrop instance collected: `{}`".format(instance))
|
||||
|
|
|
|||
|
|
@ -64,4 +64,4 @@ class CollectContextData(pyblish.api.ContextPlugin):
|
|||
context.data["scriptData"] = script_data
|
||||
context.data.update(script_data)
|
||||
|
||||
self.log.info('Context from Nuke script collected')
|
||||
self.log.debug('Context from Nuke script collected')
|
||||
|
|
|
|||
|
|
@ -43,4 +43,4 @@ class CollectGizmo(pyblish.api.InstancePlugin):
|
|||
"frameStart": first_frame,
|
||||
"frameEnd": last_frame
|
||||
})
|
||||
self.log.info("Gizmo instance collected: `{}`".format(instance))
|
||||
self.log.debug("Gizmo instance collected: `{}`".format(instance))
|
||||
|
|
|
|||
|
|
@ -43,4 +43,4 @@ class CollectModel(pyblish.api.InstancePlugin):
|
|||
"frameStart": first_frame,
|
||||
"frameEnd": last_frame
|
||||
})
|
||||
self.log.info("Model instance collected: `{}`".format(instance))
|
||||
self.log.debug("Model instance collected: `{}`".format(instance))
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class CollectSlate(pyblish.api.InstancePlugin):
|
|||
instance.data["slateNode"] = slate_node
|
||||
instance.data["slate"] = True
|
||||
instance.data["families"].append("slate")
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
"Slate node is in node graph: `{}`".format(slate.name()))
|
||||
self.log.debug(
|
||||
"__ instance.data: `{}`".format(instance.data))
|
||||
|
|
|
|||
|
|
@ -37,4 +37,6 @@ class CollectWorkfile(pyblish.api.InstancePlugin):
|
|||
# adding basic script data
|
||||
instance.data.update(script_data)
|
||||
|
||||
self.log.info("Collect script version")
|
||||
self.log.debug(
|
||||
"Collected current script version: {}".format(current_file)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -56,8 +56,6 @@ class ExtractBackdropNode(publish.Extractor):
|
|||
# connect output node
|
||||
for n, output in connections_out.items():
|
||||
opn = nuke.createNode("Output")
|
||||
self.log.info(n.name())
|
||||
self.log.info(output.name())
|
||||
output.setInput(
|
||||
next((i for i, d in enumerate(output.dependencies())
|
||||
if d.name() in n.name()), 0), opn)
|
||||
|
|
@ -102,5 +100,5 @@ class ExtractBackdropNode(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '{}' to: {}".format(
|
||||
self.log.debug("Extracted instance '{}' to: {}".format(
|
||||
instance.name, path))
|
||||
|
|
|
|||
|
|
@ -36,11 +36,8 @@ class ExtractCamera(publish.Extractor):
|
|||
step = 1
|
||||
output_range = str(nuke.FrameRange(first_frame, last_frame, step))
|
||||
|
||||
self.log.info("instance.data: `{}`".format(
|
||||
pformat(instance.data)))
|
||||
|
||||
rm_nodes = []
|
||||
self.log.info("Crating additional nodes")
|
||||
self.log.debug("Creating additional nodes for 3D Camera Extractor")
|
||||
subset = instance.data["subset"]
|
||||
staging_dir = self.staging_dir(instance)
|
||||
|
||||
|
|
@ -84,8 +81,6 @@ class ExtractCamera(publish.Extractor):
|
|||
for n in rm_nodes:
|
||||
nuke.delete(n)
|
||||
|
||||
self.log.info(file_path)
|
||||
|
||||
# create representation data
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
|
@ -112,7 +107,7 @@ class ExtractCamera(publish.Extractor):
|
|||
"frameEndHandle": last_frame,
|
||||
})
|
||||
|
||||
self.log.info("Extracted instance '{0}' to: {1}".format(
|
||||
self.log.debug("Extracted instance '{0}' to: {1}".format(
|
||||
instance.name, file_path))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -85,8 +85,5 @@ class ExtractGizmo(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '{}' to: {}".format(
|
||||
self.log.debug("Extracted instance '{}' to: {}".format(
|
||||
instance.name, path))
|
||||
|
||||
self.log.info("Data {}".format(
|
||||
instance.data))
|
||||
|
|
|
|||
|
|
@ -33,13 +33,13 @@ class ExtractModel(publish.Extractor):
|
|||
first_frame = int(nuke.root()["first_frame"].getValue())
|
||||
last_frame = int(nuke.root()["last_frame"].getValue())
|
||||
|
||||
self.log.info("instance.data: `{}`".format(
|
||||
self.log.debug("instance.data: `{}`".format(
|
||||
pformat(instance.data)))
|
||||
|
||||
rm_nodes = []
|
||||
model_node = instance.data["transientData"]["node"]
|
||||
|
||||
self.log.info("Crating additional nodes")
|
||||
self.log.debug("Creating additional nodes for Extract Model")
|
||||
subset = instance.data["subset"]
|
||||
staging_dir = self.staging_dir(instance)
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ class ExtractModel(publish.Extractor):
|
|||
for n in rm_nodes:
|
||||
nuke.delete(n)
|
||||
|
||||
self.log.info(file_path)
|
||||
self.log.debug("Filepath: {}".format(file_path))
|
||||
|
||||
# create representation data
|
||||
if "representations" not in instance.data:
|
||||
|
|
@ -104,5 +104,5 @@ class ExtractModel(publish.Extractor):
|
|||
"frameEndHandle": last_frame,
|
||||
})
|
||||
|
||||
self.log.info("Extracted instance '{0}' to: {1}".format(
|
||||
self.log.debug("Extracted instance '{0}' to: {1}".format(
|
||||
instance.name, file_path))
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class CreateOutputNode(pyblish.api.ContextPlugin):
|
|||
|
||||
if active_node:
|
||||
active_node = active_node.pop()
|
||||
self.log.info(active_node)
|
||||
self.log.debug("Active node: {}".format(active_node))
|
||||
active_node['selected'].setValue(True)
|
||||
|
||||
# select only instance render node
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ class NukeRenderLocal(publish.Extractor,
|
|||
|
||||
instance.data["representations"].append(repre)
|
||||
|
||||
self.log.info("Extracted instance '{0}' to: {1}".format(
|
||||
self.log.debug("Extracted instance '{0}' to: {1}".format(
|
||||
instance.name,
|
||||
out_dir
|
||||
))
|
||||
|
|
@ -143,7 +143,7 @@ class NukeRenderLocal(publish.Extractor,
|
|||
instance.data["families"] = families
|
||||
|
||||
collections, remainder = clique.assemble(filenames)
|
||||
self.log.info('collections: {}'.format(str(collections)))
|
||||
self.log.debug('collections: {}'.format(str(collections)))
|
||||
|
||||
if collections:
|
||||
collection = collections[0]
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class ExtractReviewDataLut(publish.Extractor):
|
|||
hosts = ["nuke"]
|
||||
|
||||
def process(self, instance):
|
||||
self.log.info("Creating staging dir...")
|
||||
self.log.debug("Creating staging dir...")
|
||||
if "representations" in instance.data:
|
||||
staging_dir = instance.data[
|
||||
"representations"][0]["stagingDir"].replace("\\", "/")
|
||||
|
|
@ -33,7 +33,7 @@ class ExtractReviewDataLut(publish.Extractor):
|
|||
staging_dir = os.path.normpath(os.path.dirname(render_path))
|
||||
instance.data["stagingDir"] = staging_dir
|
||||
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
"StagingDir `{0}`...".format(instance.data["stagingDir"]))
|
||||
|
||||
# generate data
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class ExtractReviewIntermediates(publish.Extractor):
|
|||
|
||||
task_type = instance.context.data["taskType"]
|
||||
subset = instance.data["subset"]
|
||||
self.log.info("Creating staging dir...")
|
||||
self.log.debug("Creating staging dir...")
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
|
@ -62,10 +62,10 @@ class ExtractReviewIntermediates(publish.Extractor):
|
|||
|
||||
instance.data["stagingDir"] = staging_dir
|
||||
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
"StagingDir `{0}`...".format(instance.data["stagingDir"]))
|
||||
|
||||
self.log.info(self.outputs)
|
||||
self.log.debug("Outputs: {}".format(self.outputs))
|
||||
|
||||
# generate data
|
||||
with maintained_selection():
|
||||
|
|
@ -104,9 +104,10 @@ class ExtractReviewIntermediates(publish.Extractor):
|
|||
re.search(s, subset) for s in f_subsets):
|
||||
continue
|
||||
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
"Baking output `{}` with settings: {}".format(
|
||||
o_name, o_data))
|
||||
o_name, o_data)
|
||||
)
|
||||
|
||||
# check if settings have more then one preset
|
||||
# so we dont need to add outputName to representation
|
||||
|
|
@ -155,10 +156,10 @@ class ExtractReviewIntermediates(publish.Extractor):
|
|||
instance.data["useSequenceForReview"] = False
|
||||
else:
|
||||
instance.data["families"].remove("review")
|
||||
self.log.info((
|
||||
self.log.debug(
|
||||
"Removing `review` from families. "
|
||||
"Not available baking profile."
|
||||
))
|
||||
)
|
||||
self.log.debug(instance.data["families"])
|
||||
|
||||
self.log.debug(
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ import pyblish.api
|
|||
|
||||
|
||||
class ExtractScriptSave(pyblish.api.Extractor):
|
||||
"""
|
||||
"""
|
||||
"""Save current Nuke workfile script"""
|
||||
label = 'Script Save'
|
||||
order = pyblish.api.Extractor.order - 0.1
|
||||
hosts = ['nuke']
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
self.log.info('saving script')
|
||||
self.log.debug('Saving current script')
|
||||
nuke.scriptSave()
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class ExtractSlateFrame(publish.Extractor):
|
|||
|
||||
if instance.data.get("bakePresets"):
|
||||
for o_name, o_data in instance.data["bakePresets"].items():
|
||||
self.log.info("_ o_name: {}, o_data: {}".format(
|
||||
self.log.debug("_ o_name: {}, o_data: {}".format(
|
||||
o_name, pformat(o_data)))
|
||||
self.render_slate(
|
||||
instance,
|
||||
|
|
@ -65,14 +65,14 @@ class ExtractSlateFrame(publish.Extractor):
|
|||
|
||||
def _create_staging_dir(self, instance):
|
||||
|
||||
self.log.info("Creating staging dir...")
|
||||
self.log.debug("Creating staging dir...")
|
||||
|
||||
staging_dir = os.path.normpath(
|
||||
os.path.dirname(instance.data["path"]))
|
||||
|
||||
instance.data["stagingDir"] = staging_dir
|
||||
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
"StagingDir `{0}`...".format(instance.data["stagingDir"]))
|
||||
|
||||
def _check_frames_exists(self, instance):
|
||||
|
|
@ -275,10 +275,10 @@ class ExtractSlateFrame(publish.Extractor):
|
|||
break
|
||||
|
||||
if not matching_repre:
|
||||
self.log.info((
|
||||
"Matching reresentaion was not found."
|
||||
self.log.info(
|
||||
"Matching reresentation was not found."
|
||||
" Representation files were not filled with slate."
|
||||
))
|
||||
)
|
||||
return
|
||||
|
||||
# Add frame to matching representation files
|
||||
|
|
@ -345,7 +345,7 @@ class ExtractSlateFrame(publish.Extractor):
|
|||
|
||||
try:
|
||||
node[key].setValue(value)
|
||||
self.log.info("Change key \"{}\" to value \"{}\"".format(
|
||||
self.log.debug("Change key \"{}\" to value \"{}\"".format(
|
||||
key, value
|
||||
))
|
||||
except NameError:
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ class ExtractThumbnail(publish.Extractor):
|
|||
"bake_viewer_input_process"]
|
||||
|
||||
node = instance.data["transientData"]["node"] # group node
|
||||
self.log.info("Creating staging dir...")
|
||||
self.log.debug("Creating staging dir...")
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
|
@ -79,7 +79,7 @@ class ExtractThumbnail(publish.Extractor):
|
|||
|
||||
instance.data["stagingDir"] = staging_dir
|
||||
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
"StagingDir `{0}`...".format(instance.data["stagingDir"]))
|
||||
|
||||
temporary_nodes = []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<error id="main">
|
||||
<title>Shot/Asset name</title>
|
||||
<description>
|
||||
## Publishing to a different asset context
|
||||
|
||||
There are publish instances present which are publishing into a different asset than your current context.
|
||||
|
||||
Usually this is not what you want but there can be cases where you might want to publish into another asset/shot or task.
|
||||
|
||||
If that's the case you can disable the validation on the instance to ignore it.
|
||||
|
||||
The wrong node's name is: \`{node_name}\`
|
||||
|
||||
### Correct context keys and values:
|
||||
|
||||
\`{correct_values}\`
|
||||
|
||||
### Wrong keys and values:
|
||||
|
||||
\`{wrong_values}\`.
|
||||
|
||||
|
||||
## How to repair?
|
||||
|
||||
1. Use \"Repair\" button.
|
||||
2. Hit Reload button on the publisher.
|
||||
</description>
|
||||
</error>
|
||||
</root>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<error id="main">
|
||||
<title>Shot/Asset name</title>
|
||||
<description>
|
||||
## Invalid Shot/Asset name in subset
|
||||
|
||||
Following Node with name `{node_name}`:
|
||||
Is in context of `{correct_name}` but Node _asset_ knob is set as `{wrong_name}`.
|
||||
|
||||
### How to repair?
|
||||
|
||||
1. Either use Repair or Select button.
|
||||
2. If you chose Select then rename asset knob to correct name.
|
||||
3. Hit Reload button on the publisher.
|
||||
</description>
|
||||
</error>
|
||||
</root>
|
||||
112
openpype/hosts/nuke/plugins/publish/validate_asset_context.py
Normal file
112
openpype/hosts/nuke/plugins/publish/validate_asset_context.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validate if instance asset is the same as context asset."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import (
|
||||
RepairAction,
|
||||
ValidateContentsOrder,
|
||||
PublishXmlValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
from openpype.hosts.nuke.api import SelectInstanceNodeAction
|
||||
|
||||
|
||||
class ValidateCorrectAssetContext(
|
||||
pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin
|
||||
):
|
||||
"""Validator to check if instance asset context match context asset.
|
||||
|
||||
When working in per-shot style you always publish data in context of
|
||||
current asset (shot). This validator checks if this is so. It is optional
|
||||
so it can be disabled when needed.
|
||||
|
||||
Checking `asset` and `task` keys.
|
||||
"""
|
||||
order = ValidateContentsOrder
|
||||
label = "Validate asset context"
|
||||
hosts = ["nuke"]
|
||||
actions = [
|
||||
RepairAction,
|
||||
SelectInstanceNodeAction
|
||||
]
|
||||
optional = True
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, project_settings):
|
||||
"""Apply deprecated settings from project settings.
|
||||
"""
|
||||
nuke_publish = project_settings["nuke"]["publish"]
|
||||
if "ValidateCorrectAssetName" in nuke_publish:
|
||||
settings = nuke_publish["ValidateCorrectAssetName"]
|
||||
else:
|
||||
settings = nuke_publish["ValidateCorrectAssetContext"]
|
||||
|
||||
cls.enabled = settings["enabled"]
|
||||
cls.optional = settings["optional"]
|
||||
cls.active = settings["active"]
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid_keys = self.get_invalid(instance)
|
||||
|
||||
if not invalid_keys:
|
||||
return
|
||||
|
||||
message_values = {
|
||||
"node_name": instance.data["transientData"]["node"].name(),
|
||||
"correct_values": ", ".join([
|
||||
"{} > {}".format(_key, instance.context.data[_key])
|
||||
for _key in invalid_keys
|
||||
]),
|
||||
"wrong_values": ", ".join([
|
||||
"{} > {}".format(_key, instance.data.get(_key))
|
||||
for _key in invalid_keys
|
||||
])
|
||||
}
|
||||
|
||||
msg = (
|
||||
"Instance `{node_name}` has wrong context keys:\n"
|
||||
"Correct: `{correct_values}` | Wrong: `{wrong_values}`").format(
|
||||
**message_values)
|
||||
|
||||
self.log.debug(msg)
|
||||
|
||||
raise PublishXmlValidationError(
|
||||
self, msg, formatting_data=message_values
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
"""Get invalid keys from instance data and context data."""
|
||||
|
||||
invalid_keys = []
|
||||
testing_keys = ["asset", "task"]
|
||||
for _key in testing_keys:
|
||||
if _key not in instance.data:
|
||||
invalid_keys.append(_key)
|
||||
continue
|
||||
if instance.data[_key] != instance.context.data[_key]:
|
||||
invalid_keys.append(_key)
|
||||
|
||||
return invalid_keys
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
"""Repair instance data with context data."""
|
||||
invalid_keys = cls.get_invalid(instance)
|
||||
|
||||
create_context = instance.context.data["create_context"]
|
||||
|
||||
instance_id = instance.data.get("instance_id")
|
||||
created_instance = create_context.get_instance_by_id(
|
||||
instance_id
|
||||
)
|
||||
for _key in invalid_keys:
|
||||
created_instance[_key] = instance.context.data[_key]
|
||||
|
||||
create_context.save_changes()
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validate if instance asset is the same as context asset."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import pyblish.api
|
||||
|
||||
import openpype.hosts.nuke.api.lib as nlib
|
||||
|
||||
from openpype.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
PublishXmlValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
|
||||
class SelectInvalidInstances(pyblish.api.Action):
|
||||
"""Select invalid instances in Outliner."""
|
||||
|
||||
label = "Select"
|
||||
icon = "briefcase"
|
||||
on = "failed"
|
||||
|
||||
def process(self, context, plugin):
|
||||
"""Process invalid validators and select invalid instances."""
|
||||
# Get the errored instances
|
||||
failed = []
|
||||
for result in context.data["results"]:
|
||||
if (
|
||||
result["error"] is None
|
||||
or result["instance"] is None
|
||||
or result["instance"] in failed
|
||||
or result["plugin"] != plugin
|
||||
):
|
||||
continue
|
||||
|
||||
failed.append(result["instance"])
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
||||
|
||||
if instances:
|
||||
self.deselect()
|
||||
self.log.info(
|
||||
"Selecting invalid nodes: %s" % ", ".join(
|
||||
[str(x) for x in instances]
|
||||
)
|
||||
)
|
||||
self.select(instances)
|
||||
else:
|
||||
self.log.info("No invalid nodes found.")
|
||||
self.deselect()
|
||||
|
||||
def select(self, instances):
|
||||
for inst in instances:
|
||||
if inst.data.get("transientData", {}).get("node"):
|
||||
select_node = inst.data["transientData"]["node"]
|
||||
select_node["selected"].setValue(True)
|
||||
|
||||
def deselect(self):
|
||||
nlib.reset_selection()
|
||||
|
||||
|
||||
class RepairSelectInvalidInstances(pyblish.api.Action):
|
||||
"""Repair the instance asset."""
|
||||
|
||||
label = "Repair"
|
||||
icon = "wrench"
|
||||
on = "failed"
|
||||
|
||||
def process(self, context, plugin):
|
||||
# Get the errored instances
|
||||
failed = []
|
||||
for result in context.data["results"]:
|
||||
if (
|
||||
result["error"] is None
|
||||
or result["instance"] is None
|
||||
or result["instance"] in failed
|
||||
or result["plugin"] != plugin
|
||||
):
|
||||
continue
|
||||
|
||||
failed.append(result["instance"])
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
||||
self.log.debug(instances)
|
||||
|
||||
context_asset = context.data["assetEntity"]["name"]
|
||||
for instance in instances:
|
||||
node = instance.data["transientData"]["node"]
|
||||
node_data = nlib.get_node_data(node, nlib.INSTANCE_DATA_KNOB)
|
||||
node_data["asset"] = context_asset
|
||||
nlib.set_node_data(node, nlib.INSTANCE_DATA_KNOB, node_data)
|
||||
|
||||
|
||||
class ValidateCorrectAssetName(
|
||||
pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin
|
||||
):
|
||||
"""Validator to check if instance asset match context asset.
|
||||
|
||||
When working in per-shot style you always publish data in context of
|
||||
current asset (shot). This validator checks if this is so. It is optional
|
||||
so it can be disabled when needed.
|
||||
|
||||
Action on this validator will select invalid instances in Outliner.
|
||||
"""
|
||||
order = ValidateContentsOrder
|
||||
label = "Validate correct asset name"
|
||||
hosts = ["nuke"]
|
||||
actions = [
|
||||
SelectInvalidInstances,
|
||||
RepairSelectInvalidInstances
|
||||
]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
asset = instance.data.get("asset")
|
||||
context_asset = instance.context.data["assetEntity"]["name"]
|
||||
node = instance.data["transientData"]["node"]
|
||||
|
||||
msg = (
|
||||
"Instance `{}` has wrong shot/asset name:\n"
|
||||
"Correct: `{}` | Wrong: `{}`").format(
|
||||
instance.name, asset, context_asset)
|
||||
|
||||
self.log.debug(msg)
|
||||
|
||||
if asset != context_asset:
|
||||
raise PublishXmlValidationError(
|
||||
self, msg, formatting_data={
|
||||
"node_name": node.name(),
|
||||
"wrong_name": asset,
|
||||
"correct_name": context_asset
|
||||
}
|
||||
)
|
||||
|
|
@ -43,8 +43,8 @@ class SelectCenterInNodeGraph(pyblish.api.Action):
|
|||
all_xC.append(xC)
|
||||
all_yC.append(yC)
|
||||
|
||||
self.log.info("all_xC: `{}`".format(all_xC))
|
||||
self.log.info("all_yC: `{}`".format(all_yC))
|
||||
self.log.debug("all_xC: `{}`".format(all_xC))
|
||||
self.log.debug("all_yC: `{}`".format(all_yC))
|
||||
|
||||
# zoom to nodes in node graph
|
||||
nuke.zoom(2, [min(all_xC), min(all_yC)])
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class ValidateOutputResolution(
|
|||
order = pyblish.api.ValidatorOrder
|
||||
optional = True
|
||||
families = ["render"]
|
||||
label = "Write resolution"
|
||||
label = "Validate Write resolution"
|
||||
hosts = ["nuke"]
|
||||
actions = [RepairAction]
|
||||
|
||||
|
|
@ -104,9 +104,9 @@ class ValidateOutputResolution(
|
|||
_rfn["resize"].setValue(0)
|
||||
_rfn["black_outside"].setValue(1)
|
||||
|
||||
cls.log.info("I am adding reformat node")
|
||||
cls.log.info("Adding reformat node")
|
||||
|
||||
if cls.resolution_msg == invalid:
|
||||
reformat = cls.get_reformat(instance)
|
||||
reformat["format"].setValue(nuke.root()["format"].value())
|
||||
cls.log.info("I am fixing reformat to root.format")
|
||||
cls.log.info("Fixing reformat to root.format")
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
|
|||
return
|
||||
|
||||
collections, remainder = clique.assemble(repre["files"])
|
||||
self.log.info("collections: {}".format(str(collections)))
|
||||
self.log.info("remainder: {}".format(str(remainder)))
|
||||
self.log.debug("collections: {}".format(str(collections)))
|
||||
self.log.debug("remainder: {}".format(str(remainder)))
|
||||
|
||||
collection = collections[0]
|
||||
|
||||
|
|
@ -103,15 +103,15 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
|
|||
coll_start = min(collection.indexes)
|
||||
coll_end = max(collection.indexes)
|
||||
|
||||
self.log.info("frame_length: {}".format(frame_length))
|
||||
self.log.info("collected_frames_len: {}".format(
|
||||
self.log.debug("frame_length: {}".format(frame_length))
|
||||
self.log.debug("collected_frames_len: {}".format(
|
||||
collected_frames_len))
|
||||
self.log.info("f_start_h-f_end_h: {}-{}".format(
|
||||
self.log.debug("f_start_h-f_end_h: {}-{}".format(
|
||||
f_start_h, f_end_h))
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
"coll_start-coll_end: {}-{}".format(coll_start, coll_end))
|
||||
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
"len(collection.indexes): {}".format(collected_frames_len)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class RepairNukeWriteNodeAction(pyblish.api.Action):
|
|||
|
||||
set_node_knobs_from_settings(write_node, correct_data["knobs"])
|
||||
|
||||
self.log.info("Node attributes were fixed")
|
||||
self.log.debug("Node attributes were fixed")
|
||||
|
||||
|
||||
class ValidateNukeWriteNode(
|
||||
|
|
@ -82,12 +82,6 @@ class ValidateNukeWriteNode(
|
|||
correct_data = get_write_node_template_attr(write_group_node)
|
||||
|
||||
check = []
|
||||
self.log.debug("__ write_node: {}".format(
|
||||
write_node
|
||||
))
|
||||
self.log.debug("__ correct_data: {}".format(
|
||||
correct_data
|
||||
))
|
||||
|
||||
# Collect key values of same type in a list.
|
||||
values_by_name = defaultdict(list)
|
||||
|
|
@ -96,9 +90,6 @@ class ValidateNukeWriteNode(
|
|||
|
||||
for knob_data in correct_data["knobs"]:
|
||||
knob_type = knob_data["type"]
|
||||
self.log.debug("__ knob_type: {}".format(
|
||||
knob_type
|
||||
))
|
||||
|
||||
if (
|
||||
knob_type == "__legacy__"
|
||||
|
|
@ -134,9 +125,6 @@ class ValidateNukeWriteNode(
|
|||
|
||||
fixed_values.append(value)
|
||||
|
||||
self.log.debug("__ key: {} | values: {}".format(
|
||||
key, fixed_values
|
||||
))
|
||||
if (
|
||||
node_value not in fixed_values
|
||||
and key != "file"
|
||||
|
|
@ -144,8 +132,6 @@ class ValidateNukeWriteNode(
|
|||
):
|
||||
check.append([key, value, write_node[key].value()])
|
||||
|
||||
self.log.info(check)
|
||||
|
||||
if check:
|
||||
self._make_error(check)
|
||||
|
||||
|
|
|
|||
|
|
@ -125,15 +125,19 @@ def get_any_timeline():
|
|||
return project.GetTimelineByIndex(1)
|
||||
|
||||
|
||||
def get_new_timeline():
|
||||
def get_new_timeline(timeline_name: str = None):
|
||||
"""Get new timeline object.
|
||||
|
||||
Arguments:
|
||||
timeline_name (str): New timeline name.
|
||||
|
||||
Returns:
|
||||
object: resolve.Timeline
|
||||
"""
|
||||
project = get_current_project()
|
||||
media_pool = project.GetMediaPool()
|
||||
new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name)
|
||||
new_timeline = media_pool.CreateEmptyTimeline(
|
||||
timeline_name or self.pype_timeline_name)
|
||||
project.SetCurrentTimeline(new_timeline)
|
||||
return new_timeline
|
||||
|
||||
|
|
@ -179,53 +183,52 @@ def create_bin(name: str, root: object = None) -> object:
|
|||
return media_pool.GetCurrentFolder()
|
||||
|
||||
|
||||
def create_media_pool_item(fpath: str,
|
||||
root: object = None) -> object:
|
||||
def remove_media_pool_item(media_pool_item: object) -> bool:
|
||||
media_pool = get_current_project().GetMediaPool()
|
||||
return media_pool.DeleteClips([media_pool_item])
|
||||
|
||||
|
||||
def create_media_pool_item(
|
||||
files: list,
|
||||
root: object = None,
|
||||
) -> object:
|
||||
"""
|
||||
Create media pool item.
|
||||
|
||||
Args:
|
||||
fpath (str): absolute path to a file
|
||||
files (list[str]): list of absolute paths to files
|
||||
root (resolve.Folder)[optional]: root folder / bin object
|
||||
|
||||
Returns:
|
||||
object: resolve.MediaPoolItem
|
||||
"""
|
||||
# get all variables
|
||||
media_storage = get_media_storage()
|
||||
media_pool = get_current_project().GetMediaPool()
|
||||
root_bin = root or media_pool.GetRootFolder()
|
||||
|
||||
# make sure files list is not empty and first available file exists
|
||||
filepath = next((f for f in files if os.path.isfile(f)), None)
|
||||
if not filepath:
|
||||
raise FileNotFoundError("No file found in input files list")
|
||||
|
||||
# try to search in bin if the clip does not exist
|
||||
existing_mpi = get_media_pool_item(fpath, root_bin)
|
||||
existing_mpi = get_media_pool_item(filepath, root_bin)
|
||||
|
||||
if existing_mpi:
|
||||
return existing_mpi
|
||||
|
||||
dirname, file = os.path.split(fpath)
|
||||
_name, ext = os.path.splitext(file)
|
||||
# add all data in folder to media pool
|
||||
media_pool_items = media_pool.ImportMedia(files)
|
||||
|
||||
# add all data in folder to mediapool
|
||||
media_pool_items = media_storage.AddItemListToMediaPool(
|
||||
os.path.normpath(dirname))
|
||||
|
||||
if not media_pool_items:
|
||||
return False
|
||||
|
||||
# if any are added then look into them for the right extension
|
||||
media_pool_item = [mpi for mpi in media_pool_items
|
||||
if ext in mpi.GetClipProperty("File Path")]
|
||||
|
||||
# return only first found
|
||||
return media_pool_item.pop()
|
||||
return media_pool_items.pop() if media_pool_items else False
|
||||
|
||||
|
||||
def get_media_pool_item(fpath, root: object = None) -> object:
|
||||
def get_media_pool_item(filepath, root: object = None) -> object:
|
||||
"""
|
||||
Return clip if found in folder with use of input file path.
|
||||
|
||||
Args:
|
||||
fpath (str): absolute path to a file
|
||||
filepath (str): absolute path to a file
|
||||
root (resolve.Folder)[optional]: root folder / bin object
|
||||
|
||||
Returns:
|
||||
|
|
@ -233,7 +236,7 @@ def get_media_pool_item(fpath, root: object = None) -> object:
|
|||
"""
|
||||
media_pool = get_current_project().GetMediaPool()
|
||||
root = root or media_pool.GetRootFolder()
|
||||
fname = os.path.basename(fpath)
|
||||
fname = os.path.basename(filepath)
|
||||
|
||||
for _mpi in root.GetClipList():
|
||||
_mpi_name = _mpi.GetClipProperty("File Name")
|
||||
|
|
@ -277,7 +280,6 @@ def create_timeline_item(media_pool_item: object,
|
|||
if source_end is not None:
|
||||
clip_data.update({"endFrame": source_end})
|
||||
|
||||
print(clip_data)
|
||||
# add to timeline
|
||||
media_pool.AppendToTimeline([clip_data])
|
||||
|
||||
|
|
@ -394,14 +396,22 @@ def get_current_timeline_items(
|
|||
|
||||
|
||||
def get_pype_timeline_item_by_name(name: str) -> object:
|
||||
track_itmes = get_current_timeline_items()
|
||||
for _ti in track_itmes:
|
||||
tag_data = get_timeline_item_pype_tag(_ti["clip"]["item"])
|
||||
tag_name = tag_data.get("name")
|
||||
"""Get timeline item by name.
|
||||
|
||||
Args:
|
||||
name (str): name of timeline item
|
||||
|
||||
Returns:
|
||||
object: resolve.TimelineItem
|
||||
"""
|
||||
for _ti_data in get_current_timeline_items():
|
||||
_ti_clip = _ti_data["clip"]["item"]
|
||||
tag_data = get_timeline_item_pype_tag(_ti_clip)
|
||||
tag_name = tag_data.get("namespace")
|
||||
if not tag_name:
|
||||
continue
|
||||
if tag_data.get("name") in name:
|
||||
return _ti
|
||||
if tag_name in name:
|
||||
return _ti_clip
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -544,12 +554,11 @@ def set_pype_marker(timeline_item, tag_data):
|
|||
|
||||
def get_pype_marker(timeline_item):
|
||||
timeline_item_markers = timeline_item.GetMarkers()
|
||||
for marker_frame in timeline_item_markers:
|
||||
note = timeline_item_markers[marker_frame]["note"]
|
||||
color = timeline_item_markers[marker_frame]["color"]
|
||||
name = timeline_item_markers[marker_frame]["name"]
|
||||
print(f"_ marker data: {marker_frame} | {name} | {color} | {note}")
|
||||
for marker_frame, marker in timeline_item_markers.items():
|
||||
color = marker["color"]
|
||||
name = marker["name"]
|
||||
if name == self.pype_marker_name and color == self.pype_marker_color:
|
||||
note = marker["note"]
|
||||
self.temp_marker_frame = marker_frame
|
||||
return json.loads(note)
|
||||
|
||||
|
|
@ -618,7 +627,7 @@ def create_compound_clip(clip_data, name, folder):
|
|||
if c.GetName() in name), None)
|
||||
|
||||
if cct:
|
||||
print(f"_ cct exists: {cct}")
|
||||
print(f"Compound clip exists: {cct}")
|
||||
else:
|
||||
# Create empty timeline in current folder and give name:
|
||||
cct = mp.CreateEmptyTimeline(name)
|
||||
|
|
@ -627,7 +636,7 @@ def create_compound_clip(clip_data, name, folder):
|
|||
clips = folder.GetClipList()
|
||||
cct = next((c for c in clips
|
||||
if c.GetName() in name), None)
|
||||
print(f"_ cct created: {cct}")
|
||||
print(f"Compound clip created: {cct}")
|
||||
|
||||
with maintain_current_timeline(cct, tl_origin):
|
||||
# Add input clip to the current timeline:
|
||||
|
|
|
|||
|
|
@ -127,10 +127,8 @@ def containerise(timeline_item,
|
|||
})
|
||||
|
||||
if data:
|
||||
for k, v in data.items():
|
||||
data_imprint.update({k: v})
|
||||
data_imprint.update(data)
|
||||
|
||||
print("_ data_imprint: {}".format(data_imprint))
|
||||
lib.set_timeline_item_pype_tag(timeline_item, data_imprint)
|
||||
|
||||
return timeline_item
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import re
|
||||
import uuid
|
||||
|
||||
import qargparse
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
|
|
@ -9,6 +8,7 @@ from openpype.pipeline.context_tools import get_current_project_asset
|
|||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
LoaderPlugin,
|
||||
Anatomy
|
||||
)
|
||||
|
||||
from . import lib
|
||||
|
|
@ -291,20 +291,19 @@ class ClipLoader:
|
|||
active_bin = None
|
||||
data = dict()
|
||||
|
||||
def __init__(self, cls, context, path, **options):
|
||||
def __init__(self, loader_obj, context, **options):
|
||||
""" Initialize object
|
||||
|
||||
Arguments:
|
||||
cls (openpype.pipeline.load.LoaderPlugin): plugin object
|
||||
loader_obj (openpype.pipeline.load.LoaderPlugin): plugin object
|
||||
context (dict): loader plugin context
|
||||
options (dict)[optional]: possible keys:
|
||||
projectBinPath: "path/to/binItem"
|
||||
|
||||
"""
|
||||
self.__dict__.update(cls.__dict__)
|
||||
self.__dict__.update(loader_obj.__dict__)
|
||||
self.context = context
|
||||
self.active_project = lib.get_current_project()
|
||||
self.fname = path
|
||||
|
||||
# try to get value from options or evaluate key value for `handles`
|
||||
self.with_handles = options.get("handles") or bool(
|
||||
|
|
@ -319,54 +318,54 @@ class ClipLoader:
|
|||
|
||||
# inject asset data to representation dict
|
||||
self._get_asset_data()
|
||||
print("__init__ self.data: `{}`".format(self.data))
|
||||
|
||||
# add active components to class
|
||||
if self.new_timeline:
|
||||
if options.get("timeline"):
|
||||
loader_cls = loader_obj.__class__
|
||||
if loader_cls.timeline:
|
||||
# if multiselection is set then use options sequence
|
||||
self.active_timeline = options["timeline"]
|
||||
self.active_timeline = loader_cls.timeline
|
||||
else:
|
||||
# create new sequence
|
||||
self.active_timeline = (
|
||||
lib.get_current_timeline() or
|
||||
lib.get_new_timeline()
|
||||
self.active_timeline = lib.get_new_timeline(
|
||||
"{}_{}".format(
|
||||
self.data["timeline_basename"],
|
||||
str(uuid.uuid4())[:8]
|
||||
)
|
||||
)
|
||||
loader_cls.timeline = self.active_timeline
|
||||
|
||||
else:
|
||||
self.active_timeline = lib.get_current_timeline()
|
||||
|
||||
cls.timeline = self.active_timeline
|
||||
|
||||
def _populate_data(self):
|
||||
""" Gets context and convert it to self.data
|
||||
data structure:
|
||||
{
|
||||
"name": "assetName_subsetName_representationName"
|
||||
"path": "path/to/file/created/by/get_repr..",
|
||||
"binPath": "projectBinPath",
|
||||
}
|
||||
"""
|
||||
# create name
|
||||
repr = self.context["representation"]
|
||||
repr_cntx = repr["context"]
|
||||
asset = str(repr_cntx["asset"])
|
||||
subset = str(repr_cntx["subset"])
|
||||
representation = str(repr_cntx["representation"])
|
||||
self.data["clip_name"] = "_".join([asset, subset, representation])
|
||||
representation = self.context["representation"]
|
||||
representation_context = representation["context"]
|
||||
asset = str(representation_context["asset"])
|
||||
subset = str(representation_context["subset"])
|
||||
representation_name = str(representation_context["representation"])
|
||||
self.data["clip_name"] = "_".join([
|
||||
asset,
|
||||
subset,
|
||||
representation_name
|
||||
])
|
||||
self.data["versionData"] = self.context["version"]["data"]
|
||||
# gets file path
|
||||
file = self.fname
|
||||
if not file:
|
||||
repr_id = repr["_id"]
|
||||
print(
|
||||
"Representation id `{}` is failing to load".format(repr_id))
|
||||
return None
|
||||
self.data["path"] = file.replace("\\", "/")
|
||||
|
||||
self.data["timeline_basename"] = "timeline_{}_{}".format(
|
||||
subset, representation_name)
|
||||
|
||||
# solve project bin structure path
|
||||
hierarchy = str("/".join((
|
||||
"Loader",
|
||||
repr_cntx["hierarchy"].replace("\\", "/"),
|
||||
representation_context["hierarchy"].replace("\\", "/"),
|
||||
asset
|
||||
)))
|
||||
|
||||
|
|
@ -383,25 +382,24 @@ class ClipLoader:
|
|||
asset_name = self.context["representation"]["context"]["asset"]
|
||||
self.data["assetData"] = get_current_project_asset(asset_name)["data"]
|
||||
|
||||
def load(self):
|
||||
def load(self, files):
|
||||
"""Load clip into timeline
|
||||
|
||||
Arguments:
|
||||
files (list[str]): list of files to load into timeline
|
||||
"""
|
||||
# create project bin for the media to be imported into
|
||||
self.active_bin = lib.create_bin(self.data["binPath"])
|
||||
|
||||
# create mediaItem in active project bin
|
||||
# create clip media
|
||||
handle_start = self.data["versionData"].get("handleStart") or 0
|
||||
handle_end = self.data["versionData"].get("handleEnd") or 0
|
||||
|
||||
media_pool_item = lib.create_media_pool_item(
|
||||
self.data["path"], self.active_bin)
|
||||
files,
|
||||
self.active_bin
|
||||
)
|
||||
_clip_property = media_pool_item.GetClipProperty
|
||||
|
||||
# get handles
|
||||
handle_start = self.data["versionData"].get("handleStart")
|
||||
handle_end = self.data["versionData"].get("handleEnd")
|
||||
if handle_start is None:
|
||||
handle_start = int(self.data["assetData"]["handleStart"])
|
||||
if handle_end is None:
|
||||
handle_end = int(self.data["assetData"]["handleEnd"])
|
||||
|
||||
source_in = int(_clip_property("Start"))
|
||||
source_out = int(_clip_property("End"))
|
||||
|
||||
|
|
@ -421,14 +419,16 @@ class ClipLoader:
|
|||
print("Loading clips: `{}`".format(self.data["clip_name"]))
|
||||
return timeline_item
|
||||
|
||||
def update(self, timeline_item):
|
||||
def update(self, timeline_item, files):
|
||||
# create project bin for the media to be imported into
|
||||
self.active_bin = lib.create_bin(self.data["binPath"])
|
||||
|
||||
# create mediaItem in active project bin
|
||||
# create clip media
|
||||
media_pool_item = lib.create_media_pool_item(
|
||||
self.data["path"], self.active_bin)
|
||||
files,
|
||||
self.active_bin
|
||||
)
|
||||
_clip_property = media_pool_item.GetClipProperty
|
||||
|
||||
source_in = int(_clip_property("Start"))
|
||||
|
|
@ -649,8 +649,6 @@ class PublishClip:
|
|||
|
||||
# define ui inputs if non gui mode was used
|
||||
self.shot_num = self.ti_index
|
||||
print(
|
||||
"____ self.shot_num: {}".format(self.shot_num))
|
||||
|
||||
# ui_inputs data or default values if gui was not used
|
||||
self.rename = self.ui_inputs.get(
|
||||
|
|
@ -829,3 +827,12 @@ class PublishClip:
|
|||
for key in par_split:
|
||||
parent = self._convert_to_entity(key)
|
||||
self.parents.append(parent)
|
||||
|
||||
|
||||
def get_representation_files(representation):
|
||||
anatomy = Anatomy()
|
||||
files = []
|
||||
for file_data in representation["files"]:
|
||||
path = anatomy.fill_root(file_data["path"])
|
||||
files.append(path)
|
||||
return files
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
from copy import deepcopy
|
||||
|
||||
from openpype.client import (
|
||||
get_version_by_id,
|
||||
get_last_version_by_subset_id,
|
||||
)
|
||||
# from openpype.hosts import resolve
|
||||
from openpype.client import get_last_version_by_subset_id
|
||||
from openpype.pipeline import (
|
||||
get_representation_path,
|
||||
get_current_project_name,
|
||||
get_representation_context,
|
||||
get_current_project_name
|
||||
)
|
||||
from openpype.hosts.resolve.api import lib, plugin
|
||||
from openpype.hosts.resolve.api.pipeline import (
|
||||
|
|
@ -48,48 +42,17 @@ class LoadClip(plugin.TimelineItemLoader):
|
|||
|
||||
def load(self, context, name, namespace, options):
|
||||
|
||||
# in case loader uses multiselection
|
||||
if self.timeline:
|
||||
options.update({
|
||||
"timeline": self.timeline,
|
||||
})
|
||||
|
||||
# load clip to timeline and get main variables
|
||||
path = self.filepath_from_context(context)
|
||||
files = plugin.get_representation_files(context["representation"])
|
||||
|
||||
timeline_item = plugin.ClipLoader(
|
||||
self, context, path, **options).load()
|
||||
self, context, **options).load(files)
|
||||
namespace = namespace or timeline_item.GetName()
|
||||
version = context['version']
|
||||
version_data = version.get("data", {})
|
||||
version_name = version.get("name", None)
|
||||
colorspace = version_data.get("colorspace", None)
|
||||
object_name = "{}_{}".format(name, namespace)
|
||||
|
||||
# add additional metadata from the version to imprint Avalon knob
|
||||
add_keys = [
|
||||
"frameStart", "frameEnd", "source", "author",
|
||||
"fps", "handleStart", "handleEnd"
|
||||
]
|
||||
|
||||
# move all version data keys to tag data
|
||||
data_imprint = {}
|
||||
for key in add_keys:
|
||||
data_imprint.update({
|
||||
key: version_data.get(key, str(None))
|
||||
})
|
||||
|
||||
# add variables related to version context
|
||||
data_imprint.update({
|
||||
"version": version_name,
|
||||
"colorspace": colorspace,
|
||||
"objectName": object_name
|
||||
})
|
||||
|
||||
# update color of clip regarding the version order
|
||||
self.set_item_color(timeline_item, version)
|
||||
|
||||
self.log.info("Loader done: `{}`".format(name))
|
||||
self.set_item_color(timeline_item, version=context["version"])
|
||||
|
||||
data_imprint = self.get_tag_data(context, name, namespace)
|
||||
return containerise(
|
||||
timeline_item,
|
||||
name, namespace, context,
|
||||
|
|
@ -103,53 +66,61 @@ class LoadClip(plugin.TimelineItemLoader):
|
|||
""" Updating previously loaded clips
|
||||
"""
|
||||
|
||||
# load clip to timeline and get main variables
|
||||
context = deepcopy(representation["context"])
|
||||
context.update({"representation": representation})
|
||||
context = get_representation_context(representation)
|
||||
name = container['name']
|
||||
namespace = container['namespace']
|
||||
timeline_item_data = lib.get_pype_timeline_item_by_name(namespace)
|
||||
timeline_item = timeline_item_data["clip"]["item"]
|
||||
project_name = get_current_project_name()
|
||||
version = get_version_by_id(project_name, representation["parent"])
|
||||
timeline_item = container["_timeline_item"]
|
||||
|
||||
media_pool_item = timeline_item.GetMediaPoolItem()
|
||||
|
||||
files = plugin.get_representation_files(representation)
|
||||
|
||||
loader = plugin.ClipLoader(self, context)
|
||||
timeline_item = loader.update(timeline_item, files)
|
||||
|
||||
# update color of clip regarding the version order
|
||||
self.set_item_color(timeline_item, version=context["version"])
|
||||
|
||||
# if original media pool item has no remaining usages left
|
||||
# remove it from the media pool
|
||||
if int(media_pool_item.GetClipProperty("Usage")) == 0:
|
||||
lib.remove_media_pool_item(media_pool_item)
|
||||
|
||||
data_imprint = self.get_tag_data(context, name, namespace)
|
||||
return update_container(timeline_item, data_imprint)
|
||||
|
||||
def get_tag_data(self, context, name, namespace):
|
||||
"""Return data to be imprinted on the timeline item marker"""
|
||||
|
||||
representation = context["representation"]
|
||||
version = context['version']
|
||||
version_data = version.get("data", {})
|
||||
version_name = version.get("name", None)
|
||||
colorspace = version_data.get("colorspace", None)
|
||||
object_name = "{}_{}".format(name, namespace)
|
||||
path = get_representation_path(representation)
|
||||
context["version"] = {"data": version_data}
|
||||
|
||||
loader = plugin.ClipLoader(self, context, path)
|
||||
timeline_item = loader.update(timeline_item)
|
||||
|
||||
# add additional metadata from the version to imprint Avalon knob
|
||||
add_keys = [
|
||||
# move all version data keys to tag data
|
||||
add_version_data_keys = [
|
||||
"frameStart", "frameEnd", "source", "author",
|
||||
"fps", "handleStart", "handleEnd"
|
||||
]
|
||||
|
||||
# move all version data keys to tag data
|
||||
data_imprint = {}
|
||||
for key in add_keys:
|
||||
data_imprint.update({
|
||||
key: version_data.get(key, str(None))
|
||||
})
|
||||
data = {
|
||||
key: version_data.get(key, "None") for key in add_version_data_keys
|
||||
}
|
||||
|
||||
# add variables related to version context
|
||||
data_imprint.update({
|
||||
data.update({
|
||||
"representation": str(representation["_id"]),
|
||||
"version": version_name,
|
||||
"colorspace": colorspace,
|
||||
"objectName": object_name
|
||||
})
|
||||
|
||||
# update color of clip regarding the version order
|
||||
self.set_item_color(timeline_item, version)
|
||||
|
||||
return update_container(timeline_item, data_imprint)
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def set_item_color(cls, timeline_item, version):
|
||||
"""Color timeline item based on whether it is outdated or latest"""
|
||||
# define version name
|
||||
version_name = version.get("name", None)
|
||||
# get all versions in list
|
||||
|
|
@ -169,3 +140,28 @@ class LoadClip(plugin.TimelineItemLoader):
|
|||
timeline_item.SetClipColor(cls.clip_color_last)
|
||||
else:
|
||||
timeline_item.SetClipColor(cls.clip_color)
|
||||
|
||||
def remove(self, container):
|
||||
timeline_item = container["_timeline_item"]
|
||||
media_pool_item = timeline_item.GetMediaPoolItem()
|
||||
timeline = lib.get_current_timeline()
|
||||
|
||||
# DeleteClips function was added in Resolve 18.5+
|
||||
# by checking None we can detect whether the
|
||||
# function exists in Resolve
|
||||
if timeline.DeleteClips is not None:
|
||||
timeline.DeleteClips([timeline_item])
|
||||
else:
|
||||
# Resolve versions older than 18.5 can't delete clips via API
|
||||
# so all we can do is just remove the pype marker to 'untag' it
|
||||
if lib.get_pype_marker(timeline_item):
|
||||
# Note: We must call `get_pype_marker` because
|
||||
# `delete_pype_marker` uses a global variable set by
|
||||
# `get_pype_marker` to delete the right marker
|
||||
# TODO: Improve code to avoid the global `temp_marker_frame`
|
||||
lib.delete_pype_marker(timeline_item)
|
||||
|
||||
# if media pool item has no remaining usages left
|
||||
# remove it from the media pool
|
||||
if int(media_pool_item.GetClipProperty("Usage")) == 0:
|
||||
lib.remove_media_pool_item(media_pool_item)
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
#! python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
import opentimelineio as otio
|
||||
|
||||
from openpype.pipeline import install_host
|
||||
|
||||
import openpype.hosts.resolve.api as bmdvr
|
||||
from openpype.hosts.resolve.api.testing_utils import TestGUI
|
||||
from openpype.hosts.resolve.otio import davinci_export as otio_export
|
||||
|
||||
|
||||
class ThisTestGUI(TestGUI):
|
||||
extensions = [".exr", ".jpg", ".mov", ".png", ".mp4", ".ari", ".arx"]
|
||||
|
||||
def __init__(self):
|
||||
super(ThisTestGUI, self).__init__()
|
||||
# activate resolve from openpype
|
||||
install_host(bmdvr)
|
||||
|
||||
def _open_dir_button_pressed(self, event):
|
||||
# selected_path = self.fu.RequestFile(os.path.expanduser("~"))
|
||||
selected_path = self.fu.RequestDir(os.path.expanduser("~"))
|
||||
self._widgets["inputTestSourcesFolder"].Text = selected_path
|
||||
|
||||
# main function
|
||||
def process(self, event):
|
||||
self.input_dir_path = self._widgets["inputTestSourcesFolder"].Text
|
||||
project = bmdvr.get_current_project()
|
||||
otio_timeline = otio_export.create_otio_timeline(project)
|
||||
print(f"_ otio_timeline: `{otio_timeline}`")
|
||||
edl_path = os.path.join(self.input_dir_path, "this_file_name.edl")
|
||||
print(f"_ edl_path: `{edl_path}`")
|
||||
# xml_string = otio_adapters.fcpx_xml.write_to_string(otio_timeline)
|
||||
# print(f"_ xml_string: `{xml_string}`")
|
||||
otio.adapters.write_to_file(
|
||||
otio_timeline, edl_path, adapter_name="cmx_3600")
|
||||
project = bmdvr.get_current_project()
|
||||
media_pool = project.GetMediaPool()
|
||||
timeline = media_pool.ImportTimelineFromFile(edl_path)
|
||||
# at the end close the window
|
||||
self._close_window(None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_gui = ThisTestGUI()
|
||||
test_gui.show_gui()
|
||||
sys.exit(not bool(True))
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
#! python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
import clique
|
||||
|
||||
from openpype.pipeline import install_host
|
||||
from openpype.hosts.resolve.api.testing_utils import TestGUI
|
||||
import openpype.hosts.resolve.api as bmdvr
|
||||
from openpype.hosts.resolve.api.lib import (
|
||||
create_media_pool_item,
|
||||
create_timeline_item,
|
||||
)
|
||||
|
||||
|
||||
class ThisTestGUI(TestGUI):
|
||||
extensions = [".exr", ".jpg", ".mov", ".png", ".mp4", ".ari", ".arx"]
|
||||
|
||||
def __init__(self):
|
||||
super(ThisTestGUI, self).__init__()
|
||||
# activate resolve from openpype
|
||||
install_host(bmdvr)
|
||||
|
||||
def _open_dir_button_pressed(self, event):
|
||||
# selected_path = self.fu.RequestFile(os.path.expanduser("~"))
|
||||
selected_path = self.fu.RequestDir(os.path.expanduser("~"))
|
||||
self._widgets["inputTestSourcesFolder"].Text = selected_path
|
||||
|
||||
# main function
|
||||
def process(self, event):
|
||||
self.input_dir_path = self._widgets["inputTestSourcesFolder"].Text
|
||||
|
||||
self.dir_processing(self.input_dir_path)
|
||||
|
||||
# at the end close the window
|
||||
self._close_window(None)
|
||||
|
||||
def dir_processing(self, dir_path):
|
||||
collections, reminders = clique.assemble(os.listdir(dir_path))
|
||||
|
||||
# process reminders
|
||||
for _rem in reminders:
|
||||
_rem_path = os.path.join(dir_path, _rem)
|
||||
|
||||
# go deeper if directory
|
||||
if os.path.isdir(_rem_path):
|
||||
print(_rem_path)
|
||||
self.dir_processing(_rem_path)
|
||||
else:
|
||||
self.file_processing(_rem_path)
|
||||
|
||||
# process collections
|
||||
for _coll in collections:
|
||||
_coll_path = os.path.join(dir_path, list(_coll).pop())
|
||||
self.file_processing(_coll_path)
|
||||
|
||||
def file_processing(self, fpath):
|
||||
print(f"_ fpath: `{fpath}`")
|
||||
_base, ext = os.path.splitext(fpath)
|
||||
# skip if unwanted extension
|
||||
if ext not in self.extensions:
|
||||
return
|
||||
media_pool_item = create_media_pool_item(fpath)
|
||||
print(media_pool_item)
|
||||
|
||||
track_item = create_timeline_item(media_pool_item)
|
||||
print(track_item)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_gui = ThisTestGUI()
|
||||
test_gui.show_gui()
|
||||
sys.exit(not bool(True))
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
#! python3
|
||||
from openpype.pipeline import install_host
|
||||
from openpype.hosts.resolve import api as bmdvr
|
||||
from openpype.hosts.resolve.api.lib import (
|
||||
create_media_pool_item,
|
||||
create_timeline_item,
|
||||
)
|
||||
|
||||
|
||||
def file_processing(fpath):
|
||||
media_pool_item = create_media_pool_item(fpath)
|
||||
print(media_pool_item)
|
||||
|
||||
track_item = create_timeline_item(media_pool_item)
|
||||
print(track_item)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
path = "C:/CODE/__openpype_projects/jtest03dev/shots/sq01/mainsq01sh030/publish/plate/plateMain/v006/jt3d_mainsq01sh030_plateMain_v006.0996.exr"
|
||||
|
||||
# activate resolve from openpype
|
||||
install_host(bmdvr)
|
||||
|
||||
file_processing(path)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
#! python3
|
||||
from openpype.hosts.resolve.startup import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
#! python3
|
||||
from openpype.pipeline import install_host
|
||||
from openpype.hosts.resolve import api as bmdvr
|
||||
from openpype.hosts.resolve.api.lib import get_current_project
|
||||
|
||||
if __name__ == "__main__":
|
||||
install_host(bmdvr)
|
||||
project = get_current_project()
|
||||
timeline_count = project.GetTimelineCount()
|
||||
print(f"Timeline count: {timeline_count}")
|
||||
timeline = project.GetTimelineByIndex(timeline_count)
|
||||
print(f"Timeline name: {timeline.GetName()}")
|
||||
print(timeline.GetTrackCount("video"))
|
||||
|
|
@ -13,7 +13,8 @@ from openpype.pipeline.publish import (
|
|||
)
|
||||
from openpype.lib import (
|
||||
BoolDef,
|
||||
NumberDef
|
||||
NumberDef,
|
||||
is_running_from_build
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -230,6 +231,11 @@ class FusionSubmitDeadline(
|
|||
"OPENPYPE_LOG_NO_COLORS",
|
||||
"IS_TEST"
|
||||
]
|
||||
|
||||
# Add OpenPype version if we are running from build.
|
||||
if is_running_from_build():
|
||||
keys.append("OPENPYPE_VERSION")
|
||||
|
||||
environment = dict({key: os.environ[key] for key in keys
|
||||
if key in os.environ}, **legacy_io.Session)
|
||||
|
||||
|
|
|
|||
|
|
@ -44,19 +44,25 @@ class CollectFtrackApi(pyblish.api.ContextPlugin):
|
|||
|
||||
self.log.debug("Project found: {0}".format(project_entity))
|
||||
|
||||
task_object_type = session.query(
|
||||
"ObjectType where name is 'Task'").one()
|
||||
task_object_type_id = task_object_type["id"]
|
||||
asset_entity = None
|
||||
if asset_name:
|
||||
# Find asset entity
|
||||
entity_query = (
|
||||
'TypedContext where project_id is "{0}"'
|
||||
' and name is "{1}"'
|
||||
).format(project_entity["id"], asset_name)
|
||||
"TypedContext where project_id is '{}'"
|
||||
" and name is '{}'"
|
||||
" and object_type_id != '{}'"
|
||||
).format(
|
||||
project_entity["id"],
|
||||
asset_name,
|
||||
task_object_type_id
|
||||
)
|
||||
self.log.debug("Asset entity query: < {0} >".format(entity_query))
|
||||
asset_entities = []
|
||||
for entity in session.query(entity_query).all():
|
||||
# Skip tasks
|
||||
if entity.entity_type.lower() != "task":
|
||||
asset_entities.append(entity)
|
||||
asset_entities.append(entity)
|
||||
|
||||
if len(asset_entities) == 0:
|
||||
raise AssertionError((
|
||||
|
|
@ -103,10 +109,19 @@ class CollectFtrackApi(pyblish.api.ContextPlugin):
|
|||
context.data["ftrackEntity"] = asset_entity
|
||||
context.data["ftrackTask"] = task_entity
|
||||
|
||||
self.per_instance_process(context, asset_entity, task_entity)
|
||||
self.per_instance_process(
|
||||
context,
|
||||
asset_entity,
|
||||
task_entity,
|
||||
task_object_type_id
|
||||
)
|
||||
|
||||
def per_instance_process(
|
||||
self, context, context_asset_entity, context_task_entity
|
||||
self,
|
||||
context,
|
||||
context_asset_entity,
|
||||
context_task_entity,
|
||||
task_object_type_id
|
||||
):
|
||||
context_task_name = None
|
||||
context_asset_name = None
|
||||
|
|
@ -182,23 +197,27 @@ class CollectFtrackApi(pyblish.api.ContextPlugin):
|
|||
|
||||
session = context.data["ftrackSession"]
|
||||
project_entity = context.data["ftrackProject"]
|
||||
asset_names = set()
|
||||
for asset_name in instance_by_asset_and_task.keys():
|
||||
asset_names.add(asset_name)
|
||||
asset_names = set(instance_by_asset_and_task.keys())
|
||||
|
||||
joined_asset_names = ",".join([
|
||||
"\"{}\"".format(name)
|
||||
for name in asset_names
|
||||
])
|
||||
entities = session.query((
|
||||
"TypedContext where project_id is \"{}\" and name in ({})"
|
||||
).format(project_entity["id"], joined_asset_names)).all()
|
||||
entities = session.query(
|
||||
(
|
||||
"TypedContext where project_id is \"{}\" and name in ({})"
|
||||
" and object_type_id != '{}'"
|
||||
).format(
|
||||
project_entity["id"],
|
||||
joined_asset_names,
|
||||
task_object_type_id
|
||||
)
|
||||
).all()
|
||||
|
||||
entities_by_name = {
|
||||
entity["name"]: entity
|
||||
for entity in entities
|
||||
}
|
||||
|
||||
for asset_name, by_task_data in instance_by_asset_and_task.items():
|
||||
entity = entities_by_name.get(asset_name)
|
||||
task_entity_by_name = {}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,13 @@ class TaskNotSetError(KeyError):
|
|||
super(TaskNotSetError, self).__init__(msg)
|
||||
|
||||
|
||||
class TemplateFillError(Exception):
|
||||
def __init__(self, msg=None):
|
||||
if not msg:
|
||||
msg = "Creator's subset name template is missing key value."
|
||||
super(TemplateFillError, self).__init__(msg)
|
||||
|
||||
|
||||
def get_subset_name_template(
|
||||
project_name,
|
||||
family,
|
||||
|
|
@ -112,6 +119,10 @@ def get_subset_name(
|
|||
for project. Settings are queried if not passed.
|
||||
family_filter (Optional[str]): Use different family for subset template
|
||||
filtering. Value of 'family' is used when not passed.
|
||||
|
||||
Raises:
|
||||
TemplateFillError: If filled template contains placeholder key which is not
|
||||
collected.
|
||||
"""
|
||||
|
||||
if not family:
|
||||
|
|
@ -154,4 +165,10 @@ def get_subset_name(
|
|||
for key, value in dynamic_data.items():
|
||||
fill_pairs[key] = value
|
||||
|
||||
return template.format(**prepare_template_data(fill_pairs))
|
||||
try:
|
||||
return template.format(**prepare_template_data(fill_pairs))
|
||||
except KeyError as exp:
|
||||
raise TemplateFillError(
|
||||
"Value for {} key is missing in template '{}'."
|
||||
" Available values are {}".format(str(exp), template, fill_pairs)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
"""Create jpg thumbnail from sequence using ffmpeg"""
|
||||
|
||||
label = "Extract Thumbnail"
|
||||
order = pyblish.api.ExtractorOrder
|
||||
order = pyblish.api.ExtractorOrder + 0.49
|
||||
families = [
|
||||
"imagesequence", "render", "render2d", "prerender",
|
||||
"source", "clip", "take", "online", "image"
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@
|
|||
"write"
|
||||
]
|
||||
},
|
||||
"ValidateCorrectAssetName": {
|
||||
"ValidateCorrectAssetContext": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
|
|
|
|||
|
|
@ -12,6 +12,26 @@
|
|||
"LC_ALL": "C"
|
||||
},
|
||||
"variants": {
|
||||
"2024": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Autodesk\\Maya2024\\bin\\maya.exe"
|
||||
],
|
||||
"darwin": [],
|
||||
"linux": [
|
||||
"/usr/autodesk/maya2024/bin/maya"
|
||||
]
|
||||
},
|
||||
"arguments": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
"environment": {
|
||||
"MAYA_VERSION": "2024"
|
||||
}
|
||||
},
|
||||
"2023": {
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
|
|
@ -51,66 +71,6 @@
|
|||
"environment": {
|
||||
"MAYA_VERSION": "2022"
|
||||
}
|
||||
},
|
||||
"2020": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Autodesk\\Maya2020\\bin\\maya.exe"
|
||||
],
|
||||
"darwin": [],
|
||||
"linux": [
|
||||
"/usr/autodesk/maya2020/bin/maya"
|
||||
]
|
||||
},
|
||||
"arguments": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
"environment": {
|
||||
"MAYA_VERSION": "2020"
|
||||
}
|
||||
},
|
||||
"2019": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Autodesk\\Maya2019\\bin\\maya.exe"
|
||||
],
|
||||
"darwin": [],
|
||||
"linux": [
|
||||
"/usr/autodesk/maya2019/bin/maya"
|
||||
]
|
||||
},
|
||||
"arguments": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
"environment": {
|
||||
"MAYA_VERSION": "2019"
|
||||
}
|
||||
},
|
||||
"2018": {
|
||||
"use_python_2": true,
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Autodesk\\Maya2018\\bin\\maya.exe"
|
||||
],
|
||||
"darwin": [],
|
||||
"linux": [
|
||||
"/usr/autodesk/maya2018/bin/maya"
|
||||
]
|
||||
},
|
||||
"arguments": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
"environment": {
|
||||
"MAYA_VERSION": "2018"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
"name": "template_publish_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "ValidateCorrectAssetName",
|
||||
"key": "ValidateCorrectAssetContext",
|
||||
"label": "Validate Correct Asset Name"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -103,4 +103,4 @@ class HierarchyPage(QtWidgets.QWidget):
|
|||
self._controller.refresh()
|
||||
|
||||
def _on_filter_text_changed(self, text):
|
||||
self._folders_widget.set_name_filer(text)
|
||||
self._folders_widget.set_name_filter(text)
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@ from openpype.tools.ayon_utils.widgets import (
|
|||
FoldersModel,
|
||||
FOLDERS_MODEL_SENDER_NAME,
|
||||
)
|
||||
from openpype.tools.ayon_utils.widgets.folders_widget import ITEM_ID_ROLE
|
||||
from openpype.tools.ayon_utils.widgets.folders_widget import FOLDER_ID_ROLE
|
||||
|
||||
if qtpy.API == "pyside":
|
||||
from PySide.QtGui import QStyleOptionViewItemV4
|
||||
elif qtpy.API == "pyqt4":
|
||||
from PyQt4.QtGui import QStyleOptionViewItemV4
|
||||
|
||||
UNDERLINE_COLORS_ROLE = QtCore.Qt.UserRole + 4
|
||||
UNDERLINE_COLORS_ROLE = QtCore.Qt.UserRole + 50
|
||||
|
||||
|
||||
class UnderlinesFolderDelegate(QtWidgets.QItemDelegate):
|
||||
|
|
@ -257,13 +257,11 @@ class LoaderFoldersWidget(QtWidgets.QWidget):
|
|||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
parent (QtWidgets.QWidget): The parent widget.
|
||||
handle_expected_selection (bool): If True, the widget will handle
|
||||
the expected selection. Defaults to False.
|
||||
"""
|
||||
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent, handle_expected_selection=False):
|
||||
def __init__(self, controller, parent):
|
||||
super(LoaderFoldersWidget, self).__init__(parent)
|
||||
|
||||
folders_view = DeselectableTreeView(self)
|
||||
|
|
@ -313,10 +311,9 @@ class LoaderFoldersWidget(QtWidgets.QWidget):
|
|||
self._folders_proxy_model = folders_proxy_model
|
||||
self._folders_label_delegate = folders_label_delegate
|
||||
|
||||
self._handle_expected_selection = handle_expected_selection
|
||||
self._expected_selection = None
|
||||
|
||||
def set_name_filer(self, name):
|
||||
def set_name_filter(self, name):
|
||||
"""Set filter of folder name.
|
||||
|
||||
Args:
|
||||
|
|
@ -365,7 +362,7 @@ class LoaderFoldersWidget(QtWidgets.QWidget):
|
|||
selection_model = self._folders_view.selectionModel()
|
||||
item_ids = []
|
||||
for index in selection_model.selectedIndexes():
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
item_id = index.data(FOLDER_ID_ROLE)
|
||||
if item_id is not None:
|
||||
item_ids.append(item_id)
|
||||
return item_ids
|
||||
|
|
@ -379,9 +376,6 @@ class LoaderFoldersWidget(QtWidgets.QWidget):
|
|||
self._update_expected_selection(event.data)
|
||||
|
||||
def _update_expected_selection(self, expected_data=None):
|
||||
if not self._handle_expected_selection:
|
||||
return
|
||||
|
||||
if expected_data is None:
|
||||
expected_data = self._controller.get_expected_selection_data()
|
||||
|
||||
|
|
@ -395,9 +389,6 @@ class LoaderFoldersWidget(QtWidgets.QWidget):
|
|||
self._set_expected_selection()
|
||||
|
||||
def _set_expected_selection(self):
|
||||
if not self._handle_expected_selection:
|
||||
return
|
||||
|
||||
folder_id = self._expected_selection
|
||||
selected_ids = self._get_selected_item_ids()
|
||||
self._expected_selection = None
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ class ProductsWidget(QtWidgets.QWidget):
|
|||
not controller.is_loaded_products_supported()
|
||||
)
|
||||
|
||||
def set_name_filer(self, name):
|
||||
def set_name_filter(self, name):
|
||||
"""Set filter of product name.
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -382,7 +382,7 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
self._controller.reset()
|
||||
|
||||
def _show_group_dialog(self):
|
||||
project_name = self._projects_combobox.get_current_project_name()
|
||||
project_name = self._projects_combobox.get_selected_project_name()
|
||||
if not project_name:
|
||||
return
|
||||
|
||||
|
|
@ -397,7 +397,7 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
self._group_dialog.show()
|
||||
|
||||
def _on_folder_filter_change(self, text):
|
||||
self._folders_widget.set_name_filer(text)
|
||||
self._folders_widget.set_name_filter(text)
|
||||
|
||||
def _on_product_group_change(self):
|
||||
self._products_widget.set_enable_grouping(
|
||||
|
|
@ -405,7 +405,7 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
)
|
||||
|
||||
def _on_product_filter_change(self, text):
|
||||
self._products_widget.set_name_filer(text)
|
||||
self._products_widget.set_name_filter(text)
|
||||
|
||||
def _on_product_type_filter_change(self):
|
||||
self._products_widget.set_product_type_filter(
|
||||
|
|
@ -419,7 +419,7 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
def _on_products_selection_change(self):
|
||||
items = self._products_widget.get_selected_version_info()
|
||||
self._info_widget.set_selected_version_info(
|
||||
self._projects_combobox.get_current_project_name(),
|
||||
self._projects_combobox.get_selected_project_name(),
|
||||
items
|
||||
)
|
||||
|
||||
|
|
|
|||
6
openpype/tools/ayon_sceneinventory/__init__.py
Normal file
6
openpype/tools/ayon_sceneinventory/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from .control import SceneInventoryController
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SceneInventoryController",
|
||||
)
|
||||
134
openpype/tools/ayon_sceneinventory/control.py
Normal file
134
openpype/tools/ayon_sceneinventory/control.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import ayon_api
|
||||
|
||||
from openpype.lib.events import QueuedEventSystem
|
||||
from openpype.host import ILoadHost
|
||||
from openpype.pipeline import (
|
||||
registered_host,
|
||||
get_current_context,
|
||||
)
|
||||
from openpype.tools.ayon_utils.models import HierarchyModel
|
||||
|
||||
from .models import SiteSyncModel
|
||||
|
||||
|
||||
class SceneInventoryController:
|
||||
"""This is a temporary controller for AYON.
|
||||
|
||||
Goal of this temporary controller is to provide a way to get current
|
||||
context instead of using 'AvalonMongoDB' object (or 'legacy_io').
|
||||
|
||||
Also provides (hopefully) cleaner api for site sync.
|
||||
"""
|
||||
|
||||
def __init__(self, host=None):
|
||||
if host is None:
|
||||
host = registered_host()
|
||||
self._host = host
|
||||
self._current_context = None
|
||||
self._current_project = None
|
||||
self._current_folder_id = None
|
||||
self._current_folder_set = False
|
||||
|
||||
self._site_sync_model = SiteSyncModel(self)
|
||||
# Switch dialog requirements
|
||||
self._hierarchy_model = HierarchyModel(self)
|
||||
self._event_system = self._create_event_system()
|
||||
|
||||
def emit_event(self, topic, data=None, source=None):
|
||||
if data is None:
|
||||
data = {}
|
||||
self._event_system.emit(topic, data, source)
|
||||
|
||||
def register_event_callback(self, topic, callback):
|
||||
self._event_system.add_callback(topic, callback)
|
||||
|
||||
def reset(self):
|
||||
self._current_context = None
|
||||
self._current_project = None
|
||||
self._current_folder_id = None
|
||||
self._current_folder_set = False
|
||||
|
||||
self._site_sync_model.reset()
|
||||
self._hierarchy_model.reset()
|
||||
|
||||
def get_current_context(self):
|
||||
if self._current_context is None:
|
||||
if hasattr(self._host, "get_current_context"):
|
||||
self._current_context = self._host.get_current_context()
|
||||
else:
|
||||
self._current_context = get_current_context()
|
||||
return self._current_context
|
||||
|
||||
def get_current_project_name(self):
|
||||
if self._current_project is None:
|
||||
self._current_project = self.get_current_context()["project_name"]
|
||||
return self._current_project
|
||||
|
||||
def get_current_folder_id(self):
|
||||
if self._current_folder_set:
|
||||
return self._current_folder_id
|
||||
|
||||
context = self.get_current_context()
|
||||
project_name = context["project_name"]
|
||||
folder_path = context.get("folder_path")
|
||||
folder_name = context.get("asset_name")
|
||||
folder_id = None
|
||||
if folder_path:
|
||||
folder = ayon_api.get_folder_by_path(project_name, folder_path)
|
||||
if folder:
|
||||
folder_id = folder["id"]
|
||||
elif folder_name:
|
||||
for folder in ayon_api.get_folders(
|
||||
project_name, folder_names=[folder_name]
|
||||
):
|
||||
folder_id = folder["id"]
|
||||
break
|
||||
|
||||
self._current_folder_id = folder_id
|
||||
self._current_folder_set = True
|
||||
return self._current_folder_id
|
||||
|
||||
def get_containers(self):
|
||||
host = self._host
|
||||
if isinstance(host, ILoadHost):
|
||||
return host.get_containers()
|
||||
elif hasattr(host, "ls"):
|
||||
return host.ls()
|
||||
return []
|
||||
|
||||
# Site Sync methods
|
||||
def is_sync_server_enabled(self):
|
||||
return self._site_sync_model.is_sync_server_enabled()
|
||||
|
||||
def get_sites_information(self):
|
||||
return self._site_sync_model.get_sites_information()
|
||||
|
||||
def get_site_provider_icons(self):
|
||||
return self._site_sync_model.get_site_provider_icons()
|
||||
|
||||
def get_representations_site_progress(self, representation_ids):
|
||||
return self._site_sync_model.get_representations_site_progress(
|
||||
representation_ids
|
||||
)
|
||||
|
||||
def resync_representations(self, representation_ids, site_type):
|
||||
return self._site_sync_model.resync_representations(
|
||||
representation_ids, site_type
|
||||
)
|
||||
|
||||
# Switch dialog methods
|
||||
def get_folder_items(self, project_name, sender=None):
|
||||
return self._hierarchy_model.get_folder_items(project_name, sender)
|
||||
|
||||
def get_folder_label(self, folder_id):
|
||||
if not folder_id:
|
||||
return None
|
||||
project_name = self.get_current_project_name()
|
||||
folder_item = self._hierarchy_model.get_folder_item(
|
||||
project_name, folder_id)
|
||||
if folder_item is None:
|
||||
return None
|
||||
return folder_item.label
|
||||
|
||||
def _create_event_system(self):
|
||||
return QueuedEventSystem()
|
||||
622
openpype/tools/ayon_sceneinventory/model.py
Normal file
622
openpype/tools/ayon_sceneinventory/model.py
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
import collections
|
||||
import re
|
||||
import logging
|
||||
import uuid
|
||||
import copy
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from qtpy import QtCore, QtGui
|
||||
import qtawesome
|
||||
|
||||
from openpype.client import (
|
||||
get_assets,
|
||||
get_subsets,
|
||||
get_versions,
|
||||
get_last_version_by_subset_id,
|
||||
get_representations,
|
||||
)
|
||||
from openpype.pipeline import (
|
||||
get_current_project_name,
|
||||
schema,
|
||||
HeroVersionType,
|
||||
)
|
||||
from openpype.style import get_default_entity_icon_color
|
||||
from openpype.tools.utils.models import TreeModel, Item
|
||||
|
||||
|
||||
def walk_hierarchy(node):
|
||||
"""Recursively yield group node."""
|
||||
for child in node.children():
|
||||
if child.get("isGroupNode"):
|
||||
yield child
|
||||
|
||||
for _child in walk_hierarchy(child):
|
||||
yield _child
|
||||
|
||||
|
||||
class InventoryModel(TreeModel):
|
||||
"""The model for the inventory"""
|
||||
|
||||
Columns = [
|
||||
"Name",
|
||||
"version",
|
||||
"count",
|
||||
"family",
|
||||
"group",
|
||||
"loader",
|
||||
"objectName",
|
||||
"active_site",
|
||||
"remote_site",
|
||||
]
|
||||
active_site_col = Columns.index("active_site")
|
||||
remote_site_col = Columns.index("remote_site")
|
||||
|
||||
OUTDATED_COLOR = QtGui.QColor(235, 30, 30)
|
||||
CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30)
|
||||
GRAYOUT_COLOR = QtGui.QColor(160, 160, 160)
|
||||
|
||||
UniqueRole = QtCore.Qt.UserRole + 2 # unique label role
|
||||
|
||||
def __init__(self, controller, parent=None):
|
||||
super(InventoryModel, self).__init__(parent)
|
||||
self.log = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
self._controller = controller
|
||||
|
||||
self._hierarchy_view = False
|
||||
|
||||
self._default_icon_color = get_default_entity_icon_color()
|
||||
|
||||
site_icons = self._controller.get_site_provider_icons()
|
||||
|
||||
self._site_icons = {
|
||||
provider: QtGui.QIcon(icon_path)
|
||||
for provider, icon_path in site_icons.items()
|
||||
}
|
||||
|
||||
def outdated(self, item):
|
||||
value = item.get("version")
|
||||
if isinstance(value, HeroVersionType):
|
||||
return False
|
||||
|
||||
if item.get("version") == item.get("highest_version"):
|
||||
return False
|
||||
return True
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
item = index.internalPointer()
|
||||
|
||||
if role == QtCore.Qt.FontRole:
|
||||
# Make top-level entries bold
|
||||
if item.get("isGroupNode") or item.get("isNotSet"): # group-item
|
||||
font = QtGui.QFont()
|
||||
font.setBold(True)
|
||||
return font
|
||||
|
||||
if role == QtCore.Qt.ForegroundRole:
|
||||
# Set the text color to the OUTDATED_COLOR when the
|
||||
# collected version is not the same as the highest version
|
||||
key = self.Columns[index.column()]
|
||||
if key == "version": # version
|
||||
if item.get("isGroupNode"): # group-item
|
||||
if self.outdated(item):
|
||||
return self.OUTDATED_COLOR
|
||||
|
||||
if self._hierarchy_view:
|
||||
# If current group is not outdated, check if any
|
||||
# outdated children.
|
||||
for _node in walk_hierarchy(item):
|
||||
if self.outdated(_node):
|
||||
return self.CHILD_OUTDATED_COLOR
|
||||
else:
|
||||
|
||||
if self._hierarchy_view:
|
||||
# Although this is not a group item, we still need
|
||||
# to distinguish which one contain outdated child.
|
||||
for _node in walk_hierarchy(item):
|
||||
if self.outdated(_node):
|
||||
return self.CHILD_OUTDATED_COLOR.darker(150)
|
||||
|
||||
return self.GRAYOUT_COLOR
|
||||
|
||||
if key == "Name" and not item.get("isGroupNode"):
|
||||
return self.GRAYOUT_COLOR
|
||||
|
||||
# Add icons
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
if index.column() == 0:
|
||||
# Override color
|
||||
color = item.get("color", self._default_icon_color)
|
||||
if item.get("isGroupNode"): # group-item
|
||||
return qtawesome.icon("fa.folder", color=color)
|
||||
if item.get("isNotSet"):
|
||||
return qtawesome.icon("fa.exclamation-circle", color=color)
|
||||
|
||||
return qtawesome.icon("fa.file-o", color=color)
|
||||
|
||||
if index.column() == 3:
|
||||
# Family icon
|
||||
return item.get("familyIcon", None)
|
||||
|
||||
column_name = self.Columns[index.column()]
|
||||
|
||||
if column_name == "group" and item.get("group"):
|
||||
return qtawesome.icon("fa.object-group",
|
||||
color=get_default_entity_icon_color())
|
||||
|
||||
if item.get("isGroupNode"):
|
||||
if column_name == "active_site":
|
||||
provider = item.get("active_site_provider")
|
||||
return self._site_icons.get(provider)
|
||||
|
||||
if column_name == "remote_site":
|
||||
provider = item.get("remote_site_provider")
|
||||
return self._site_icons.get(provider)
|
||||
|
||||
if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"):
|
||||
column_name = self.Columns[index.column()]
|
||||
progress = None
|
||||
if column_name == "active_site":
|
||||
progress = item.get("active_site_progress", 0)
|
||||
elif column_name == "remote_site":
|
||||
progress = item.get("remote_site_progress", 0)
|
||||
if progress is not None:
|
||||
return "{}%".format(max(progress, 0) * 100)
|
||||
|
||||
if role == self.UniqueRole:
|
||||
return item["representation"] + item.get("objectName", "<none>")
|
||||
|
||||
return super(InventoryModel, self).data(index, role)
|
||||
|
||||
def set_hierarchy_view(self, state):
|
||||
"""Set whether to display subsets in hierarchy view."""
|
||||
state = bool(state)
|
||||
|
||||
if state != self._hierarchy_view:
|
||||
self._hierarchy_view = state
|
||||
|
||||
def refresh(self, selected=None, containers=None):
|
||||
"""Refresh the model"""
|
||||
|
||||
# for debugging or testing, injecting items from outside
|
||||
if containers is None:
|
||||
containers = self._controller.get_containers()
|
||||
|
||||
self.clear()
|
||||
if not selected or not self._hierarchy_view:
|
||||
self._add_containers(containers)
|
||||
return
|
||||
|
||||
# Filter by cherry-picked items
|
||||
self._add_containers((
|
||||
container
|
||||
for container in containers
|
||||
if container["objectName"] in selected
|
||||
))
|
||||
|
||||
def _add_containers(self, containers, parent=None):
|
||||
"""Add the items to the model.
|
||||
|
||||
The items should be formatted similar to `api.ls()` returns, an item
|
||||
is then represented as:
|
||||
{"filename_v001.ma": [full/filename/of/loaded/filename_v001.ma,
|
||||
full/filename/of/loaded/filename_v001.ma],
|
||||
"nodetype" : "reference",
|
||||
"node": "referenceNode1"}
|
||||
|
||||
Note: When performing an additional call to `add_items` it will *not*
|
||||
group the new items with previously existing item groups of the
|
||||
same type.
|
||||
|
||||
Args:
|
||||
containers (generator): Container items.
|
||||
parent (Item, optional): Set this item as parent for the added
|
||||
items when provided. Defaults to the root of the model.
|
||||
|
||||
Returns:
|
||||
node.Item: root node which has children added based on the data
|
||||
"""
|
||||
|
||||
project_name = get_current_project_name()
|
||||
|
||||
self.beginResetModel()
|
||||
|
||||
# Group by representation
|
||||
grouped = defaultdict(lambda: {"containers": list()})
|
||||
for container in containers:
|
||||
repre_id = container["representation"]
|
||||
grouped[repre_id]["containers"].append(container)
|
||||
|
||||
(
|
||||
repres_by_id,
|
||||
versions_by_id,
|
||||
products_by_id,
|
||||
folders_by_id,
|
||||
) = self._query_entities(project_name, set(grouped.keys()))
|
||||
# Add to model
|
||||
not_found = defaultdict(list)
|
||||
not_found_ids = []
|
||||
for repre_id, group_dict in sorted(grouped.items()):
|
||||
group_containers = group_dict["containers"]
|
||||
representation = repres_by_id.get(repre_id)
|
||||
if not representation:
|
||||
not_found["representation"].extend(group_containers)
|
||||
not_found_ids.append(repre_id)
|
||||
continue
|
||||
|
||||
version = versions_by_id.get(representation["parent"])
|
||||
if not version:
|
||||
not_found["version"].extend(group_containers)
|
||||
not_found_ids.append(repre_id)
|
||||
continue
|
||||
|
||||
product = products_by_id.get(version["parent"])
|
||||
if not product:
|
||||
not_found["product"].extend(group_containers)
|
||||
not_found_ids.append(repre_id)
|
||||
continue
|
||||
|
||||
folder = folders_by_id.get(product["parent"])
|
||||
if not folder:
|
||||
not_found["folder"].extend(group_containers)
|
||||
not_found_ids.append(repre_id)
|
||||
continue
|
||||
|
||||
group_dict.update({
|
||||
"representation": representation,
|
||||
"version": version,
|
||||
"subset": product,
|
||||
"asset": folder
|
||||
})
|
||||
|
||||
for _repre_id in not_found_ids:
|
||||
grouped.pop(_repre_id)
|
||||
|
||||
for where, group_containers in not_found.items():
|
||||
# create the group header
|
||||
group_node = Item()
|
||||
name = "< NOT FOUND - {} >".format(where)
|
||||
group_node["Name"] = name
|
||||
group_node["representation"] = name
|
||||
group_node["count"] = len(group_containers)
|
||||
group_node["isGroupNode"] = False
|
||||
group_node["isNotSet"] = True
|
||||
|
||||
self.add_child(group_node, parent=parent)
|
||||
|
||||
for container in group_containers:
|
||||
item_node = Item()
|
||||
item_node.update(container)
|
||||
item_node["Name"] = container.get("objectName", "NO NAME")
|
||||
item_node["isNotFound"] = True
|
||||
self.add_child(item_node, parent=group_node)
|
||||
|
||||
# TODO Use product icons
|
||||
family_icon = qtawesome.icon(
|
||||
"fa.folder", color="#0091B2"
|
||||
)
|
||||
# Prepare site sync specific data
|
||||
progress_by_id = self._controller.get_representations_site_progress(
|
||||
set(grouped.keys())
|
||||
)
|
||||
sites_info = self._controller.get_sites_information()
|
||||
|
||||
for repre_id, group_dict in sorted(grouped.items()):
|
||||
group_containers = group_dict["containers"]
|
||||
representation = group_dict["representation"]
|
||||
version = group_dict["version"]
|
||||
subset = group_dict["subset"]
|
||||
asset = group_dict["asset"]
|
||||
|
||||
# Get the primary family
|
||||
maj_version, _ = schema.get_schema_version(subset["schema"])
|
||||
if maj_version < 3:
|
||||
src_doc = version
|
||||
else:
|
||||
src_doc = subset
|
||||
|
||||
prim_family = src_doc["data"].get("family")
|
||||
if not prim_family:
|
||||
families = src_doc["data"].get("families")
|
||||
if families:
|
||||
prim_family = families[0]
|
||||
|
||||
# Store the highest available version so the model can know
|
||||
# whether current version is currently up-to-date.
|
||||
highest_version = get_last_version_by_subset_id(
|
||||
project_name, version["parent"]
|
||||
)
|
||||
|
||||
# create the group header
|
||||
group_node = Item()
|
||||
group_node["Name"] = "{}_{}: ({})".format(
|
||||
asset["name"], subset["name"], representation["name"]
|
||||
)
|
||||
group_node["representation"] = repre_id
|
||||
group_node["version"] = version["name"]
|
||||
group_node["highest_version"] = highest_version["name"]
|
||||
group_node["family"] = prim_family or ""
|
||||
group_node["familyIcon"] = family_icon
|
||||
group_node["count"] = len(group_containers)
|
||||
group_node["isGroupNode"] = True
|
||||
group_node["group"] = subset["data"].get("subsetGroup")
|
||||
|
||||
# Site sync specific data
|
||||
progress = progress_by_id[repre_id]
|
||||
group_node.update(sites_info)
|
||||
group_node["active_site_progress"] = progress["active_site"]
|
||||
group_node["remote_site_progress"] = progress["remote_site"]
|
||||
|
||||
self.add_child(group_node, parent=parent)
|
||||
|
||||
for container in group_containers:
|
||||
item_node = Item()
|
||||
item_node.update(container)
|
||||
|
||||
# store the current version on the item
|
||||
item_node["version"] = version["name"]
|
||||
|
||||
# Remapping namespace to item name.
|
||||
# Noted that the name key is capital "N", by doing this, we
|
||||
# can view namespace in GUI without changing container data.
|
||||
item_node["Name"] = container["namespace"]
|
||||
|
||||
self.add_child(item_node, parent=group_node)
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
return self._root_item
|
||||
|
||||
def _query_entities(self, project_name, repre_ids):
|
||||
"""Query entities for representations from containers.
|
||||
|
||||
Returns:
|
||||
tuple[dict, dict, dict, dict]: Representation, version, product
|
||||
and folder documents by id.
|
||||
"""
|
||||
|
||||
repres_by_id = {}
|
||||
versions_by_id = {}
|
||||
products_by_id = {}
|
||||
folders_by_id = {}
|
||||
output = (
|
||||
repres_by_id,
|
||||
versions_by_id,
|
||||
products_by_id,
|
||||
folders_by_id,
|
||||
)
|
||||
|
||||
filtered_repre_ids = set()
|
||||
for repre_id in repre_ids:
|
||||
# Filter out invalid representation ids
|
||||
# NOTE: This is added because scenes from OpenPype did contain
|
||||
# ObjectId from mongo.
|
||||
try:
|
||||
uuid.UUID(repre_id)
|
||||
filtered_repre_ids.add(repre_id)
|
||||
except ValueError:
|
||||
continue
|
||||
if not filtered_repre_ids:
|
||||
return output
|
||||
|
||||
repre_docs = get_representations(project_name, repre_ids)
|
||||
repres_by_id.update({
|
||||
repre_doc["_id"]: repre_doc
|
||||
for repre_doc in repre_docs
|
||||
})
|
||||
version_ids = {
|
||||
repre_doc["parent"] for repre_doc in repres_by_id.values()
|
||||
}
|
||||
if not version_ids:
|
||||
return output
|
||||
|
||||
version_docs = get_versions(project_name, version_ids, hero=True)
|
||||
versions_by_id.update({
|
||||
version_doc["_id"]: version_doc
|
||||
for version_doc in version_docs
|
||||
})
|
||||
hero_versions_by_subversion_id = collections.defaultdict(list)
|
||||
for version_doc in versions_by_id.values():
|
||||
if version_doc["type"] != "hero_version":
|
||||
continue
|
||||
subversion = version_doc["version_id"]
|
||||
hero_versions_by_subversion_id[subversion].append(version_doc)
|
||||
|
||||
if hero_versions_by_subversion_id:
|
||||
subversion_ids = set(
|
||||
hero_versions_by_subversion_id.keys()
|
||||
)
|
||||
subversion_docs = get_versions(project_name, subversion_ids)
|
||||
for subversion_doc in subversion_docs:
|
||||
subversion_id = subversion_doc["_id"]
|
||||
subversion_ids.discard(subversion_id)
|
||||
h_version_docs = hero_versions_by_subversion_id[subversion_id]
|
||||
for version_doc in h_version_docs:
|
||||
version_doc["name"] = HeroVersionType(
|
||||
subversion_doc["name"]
|
||||
)
|
||||
version_doc["data"] = copy.deepcopy(
|
||||
subversion_doc["data"]
|
||||
)
|
||||
|
||||
for subversion_id in subversion_ids:
|
||||
h_version_docs = hero_versions_by_subversion_id[subversion_id]
|
||||
for version_doc in h_version_docs:
|
||||
versions_by_id.pop(version_doc["_id"])
|
||||
|
||||
product_ids = {
|
||||
version_doc["parent"]
|
||||
for version_doc in versions_by_id.values()
|
||||
}
|
||||
if not product_ids:
|
||||
return output
|
||||
product_docs = get_subsets(project_name, product_ids)
|
||||
products_by_id.update({
|
||||
product_doc["_id"]: product_doc
|
||||
for product_doc in product_docs
|
||||
})
|
||||
folder_ids = {
|
||||
product_doc["parent"]
|
||||
for product_doc in products_by_id.values()
|
||||
}
|
||||
if not folder_ids:
|
||||
return output
|
||||
|
||||
folder_docs = get_assets(project_name, folder_ids)
|
||||
folders_by_id.update({
|
||||
folder_doc["_id"]: folder_doc
|
||||
for folder_doc in folder_docs
|
||||
})
|
||||
return output
|
||||
|
||||
|
||||
class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""Filter model to where key column's value is in the filtered tags"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FilterProxyModel, self).__init__(*args, **kwargs)
|
||||
self._filter_outdated = False
|
||||
self._hierarchy_view = False
|
||||
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
model = self.sourceModel()
|
||||
source_index = model.index(row, self.filterKeyColumn(), parent)
|
||||
|
||||
# Always allow bottom entries (individual containers), since their
|
||||
# parent group hidden if it wouldn't have been validated.
|
||||
rows = model.rowCount(source_index)
|
||||
if not rows:
|
||||
return True
|
||||
|
||||
# Filter by regex
|
||||
if hasattr(self, "filterRegExp"):
|
||||
regex = self.filterRegExp()
|
||||
else:
|
||||
regex = self.filterRegularExpression()
|
||||
pattern = regex.pattern()
|
||||
if pattern:
|
||||
pattern = re.escape(pattern)
|
||||
|
||||
if not self._matches(row, parent, pattern):
|
||||
return False
|
||||
|
||||
if self._filter_outdated:
|
||||
# When filtering to outdated we filter the up to date entries
|
||||
# thus we "allow" them when they are outdated
|
||||
if not self._is_outdated(row, parent):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def set_filter_outdated(self, state):
|
||||
"""Set whether to show the outdated entries only."""
|
||||
state = bool(state)
|
||||
|
||||
if state != self._filter_outdated:
|
||||
self._filter_outdated = bool(state)
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_hierarchy_view(self, state):
|
||||
state = bool(state)
|
||||
|
||||
if state != self._hierarchy_view:
|
||||
self._hierarchy_view = state
|
||||
|
||||
def _is_outdated(self, row, parent):
|
||||
"""Return whether row is outdated.
|
||||
|
||||
A row is considered outdated if it has "version" and "highest_version"
|
||||
data and in the internal data structure, and they are not of an
|
||||
equal value.
|
||||
|
||||
"""
|
||||
def outdated(node):
|
||||
version = node.get("version", None)
|
||||
highest = node.get("highest_version", None)
|
||||
|
||||
# Always allow indices that have no version data at all
|
||||
if version is None and highest is None:
|
||||
return True
|
||||
|
||||
# If either a version or highest is present but not the other
|
||||
# consider the item invalid.
|
||||
if not self._hierarchy_view:
|
||||
# Skip this check if in hierarchy view, or the child item
|
||||
# node will be hidden even it's actually outdated.
|
||||
if version is None or highest is None:
|
||||
return False
|
||||
return version != highest
|
||||
|
||||
index = self.sourceModel().index(row, self.filterKeyColumn(), parent)
|
||||
|
||||
# The scene contents are grouped by "representation", e.g. the same
|
||||
# "representation" loaded twice is grouped under the same header.
|
||||
# Since the version check filters these parent groups we skip that
|
||||
# check for the individual children.
|
||||
has_parent = index.parent().isValid()
|
||||
if has_parent and not self._hierarchy_view:
|
||||
return True
|
||||
|
||||
# Filter to those that have the different version numbers
|
||||
node = index.internalPointer()
|
||||
if outdated(node):
|
||||
return True
|
||||
|
||||
if self._hierarchy_view:
|
||||
for _node in walk_hierarchy(node):
|
||||
if outdated(_node):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _matches(self, row, parent, pattern):
|
||||
"""Return whether row matches regex pattern.
|
||||
|
||||
Args:
|
||||
row (int): row number in model
|
||||
parent (QtCore.QModelIndex): parent index
|
||||
pattern (regex.pattern): pattern to check for in key
|
||||
|
||||
Returns:
|
||||
bool
|
||||
|
||||
"""
|
||||
model = self.sourceModel()
|
||||
column = self.filterKeyColumn()
|
||||
role = self.filterRole()
|
||||
|
||||
def matches(row, parent, pattern):
|
||||
index = model.index(row, column, parent)
|
||||
key = model.data(index, role)
|
||||
if re.search(pattern, key, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
if matches(row, parent, pattern):
|
||||
return True
|
||||
|
||||
# Also allow if any of the children matches
|
||||
source_index = model.index(row, column, parent)
|
||||
rows = model.rowCount(source_index)
|
||||
|
||||
if any(
|
||||
matches(idx, source_index, pattern)
|
||||
for idx in range(rows)
|
||||
):
|
||||
return True
|
||||
|
||||
if not self._hierarchy_view:
|
||||
return False
|
||||
|
||||
for idx in range(rows):
|
||||
child_index = model.index(idx, column, source_index)
|
||||
child_rows = model.rowCount(child_index)
|
||||
return any(
|
||||
self._matches(child_idx, child_index, pattern)
|
||||
for child_idx in range(child_rows)
|
||||
)
|
||||
|
||||
return True
|
||||
6
openpype/tools/ayon_sceneinventory/models/__init__.py
Normal file
6
openpype/tools/ayon_sceneinventory/models/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from .site_sync import SiteSyncModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SiteSyncModel",
|
||||
)
|
||||
176
openpype/tools/ayon_sceneinventory/models/site_sync.py
Normal file
176
openpype/tools/ayon_sceneinventory/models/site_sync.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
from openpype.client import get_representations
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
NOT_SET = object()
|
||||
|
||||
|
||||
class SiteSyncModel:
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
|
||||
self._sync_server_module = NOT_SET
|
||||
self._sync_server_enabled = None
|
||||
self._active_site = NOT_SET
|
||||
self._remote_site = NOT_SET
|
||||
self._active_site_provider = NOT_SET
|
||||
self._remote_site_provider = NOT_SET
|
||||
|
||||
def reset(self):
|
||||
self._sync_server_module = NOT_SET
|
||||
self._sync_server_enabled = None
|
||||
self._active_site = NOT_SET
|
||||
self._remote_site = NOT_SET
|
||||
self._active_site_provider = NOT_SET
|
||||
self._remote_site_provider = NOT_SET
|
||||
|
||||
def is_sync_server_enabled(self):
|
||||
"""Site sync is enabled.
|
||||
|
||||
Returns:
|
||||
bool: Is enabled or not.
|
||||
"""
|
||||
|
||||
self._cache_sync_server_module()
|
||||
return self._sync_server_enabled
|
||||
|
||||
def get_site_provider_icons(self):
|
||||
"""Icon paths per provider.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Path by provider name.
|
||||
"""
|
||||
|
||||
site_sync = self._get_sync_server_module()
|
||||
if site_sync is None:
|
||||
return {}
|
||||
return site_sync.get_site_icons()
|
||||
|
||||
def get_sites_information(self):
|
||||
return {
|
||||
"active_site": self._get_active_site(),
|
||||
"active_site_provider": self._get_active_site_provider(),
|
||||
"remote_site": self._get_remote_site(),
|
||||
"remote_site_provider": self._get_remote_site_provider()
|
||||
}
|
||||
|
||||
def get_representations_site_progress(self, representation_ids):
|
||||
"""Get progress of representations sync."""
|
||||
|
||||
representation_ids = set(representation_ids)
|
||||
output = {
|
||||
repre_id: {
|
||||
"active_site": 0,
|
||||
"remote_site": 0,
|
||||
}
|
||||
for repre_id in representation_ids
|
||||
}
|
||||
if not self.is_sync_server_enabled():
|
||||
return output
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
site_sync = self._get_sync_server_module()
|
||||
repre_docs = get_representations(project_name, representation_ids)
|
||||
active_site = self._get_active_site()
|
||||
remote_site = self._get_remote_site()
|
||||
|
||||
for repre_doc in repre_docs:
|
||||
repre_output = output[repre_doc["_id"]]
|
||||
result = site_sync.get_progress_for_repre(
|
||||
repre_doc, active_site, remote_site
|
||||
)
|
||||
repre_output["active_site"] = result[active_site]
|
||||
repre_output["remote_site"] = result[remote_site]
|
||||
|
||||
return output
|
||||
|
||||
def resync_representations(self, representation_ids, site_type):
|
||||
"""
|
||||
|
||||
Args:
|
||||
representation_ids (Iterable[str]): Representation ids.
|
||||
site_type (Literal[active_site, remote_site]): Site type.
|
||||
"""
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
site_sync = self._get_sync_server_module()
|
||||
active_site = self._get_active_site()
|
||||
remote_site = self._get_remote_site()
|
||||
progress = self.get_representations_site_progress(
|
||||
representation_ids
|
||||
)
|
||||
for repre_id in representation_ids:
|
||||
repre_progress = progress.get(repre_id)
|
||||
if not repre_progress:
|
||||
continue
|
||||
|
||||
if site_type == "active_site":
|
||||
# check opposite from added site, must be 1 or unable to sync
|
||||
check_progress = repre_progress["remote_site"]
|
||||
site = active_site
|
||||
else:
|
||||
check_progress = repre_progress["active_site"]
|
||||
site = remote_site
|
||||
|
||||
if check_progress == 1:
|
||||
site_sync.add_site(
|
||||
project_name, repre_id, site, force=True
|
||||
)
|
||||
|
||||
def _get_sync_server_module(self):
|
||||
self._cache_sync_server_module()
|
||||
return self._sync_server_module
|
||||
|
||||
def _cache_sync_server_module(self):
|
||||
if self._sync_server_module is not NOT_SET:
|
||||
return self._sync_server_module
|
||||
manager = ModulesManager()
|
||||
site_sync = manager.modules_by_name.get("sync_server")
|
||||
sync_enabled = site_sync is not None and site_sync.enabled
|
||||
self._sync_server_module = site_sync
|
||||
self._sync_server_enabled = sync_enabled
|
||||
|
||||
def _get_active_site(self):
|
||||
if self._active_site is NOT_SET:
|
||||
self._cache_sites()
|
||||
return self._active_site
|
||||
|
||||
def _get_remote_site(self):
|
||||
if self._remote_site is NOT_SET:
|
||||
self._cache_sites()
|
||||
return self._remote_site
|
||||
|
||||
def _get_active_site_provider(self):
|
||||
if self._active_site_provider is NOT_SET:
|
||||
self._cache_sites()
|
||||
return self._active_site_provider
|
||||
|
||||
def _get_remote_site_provider(self):
|
||||
if self._remote_site_provider is NOT_SET:
|
||||
self._cache_sites()
|
||||
return self._remote_site_provider
|
||||
|
||||
def _cache_sites(self):
|
||||
site_sync = self._get_sync_server_module()
|
||||
active_site = None
|
||||
remote_site = None
|
||||
active_site_provider = None
|
||||
remote_site_provider = None
|
||||
if site_sync is not None:
|
||||
project_name = self._controller.get_current_project_name()
|
||||
active_site = site_sync.get_active_site(project_name)
|
||||
remote_site = site_sync.get_remote_site(project_name)
|
||||
active_site_provider = "studio"
|
||||
remote_site_provider = "studio"
|
||||
if active_site != "studio":
|
||||
active_site_provider = site_sync.get_active_provider(
|
||||
project_name, active_site
|
||||
)
|
||||
if remote_site != "studio":
|
||||
remote_site_provider = site_sync.get_active_provider(
|
||||
project_name, remote_site
|
||||
)
|
||||
|
||||
self._active_site = active_site
|
||||
self._remote_site = remote_site
|
||||
self._active_site_provider = active_site_provider
|
||||
self._remote_site_provider = remote_site_provider
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from .dialog import SwitchAssetDialog
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SwitchAssetDialog",
|
||||
)
|
||||
1333
openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
Normal file
1333
openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,307 @@
|
|||
from qtpy import QtWidgets, QtCore
|
||||
import qtawesome
|
||||
|
||||
from openpype.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
BaseClickableFrame,
|
||||
set_style_property,
|
||||
)
|
||||
from openpype.tools.ayon_utils.widgets import FoldersWidget
|
||||
|
||||
NOT_SET = object()
|
||||
|
||||
|
||||
class ClickableLineEdit(QtWidgets.QLineEdit):
|
||||
"""QLineEdit capturing left mouse click.
|
||||
|
||||
Triggers `clicked` signal on mouse click.
|
||||
"""
|
||||
clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ClickableLineEdit, self).__init__(*args, **kwargs)
|
||||
self.setReadOnly(True)
|
||||
self._mouse_pressed = False
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self._mouse_pressed = True
|
||||
event.accept()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
event.accept()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self._mouse_pressed:
|
||||
self._mouse_pressed = False
|
||||
if self.rect().contains(event.pos()):
|
||||
self.clicked.emit()
|
||||
event.accept()
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
event.accept()
|
||||
|
||||
|
||||
class ControllerWrap:
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
self._selected_folder_id = None
|
||||
|
||||
def emit_event(self, *args, **kwargs):
|
||||
self._controller.emit_event(*args, **kwargs)
|
||||
|
||||
def register_event_callback(self, *args, **kwargs):
|
||||
self._controller.register_event_callback(*args, **kwargs)
|
||||
|
||||
def get_current_project_name(self):
|
||||
return self._controller.get_current_project_name()
|
||||
|
||||
def get_folder_items(self, *args, **kwargs):
|
||||
return self._controller.get_folder_items(*args, **kwargs)
|
||||
|
||||
def set_selected_folder(self, folder_id):
|
||||
self._selected_folder_id = folder_id
|
||||
|
||||
def get_selected_folder_id(self):
|
||||
return self._selected_folder_id
|
||||
|
||||
|
||||
class FoldersDialog(QtWidgets.QDialog):
|
||||
"""Dialog to select asset for a context of instance."""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(FoldersDialog, self).__init__(parent)
|
||||
self.setWindowTitle("Select folder")
|
||||
|
||||
filter_input = PlaceholderLineEdit(self)
|
||||
filter_input.setPlaceholderText("Filter folders..")
|
||||
|
||||
controller_wrap = ControllerWrap(controller)
|
||||
folders_widget = FoldersWidget(controller_wrap, self)
|
||||
folders_widget.set_deselectable(True)
|
||||
|
||||
ok_btn = QtWidgets.QPushButton("OK", self)
|
||||
cancel_btn = QtWidgets.QPushButton("Cancel", self)
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout()
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(ok_btn)
|
||||
btns_layout.addWidget(cancel_btn)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(filter_input, 0)
|
||||
layout.addWidget(folders_widget, 1)
|
||||
layout.addLayout(btns_layout, 0)
|
||||
|
||||
folders_widget.double_clicked.connect(self._on_ok_clicked)
|
||||
folders_widget.refreshed.connect(self._on_folders_refresh)
|
||||
filter_input.textChanged.connect(self._on_filter_change)
|
||||
ok_btn.clicked.connect(self._on_ok_clicked)
|
||||
cancel_btn.clicked.connect(self._on_cancel_clicked)
|
||||
|
||||
self._filter_input = filter_input
|
||||
self._ok_btn = ok_btn
|
||||
self._cancel_btn = cancel_btn
|
||||
|
||||
self._folders_widget = folders_widget
|
||||
self._controller_wrap = controller_wrap
|
||||
|
||||
# Set selected folder only when user confirms the dialog
|
||||
self._selected_folder_id = None
|
||||
self._selected_folder_label = None
|
||||
|
||||
self._folder_id_to_select = NOT_SET
|
||||
|
||||
self._first_show = True
|
||||
self._default_height = 500
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Refresh asset model on show."""
|
||||
super(FoldersDialog, self).showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self._on_first_show()
|
||||
|
||||
def refresh(self):
|
||||
project_name = self._controller_wrap.get_current_project_name()
|
||||
self._folders_widget.set_project_name(project_name)
|
||||
|
||||
def _on_first_show(self):
|
||||
center = self.rect().center()
|
||||
size = self.size()
|
||||
size.setHeight(self._default_height)
|
||||
|
||||
self.resize(size)
|
||||
new_pos = self.mapToGlobal(center)
|
||||
new_pos.setX(new_pos.x() - int(self.width() / 2))
|
||||
new_pos.setY(new_pos.y() - int(self.height() / 2))
|
||||
self.move(new_pos)
|
||||
|
||||
def _on_folders_refresh(self):
|
||||
if self._folder_id_to_select is NOT_SET:
|
||||
return
|
||||
self._folders_widget.set_selected_folder(self._folder_id_to_select)
|
||||
self._folder_id_to_select = NOT_SET
|
||||
|
||||
def _on_filter_change(self, text):
|
||||
"""Trigger change of filter of folders."""
|
||||
|
||||
self._folders_widget.set_name_filter(text)
|
||||
|
||||
def _on_cancel_clicked(self):
|
||||
self.done(0)
|
||||
|
||||
def _on_ok_clicked(self):
|
||||
self._selected_folder_id = (
|
||||
self._folders_widget.get_selected_folder_id()
|
||||
)
|
||||
self._selected_folder_label = (
|
||||
self._folders_widget.get_selected_folder_label()
|
||||
)
|
||||
self.done(1)
|
||||
|
||||
def set_selected_folder(self, folder_id):
|
||||
"""Change preselected folder before showing the dialog.
|
||||
|
||||
This also resets model and clean filter.
|
||||
"""
|
||||
|
||||
if (
|
||||
self._folders_widget.is_refreshing
|
||||
or self._folders_widget.get_project_name() is None
|
||||
):
|
||||
self._folder_id_to_select = folder_id
|
||||
else:
|
||||
self._folders_widget.set_selected_folder(folder_id)
|
||||
|
||||
def get_selected_folder_id(self):
|
||||
"""Get selected folder id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Selected folder id or None if nothing
|
||||
is selected.
|
||||
"""
|
||||
return self._selected_folder_id
|
||||
|
||||
def get_selected_folder_label(self):
|
||||
return self._selected_folder_label
|
||||
|
||||
|
||||
class FoldersField(BaseClickableFrame):
|
||||
"""Field where asset name of selected instance/s is showed.
|
||||
|
||||
Click on the field will trigger `FoldersDialog`.
|
||||
"""
|
||||
value_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(FoldersField, self).__init__(parent)
|
||||
self.setObjectName("AssetNameInputWidget")
|
||||
|
||||
# Don't use 'self' for parent!
|
||||
# - this widget has specific styles
|
||||
dialog = FoldersDialog(controller, parent)
|
||||
|
||||
name_input = ClickableLineEdit(self)
|
||||
name_input.setObjectName("AssetNameInput")
|
||||
|
||||
icon = qtawesome.icon("fa.window-maximize", color="white")
|
||||
icon_btn = QtWidgets.QPushButton(self)
|
||||
icon_btn.setIcon(icon)
|
||||
icon_btn.setObjectName("AssetNameInputButton")
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(name_input, 1)
|
||||
layout.addWidget(icon_btn, 0)
|
||||
|
||||
# Make sure all widgets are vertically extended to highest widget
|
||||
for widget in (
|
||||
name_input,
|
||||
icon_btn
|
||||
):
|
||||
w_size_policy = widget.sizePolicy()
|
||||
w_size_policy.setVerticalPolicy(
|
||||
QtWidgets.QSizePolicy.MinimumExpanding)
|
||||
widget.setSizePolicy(w_size_policy)
|
||||
|
||||
size_policy = self.sizePolicy()
|
||||
size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Maximum)
|
||||
self.setSizePolicy(size_policy)
|
||||
|
||||
name_input.clicked.connect(self._mouse_release_callback)
|
||||
icon_btn.clicked.connect(self._mouse_release_callback)
|
||||
dialog.finished.connect(self._on_dialog_finish)
|
||||
|
||||
self._controller = controller
|
||||
self._dialog = dialog
|
||||
self._name_input = name_input
|
||||
self._icon_btn = icon_btn
|
||||
|
||||
self._selected_folder_id = None
|
||||
self._selected_folder_label = None
|
||||
self._selected_items = []
|
||||
self._is_valid = True
|
||||
|
||||
def refresh(self):
|
||||
self._dialog.refresh()
|
||||
|
||||
def is_valid(self):
|
||||
"""Is asset valid."""
|
||||
return self._is_valid
|
||||
|
||||
def get_selected_folder_id(self):
|
||||
"""Selected asset names."""
|
||||
return self._selected_folder_id
|
||||
|
||||
def get_selected_folder_label(self):
|
||||
return self._selected_folder_label
|
||||
|
||||
def set_text(self, text):
|
||||
"""Set text in text field.
|
||||
|
||||
Does not change selected items (assets).
|
||||
"""
|
||||
self._name_input.setText(text)
|
||||
|
||||
def set_valid(self, is_valid):
|
||||
state = ""
|
||||
if not is_valid:
|
||||
state = "invalid"
|
||||
self._set_state_property(state)
|
||||
|
||||
def set_selected_item(self, folder_id=None, folder_label=None):
|
||||
"""Set folder for selection.
|
||||
|
||||
Args:
|
||||
folder_id (Optional[str]): Folder id to select.
|
||||
folder_label (Optional[str]): Folder label.
|
||||
"""
|
||||
|
||||
self._selected_folder_id = folder_id
|
||||
if not folder_id:
|
||||
folder_label = None
|
||||
elif folder_id and not folder_label:
|
||||
folder_label = self._controller.get_folder_label(folder_id)
|
||||
self._selected_folder_label = folder_label
|
||||
self.set_text(folder_label if folder_label else "<folder>")
|
||||
|
||||
def _on_dialog_finish(self, result):
|
||||
if not result:
|
||||
return
|
||||
|
||||
folder_id = self._dialog.get_selected_folder_id()
|
||||
folder_label = self._dialog.get_selected_folder_label()
|
||||
self.set_selected_item(folder_id, folder_label)
|
||||
|
||||
self.value_changed.emit()
|
||||
|
||||
def _mouse_release_callback(self):
|
||||
self._dialog.set_selected_folder(self._selected_folder_id)
|
||||
self._dialog.open()
|
||||
|
||||
def _set_state_property(self, state):
|
||||
set_style_property(self, "state", state)
|
||||
set_style_property(self._name_input, "state", state)
|
||||
set_style_property(self._icon_btn, "state", state)
|
||||
94
openpype/tools/ayon_sceneinventory/switch_dialog/widgets.py
Normal file
94
openpype/tools/ayon_sceneinventory/switch_dialog/widgets.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from openpype import style
|
||||
|
||||
|
||||
class ButtonWithMenu(QtWidgets.QToolButton):
|
||||
def __init__(self, parent=None):
|
||||
super(ButtonWithMenu, self).__init__(parent)
|
||||
|
||||
self.setObjectName("ButtonWithMenu")
|
||||
|
||||
self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
|
||||
menu = QtWidgets.QMenu(self)
|
||||
|
||||
self.setMenu(menu)
|
||||
|
||||
self._menu = menu
|
||||
self._actions = []
|
||||
|
||||
def menu(self):
|
||||
return self._menu
|
||||
|
||||
def clear_actions(self):
|
||||
if self._menu is not None:
|
||||
self._menu.clear()
|
||||
self._actions = []
|
||||
|
||||
def add_action(self, action):
|
||||
self._actions.append(action)
|
||||
self._menu.addAction(action)
|
||||
|
||||
def _on_action_trigger(self):
|
||||
action = self.sender()
|
||||
if action not in self._actions:
|
||||
return
|
||||
action.trigger()
|
||||
|
||||
|
||||
class SearchComboBox(QtWidgets.QComboBox):
|
||||
"""Searchable ComboBox with empty placeholder value as first value"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super(SearchComboBox, self).__init__(parent)
|
||||
|
||||
self.setEditable(True)
|
||||
self.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
|
||||
|
||||
combobox_delegate = QtWidgets.QStyledItemDelegate(self)
|
||||
self.setItemDelegate(combobox_delegate)
|
||||
|
||||
completer = self.completer()
|
||||
completer.setCompletionMode(
|
||||
QtWidgets.QCompleter.PopupCompletion
|
||||
)
|
||||
completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
completer_view = completer.popup()
|
||||
completer_view.setObjectName("CompleterView")
|
||||
completer_delegate = QtWidgets.QStyledItemDelegate(completer_view)
|
||||
completer_view.setItemDelegate(completer_delegate)
|
||||
completer_view.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
self._combobox_delegate = combobox_delegate
|
||||
|
||||
self._completer_delegate = completer_delegate
|
||||
self._completer = completer
|
||||
|
||||
def set_placeholder(self, placeholder):
|
||||
self.lineEdit().setPlaceholderText(placeholder)
|
||||
|
||||
def populate(self, items):
|
||||
self.clear()
|
||||
self.addItems([""]) # ensure first item is placeholder
|
||||
self.addItems(items)
|
||||
|
||||
def get_valid_value(self):
|
||||
"""Return the current text if it's a valid value else None
|
||||
|
||||
Note: The empty placeholder value is valid and returns as ""
|
||||
|
||||
"""
|
||||
|
||||
text = self.currentText()
|
||||
lookup = set(self.itemText(i) for i in range(self.count()))
|
||||
if text not in lookup:
|
||||
return None
|
||||
|
||||
return text or None
|
||||
|
||||
def set_valid_value(self, value):
|
||||
"""Try to locate 'value' and pre-select it in dropdown."""
|
||||
index = self.findText(value)
|
||||
if index > -1:
|
||||
self.setCurrentIndex(index)
|
||||
825
openpype/tools/ayon_sceneinventory/view.py
Normal file
825
openpype/tools/ayon_sceneinventory/view.py
Normal file
|
|
@ -0,0 +1,825 @@
|
|||
import uuid
|
||||
import collections
|
||||
import logging
|
||||
import itertools
|
||||
from functools import partial
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
import qtawesome
|
||||
|
||||
from openpype.client import (
|
||||
get_version_by_id,
|
||||
get_versions,
|
||||
get_hero_versions,
|
||||
get_representation_by_id,
|
||||
get_representations,
|
||||
)
|
||||
from openpype import style
|
||||
from openpype.pipeline import (
|
||||
HeroVersionType,
|
||||
update_container,
|
||||
remove_container,
|
||||
discover_inventory_actions,
|
||||
)
|
||||
from openpype.tools.utils.lib import (
|
||||
iter_model_rows,
|
||||
format_version
|
||||
)
|
||||
|
||||
from .switch_dialog import SwitchAssetDialog
|
||||
from .model import InventoryModel
|
||||
|
||||
|
||||
DEFAULT_COLOR = "#fb9c15"
|
||||
|
||||
log = logging.getLogger("SceneInventory")
|
||||
|
||||
|
||||
class SceneInventoryView(QtWidgets.QTreeView):
|
||||
data_changed = QtCore.Signal()
|
||||
hierarchy_view_changed = QtCore.Signal(bool)
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(SceneInventoryView, self).__init__(parent=parent)
|
||||
|
||||
# view settings
|
||||
self.setIndentation(12)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setSortingEnabled(True)
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
|
||||
self.customContextMenuRequested.connect(self._show_right_mouse_menu)
|
||||
|
||||
self._hierarchy_view = False
|
||||
self._selected = None
|
||||
|
||||
self._controller = controller
|
||||
|
||||
def _set_hierarchy_view(self, enabled):
|
||||
if enabled == self._hierarchy_view:
|
||||
return
|
||||
self._hierarchy_view = enabled
|
||||
self.hierarchy_view_changed.emit(enabled)
|
||||
|
||||
def _enter_hierarchy(self, items):
|
||||
self._selected = set(i["objectName"] for i in items)
|
||||
self._set_hierarchy_view(True)
|
||||
self.data_changed.emit()
|
||||
self.expandToDepth(1)
|
||||
self.setStyleSheet("""
|
||||
QTreeView {
|
||||
border-color: #fb9c15;
|
||||
}
|
||||
""")
|
||||
|
||||
def _leave_hierarchy(self):
|
||||
self._set_hierarchy_view(False)
|
||||
self.data_changed.emit()
|
||||
self.setStyleSheet("QTreeView {}")
|
||||
|
||||
def _build_item_menu_for_selection(self, items, menu):
|
||||
# Exclude items that are "NOT FOUND" since setting versions, updating
|
||||
# and removal won't work for those items.
|
||||
items = [item for item in items if not item.get("isNotFound")]
|
||||
if not items:
|
||||
return
|
||||
|
||||
# An item might not have a representation, for example when an item
|
||||
# is listed as "NOT FOUND"
|
||||
repre_ids = set()
|
||||
for item in items:
|
||||
repre_id = item["representation"]
|
||||
try:
|
||||
uuid.UUID(repre_id)
|
||||
repre_ids.add(repre_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
repre_docs = get_representations(
|
||||
project_name, representation_ids=repre_ids, fields=["parent"]
|
||||
)
|
||||
|
||||
version_ids = {
|
||||
repre_doc["parent"]
|
||||
for repre_doc in repre_docs
|
||||
}
|
||||
|
||||
loaded_versions = get_versions(
|
||||
project_name, version_ids=version_ids, hero=True
|
||||
)
|
||||
|
||||
loaded_hero_versions = []
|
||||
versions_by_parent_id = collections.defaultdict(list)
|
||||
subset_ids = set()
|
||||
for version in loaded_versions:
|
||||
if version["type"] == "hero_version":
|
||||
loaded_hero_versions.append(version)
|
||||
else:
|
||||
parent_id = version["parent"]
|
||||
versions_by_parent_id[parent_id].append(version)
|
||||
subset_ids.add(parent_id)
|
||||
|
||||
all_versions = get_versions(
|
||||
project_name, subset_ids=subset_ids, hero=True
|
||||
)
|
||||
hero_versions = []
|
||||
versions = []
|
||||
for version in all_versions:
|
||||
if version["type"] == "hero_version":
|
||||
hero_versions.append(version)
|
||||
else:
|
||||
versions.append(version)
|
||||
|
||||
has_loaded_hero_versions = len(loaded_hero_versions) > 0
|
||||
has_available_hero_version = len(hero_versions) > 0
|
||||
has_outdated = False
|
||||
|
||||
for version in versions:
|
||||
parent_id = version["parent"]
|
||||
current_versions = versions_by_parent_id[parent_id]
|
||||
for current_version in current_versions:
|
||||
if current_version["name"] < version["name"]:
|
||||
has_outdated = True
|
||||
break
|
||||
|
||||
if has_outdated:
|
||||
break
|
||||
|
||||
switch_to_versioned = None
|
||||
if has_loaded_hero_versions:
|
||||
def _on_switch_to_versioned(items):
|
||||
repre_ids = {
|
||||
item["representation"]
|
||||
for item in items
|
||||
}
|
||||
|
||||
repre_docs = get_representations(
|
||||
project_name,
|
||||
representation_ids=repre_ids,
|
||||
fields=["parent"]
|
||||
)
|
||||
|
||||
version_ids = set()
|
||||
version_id_by_repre_id = {}
|
||||
for repre_doc in repre_docs:
|
||||
version_id = repre_doc["parent"]
|
||||
repre_id = str(repre_doc["_id"])
|
||||
version_id_by_repre_id[repre_id] = version_id
|
||||
version_ids.add(version_id)
|
||||
|
||||
hero_versions = get_hero_versions(
|
||||
project_name,
|
||||
version_ids=version_ids,
|
||||
fields=["version_id"]
|
||||
)
|
||||
|
||||
hero_src_version_ids = set()
|
||||
for hero_version in hero_versions:
|
||||
version_id = hero_version["version_id"]
|
||||
hero_src_version_ids.add(version_id)
|
||||
hero_version_id = hero_version["_id"]
|
||||
for _repre_id, current_version_id in (
|
||||
version_id_by_repre_id.items()
|
||||
):
|
||||
if current_version_id == hero_version_id:
|
||||
version_id_by_repre_id[_repre_id] = version_id
|
||||
|
||||
version_docs = get_versions(
|
||||
project_name,
|
||||
version_ids=hero_src_version_ids,
|
||||
fields=["name"]
|
||||
)
|
||||
version_name_by_id = {}
|
||||
for version_doc in version_docs:
|
||||
version_name_by_id[version_doc["_id"]] = \
|
||||
version_doc["name"]
|
||||
|
||||
# Specify version per item to update to
|
||||
update_items = []
|
||||
update_versions = []
|
||||
for item in items:
|
||||
repre_id = item["representation"]
|
||||
version_id = version_id_by_repre_id.get(repre_id)
|
||||
version_name = version_name_by_id.get(version_id)
|
||||
if version_name is not None:
|
||||
update_items.append(item)
|
||||
update_versions.append(version_name)
|
||||
self._update_containers(update_items, update_versions)
|
||||
|
||||
update_icon = qtawesome.icon(
|
||||
"fa.asterisk",
|
||||
color=DEFAULT_COLOR
|
||||
)
|
||||
switch_to_versioned = QtWidgets.QAction(
|
||||
update_icon,
|
||||
"Switch to versioned",
|
||||
menu
|
||||
)
|
||||
switch_to_versioned.triggered.connect(
|
||||
lambda: _on_switch_to_versioned(items)
|
||||
)
|
||||
|
||||
update_to_latest_action = None
|
||||
if has_outdated or has_loaded_hero_versions:
|
||||
update_icon = qtawesome.icon(
|
||||
"fa.angle-double-up",
|
||||
color=DEFAULT_COLOR
|
||||
)
|
||||
update_to_latest_action = QtWidgets.QAction(
|
||||
update_icon,
|
||||
"Update to latest",
|
||||
menu
|
||||
)
|
||||
update_to_latest_action.triggered.connect(
|
||||
lambda: self._update_containers(items, version=-1)
|
||||
)
|
||||
|
||||
change_to_hero = None
|
||||
if has_available_hero_version:
|
||||
# TODO change icon
|
||||
change_icon = qtawesome.icon(
|
||||
"fa.asterisk",
|
||||
color="#00b359"
|
||||
)
|
||||
change_to_hero = QtWidgets.QAction(
|
||||
change_icon,
|
||||
"Change to hero",
|
||||
menu
|
||||
)
|
||||
change_to_hero.triggered.connect(
|
||||
lambda: self._update_containers(items,
|
||||
version=HeroVersionType(-1))
|
||||
)
|
||||
|
||||
# set version
|
||||
set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR)
|
||||
set_version_action = QtWidgets.QAction(
|
||||
set_version_icon,
|
||||
"Set version",
|
||||
menu
|
||||
)
|
||||
set_version_action.triggered.connect(
|
||||
lambda: self._show_version_dialog(items))
|
||||
|
||||
# switch folder
|
||||
switch_folder_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR)
|
||||
switch_folder_action = QtWidgets.QAction(
|
||||
switch_folder_icon,
|
||||
"Switch Folder",
|
||||
menu
|
||||
)
|
||||
switch_folder_action.triggered.connect(
|
||||
lambda: self._show_switch_dialog(items))
|
||||
|
||||
# remove
|
||||
remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR)
|
||||
remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu)
|
||||
remove_action.triggered.connect(
|
||||
lambda: self._show_remove_warning_dialog(items))
|
||||
|
||||
# add the actions
|
||||
if switch_to_versioned:
|
||||
menu.addAction(switch_to_versioned)
|
||||
|
||||
if update_to_latest_action:
|
||||
menu.addAction(update_to_latest_action)
|
||||
|
||||
if change_to_hero:
|
||||
menu.addAction(change_to_hero)
|
||||
|
||||
menu.addAction(set_version_action)
|
||||
menu.addAction(switch_folder_action)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
menu.addAction(remove_action)
|
||||
|
||||
self._handle_sync_server(menu, repre_ids)
|
||||
|
||||
def _handle_sync_server(self, menu, repre_ids):
|
||||
"""Adds actions for download/upload when SyncServer is enabled
|
||||
|
||||
Args:
|
||||
menu (OptionMenu)
|
||||
repre_ids (list) of object_ids
|
||||
|
||||
Returns:
|
||||
(OptionMenu)
|
||||
"""
|
||||
|
||||
if not self._controller.is_sync_server_enabled():
|
||||
return
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR)
|
||||
download_active_action = QtWidgets.QAction(
|
||||
download_icon,
|
||||
"Download",
|
||||
menu
|
||||
)
|
||||
download_active_action.triggered.connect(
|
||||
lambda: self._add_sites(repre_ids, "active_site"))
|
||||
|
||||
upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR)
|
||||
upload_remote_action = QtWidgets.QAction(
|
||||
upload_icon,
|
||||
"Upload",
|
||||
menu
|
||||
)
|
||||
upload_remote_action.triggered.connect(
|
||||
lambda: self._add_sites(repre_ids, "remote_site"))
|
||||
|
||||
menu.addAction(download_active_action)
|
||||
menu.addAction(upload_remote_action)
|
||||
|
||||
def _add_sites(self, repre_ids, site_type):
|
||||
"""(Re)sync all 'repre_ids' to specific site.
|
||||
|
||||
It checks if opposite site has fully available content to limit
|
||||
accidents. (ReSync active when no remote >> losing active content)
|
||||
|
||||
Args:
|
||||
repre_ids (list)
|
||||
site_type (Literal[active_site, remote_site]): Site type.
|
||||
"""
|
||||
|
||||
self._controller.resync_representations(repre_ids, site_type)
|
||||
|
||||
self.data_changed.emit()
|
||||
|
||||
def _build_item_menu(self, items=None):
|
||||
"""Create menu for the selected items"""
|
||||
|
||||
if not items:
|
||||
items = []
|
||||
|
||||
menu = QtWidgets.QMenu(self)
|
||||
|
||||
# add the actions
|
||||
self._build_item_menu_for_selection(items, menu)
|
||||
|
||||
# These two actions should be able to work without selection
|
||||
# expand all items
|
||||
expandall_action = QtWidgets.QAction(menu, text="Expand all items")
|
||||
expandall_action.triggered.connect(self.expandAll)
|
||||
|
||||
# collapse all items
|
||||
collapse_action = QtWidgets.QAction(menu, text="Collapse all items")
|
||||
collapse_action.triggered.connect(self.collapseAll)
|
||||
|
||||
menu.addAction(expandall_action)
|
||||
menu.addAction(collapse_action)
|
||||
|
||||
custom_actions = self._get_custom_actions(containers=items)
|
||||
if custom_actions:
|
||||
submenu = QtWidgets.QMenu("Actions", self)
|
||||
for action in custom_actions:
|
||||
color = action.color or DEFAULT_COLOR
|
||||
icon = qtawesome.icon("fa.%s" % action.icon, color=color)
|
||||
action_item = QtWidgets.QAction(icon, action.label, submenu)
|
||||
action_item.triggered.connect(
|
||||
partial(self._process_custom_action, action, items))
|
||||
|
||||
submenu.addAction(action_item)
|
||||
|
||||
menu.addMenu(submenu)
|
||||
|
||||
# go back to flat view
|
||||
back_to_flat_action = None
|
||||
if self._hierarchy_view:
|
||||
back_to_flat_icon = qtawesome.icon("fa.list", color=DEFAULT_COLOR)
|
||||
back_to_flat_action = QtWidgets.QAction(
|
||||
back_to_flat_icon,
|
||||
"Back to Full-View",
|
||||
menu
|
||||
)
|
||||
back_to_flat_action.triggered.connect(self._leave_hierarchy)
|
||||
|
||||
# send items to hierarchy view
|
||||
enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8")
|
||||
enter_hierarchy_action = QtWidgets.QAction(
|
||||
enter_hierarchy_icon,
|
||||
"Cherry-Pick (Hierarchy)",
|
||||
menu
|
||||
)
|
||||
enter_hierarchy_action.triggered.connect(
|
||||
lambda: self._enter_hierarchy(items))
|
||||
|
||||
if items:
|
||||
menu.addAction(enter_hierarchy_action)
|
||||
|
||||
if back_to_flat_action is not None:
|
||||
menu.addAction(back_to_flat_action)
|
||||
|
||||
return menu
|
||||
|
||||
def _get_custom_actions(self, containers):
|
||||
"""Get the registered Inventory Actions
|
||||
|
||||
Args:
|
||||
containers(list): collection of containers
|
||||
|
||||
Returns:
|
||||
list: collection of filter and initialized actions
|
||||
"""
|
||||
|
||||
def sorter(Plugin):
|
||||
"""Sort based on order attribute of the plugin"""
|
||||
return Plugin.order
|
||||
|
||||
# Fedd an empty dict if no selection, this will ensure the compat
|
||||
# lookup always work, so plugin can interact with Scene Inventory
|
||||
# reversely.
|
||||
containers = containers or [dict()]
|
||||
|
||||
# Check which action will be available in the menu
|
||||
Plugins = discover_inventory_actions()
|
||||
compatible = [p() for p in Plugins if
|
||||
any(p.is_compatible(c) for c in containers)]
|
||||
|
||||
return sorted(compatible, key=sorter)
|
||||
|
||||
def _process_custom_action(self, action, containers):
|
||||
"""Run action and if results are returned positive update the view
|
||||
|
||||
If the result is list or dict, will select view items by the result.
|
||||
|
||||
Args:
|
||||
action (InventoryAction): Inventory Action instance
|
||||
containers (list): Data of currently selected items
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
result = action.process(containers)
|
||||
if result:
|
||||
self.data_changed.emit()
|
||||
|
||||
if isinstance(result, (list, set)):
|
||||
self._select_items_by_action(result)
|
||||
|
||||
if isinstance(result, dict):
|
||||
self._select_items_by_action(
|
||||
result["objectNames"], result["options"]
|
||||
)
|
||||
|
||||
def _select_items_by_action(self, object_names, options=None):
|
||||
"""Select view items by the result of action
|
||||
|
||||
Args:
|
||||
object_names (list or set): A list/set of container object name
|
||||
options (dict): GUI operation options.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
options = options or dict()
|
||||
|
||||
if options.get("clear", True):
|
||||
self.clearSelection()
|
||||
|
||||
object_names = set(object_names)
|
||||
if (
|
||||
self._hierarchy_view
|
||||
and not self._selected.issuperset(object_names)
|
||||
):
|
||||
# If any container not in current cherry-picked view, update
|
||||
# view before selecting them.
|
||||
self._selected.update(object_names)
|
||||
self.data_changed.emit()
|
||||
|
||||
model = self.model()
|
||||
selection_model = self.selectionModel()
|
||||
|
||||
select_mode = {
|
||||
"select": QtCore.QItemSelectionModel.Select,
|
||||
"deselect": QtCore.QItemSelectionModel.Deselect,
|
||||
"toggle": QtCore.QItemSelectionModel.Toggle,
|
||||
}[options.get("mode", "select")]
|
||||
|
||||
for index in iter_model_rows(model, 0):
|
||||
item = index.data(InventoryModel.ItemRole)
|
||||
if item.get("isGroupNode"):
|
||||
continue
|
||||
|
||||
name = item.get("objectName")
|
||||
if name in object_names:
|
||||
self.scrollTo(index) # Ensure item is visible
|
||||
flags = select_mode | QtCore.QItemSelectionModel.Rows
|
||||
selection_model.select(index, flags)
|
||||
|
||||
object_names.remove(name)
|
||||
|
||||
if len(object_names) == 0:
|
||||
break
|
||||
|
||||
def _show_right_mouse_menu(self, pos):
|
||||
"""Display the menu when at the position of the item clicked"""
|
||||
|
||||
globalpos = self.viewport().mapToGlobal(pos)
|
||||
|
||||
if not self.selectionModel().hasSelection():
|
||||
print("No selection")
|
||||
# Build menu without selection, feed an empty list
|
||||
menu = self._build_item_menu()
|
||||
menu.exec_(globalpos)
|
||||
return
|
||||
|
||||
active = self.currentIndex() # index under mouse
|
||||
active = active.sibling(active.row(), 0) # get first column
|
||||
|
||||
# move index under mouse
|
||||
indices = self.get_indices()
|
||||
if active in indices:
|
||||
indices.remove(active)
|
||||
|
||||
indices.append(active)
|
||||
|
||||
# Extend to the sub-items
|
||||
all_indices = self._extend_to_children(indices)
|
||||
items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices
|
||||
if i.parent().isValid()]
|
||||
|
||||
if self._hierarchy_view:
|
||||
# Ensure no group item
|
||||
items = [n for n in items if not n.get("isGroupNode")]
|
||||
|
||||
menu = self._build_item_menu(items)
|
||||
menu.exec_(globalpos)
|
||||
|
||||
def get_indices(self):
|
||||
"""Get the selected rows"""
|
||||
selection_model = self.selectionModel()
|
||||
return selection_model.selectedRows()
|
||||
|
||||
def _extend_to_children(self, indices):
|
||||
"""Extend the indices to the children indices.
|
||||
|
||||
Top-level indices are extended to its children indices. Sub-items
|
||||
are kept as is.
|
||||
|
||||
Args:
|
||||
indices (list): The indices to extend.
|
||||
|
||||
Returns:
|
||||
list: The children indices
|
||||
|
||||
"""
|
||||
def get_children(i):
|
||||
model = i.model()
|
||||
rows = model.rowCount(parent=i)
|
||||
for row in range(rows):
|
||||
child = model.index(row, 0, parent=i)
|
||||
yield child
|
||||
|
||||
subitems = set()
|
||||
for i in indices:
|
||||
valid_parent = i.parent().isValid()
|
||||
if valid_parent and i not in subitems:
|
||||
subitems.add(i)
|
||||
|
||||
if self._hierarchy_view:
|
||||
# Assume this is a group item
|
||||
for child in get_children(i):
|
||||
subitems.add(child)
|
||||
else:
|
||||
# is top level item
|
||||
for child in get_children(i):
|
||||
subitems.add(child)
|
||||
|
||||
return list(subitems)
|
||||
|
||||
def _show_version_dialog(self, items):
|
||||
"""Create a dialog with the available versions for the selected file
|
||||
|
||||
Args:
|
||||
items (list): list of items to run the "set_version" for
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
active = items[-1]
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
# Get available versions for active representation
|
||||
repre_doc = get_representation_by_id(
|
||||
project_name,
|
||||
active["representation"],
|
||||
fields=["parent"]
|
||||
)
|
||||
|
||||
repre_version_doc = get_version_by_id(
|
||||
project_name,
|
||||
repre_doc["parent"],
|
||||
fields=["parent"]
|
||||
)
|
||||
|
||||
version_docs = list(get_versions(
|
||||
project_name,
|
||||
subset_ids=[repre_version_doc["parent"]],
|
||||
hero=True
|
||||
))
|
||||
hero_version = None
|
||||
standard_versions = []
|
||||
for version_doc in version_docs:
|
||||
if version_doc["type"] == "hero_version":
|
||||
hero_version = version_doc
|
||||
else:
|
||||
standard_versions.append(version_doc)
|
||||
versions = list(reversed(
|
||||
sorted(standard_versions, key=lambda item: item["name"])
|
||||
))
|
||||
if hero_version:
|
||||
_version_id = hero_version["version_id"]
|
||||
for _version in versions:
|
||||
if _version["_id"] != _version_id:
|
||||
continue
|
||||
|
||||
hero_version["name"] = HeroVersionType(
|
||||
_version["name"]
|
||||
)
|
||||
hero_version["data"] = _version["data"]
|
||||
break
|
||||
|
||||
# Get index among the listed versions
|
||||
current_item = None
|
||||
current_version = active["version"]
|
||||
if isinstance(current_version, HeroVersionType):
|
||||
current_item = hero_version
|
||||
else:
|
||||
for version in versions:
|
||||
if version["name"] == current_version:
|
||||
current_item = version
|
||||
break
|
||||
|
||||
all_versions = []
|
||||
if hero_version:
|
||||
all_versions.append(hero_version)
|
||||
all_versions.extend(versions)
|
||||
|
||||
if current_item:
|
||||
index = all_versions.index(current_item)
|
||||
else:
|
||||
index = 0
|
||||
|
||||
versions_by_label = dict()
|
||||
labels = []
|
||||
for version in all_versions:
|
||||
is_hero = version["type"] == "hero_version"
|
||||
label = format_version(version["name"], is_hero)
|
||||
labels.append(label)
|
||||
versions_by_label[label] = version["name"]
|
||||
|
||||
label, state = QtWidgets.QInputDialog.getItem(
|
||||
self,
|
||||
"Set version..",
|
||||
"Set version number to",
|
||||
labels,
|
||||
current=index,
|
||||
editable=False
|
||||
)
|
||||
if not state:
|
||||
return
|
||||
|
||||
if label:
|
||||
version = versions_by_label[label]
|
||||
self._update_containers(items, version)
|
||||
|
||||
def _show_switch_dialog(self, items):
|
||||
"""Display Switch dialog"""
|
||||
dialog = SwitchAssetDialog(self._controller, self, items)
|
||||
dialog.switched.connect(self.data_changed.emit)
|
||||
dialog.show()
|
||||
|
||||
def _show_remove_warning_dialog(self, items):
|
||||
"""Prompt a dialog to inform the user the action will remove items"""
|
||||
|
||||
accept = QtWidgets.QMessageBox.Ok
|
||||
buttons = accept | QtWidgets.QMessageBox.Cancel
|
||||
|
||||
state = QtWidgets.QMessageBox.question(
|
||||
self,
|
||||
"Are you sure?",
|
||||
"Are you sure you want to remove {} item(s)".format(len(items)),
|
||||
buttons=buttons,
|
||||
defaultButton=accept
|
||||
)
|
||||
|
||||
if state != accept:
|
||||
return
|
||||
|
||||
for item in items:
|
||||
remove_container(item)
|
||||
self.data_changed.emit()
|
||||
|
||||
def _show_version_error_dialog(self, version, items):
|
||||
"""Shows QMessageBox when version switch doesn't work
|
||||
|
||||
Args:
|
||||
version: str or int or None
|
||||
"""
|
||||
if version == -1:
|
||||
version_str = "latest"
|
||||
elif isinstance(version, HeroVersionType):
|
||||
version_str = "hero"
|
||||
elif isinstance(version, int):
|
||||
version_str = "v{:03d}".format(version)
|
||||
else:
|
||||
version_str = version
|
||||
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
dialog.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
dialog.setStyleSheet(style.load_stylesheet())
|
||||
dialog.setWindowTitle("Update failed")
|
||||
|
||||
switch_btn = dialog.addButton(
|
||||
"Switch Folder",
|
||||
QtWidgets.QMessageBox.ActionRole
|
||||
)
|
||||
switch_btn.clicked.connect(lambda: self._show_switch_dialog(items))
|
||||
|
||||
dialog.addButton(QtWidgets.QMessageBox.Cancel)
|
||||
|
||||
msg = (
|
||||
"Version update to '{}' failed as representation doesn't exist."
|
||||
"\n\nPlease update to version with a valid representation"
|
||||
" OR \n use 'Switch Folder' button to change folder."
|
||||
).format(version_str)
|
||||
dialog.setText(msg)
|
||||
dialog.exec_()
|
||||
|
||||
def update_all(self):
|
||||
"""Update all items that are currently 'outdated' in the view"""
|
||||
# Get the source model through the proxy model
|
||||
model = self.model().sourceModel()
|
||||
|
||||
# Get all items from outdated groups
|
||||
outdated_items = []
|
||||
for index in iter_model_rows(model,
|
||||
column=0,
|
||||
include_root=False):
|
||||
item = index.data(model.ItemRole)
|
||||
|
||||
if not item.get("isGroupNode"):
|
||||
continue
|
||||
|
||||
# Only the group nodes contain the "highest_version" data and as
|
||||
# such we find only the groups and take its children.
|
||||
if not model.outdated(item):
|
||||
continue
|
||||
|
||||
# Collect all children which we want to update
|
||||
children = item.children()
|
||||
outdated_items.extend(children)
|
||||
|
||||
if not outdated_items:
|
||||
log.info("Nothing to update.")
|
||||
return
|
||||
|
||||
# Trigger update to latest
|
||||
self._update_containers(outdated_items, version=-1)
|
||||
|
||||
def _update_containers(self, items, version):
|
||||
"""Helper to update items to given version (or version per item)
|
||||
|
||||
If at least one item is specified this will always try to refresh
|
||||
the inventory even if errors occurred on any of the items.
|
||||
|
||||
Arguments:
|
||||
items (list): Items to update
|
||||
version (int or list): Version to set to.
|
||||
This can be a list specifying a version for each item.
|
||||
Like `update_container` version -1 sets the latest version
|
||||
and HeroTypeVersion instances set the hero version.
|
||||
|
||||
"""
|
||||
|
||||
if isinstance(version, (list, tuple)):
|
||||
# We allow a unique version to be specified per item. In that case
|
||||
# the length must match with the items
|
||||
assert len(items) == len(version), (
|
||||
"Number of items mismatches number of versions: "
|
||||
"{} items - {} versions".format(len(items), len(version))
|
||||
)
|
||||
versions = version
|
||||
else:
|
||||
# Repeat the same version infinitely
|
||||
versions = itertools.repeat(version)
|
||||
|
||||
# Trigger update to latest
|
||||
try:
|
||||
for item, item_version in zip(items, versions):
|
||||
try:
|
||||
update_container(item, item_version)
|
||||
except AssertionError:
|
||||
self._show_version_error_dialog(item_version, [item])
|
||||
log.warning("Update failed", exc_info=True)
|
||||
finally:
|
||||
# Always update the scene inventory view, even if errors occurred
|
||||
self.data_changed.emit()
|
||||
200
openpype/tools/ayon_sceneinventory/window.py
Normal file
200
openpype/tools/ayon_sceneinventory/window.py
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
import qtawesome
|
||||
|
||||
from openpype import style, resources
|
||||
from openpype.tools.utils.delegates import VersionDelegate
|
||||
from openpype.tools.utils.lib import (
|
||||
preserve_expanded_rows,
|
||||
preserve_selection,
|
||||
)
|
||||
from openpype.tools.ayon_sceneinventory import SceneInventoryController
|
||||
|
||||
from .model import (
|
||||
InventoryModel,
|
||||
FilterProxyModel
|
||||
)
|
||||
from .view import SceneInventoryView
|
||||
|
||||
|
||||
class ControllerVersionDelegate(VersionDelegate):
|
||||
"""Version delegate that uses controller to get project.
|
||||
|
||||
Original VersionDelegate is using 'AvalonMongoDB' object instead. Don't
|
||||
worry about the variable name, object is stored to '_dbcon' attribute.
|
||||
"""
|
||||
|
||||
def get_project_name(self):
|
||||
self._dbcon.get_current_project_name()
|
||||
|
||||
|
||||
class SceneInventoryWindow(QtWidgets.QDialog):
|
||||
"""Scene Inventory window"""
|
||||
|
||||
def __init__(self, controller=None, parent=None):
|
||||
super(SceneInventoryWindow, self).__init__(parent)
|
||||
|
||||
if controller is None:
|
||||
controller = SceneInventoryController()
|
||||
|
||||
project_name = controller.get_current_project_name()
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
self.setWindowTitle("Scene Inventory - {}".format(project_name))
|
||||
self.setObjectName("SceneInventory")
|
||||
|
||||
self.resize(1100, 480)
|
||||
|
||||
# region control
|
||||
|
||||
filter_label = QtWidgets.QLabel("Search", self)
|
||||
text_filter = QtWidgets.QLineEdit(self)
|
||||
|
||||
outdated_only_checkbox = QtWidgets.QCheckBox(
|
||||
"Filter to outdated", self
|
||||
)
|
||||
outdated_only_checkbox.setToolTip("Show outdated files only")
|
||||
outdated_only_checkbox.setChecked(False)
|
||||
|
||||
icon = qtawesome.icon("fa.arrow-up", color="white")
|
||||
update_all_button = QtWidgets.QPushButton(self)
|
||||
update_all_button.setToolTip("Update all outdated to latest version")
|
||||
update_all_button.setIcon(icon)
|
||||
|
||||
icon = qtawesome.icon("fa.refresh", color="white")
|
||||
refresh_button = QtWidgets.QPushButton(self)
|
||||
refresh_button.setToolTip("Refresh")
|
||||
refresh_button.setIcon(icon)
|
||||
|
||||
control_layout = QtWidgets.QHBoxLayout()
|
||||
control_layout.addWidget(filter_label)
|
||||
control_layout.addWidget(text_filter)
|
||||
control_layout.addWidget(outdated_only_checkbox)
|
||||
control_layout.addWidget(update_all_button)
|
||||
control_layout.addWidget(refresh_button)
|
||||
|
||||
model = InventoryModel(controller)
|
||||
proxy = FilterProxyModel()
|
||||
proxy.setSourceModel(model)
|
||||
proxy.setDynamicSortFilter(True)
|
||||
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
view = SceneInventoryView(controller, self)
|
||||
view.setModel(proxy)
|
||||
|
||||
sync_enabled = controller.is_sync_server_enabled()
|
||||
view.setColumnHidden(model.active_site_col, not sync_enabled)
|
||||
view.setColumnHidden(model.remote_site_col, not sync_enabled)
|
||||
|
||||
# set some nice default widths for the view
|
||||
view.setColumnWidth(0, 250) # name
|
||||
view.setColumnWidth(1, 55) # version
|
||||
view.setColumnWidth(2, 55) # count
|
||||
view.setColumnWidth(3, 150) # family
|
||||
view.setColumnWidth(4, 120) # group
|
||||
view.setColumnWidth(5, 150) # loader
|
||||
|
||||
# apply delegates
|
||||
version_delegate = ControllerVersionDelegate(controller, self)
|
||||
column = model.Columns.index("version")
|
||||
view.setItemDelegateForColumn(column, version_delegate)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addLayout(control_layout)
|
||||
layout.addWidget(view)
|
||||
|
||||
show_timer = QtCore.QTimer()
|
||||
show_timer.setInterval(0)
|
||||
show_timer.setSingleShot(False)
|
||||
|
||||
# signals
|
||||
show_timer.timeout.connect(self._on_show_timer)
|
||||
text_filter.textChanged.connect(self._on_text_filter_change)
|
||||
outdated_only_checkbox.stateChanged.connect(
|
||||
self._on_outdated_state_change
|
||||
)
|
||||
view.hierarchy_view_changed.connect(
|
||||
self._on_hierarchy_view_change
|
||||
)
|
||||
view.data_changed.connect(self._on_refresh_request)
|
||||
refresh_button.clicked.connect(self._on_refresh_request)
|
||||
update_all_button.clicked.connect(self._on_update_all)
|
||||
|
||||
self._show_timer = show_timer
|
||||
self._show_counter = 0
|
||||
self._controller = controller
|
||||
self._update_all_button = update_all_button
|
||||
self._outdated_only_checkbox = outdated_only_checkbox
|
||||
self._view = view
|
||||
self._model = model
|
||||
self._proxy = proxy
|
||||
self._version_delegate = version_delegate
|
||||
|
||||
self._first_show = True
|
||||
self._first_refresh = True
|
||||
|
||||
def showEvent(self, event):
|
||||
super(SceneInventoryWindow, self).showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
self._show_counter = 0
|
||||
self._show_timer.start()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Custom keyPressEvent.
|
||||
|
||||
Override keyPressEvent to do nothing so that Maya's panels won't
|
||||
take focus when pressing "SHIFT" whilst mouse is over viewport or
|
||||
outliner. This way users don't accidentally perform Maya commands
|
||||
whilst trying to name an instance.
|
||||
|
||||
"""
|
||||
|
||||
def _on_refresh_request(self):
|
||||
"""Signal callback to trigger 'refresh' without any arguments."""
|
||||
|
||||
self.refresh()
|
||||
|
||||
def refresh(self, containers=None):
|
||||
self._first_refresh = False
|
||||
self._controller.reset()
|
||||
with preserve_expanded_rows(
|
||||
tree_view=self._view,
|
||||
role=self._model.UniqueRole
|
||||
):
|
||||
with preserve_selection(
|
||||
tree_view=self._view,
|
||||
role=self._model.UniqueRole,
|
||||
current_index=False
|
||||
):
|
||||
kwargs = {"containers": containers}
|
||||
# TODO do not touch view's inner attribute
|
||||
if self._view._hierarchy_view:
|
||||
kwargs["selected"] = self._view._selected
|
||||
self._model.refresh(**kwargs)
|
||||
|
||||
def _on_show_timer(self):
|
||||
if self._show_counter < 3:
|
||||
self._show_counter += 1
|
||||
return
|
||||
self._show_timer.stop()
|
||||
self.refresh()
|
||||
|
||||
def _on_hierarchy_view_change(self, enabled):
|
||||
self._proxy.set_hierarchy_view(enabled)
|
||||
self._model.set_hierarchy_view(enabled)
|
||||
|
||||
def _on_text_filter_change(self, text_filter):
|
||||
if hasattr(self._proxy, "setFilterRegExp"):
|
||||
self._proxy.setFilterRegExp(text_filter)
|
||||
else:
|
||||
self._proxy.setFilterRegularExpression(text_filter)
|
||||
|
||||
def _on_outdated_state_change(self):
|
||||
self._proxy.set_filter_outdated(
|
||||
self._outdated_only_checkbox.isChecked()
|
||||
)
|
||||
|
||||
def _on_update_all(self):
|
||||
self._view.update_all()
|
||||
|
|
@ -29,16 +29,21 @@ class FolderItem:
|
|||
parent_id (Union[str, None]): Parent folder id. If 'None' then project
|
||||
is parent.
|
||||
name (str): Name of folder.
|
||||
path (str): Folder path.
|
||||
folder_type (str): Type of folder.
|
||||
label (Union[str, None]): Folder label.
|
||||
icon (Union[dict[str, Any], None]): Icon definition.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, entity_id, parent_id, name, label, icon
|
||||
self, entity_id, parent_id, name, path, folder_type, label, icon
|
||||
):
|
||||
self.entity_id = entity_id
|
||||
self.parent_id = parent_id
|
||||
self.name = name
|
||||
self.path = path
|
||||
self.folder_type = folder_type
|
||||
self.label = label or name
|
||||
if not icon:
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
|
|
@ -46,7 +51,6 @@ class FolderItem:
|
|||
"color": get_default_entity_icon_color()
|
||||
}
|
||||
self.icon = icon
|
||||
self.label = label or name
|
||||
|
||||
def to_data(self):
|
||||
"""Converts folder item to data.
|
||||
|
|
@ -59,6 +63,8 @@ class FolderItem:
|
|||
"entity_id": self.entity_id,
|
||||
"parent_id": self.parent_id,
|
||||
"name": self.name,
|
||||
"path": self.path,
|
||||
"folder_type": self.folder_type,
|
||||
"label": self.label,
|
||||
"icon": self.icon,
|
||||
}
|
||||
|
|
@ -90,8 +96,7 @@ class TaskItem:
|
|||
name (str): Name of task.
|
||||
task_type (str): Type of task.
|
||||
parent_id (str): Parent folder id.
|
||||
icon_name (str): Name of icon from font awesome.
|
||||
icon_color (str): Hex color string that will be used for icon.
|
||||
icon (Union[dict[str, Any], None]): Icon definitions.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -183,12 +188,31 @@ def _get_task_items_from_tasks(tasks):
|
|||
|
||||
|
||||
def _get_folder_item_from_hierarchy_item(item):
|
||||
name = item["name"]
|
||||
path_parts = list(item["parents"])
|
||||
path_parts.append(name)
|
||||
|
||||
return FolderItem(
|
||||
item["id"],
|
||||
item["parentId"],
|
||||
item["name"],
|
||||
name,
|
||||
"/".join(path_parts),
|
||||
item["folderType"],
|
||||
item["label"],
|
||||
None
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def _get_folder_item_from_entity(entity):
|
||||
name = entity["name"]
|
||||
return FolderItem(
|
||||
entity["id"],
|
||||
entity["parentId"],
|
||||
name,
|
||||
entity["path"],
|
||||
entity["folderType"],
|
||||
entity["label"] or name,
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -223,13 +247,84 @@ class HierarchyModel(object):
|
|||
self._tasks_by_id.reset()
|
||||
|
||||
def refresh_project(self, project_name):
|
||||
"""Force to refresh folder items for a project.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project to refresh.
|
||||
"""
|
||||
|
||||
self._refresh_folders_cache(project_name)
|
||||
|
||||
def get_folder_items(self, project_name, sender):
|
||||
"""Get folder items by project name.
|
||||
|
||||
The folders are cached per project name. If the cache is not valid
|
||||
then the folders are queried from server.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project where to look for folders.
|
||||
sender (Union[str, None]): Who requested the folder ids.
|
||||
|
||||
Returns:
|
||||
dict[str, FolderItem]: Folder items by id.
|
||||
"""
|
||||
|
||||
if not self._folders_items[project_name].is_valid:
|
||||
self._refresh_folders_cache(project_name, sender)
|
||||
return self._folders_items[project_name].get_data()
|
||||
|
||||
def get_folder_items_by_id(self, project_name, folder_ids):
|
||||
"""Get folder items by ids.
|
||||
|
||||
This function will query folders if they are not in cache. But the
|
||||
queried items are not added to cache back.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project where to look for folders.
|
||||
folder_ids (Iterable[str]): Folder ids.
|
||||
|
||||
Returns:
|
||||
dict[str, Union[FolderItem, None]]: Folder items by id.
|
||||
"""
|
||||
|
||||
folder_ids = set(folder_ids)
|
||||
if self._folders_items[project_name].is_valid:
|
||||
cache_data = self._folders_items[project_name].get_data()
|
||||
return {
|
||||
folder_id: cache_data.get(folder_id)
|
||||
for folder_id in folder_ids
|
||||
}
|
||||
folders = ayon_api.get_folders(
|
||||
project_name,
|
||||
folder_ids=folder_ids,
|
||||
fields=["id", "name", "label", "parentId", "path", "folderType"]
|
||||
)
|
||||
# Make sure all folder ids are in output
|
||||
output = {folder_id: None for folder_id in folder_ids}
|
||||
output.update({
|
||||
folder["id"]: _get_folder_item_from_entity(folder)
|
||||
for folder in folders
|
||||
})
|
||||
return output
|
||||
|
||||
def get_folder_item(self, project_name, folder_id):
|
||||
"""Get folder items by id.
|
||||
|
||||
This function will query folder if they is not in cache. But the
|
||||
queried items are not added to cache back.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project where to look for folders.
|
||||
folder_id (str): Folder id.
|
||||
|
||||
Returns:
|
||||
Union[FolderItem, None]: Folder item.
|
||||
"""
|
||||
items = self.get_folder_items_by_id(
|
||||
project_name, [folder_id]
|
||||
)
|
||||
return items.get(folder_id)
|
||||
|
||||
def get_task_items(self, project_name, folder_id, sender):
|
||||
if not project_name or not folder_id:
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -4,14 +4,16 @@ from qtpy import QtWidgets, QtGui, QtCore
|
|||
|
||||
from openpype.tools.utils import (
|
||||
RecursiveSortFilterProxyModel,
|
||||
DeselectableTreeView,
|
||||
TreeView,
|
||||
)
|
||||
|
||||
from .utils import RefreshThread, get_qt_icon
|
||||
|
||||
FOLDERS_MODEL_SENDER_NAME = "qt_folders_model"
|
||||
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
ITEM_NAME_ROLE = QtCore.Qt.UserRole + 2
|
||||
FOLDER_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
FOLDER_NAME_ROLE = QtCore.Qt.UserRole + 2
|
||||
FOLDER_PATH_ROLE = QtCore.Qt.UserRole + 3
|
||||
FOLDER_TYPE_ROLE = QtCore.Qt.UserRole + 4
|
||||
|
||||
|
||||
class FoldersModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -84,6 +86,15 @@ class FoldersModel(QtGui.QStandardItemModel):
|
|||
return QtCore.QModelIndex()
|
||||
return self.indexFromItem(item)
|
||||
|
||||
def get_project_name(self):
|
||||
"""Project name which model currently use.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Currently used project name.
|
||||
"""
|
||||
|
||||
return self._last_project_name
|
||||
|
||||
def set_project_name(self, project_name):
|
||||
"""Refresh folders items.
|
||||
|
||||
|
|
@ -151,12 +162,13 @@ class FoldersModel(QtGui.QStandardItemModel):
|
|||
"""
|
||||
|
||||
icon = get_qt_icon(folder_item.icon)
|
||||
item.setData(folder_item.entity_id, ITEM_ID_ROLE)
|
||||
item.setData(folder_item.name, ITEM_NAME_ROLE)
|
||||
item.setData(folder_item.entity_id, FOLDER_ID_ROLE)
|
||||
item.setData(folder_item.name, FOLDER_NAME_ROLE)
|
||||
item.setData(folder_item.path, FOLDER_PATH_ROLE)
|
||||
item.setData(folder_item.folder_type, FOLDER_TYPE_ROLE)
|
||||
item.setData(folder_item.label, QtCore.Qt.DisplayRole)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
|
||||
|
||||
def _fill_items(self, folder_items_by_id):
|
||||
if not folder_items_by_id:
|
||||
if folder_items_by_id is not None:
|
||||
|
|
@ -193,7 +205,7 @@ class FoldersModel(QtGui.QStandardItemModel):
|
|||
folder_ids_to_add = set(folder_items)
|
||||
for row_idx in reversed(range(parent_item.rowCount())):
|
||||
child_item = parent_item.child(row_idx)
|
||||
child_id = child_item.data(ITEM_ID_ROLE)
|
||||
child_id = child_item.data(FOLDER_ID_ROLE)
|
||||
if child_id in ids_to_remove:
|
||||
removed_items.append(parent_item.takeRow(row_idx))
|
||||
else:
|
||||
|
|
@ -259,10 +271,14 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
the expected selection. Defaults to False.
|
||||
"""
|
||||
|
||||
double_clicked = QtCore.Signal(QtGui.QMouseEvent)
|
||||
selection_changed = QtCore.Signal()
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent, handle_expected_selection=False):
|
||||
super(FoldersWidget, self).__init__(parent)
|
||||
|
||||
folders_view = DeselectableTreeView(self)
|
||||
folders_view = TreeView(self)
|
||||
folders_view.setHeaderHidden(True)
|
||||
|
||||
folders_model = FoldersModel(controller)
|
||||
|
|
@ -295,7 +311,7 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
|
||||
selection_model = folders_view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self._on_selection_change)
|
||||
|
||||
folders_view.double_clicked.connect(self.double_clicked)
|
||||
folders_model.refreshed.connect(self._on_model_refresh)
|
||||
|
||||
self._controller = controller
|
||||
|
|
@ -306,7 +322,27 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
self._handle_expected_selection = handle_expected_selection
|
||||
self._expected_selection = None
|
||||
|
||||
def set_name_filer(self, name):
|
||||
@property
|
||||
def is_refreshing(self):
|
||||
"""Model is refreshing.
|
||||
|
||||
Returns:
|
||||
bool: True if model is refreshing.
|
||||
"""
|
||||
|
||||
return self._folders_model.is_refreshing
|
||||
|
||||
@property
|
||||
def has_content(self):
|
||||
"""Has at least one folder.
|
||||
|
||||
Returns:
|
||||
bool: True if model has at least one folder.
|
||||
"""
|
||||
|
||||
return self._folders_model.has_content
|
||||
|
||||
def set_name_filter(self, name):
|
||||
"""Set filter of folder name.
|
||||
|
||||
Args:
|
||||
|
|
@ -323,16 +359,108 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
|
||||
self._folders_model.refresh()
|
||||
|
||||
def get_project_name(self):
|
||||
"""Project name in which folders widget currently is.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Currently used project name.
|
||||
"""
|
||||
|
||||
return self._folders_model.get_project_name()
|
||||
|
||||
def set_project_name(self, project_name):
|
||||
"""Set project name.
|
||||
|
||||
Do not use this method when controller is handling selection of
|
||||
project using 'selection.project.changed' event.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
"""
|
||||
|
||||
self._folders_model.set_project_name(project_name)
|
||||
|
||||
def get_selected_folder_id(self):
|
||||
"""Get selected folder id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Folder id which is selected.
|
||||
"""
|
||||
|
||||
return self._get_selected_item_id()
|
||||
|
||||
def get_selected_folder_label(self):
|
||||
"""Selected folder label.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Selected folder label.
|
||||
"""
|
||||
|
||||
item_id = self._get_selected_item_id()
|
||||
return self.get_folder_label(item_id)
|
||||
|
||||
def get_folder_label(self, folder_id):
|
||||
"""Folder label for a given folder id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Folder label.
|
||||
"""
|
||||
|
||||
index = self._folders_model.get_index_by_id(folder_id)
|
||||
if index.isValid():
|
||||
return index.data(QtCore.Qt.DisplayRole)
|
||||
return None
|
||||
|
||||
def set_selected_folder(self, folder_id):
|
||||
"""Change selection.
|
||||
|
||||
Args:
|
||||
folder_id (Union[str, None]): Folder id or None to deselect.
|
||||
"""
|
||||
|
||||
if folder_id is None:
|
||||
self._folders_view.clearSelection()
|
||||
return True
|
||||
|
||||
if folder_id == self._get_selected_item_id():
|
||||
return True
|
||||
index = self._folders_model.get_index_by_id(folder_id)
|
||||
if not index.isValid():
|
||||
return False
|
||||
|
||||
proxy_index = self._folders_proxy_model.mapFromSource(index)
|
||||
if not proxy_index.isValid():
|
||||
return False
|
||||
|
||||
selection_model = self._folders_view.selectionModel()
|
||||
selection_model.setCurrentIndex(
|
||||
proxy_index, QtCore.QItemSelectionModel.SelectCurrent
|
||||
)
|
||||
return True
|
||||
|
||||
def set_deselectable(self, enabled):
|
||||
"""Set deselectable mode.
|
||||
|
||||
Items in view can be deselected.
|
||||
|
||||
Args:
|
||||
enabled (bool): Enable deselectable mode.
|
||||
"""
|
||||
|
||||
self._folders_view.set_deselectable(enabled)
|
||||
|
||||
def _get_selected_index(self):
|
||||
return self._folders_model.get_index_by_id(
|
||||
self.get_selected_folder_id()
|
||||
)
|
||||
|
||||
def _on_project_selection_change(self, event):
|
||||
project_name = event["project_name"]
|
||||
self._set_project_name(project_name)
|
||||
|
||||
def _set_project_name(self, project_name):
|
||||
self._folders_model.set_project_name(project_name)
|
||||
self.set_project_name(project_name)
|
||||
|
||||
def _on_folders_refresh_finished(self, event):
|
||||
if event["sender"] != FOLDERS_MODEL_SENDER_NAME:
|
||||
self._set_project_name(event["project_name"])
|
||||
self.set_project_name(event["project_name"])
|
||||
|
||||
def _on_controller_refresh(self):
|
||||
self._update_expected_selection()
|
||||
|
|
@ -341,11 +469,12 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
if self._expected_selection:
|
||||
self._set_expected_selection()
|
||||
self._folders_proxy_model.sort(0)
|
||||
self.refreshed.emit()
|
||||
|
||||
def _get_selected_item_id(self):
|
||||
selection_model = self._folders_view.selectionModel()
|
||||
for index in selection_model.selectedIndexes():
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
item_id = index.data(FOLDER_ID_ROLE)
|
||||
if item_id is not None:
|
||||
return item_id
|
||||
return None
|
||||
|
|
@ -353,6 +482,7 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
def _on_selection_change(self):
|
||||
item_id = self._get_selected_item_id()
|
||||
self._controller.set_selected_folder(item_id)
|
||||
self.selection_changed.emit()
|
||||
|
||||
# Expected selection handling
|
||||
def _on_expected_selection_change(self, event):
|
||||
|
|
@ -380,12 +510,6 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
|
||||
folder_id = self._expected_selection
|
||||
self._expected_selection = None
|
||||
if (
|
||||
folder_id is not None
|
||||
and folder_id != self._get_selected_item_id()
|
||||
):
|
||||
index = self._folders_model.get_index_by_id(folder_id)
|
||||
if index.isValid():
|
||||
proxy_index = self._folders_proxy_model.mapFromSource(index)
|
||||
self._folders_view.setCurrentIndex(proxy_index)
|
||||
if folder_id is not None:
|
||||
self.set_selected_folder(folder_id)
|
||||
self._controller.expected_folder_selected(folder_id)
|
||||
|
|
|
|||
|
|
@ -395,6 +395,7 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
|||
|
||||
class ProjectsCombobox(QtWidgets.QWidget):
|
||||
refreshed = QtCore.Signal()
|
||||
selection_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent, handle_expected_selection=False):
|
||||
super(ProjectsCombobox, self).__init__(parent)
|
||||
|
|
@ -482,7 +483,7 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
|
||||
self._listen_selection_change = listen
|
||||
|
||||
def get_current_project_name(self):
|
||||
def get_selected_project_name(self):
|
||||
"""Name of selected project.
|
||||
|
||||
Returns:
|
||||
|
|
@ -502,7 +503,7 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
if not self._select_item_visible:
|
||||
return
|
||||
if "project_name" not in kwargs:
|
||||
project_name = self.get_current_project_name()
|
||||
project_name = self.get_selected_project_name()
|
||||
else:
|
||||
project_name = kwargs.get("project_name")
|
||||
|
||||
|
|
@ -536,6 +537,7 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
idx, PROJECT_NAME_ROLE)
|
||||
self._update_select_item_visiblity(project_name=project_name)
|
||||
self._controller.set_selected_project(project_name)
|
||||
self.selection_changed.emit()
|
||||
|
||||
def _on_model_refresh(self):
|
||||
self._projects_proxy_model.sort(0)
|
||||
|
|
@ -561,7 +563,7 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
return
|
||||
project_name = self._expected_selection
|
||||
if project_name is not None:
|
||||
if project_name != self.get_current_project_name():
|
||||
if project_name != self.get_selected_project_name():
|
||||
self.set_selection(project_name)
|
||||
else:
|
||||
# Fake project change
|
||||
|
|
|
|||
|
|
@ -296,6 +296,9 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
handle_expected_selection (Optional[bool]): Handle expected selection.
|
||||
"""
|
||||
|
||||
refreshed = QtCore.Signal()
|
||||
selection_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent, handle_expected_selection=False):
|
||||
super(TasksWidget, self).__init__(parent)
|
||||
|
||||
|
|
@ -380,6 +383,7 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
if not self._set_expected_selection():
|
||||
self._on_selection_change()
|
||||
self._tasks_proxy_model.sort(0)
|
||||
self.refreshed.emit()
|
||||
|
||||
def _get_selected_item_ids(self):
|
||||
selection_model = self._tasks_view.selectionModel()
|
||||
|
|
@ -400,6 +404,7 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
|
||||
parent_id, task_id, task_name = self._get_selected_item_ids()
|
||||
self._controller.set_selected_task(task_id, task_name)
|
||||
self.selection_changed.emit()
|
||||
|
||||
# Expected selection handling
|
||||
def _on_expected_selection_change(self, event):
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ from openpype.style import (
|
|||
get_default_entity_icon_color,
|
||||
get_disabled_entity_icon_color,
|
||||
)
|
||||
from openpype.tools.utils import TreeView
|
||||
from openpype.tools.utils.delegates import PrettyTimeDelegate
|
||||
|
||||
from .utils import TreeView, BaseOverlayFrame
|
||||
from .utils import BaseOverlayFrame
|
||||
|
||||
|
||||
REPRE_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
|
|
@ -306,7 +307,7 @@ class PublishedFilesWidget(QtWidgets.QWidget):
|
|||
|
||||
selection_model = view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self._on_selection_change)
|
||||
view.double_clicked_left.connect(self._on_left_double_click)
|
||||
view.double_clicked.connect(self._on_mouse_double_click)
|
||||
|
||||
controller.register_event_callback(
|
||||
"expected_selection_changed",
|
||||
|
|
@ -350,8 +351,9 @@ class PublishedFilesWidget(QtWidgets.QWidget):
|
|||
repre_id = self.get_selected_repre_id()
|
||||
self._controller.set_selected_representation_id(repre_id)
|
||||
|
||||
def _on_left_double_click(self):
|
||||
self.save_as_requested.emit()
|
||||
def _on_mouse_double_click(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.save_as_requested.emit()
|
||||
|
||||
def _on_expected_selection_change(self, event):
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ from openpype.style import (
|
|||
get_default_entity_icon_color,
|
||||
get_disabled_entity_icon_color,
|
||||
)
|
||||
from openpype.tools.utils import TreeView
|
||||
from openpype.tools.utils.delegates import PrettyTimeDelegate
|
||||
|
||||
from .utils import TreeView
|
||||
|
||||
FILENAME_ROLE = QtCore.Qt.UserRole + 1
|
||||
FILEPATH_ROLE = QtCore.Qt.UserRole + 2
|
||||
DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3
|
||||
|
|
@ -271,7 +270,7 @@ class WorkAreaFilesWidget(QtWidgets.QWidget):
|
|||
|
||||
selection_model = view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self._on_selection_change)
|
||||
view.double_clicked_left.connect(self._on_left_double_click)
|
||||
view.double_clicked.connect(self._on_mouse_double_click)
|
||||
view.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
controller.register_event_callback(
|
||||
|
|
@ -333,8 +332,9 @@ class WorkAreaFilesWidget(QtWidgets.QWidget):
|
|||
filepath = self.get_selected_path()
|
||||
self._controller.set_selected_workfile_path(filepath)
|
||||
|
||||
def _on_left_double_click(self):
|
||||
self.open_current_requested.emit()
|
||||
def _on_mouse_double_click(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.save_as_requested.emit()
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
index = self._view.indexAt(point)
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
|
||||
self._expected_selection = None
|
||||
|
||||
def set_name_filer(self, name):
|
||||
def set_name_filter(self, name):
|
||||
self._folders_proxy_model.setFilterFixedString(name)
|
||||
|
||||
def _clear(self):
|
||||
|
|
|
|||
|
|
@ -1,70 +1,4 @@
|
|||
from qtpy import QtWidgets, QtCore
|
||||
from openpype.tools.flickcharm import FlickCharm
|
||||
|
||||
|
||||
class TreeView(QtWidgets.QTreeView):
|
||||
"""Ultimate TreeView with flick charm and double click signals.
|
||||
|
||||
Tree view have deselectable mode, which allows to deselect items by
|
||||
clicking on item area without any items.
|
||||
|
||||
Todos:
|
||||
Add to tools utils.
|
||||
"""
|
||||
|
||||
double_clicked_left = QtCore.Signal()
|
||||
double_clicked_right = QtCore.Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TreeView, self).__init__(*args, **kwargs)
|
||||
self._deselectable = False
|
||||
|
||||
self._flick_charm_activated = False
|
||||
self._flick_charm = FlickCharm(parent=self)
|
||||
self._before_flick_scroll_mode = None
|
||||
|
||||
def is_deselectable(self):
|
||||
return self._deselectable
|
||||
|
||||
def set_deselectable(self, deselectable):
|
||||
self._deselectable = deselectable
|
||||
|
||||
deselectable = property(is_deselectable, set_deselectable)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if self._deselectable:
|
||||
index = self.indexAt(event.pos())
|
||||
if not index.isValid():
|
||||
# clear the selection
|
||||
self.clearSelection()
|
||||
# clear the current index
|
||||
self.setCurrentIndex(QtCore.QModelIndex())
|
||||
super(TreeView, self).mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.double_clicked_left.emit()
|
||||
|
||||
elif event.button() == QtCore.Qt.RightButton:
|
||||
self.double_clicked_right.emit()
|
||||
|
||||
return super(TreeView, self).mouseDoubleClickEvent(event)
|
||||
|
||||
def activate_flick_charm(self):
|
||||
if self._flick_charm_activated:
|
||||
return
|
||||
self._flick_charm_activated = True
|
||||
self._before_flick_scroll_mode = self.verticalScrollMode()
|
||||
self._flick_charm.activateOn(self)
|
||||
self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
|
||||
|
||||
def deactivate_flick_charm(self):
|
||||
if not self._flick_charm_activated:
|
||||
return
|
||||
self._flick_charm_activated = False
|
||||
self._flick_charm.deactivateFrom(self)
|
||||
if self._before_flick_scroll_mode is not None:
|
||||
self.setVerticalScrollMode(self._before_flick_scroll_mode)
|
||||
|
||||
|
||||
class BaseOverlayFrame(QtWidgets.QFrame):
|
||||
|
|
|
|||
|
|
@ -338,7 +338,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
self._side_panel.set_published_mode(published_mode)
|
||||
|
||||
def _on_folder_filter_change(self, text):
|
||||
self._folder_widget.set_name_filer(text)
|
||||
self._folder_widget.set_name_filter(text)
|
||||
|
||||
def _on_go_to_current_clicked(self):
|
||||
self._controller.go_to_current_context()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ from .widgets import (
|
|||
RefreshButton,
|
||||
GoToCurrentButton,
|
||||
)
|
||||
from .views import DeselectableTreeView
|
||||
from .views import (
|
||||
DeselectableTreeView,
|
||||
TreeView,
|
||||
)
|
||||
from .error_dialog import ErrorMessageBox
|
||||
from .lib import (
|
||||
WrappedCallbackItem,
|
||||
|
|
@ -71,6 +74,7 @@ __all__ = (
|
|||
"GoToCurrentButton",
|
||||
|
||||
"DeselectableTreeView",
|
||||
"TreeView",
|
||||
|
||||
"ErrorMessageBox",
|
||||
|
||||
|
|
|
|||
|
|
@ -24,9 +24,12 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
lock = False
|
||||
|
||||
def __init__(self, dbcon, *args, **kwargs):
|
||||
self.dbcon = dbcon
|
||||
self._dbcon = dbcon
|
||||
super(VersionDelegate, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_project_name(self):
|
||||
return self._dbcon.active_project()
|
||||
|
||||
def displayText(self, value, locale):
|
||||
if isinstance(value, HeroVersionType):
|
||||
return lib.format_version(value, True)
|
||||
|
|
@ -120,7 +123,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
"Version is not integer"
|
||||
)
|
||||
|
||||
project_name = self.dbcon.active_project()
|
||||
project_name = self.get_project_name()
|
||||
# Add all available versions to the editor
|
||||
parent_id = item["version_document"]["parent"]
|
||||
version_docs = [
|
||||
|
|
|
|||
|
|
@ -171,14 +171,23 @@ class HostToolsHelper:
|
|||
def get_scene_inventory_tool(self, parent):
|
||||
"""Create, cache and return scene inventory tool window."""
|
||||
if self._scene_inventory_tool is None:
|
||||
from openpype.tools.sceneinventory import SceneInventoryWindow
|
||||
|
||||
host = registered_host()
|
||||
ILoadHost.validate_load_methods(host)
|
||||
|
||||
scene_inventory_window = SceneInventoryWindow(
|
||||
parent=parent or self._parent
|
||||
)
|
||||
if AYON_SERVER_ENABLED:
|
||||
from openpype.tools.ayon_sceneinventory.window import (
|
||||
SceneInventoryWindow)
|
||||
|
||||
scene_inventory_window = SceneInventoryWindow(
|
||||
parent=parent or self._parent
|
||||
)
|
||||
|
||||
else:
|
||||
from openpype.tools.sceneinventory import SceneInventoryWindow
|
||||
|
||||
scene_inventory_window = SceneInventoryWindow(
|
||||
parent=parent or self._parent
|
||||
)
|
||||
self._scene_inventory_tool = scene_inventory_window
|
||||
|
||||
return self._scene_inventory_tool
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
from openpype.resources import get_image_path
|
||||
from openpype.tools.flickcharm import FlickCharm
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui, QtSvg
|
||||
|
||||
|
||||
|
|
@ -57,3 +59,63 @@ class TreeViewSpinner(QtWidgets.QTreeView):
|
|||
self.paint_empty(event)
|
||||
else:
|
||||
super(TreeViewSpinner, self).paintEvent(event)
|
||||
|
||||
|
||||
class TreeView(QtWidgets.QTreeView):
|
||||
"""Ultimate TreeView with flick charm and double click signals.
|
||||
|
||||
Tree view have deselectable mode, which allows to deselect items by
|
||||
clicking on item area without any items.
|
||||
|
||||
Todos:
|
||||
Add refresh animation.
|
||||
"""
|
||||
|
||||
double_clicked = QtCore.Signal(QtGui.QMouseEvent)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TreeView, self).__init__(*args, **kwargs)
|
||||
self._deselectable = False
|
||||
|
||||
self._flick_charm_activated = False
|
||||
self._flick_charm = FlickCharm(parent=self)
|
||||
self._before_flick_scroll_mode = None
|
||||
|
||||
def is_deselectable(self):
|
||||
return self._deselectable
|
||||
|
||||
def set_deselectable(self, deselectable):
|
||||
self._deselectable = deselectable
|
||||
|
||||
deselectable = property(is_deselectable, set_deselectable)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if self._deselectable:
|
||||
index = self.indexAt(event.pos())
|
||||
if not index.isValid():
|
||||
# clear the selection
|
||||
self.clearSelection()
|
||||
# clear the current index
|
||||
self.setCurrentIndex(QtCore.QModelIndex())
|
||||
super(TreeView, self).mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
self.double_clicked.emit(event)
|
||||
|
||||
return super(TreeView, self).mouseDoubleClickEvent(event)
|
||||
|
||||
def activate_flick_charm(self):
|
||||
if self._flick_charm_activated:
|
||||
return
|
||||
self._flick_charm_activated = True
|
||||
self._before_flick_scroll_mode = self.verticalScrollMode()
|
||||
self._flick_charm.activateOn(self)
|
||||
self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
|
||||
|
||||
def deactivate_flick_charm(self):
|
||||
if not self._flick_charm_activated:
|
||||
return
|
||||
self._flick_charm_activated = False
|
||||
self._flick_charm.deactivateFrom(self)
|
||||
if self._before_flick_scroll_mode is not None:
|
||||
self.setVerticalScrollMode(self._before_flick_scroll_mode)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.17.2-nightly.4"
|
||||
__version__ = "3.17.3-nightly.2"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "OpenPype"
|
||||
version = "3.17.1" # OpenPype
|
||||
version = "3.17.2" # OpenPype
|
||||
description = "Open VFX and Animation pipeline with support."
|
||||
authors = ["OpenPype Team <info@openpype.io>"]
|
||||
license = "MIT License"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,26 @@
|
|||
"host_name": "maya",
|
||||
"environment": "{\n \"MAYA_DISABLE_CLIC_IPM\": \"Yes\",\n \"MAYA_DISABLE_CIP\": \"Yes\",\n \"MAYA_DISABLE_CER\": \"Yes\",\n \"PYMEL_SKIP_MEL_INIT\": \"Yes\",\n \"LC_ALL\": \"C\"\n}\n",
|
||||
"variants": [
|
||||
{
|
||||
"name": "2024",
|
||||
"label": "2024",
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Autodesk\\Maya2024\\bin\\maya.exe"
|
||||
],
|
||||
"darwin": [],
|
||||
"linux": [
|
||||
"/usr/autodesk/maya2024/bin/maya"
|
||||
]
|
||||
},
|
||||
"arguments": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
"environment": "{\n \"MAYA_VERSION\": \"2024\"\n}",
|
||||
"use_python_2": false
|
||||
},
|
||||
{
|
||||
"name": "2023",
|
||||
"label": "2023",
|
||||
|
|
@ -45,66 +65,6 @@
|
|||
"linux": []
|
||||
},
|
||||
"environment": "{\n \"MAYA_VERSION\": \"2022\"\n}",
|
||||
"use_python_2": false
|
||||
},
|
||||
{
|
||||
"name": "2020",
|
||||
"label": "2020",
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Autodesk\\Maya2020\\bin\\maya.exe"
|
||||
],
|
||||
"darwin": [],
|
||||
"linux": [
|
||||
"/usr/autodesk/maya2020/bin/maya"
|
||||
]
|
||||
},
|
||||
"arguments": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
"environment": "{\n \"MAYA_VERSION\": \"2020\"\n}",
|
||||
"use_python_2": true
|
||||
},
|
||||
{
|
||||
"name": "2019",
|
||||
"label": "2019",
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Autodesk\\Maya2019\\bin\\maya.exe"
|
||||
],
|
||||
"darwin": [],
|
||||
"linux": [
|
||||
"/usr/autodesk/maya2019/bin/maya"
|
||||
]
|
||||
},
|
||||
"arguments": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
"environment": "{\n \"MAYA_VERSION\": \"2019\"\n}",
|
||||
"use_python_2": true
|
||||
},
|
||||
{
|
||||
"name": "2018",
|
||||
"label": "2018",
|
||||
"executables": {
|
||||
"windows": [
|
||||
"C:\\Program Files\\Autodesk\\Maya2018\\bin\\maya.exe"
|
||||
],
|
||||
"darwin": [],
|
||||
"linux": [
|
||||
"/usr/autodesk/maya2018/bin/maya"
|
||||
]
|
||||
},
|
||||
"arguments": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
"environment": "{\n \"MAYA_VERSION\": \"2018\"\n}",
|
||||
"use_python_2": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -115,9 +115,7 @@ class ToolGroupModel(BaseSettingsModel):
|
|||
name: str = Field("", title="Name")
|
||||
label: str = Field("", title="Label")
|
||||
environment: str = Field("{}", title="Environments", widget="textarea")
|
||||
variants: list[ToolVariantModel] = Field(
|
||||
default_factory=ToolVariantModel
|
||||
)
|
||||
variants: list[ToolVariantModel] = Field(default_factory=list)
|
||||
|
||||
@validator("environment")
|
||||
def validate_json(cls, value):
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.1"
|
||||
__version__ = "0.1.2"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from pydantic import Field
|
||||
|
||||
from ayon_server.settings import BaseSettingsModel
|
||||
from ayon_server.settings import task_types_enum
|
||||
|
||||
|
||||
class CreateLookModel(BaseSettingsModel):
|
||||
|
|
@ -120,6 +121,16 @@ class CreateVrayProxyModel(BaseSettingsModel):
|
|||
default_factory=list, title="Default Products")
|
||||
|
||||
|
||||
class CreateMultishotLayout(BasicCreatorModel):
|
||||
shotParent: str = Field(title="Shot Parent Folder")
|
||||
groupLoadedAssets: bool = Field(title="Group Loaded Assets")
|
||||
task_type: list[str] = Field(
|
||||
title="Task types",
|
||||
enum_resolver=task_types_enum
|
||||
)
|
||||
task_name: str = Field(title="Task name (regex)")
|
||||
|
||||
|
||||
class CreatorsModel(BaseSettingsModel):
|
||||
CreateLook: CreateLookModel = Field(
|
||||
default_factory=CreateLookModel,
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ class PublishPuginsModel(BaseSettingsModel):
|
|||
default_factory=CollectInstanceDataModel,
|
||||
section="Collectors"
|
||||
)
|
||||
ValidateCorrectAssetName: OptionalPluginModel = Field(
|
||||
ValidateCorrectAssetContext: OptionalPluginModel = Field(
|
||||
title="Validate Correct Folder Name",
|
||||
default_factory=OptionalPluginModel,
|
||||
section="Validators"
|
||||
|
|
@ -308,7 +308,7 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = {
|
|||
"write"
|
||||
]
|
||||
},
|
||||
"ValidateCorrectAssetName": {
|
||||
"ValidateCorrectAssetContext": {
|
||||
"enabled": True,
|
||||
"optional": True,
|
||||
"active": True
|
||||
|
|
|
|||
28
tests/integration/hosts/maya/input/startup/userSetup.py
Normal file
28
tests/integration/hosts/maya/input/startup/userSetup.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import logging
|
||||
import sys
|
||||
|
||||
from maya import cmds
|
||||
|
||||
import pyblish.util
|
||||
|
||||
|
||||
def setup_pyblish_logging():
|
||||
log = logging.getLogger("pyblish")
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
formatter = logging.Formatter(
|
||||
"pyblish (%(levelname)s) (line: %(lineno)d) %(name)s:"
|
||||
"\n%(message)s"
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
|
||||
|
||||
def _run_publish_test_deferred():
|
||||
try:
|
||||
setup_pyblish_logging()
|
||||
pyblish.util.publish()
|
||||
finally:
|
||||
cmds.quit(force=True)
|
||||
|
||||
|
||||
cmds.evalDeferred("_run_publish_test_deferred()", lowestPriority=True)
|
||||
|
|
@ -33,16 +33,16 @@ class MayaHostFixtures(HostFixtures):
|
|||
yield dest_path
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def startup_scripts(self, monkeypatch_session, download_test_data):
|
||||
def startup_scripts(self, monkeypatch_session):
|
||||
"""Points Maya to userSetup file from input data"""
|
||||
startup_path = os.path.join(download_test_data,
|
||||
"input",
|
||||
"startup")
|
||||
startup_path = os.path.join(
|
||||
os.path.dirname(__file__), "input", "startup"
|
||||
)
|
||||
original_pythonpath = os.environ.get("PYTHONPATH")
|
||||
monkeypatch_session.setenv("PYTHONPATH",
|
||||
"{}{}{}".format(startup_path,
|
||||
os.pathsep,
|
||||
original_pythonpath))
|
||||
monkeypatch_session.setenv(
|
||||
"PYTHONPATH",
|
||||
"{}{}{}".format(startup_path, os.pathsep, original_pythonpath)
|
||||
)
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def skip_compare_folders(self):
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class ModuleUnitTest(BaseTest):
|
|||
yield path
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def env_var(self, monkeypatch_session, download_test_data):
|
||||
def env_var(self, monkeypatch_session, download_test_data, mongo_url):
|
||||
"""Sets temporary env vars from json file."""
|
||||
env_url = os.path.join(download_test_data, "input",
|
||||
"env_vars", "env_var.json")
|
||||
|
|
@ -129,6 +129,9 @@ class ModuleUnitTest(BaseTest):
|
|||
monkeypatch_session.setenv(key, str(value))
|
||||
|
||||
#reset connection to openpype DB with new env var
|
||||
if mongo_url:
|
||||
monkeypatch_session.setenv("OPENPYPE_MONGO", mongo_url)
|
||||
|
||||
import openpype.settings.lib as sett_lib
|
||||
sett_lib._SETTINGS_HANDLER = None
|
||||
sett_lib._LOCAL_SETTINGS_HANDLER = None
|
||||
|
|
@ -150,8 +153,7 @@ class ModuleUnitTest(BaseTest):
|
|||
request, mongo_url):
|
||||
"""Restore prepared MongoDB dumps into selected DB."""
|
||||
backup_dir = os.path.join(download_test_data, "input", "dumps")
|
||||
|
||||
uri = mongo_url or os.environ.get("OPENPYPE_MONGO")
|
||||
uri = os.environ.get("OPENPYPE_MONGO")
|
||||
db_handler = DBHandler(uri)
|
||||
db_handler.setup_from_dump(self.TEST_DB_NAME, backup_dir,
|
||||
overwrite=True,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue